플랫폼 코드 품질·안정성 개선 — TS 전환 · 권한 서버 전환 · 테스트 체계
#frontend#TypeScript#Vitest#Zustand#lint-staged#GitLab CI
Seoyoung Kim · TS 전환 설계·주도 · 권한 전환 FE 담당 · 테스트 체계 도입 @ (주)썬더소프트코리아 → (주)사파이어스트림테크놀로지 / System SW
2026-05-20 ~ 진행 중
진행 중 (2026.05 ~). 권한 전환은 진행 중이고, 테스트 체계는 2026년 10월 도입. 보안 정책상 코드 발췌와 사내 용어는 싣지 않는다.
| 지표 | 결과 |
|---|---|
| TS 비율 — 도입 후 신규 파일 | 80% (2026.05.20 도입 이후 신규 파일 440개 중 351개) |
| TS 비율 — shared 레이어 | 72% (전체는 27%. 주로 신규 파일로 올랐고 기존 JS 변환은 40건) |
| 권한 판정 | 화면 곳곳의 규칙 → 기능 단위 · 대상 단위 두 함수 |
| 머지 검증 | MR마다 저장소 전체 lint · tsc · 테스트, 통과해야 머지 |
배경
통합 플랫폼이 29개 페이지까지 커지면서 세 가지 문제가 쌓였다.
- 타입 — JS + JSDoc으로 타입을 관리했는데, 기능이 바뀔 때 JSDoc 갱신이 자주 누락돼 타입 정보가 코드와 어긋났다.
- 권한 — 권한 규칙이 FE 코드에만 있었다. 화면에서 버튼을 숨길 뿐이라 화면을 거치지 않는 API 호출은 막을 수 없었고, 권한을 바꿀 때마다 FE 배포가 필요했다.
- 검증 — 테스트 러너가 없고 타입 검사는 각자 로컬에서만 돌렸다. 타입 에러가 main에 들어가는 걸 막는 장치가 없었다.
세 작업 모두 한 번에 갈아엎지 않고, 기능 개발과 병행할 수 있게 단계로 나눴다.
1. TypeScript 점진 전환
- 도입 설계 문서와 팀 TS 가이드(any 금지 · unknown으로 좁히기, enum 대신 union, export 함수 반환 타입 명시 등 15개 항목)를 작성해 팀에 공유
allowJs: true,checkJs: false— 기존 JS는 그대로 두고 TS와 공존. 기준은strict: true- 순서 — 다른 코드가 가장 많이 import하는 shared/ui · shared/hooks를 먼저 TS로. 이 레이어가 JS로 남아 있으면 새 TS 파일에도 타입 없는 코드가 늘어나기 때문
- 규칙 — 새 파일은 TS로만 작성(빌드 설정 등 예외만 JS). TS에서 기존 JS를 쓰다 타입 문제가 생기면 먼저 JSDoc을 보강하고, 같은 모듈에서 두 번 이상 걸리면 그 모듈을 TS로 변환. 기존 파일 변환은 마이그레이션 전용 PR에서만
지금 상태 — 신규 파일 80%, shared 72%가 TS. widgets(2%)가 다음 차례다. 규칙은 컨벤션 문서로 관리하고, lint로 강제하지는 않는다.
2. 권한 서버 전환 (FE 담당)
| 변경 전 | 변경 후 | |
|---|---|---|
| 권한 정의 | FE 코드 여러 곳 (규칙 파일 · 사용자 목록 · 화면 인라인) | BE DB → 권한 조회 API 응답 |
| 판정 기준 | 역할 · 소속 · 사용자 ID 조합 하드코딩 | 리소스 · 액션별 granted + relations |
| 관리자 | FE에서 무조건 통과 | FE 우회 없음, 서버가 내려준 대로 |
| 규칙이 없을 때 | 허용/거부를 화면마다 선택 | 항상 거부 (fail closed) |
| 작성자 · 소속 조건 | 화면마다 직접 비교 | 서버가 관계 조건을 내려주고 FE가 대상 데이터로 판정 |
| 권한 변경 | FE 배포 필요 | DB 수정만으로 반영 |
- 판정 함수 두 개로 통일 — 기능 단위 함수(resource, action)는 기능이 있는지(라우트 · 메뉴 · 새 글 버튼), 대상 단위 함수(resource, action, target)는 특정 대상에 대해(내 글 수정, 담당 소속만 수정). 관계 조건이 여러 개면 하나라도 맞으면 통과
- 공용 권한 레이어를 TS로 새로 작성 — API → 응답 정규화(방어적 adapter) → 순수 판정 함수 → store → 반응형 훅 / 비훅 함수. 렌더 안에서는 훅을, 컬럼 빌더 · 이벤트 핸들러에서는 store를 직접 읽는 함수를 쓴다
- 깜빡임 방지 — 권한을 받기 전에는 라우트 가드가 로딩을, 버튼 가드가 빈 화면을 그려 "안 보임 → 보임"으로 바뀌지 않게 했다
- 계정 전환 안전장치 — 저장해 둔 권한에 소유자 ID를 함께 저장해, 다른 계정으로 로그인하면 이전 계정의 권한으로 판정하지 않는다
- 7단계 교체 — 인프라 → 페이지 접근 → 단순 액션 → 소속 조건 → 작성자 조건 → 사용자 · 기타 → 정리. 교체가 끝난 기존 규칙은 같은 단계에서 삭제해 두 체계가 오래 공존하지 않게 했다
- 권한 DB · API는 BE가 구현했다. 권한 DB에 아직 리소스가 없는 레거시 규칙 일부는 기존 방식을 유지한다
3. 테스트 · 머지 검증
- 대상을 좁혔다 — 전체 커버리지가 목표가 아니다. 틀리면 권한이 열리거나 닫히는 판정 로직처럼 보안 · 데이터에 영향이 가는 로직, 입력 케이스가 5개 이상 갈리는 순수 함수, 결과를 보장해야 하는 리팩터링만 테스트한다. 컴포넌트 · 훅은 수동 확인이 더 싸다고 판단했다
- Vitest를 고른 이유 — Vite 설정과 경로 alias를 그대로 공유해 별도 변환 설정이 없고, Jest와 같은 API. 37건 기준 1.5초
- 테스트하기 좋게 분리 — store나 훅에 묶인 로직은 의존성을 인자로 받는 순수 함수로 먼저 분리했다. 호출처는 이름과 시그니처가 같은 함수를 그대로 쓴다
- 두 단계 검증
| 커밋 시 (pre-commit) | MR 파이프라인 | |
|---|---|---|
| 범위 | 커밋한 파일과 관련 테스트만 | 저장소 전체 |
| 실행 | eslint --fix · prettier · 관련 테스트 | eslint · tsc --noEmit · 전체 테스트 |
| 우회 | 가능 (--no-verify 등) | 불가, 통과해야 머지 |
로컬은 빠르게 고쳐 주는 층, MR은 아무도 우회하지 못하는 확인 층이다. MR 파이프라인의 타입 검사가 타입 에러가 main에 들어가는 걸 막는 첫 장치가 됐다.
배운 점과 남은 과제
- 큰 전환은 "한 번에"보다 "멈추지 않고"가 중요했다. 세 작업 모두 기능 개발과 병행할 수 있게 순서와 예외 규칙부터 정했다
- 결정은 문서로 남겼다 — TS 도입 설계 · 가이드, 테스트 도입 문서, 권한 전환 문서를 작성해 팀에 공유
- 남은 과제 — 레이어 의존 방향을 lint로 강제하기, widgets 레이어 TS 전환, 막 도입한 테스트 체계의 운영 경험 쌓기