Skip to content

Conversation

@useon
Copy link
Contributor

@useon useon commented Nov 21, 2025

🚩 연관 이슈

closed #382

📝 작업 내용

🚀 배경

GA 분석 결과 영미권, 중화권의 접속이 많아 서비스의 언어로 영어, 중국어를 추가로 지원하기 위한 i18n 작업을 진행하기로 했습니다.
작업을 진행 중 수많은 한글 텍스트를 t()로 감싸고, 훅과 import문을 선언하고, 번역 키를 언어마다 추가하고 수정하는 비효율성과 과정에서 발생하는 휴먼 에러를 방지 및 해결하고자 자동화 스크립트를 만들었습니다!! 추가로 i18n의 초기세팅도 함께 진행하였습니다.

✨ 주요 기능

  • React 컴포넌트 내의 한글 텍스트 및 JSX 내용을 t() 함수로 자동 변환
  • 변수가 포함된 템플릿 리터럴을 interpolation 문법에 맞게 자동 변환 interpolation 참고자료
  • useTranslation 훅 및 react-i18next import 구문 자동 주입
  • 생성된 번역 키를 public/locales/*/translation.json 파일에 자동 추가

🗂️ 파일별 동작 원리

스크립트는 여러 파일과 각 단계별로 구성되어 있습니다.

1. scripts/i18nTransform.ts

이 파일은 전체 변환 프로세스를 시작하고 조율합니다.

  1. 파일 탐색: glob을 사용하여 src 폴더 내의 모든 .tsx 파일을 찾아 목록을 만듭니다.
  2. 파일 순회: 찾은 파일 목록을 하나씩 순회하며, 각 파일을 아래의 processFile 함수에 전달합니다.
  3. processFile함수:
    1. 파일을 텍스트로 읽습니다.
    2. astUtils.tsparseCode를 호출하여 코드를 AST로 변환합니다.
    3. astUtils.tstransformAST를 호출하여 AST를 분석하고 수정합니다.
    4. translationUtils.tsupdateTranslationFiles를 호출하여 translation.json 파일을 업데이트합니다.
    5. astUtils.tsgenerateCode를 호출하여 수정된 AST를 다시 코드 문자열로 변환합니다.
    6. 변경된 코드가 있을 경우, 파일에 덮어씁니다.

2. scripts/utils/astUtils.ts

이 파일은 실제 코드 분석과 변환의 모든 핵심 로직을 담당합니다.

  • parseCode(code)함수:

    • 코드 문자열을 입력받아 Babel 파서를 사용해 AST로 변환합니다.
  • isReactComponentFunction(path)함수:

    • 현재 분석 중인 코드가 React 컴포넌트 내부에 있는지 판별합니다. 함수 선언문, 화살표 함수 표현식 또는 함수 표현식, 변수 선언문, 합성 컴포넌트인지 검사하여 작업 대상에 포함시킬지 판별합니다.
  • transformAST(ast) 함수:

    1. 정보 수집
      • Babel의 traverse를 사용하여 AST를 순회합니다.
      • StringLiteral, JSXText, TemplateLiteral 노드를 만날 때마다, React 컴포넌트 내부에 있는지 확인합니다.
      • 컴포넌트 내부에 있을 경우에만, 변환할 노드의 위치를 simpleStringsToTransform(템플릿 리터럴이 아닌 문자열) 또는 templateLiteralsToTransform(템플릿 리터럴인 문자열) 목록에 저장합니다.
      • 번역 파일에 추가할 키는 koreanKeys에, 훅을 주입할 컴포넌트는 componentsToModify에 기록합니다.
    2. 의존성 주입
      • 수집된 정보를 바탕으로, useTranslation 훅과 import 구문을 코드 상단에 삽입합니다. (이미 삽입되어 있는 경우는 삽입하지 않습니다.)
    3. 코드 변환
      • templateLiteralsToTransformsimpleStringsToTransform 목록을 순회하며, 저장된 위치의 노드들만 t() 함수 호출로 변환합니다.
  • generateCode(ast) 함수:

    • 수정된 AST를 다시 코드 문자열로 변환합니다.

3. scripts/utils/translationUtils.ts

수집된 번역 키를 관리하고 파일에 기록하는 역할을 합니다. 또한 작업 과정을 로그로 보여줍니다.

  • updateTranslationFiles(keys) 함수:
    1. public/locales/ko/translation.jsonen/translation.json 파일을 읽습니다.
    2. 전달받은 keys 목록을 순회하며, JSON 파일에 아직 존재하지 않는 키만 추가합니다. (ko는 원문, en은 빈 문자열로 초기화)
    3. 변경된 내용을 다시 파일에 씁니다.

4. scripts/utils/fileUtils.ts

파일 시스템에 접근하여 파일을 읽고 쓰는 기본적인 입출력 작업을 처리하는 헬퍼 함수들의 모음입니다.

  • readFile(filePath) / writeFile(filePath, data)함수:

    • 가장 기본적인 파일 읽기/쓰기 함수입니다.
  • readJSON(filePath) / writeJSON(filePath, data)함수:

    • readJSON은 파일을 읽고 그 내용을 자바스크립트 객체로 파싱하며, updateTranslationFiles에서 기존 번역 파일을 읽을 때 사용됩니다.
    • writeJSON은 자바스크립트 객체를 JSON 문자열로 변환하여 파일에 저장하며, updateTranslationFiles에서 새로운 키가 추가된 번역 객체를 저장할 때 사용됩니다.
  • ensureFile(filePath)함수:

    • 파일을 쓰기 전에, 해당 파일이 존재하지 않으면 파일을 생성하고, 상위 디렉토리가 없으면 디렉토리까지 생성하여 파일 쓰기 작업이 안전하게 이루어지도록 보장합니다.

🤔 자동화에서 제외된 기능

추가로 구현하고 싶다고 공유했던 내용 중 구현을 해보고 자동화에서 제외한 기능들입니다.

  1. 컴포넌트 외부 상수/props 기본값 번역

    • 리액트 훅을 쓸 수 없는 위치라 t() 함수 사용이 불가능하며, i18n.t()는 실시간 언어 변경에 반응하지 않는 문제가 있었습니다.
    • 추후 자동화를 적용하여 작업 진행시에 더 고민해서 컴포넌트 구조를 수정하거나 해야할 것 같아 제외했습니다.
  2. 숫자/문자 추출
    image
    -30초, +30초 등 각각 키로 추출되는 것을 한글인 '초'만 추출하고 싶었습니다. 하지만 아래의 이유들로 제외하였습니다.

    1. 현재 프로젝트는 동적 데이터는 템플릿 리터럴로 관리하고 있기 때문에 하드코딩된 숫자로 인한 키가 엄청 많아지는 문제가 발생할 가능성이 낮다고 판단했습니다.
    2. 기능 구현으로 인한 스크립트 복잡성 증가 대비, 현재 얻을 수 있는 이득이 크지 않다고 생각했습니다. (줄어드는 키가 10개 미만이었음)

📖 사용법

아래 명령어를 통해 스크립트를 실행할 수 있습니다!

npm run i18n:transform

추후에 자동화 스크립트가 완성되면 커밋 전 자동으로 실행되도록 하는 방안까지 고려해 보겠습니다!!

🏞️ 스크린샷 (선택)

Adobe Express - i18n자동화 스크립트 실행

🗣️ 리뷰 요구사항 (선택)

추가로 고려해야할 부분이 있거나 궁금하신 점 댓글이나 디스코드로 편하게 남겨주시면 바로 달려갑니다 = 33
자동화에 이것저것 기능을 넣어 설명이 길어졌는데 열심히 읽어주셔서 감사합니다 🥹

Summary by CodeRabbit

  • 새로운 기능

    • 한국어·영어 기반 국제화(i18n) 도입 및 앱 시작 시 i18n 초기화
    • URL 기반 언어 라우팅과 라우트별 언어 동기화 지원
    • 번역 자동 추출 및 코드 내 텍스트를 자동으로 i18n 키로 변환하는 도구와 변환 결과 동기화 기능 추가
  • Chore

    • i18n 관련 런타임 라이브러리 및 변환/빌드용 개발 도구·스크립트(추출 스크립트 포함) 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@useon useon self-assigned this Nov 21, 2025
@useon useon added the feat 기능 개발 label Nov 21, 2025
@useon useon linked an issue Nov 21, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Nov 21, 2025

Walkthrough

프로젝트에 i18n 런타임 초기화, 언어 기반 라우팅, AST 기반 한글 추출·자동 래핑 스크립트 및 번역 파일 동기화 유틸리티와 관련 스크립트·의존성이 추가되었습니다.

Changes

코호트 / 파일(s) 변경 사항
패키지 의존성 및 스크립트
package.json
i18n 런타임 의존성(i18next, react-i18next, i18next-browser-languagedetector, i18next-http-backend) 및 AST/TS 도구·타입(@babel/*, @types/*, glob, tsx) 추가. i18n:transform 스크립트 추가.
i18n 초기화
src/i18n.ts, src/main.tsx
src/i18n.ts 신규 추가로 i18next 초기화 구성(ko/en, 브라우저 감지, HTTP backend). src/main.tsxsrc/i18n.ts 임포트 추가해 앱 시작 시 초기화 실행.
언어 기반 라우팅
src/routes/LanguageWrapper.tsx, src/routes/routes.tsx
LanguageWrapper 컴포넌트 추가로 URL의 lang 파라미터를 i18n과 동기화. routes.tsx에서 라우트 트리를 언어별로 재구성하고 ProtectedRoute 적용 방식 변경 및 NotFound 라우트 포함.
i18n 변환 CLI 스크립트
scripts/i18nTransform.ts
src/**/*.tsx를 스캔해 AST로 파싱 → 한글 문자열 추출 → t(...) 호출로 변환 → 번역 키 수집 → 번역 파일 동기화 → 변경된 파일만 덮어쓰기하는 CLI 스크립트 추가(비동기 main/error 로직 포함).
AST 유틸리티
scripts/utils/astUtils.ts
parseCode(code), transformAST(ast)(한글 탐지, 기존 t() 회피, useTranslation 주입, 템플릿 리터럴 처리), generateCode(ast) 등 AST 분석·변환 함수 추가. 변환 시 생성된 i18n 키 집합 반환.
파일 I/O 유틸리티
scripts/utils/fileUtils.ts
readFile, readJSON, ensureFile, writeFile, writeJSON 등 파일 읽기/쓰기 및 디렉토리 보장 유틸 추가(로그 포함).
번역 동기화 유틸
scripts/utils/translationUtils.ts
updateTranslationFiles(keys, options?) 추가 — 기본 언어 ['ko','en'], 기본 디렉터리 public/localestranslation.json 보장 및 누락 키 추가(ko: 키값, en: 빈 문자열), 변경 사항 파일로 기록.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as npm run i18n:transform
    participant Glob as glob (파일 스캐너)
    participant FS as File I/O (read/write)
    participant Parser as parseCode (Babel)
    participant Transformer as transformAST
    participant Generator as generateCode
    participant TransUpdate as updateTranslationFiles

    CLI->>Glob: src/**/*.tsx 조회
    Glob-->>CLI: 파일 목록
    loop 파일별 처리
        CLI->>FS: 파일 읽기
        FS-->>Parser: 코드 전달
        Parser->>Transformer: AST 전달
        Transformer-->>Generator: 변환된 AST + 키 집합
        Generator->>FS: 변환 코드 생성/비교
        FS-->>CLI: 변경된 경우만 쓰기
    end
    CLI->>TransUpdate: 집계된 키 전달
    TransUpdate->>FS: 각 언어 translation.json 보장·읽기·쓰기
    FS-->>TransUpdate: 파일 갱신 완료
    TransUpdate-->>CLI: 완료
Loading
sequenceDiagram
    participant User as 브라우저 사용자
    participant Router as React Router
    participant LWrap as LanguageWrapper
    participant i18n as i18next

    User->>Router: /ko/... 또는 /en/... 요청
    Router->>LWrap: lang 파라미터 전달
    LWrap->>i18n: 언어 유효성 검사 및 changeLanguage()
    i18n-->>LWrap: 언어 변경 완료
    LWrap->>Router: Outlet 렌더링
    Router-->>User: 해당 언어로 렌더링된 페이지 응답
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50분

  • 주의 깊게 확인할 파일/영역:
    • scripts/utils/astUtils.ts: AST 순회·변환의 정확성(중복 변환 방지, 기존 t() 회피, 템플릿 플레이스홀더 매핑).
    • scripts/i18nTransform.ts: 파일 스캔/병렬성, 변경감지·선택적 쓰기, 에러 전파 로직.
    • scripts/utils/translationUtils.ts & fileUtils.ts: 파일 보장/쓰기 시 권한·인코딩·동시성 처리.
    • src/routes/routes.tsx: 기존 라우트와 호환성(ProtectedRoute 래핑, NotFound, 상대 경로 전환).

Possibly related PRs

Suggested reviewers

  • eunwoo-levi
  • jaeml06
  • i-meant-to-be

Poem

🐰 깡충, 토끼가 펜을 들었네
코드 숲에서 한글을 모아 찰싹 붙이고,
t()로 포장해 번역 상자에 넣었지.
라우터는 언어를 잘 기억하고,
모두 함께 다국어로 폴짝! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 '[FEAT] i18n 자동화 스크립트 도입'으로 변경사항의 주요 내용인 i18n 자동화 스크립트 도입을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed PR은 #382의 목표인 한글 문자열을 자동으로 t()로 래핑하는 스크립트 구현과 번역 파일 자동 업데이트 요구사항을 완전히 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #382 이슈의 범위 내에 있으며, i18n 자동화 스크립트와 초기 설정에 관련된 파일들만 추가되었습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#382

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4ee51c and 9914402.

📒 Files selected for processing (1)
  • scripts/utils/fileUtils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/utils/fileUtils.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
src/routes/LanguageWrapper.tsx (1)

1-20: URL 기반 언어 동기화 흐름이 단순하고 명확합니다

useParams:lang을 읽고, 지원 언어(ko, en)만 허용하면서 i18n과 동기화하는 구조가 깔끔합니다. 기본값을 ko로 두는 것도 주석과 구현이 잘 맞습니다.

선택적으로 고려해볼 부분은 있습니다:

  • 타입 안정성:
    useParams<{ lang?: 'ko' | 'en' }>()처럼 제네릭을 주거나, type SupportedLang = (typeof supportedLangs)[number]를 도입하면 추후 언어 추가 시 타입 레벨에서 누락을 잡기 좋습니다.
  • 효과 의존성:
    현재 useEffect deps가 [lang]만인데, 모듈 상수(supportedLangs)와 i18n 인스턴스는 변경되지 않으므로 기능상 문제는 없습니다. 다만 react-hooks/exhaustive-deps 규칙을 사용 중이라면 린트 경고 여부를 한 번 확인해 보셔도 좋겠습니다.

전반적으로 동작에는 문제가 없고, 위 사항은 선택적인 개선 포인트입니다.

src/routes/routes.tsx (1)

16-80: 언어별 라우트 구조와 인증 래핑은 깔끔하지만, 리다이렉트 시 언어 prefix 손실 가능성을 확인해 주세요

  • appRoutes + protectedAppRoutes로 공통 라우트 정의 후, '/'':lang' 두 레이어에서 재사용하는 패턴은 중복을 줄이면서 언어별 라우팅을 잘 표현하고 있습니다.
  • path: ''가 LanguageWrapper 하위에서 index 역할을 하므로, /TableListPage, /:lang → 언어별 기본 페이지로 동작하는 구조도 자연스럽습니다.
  • NotFound(path: '*')를 공통으로 두 라우트 트리 아래에 두어 /.../:lang/... 모두에 대해 404를 처리하는 점도 일관적입니다.

다만 인증 흐름과 언어 prefix의 조합은 한 번 점검해 보는 것이 좋겠습니다:

  • 현재 ProtectedRoute 구현(별도 파일 참고)이 Navigate to={'/home'}로 고정되어 있다면, 예를 들어 /en/table/customize/123/end 접근 시 인증 실패 → /home으로 리다이렉트되어 언어 prefix(en)가 사라집니다.
  • 다국어 URL 전략을 강하게 가져가려면, 현재 URL의 언어 세그먼트를 유지해 /en/home처럼 리다이렉트하는 쪽이 UX 일관성 측면에서 더 나을 수 있습니다.

개선 아이디어(예시):

  • ProtectedRoute 내부에서 useParams<{ lang?: string }>() 또는 useLocation().pathname을 사용해 언어 세그먼트를 추출한 뒤,
    Navigate to={lang ? \/${lang}/home` : '/home'}`처럼 prefix를 보존하도록 조정.
  • 또는 현재 라우터에서 lang 정보를 Context로 노출하고, ProtectedRoute가 해당 Context를 사용하도록 하는 방식도 가능합니다.

라우터 구조 자체는 잘 정리되어 있고, 위 사항은 언어별 URL UX 일관성을 높이기 위한 권장 리팩터입니다.

Also applies to: 89-100

scripts/utils/translationUtils.ts (1)

1-45: 번역 JSON 자동 업데이트 로직은 직관적이며, 언어 코드 관리와 구조 전략만 맞춰두면 좋겠습니다

  • keys가 없으면 바로 반환하고, 각 언어별로 파일 생성/로딩 후 누락된 키만 추가하는 흐름이 단순하고 안전합니다.
  • ko는 원문을 그대로 값으로, 그 외 언어(기본 en)는 빈 문자열로 초기화하는 정책도 실제 번역 워크플로와 잘 맞습니다.
  • updated 플래그를 사용해 변경이 있을 때만 writeJSON을 호출하는 점도 I/O 측면에서 효율적입니다.

권장 리팩터/정책 측면에서 고려해볼 부분:

  1. 언어 코드의 단일 소스 관리
    • 여기의 languages = ['ko', 'en'], src/i18n.tssupportedLngs, LanguageWrappersupportedLangs가 모두 별도 상수로 존재합니다.
    • 추후 언어를 추가/제거할 때 실수 가능성을 줄이려면, 공용 상수 모듈(예: shared/i18nConfig.ts 또는 scripts/config/i18n.ts)로 분리해 스크립트와 런타임에서 함께 사용하는 방안을 고려해 볼 만합니다.
  2. 중첩 키 전략 명시
    • 현재 타입을 Record<string, string>으로 잡았기 때문에, "home.title"처럼 플랫 키를 쓰는 전략에 최적화되어 있습니다.
    • 향후 중첩 오브젝트 구조(예: { home: { title: '...' } })를 사용할 계획이 없다면, 주석으로 "플랫 키만 지원"을 명시해 두면 사용하는 쪽에서 오해를 줄일 수 있습니다.

현재 동작에는 문제 없어 보이고, 위 내용은 유지보수성과 확장성을 높이기 위한 권장 수준의 개선 제안입니다.

scripts/i18nTransform.ts (2)

6-26: 개별 파일 오류 처리 추가를 권장합니다.

현재 processFile에서 예외가 발생하면 전체 스크립트가 중단됩니다. 대량의 파일을 처리할 때 일부 파일의 오류로 인해 전체 작업이 중단되는 것을 방지하기 위해 try-catch 블록을 추가하는 것이 좋습니다.

다음과 같이 개선할 수 있습니다:

 async function processFile(filePath: string) {
+  try {
     console.log(`\n파일 처리 중: ${filePath}`);
     const originalCode = await fs.readFile(filePath, 'utf-8');
     const ast = parseCode(originalCode);
 
     const koreanKeys = transformAST(ast);
     if (koreanKeys.size === 0) {
       console.log('한글 텍스트를 찾지 못했습니다.');
       return;
     }
 
     await updateTranslationFiles(koreanKeys);
 
     const newCode = generateCode(ast);
     if (newCode !== originalCode) {
       await fs.writeFile(filePath, newCode, 'utf-8');
       console.log(`파일 업데이트 완료: ${filePath}`);
     } else {
       console.log('변경 사항이 없습니다.');
     }
+  } catch (error) {
+    console.error(`파일 처리 실패: ${filePath}`, error);
+    // 에러를 기록하고 계속 진행
+  }
 }

37-39: 선택사항: 대량 파일 처리 시 병렬 처리 고려

현재 파일들을 순차적으로 처리하고 있습니다. 파일 수가 많을 경우 성능 개선을 위해 제한된 동시성으로 병렬 처리를 고려할 수 있습니다.

예시:

// Promise.allSettled를 사용한 병렬 처리
const results = await Promise.allSettled(
  files.map(file => processFile(file))
);

// 실패한 파일 보고
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
  console.log(`\n처리 실패한 파일: ${failed.length}개`);
}
scripts/utils/astUtils.ts (1)

205-217: 훅 중복 체크 로직 개선 권장

Line 208에서 stmt.node.declarations[0]만 확인하고 있습니다. 이론적으로 하나의 VariableDeclaration에 여러 선언이 있을 수 있으므로, 모든 선언을 확인하는 것이 더 안전합니다.

다음과 같이 개선할 수 있습니다:

     bodyPath.get('body').forEach((stmt) => {
       if (stmt.isVariableDeclaration()) {
-        const declaration = stmt.node.declarations[0];
-        if (
-          declaration?.init?.type === 'CallExpression' &&
-          t.isIdentifier(declaration.init.callee) &&
-          declaration.init.callee.name === 'useTranslation'
-        ) {
-          hasHook = true;
-        }
+        stmt.node.declarations.forEach((declaration) => {
+          if (
+            declaration?.init?.type === 'CallExpression' &&
+            t.isIdentifier(declaration.init.callee) &&
+            declaration.init.callee.name === 'useTranslation'
+          ) {
+            hasHook = true;
+          }
+        });
       }
     });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2095001 and 21ee43c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • package.json (4 hunks)
  • scripts/i18nTransform.ts (1 hunks)
  • scripts/utils/astUtils.ts (1 hunks)
  • scripts/utils/fileUtils.ts (1 hunks)
  • scripts/utils/translationUtils.ts (1 hunks)
  • src/i18n.ts (1 hunks)
  • src/main.tsx (1 hunks)
  • src/routes/LanguageWrapper.tsx (1 hunks)
  • src/routes/routes.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
scripts/utils/translationUtils.ts (1)
scripts/utils/fileUtils.ts (3)
  • ensureFile (16-34)
  • readJSON (10-13)
  • writeJSON (48-55)
scripts/i18nTransform.ts (2)
scripts/utils/astUtils.ts (3)
  • parseCode (16-21)
  • transformAST (60-269)
  • generateCode (274-280)
scripts/utils/translationUtils.ts (1)
  • updateTranslationFiles (14-45)
src/routes/routes.tsx (2)
src/routes/ProtectedRoute.tsx (1)
  • ProtectedRoute (6-17)
src/routes/LanguageWrapper.tsx (1)
  • LanguageWrapper (7-20)
🪛 Gitleaks (8.29.0)
package.json

[high] 18-18: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (13)
src/main.tsx (1)

8-8: i18n 초기화 시점 연결이 적절합니다

엔트리포인트에서 ./i18n을 사이드 이펙트 import로 불러와서 렌더 전에 i18n이 초기화되도록 한 구조가 자연스럽고 문제 없어 보입니다.

src/i18n.ts (1)

1-31: i18n 설정이 전체 구조와 잘 맞지만, Suspense 및 감지 전략은 한 번 점검해 보세요

  • supportedLngs: ['ko', 'en'], fallbackLng: 'ko', backend.loadPath: '/locales/{{lng}}/translation.json' 구성이 public/locales/{lng}/translation.json 구조 및 updateTranslationFiles와 잘 맞아서 런타임/스크립트가 일관되게 동작할 것으로 보입니다.
  • detection.order: ['path', 'localStorage', 'navigator']LanguageWrapper:lang 처리(지원 언어만 허용, 없으면 ko 기본값)가 서로 보완 관계에 있어 설계 방향도 자연스럽습니다.

다만 아래 두 가지는 실제 앱 구조와 맞는지 한 번만 더 확인해 주세요:

  1. React Suspense 사용 여부
    react: { useSuspense: true } 설정이면 번역 로딩 중에 useTranslation이 Suspense를 던지므로, 상위 트리(예: RouterProvider 바깥 또는 App 루트)에 <Suspense fallback={...}>가 존재해야 합니다. 기존에 Suspense를 사용하지 않았다면, 어디에 boundary를 둘지 결정이 필요합니다.
  2. path 기반 언어 감지 + 라우팅 조합
    현재는 LanguageDetector의 path 감지와 LanguageWrapper의 수동 changeLanguage 호출이 함께 동작할 수 있습니다. 실제로는 둘 다 같은 언어로 맞춰 줄 가능성이 높지만, 초기 진입 시 어떤 순서로 언어가 결정되는지(특히 / vs /:lang 접근 시)를 한 번 디버깅/로그 확인해 두면 향후 디버깅에 도움이 됩니다.

전체 설정 자체는 합리적으로 보이며, 위 사항은 런타임 환경과의 정합성 검증 관점에서의 확인 요청입니다.

scripts/i18nTransform.ts (1)

1-4: LGTM!

import 경로에 .ts 확장자를 포함한 것은 ts-node로 직접 실행하기 위한 올바른 접근입니다.

scripts/utils/fileUtils.ts (2)

5-13: LGTM!

파일 읽기 유틸리티 함수들이 깔끔하게 구현되었습니다.


48-55: LGTM!

JSON 직렬화와 파일 쓰기가 적절하게 구현되었습니다. 들여쓰기 2칸으로 가독성도 좋습니다.

scripts/utils/astUtils.ts (6)

1-9: LGTM!

Babel 패키지의 CommonJS/ESM interop 문제를 해결하기 위한 타입 캐스팅이 적절히 적용되었습니다.


16-21: LGTM!

React TypeScript 파일 파싱을 위한 Babel 설정이 적절합니다.


72-184: LGTM!

한글 문자열 탐색 및 수집 로직이 잘 구현되었습니다. 특히:

  • 이미 t() 로 감싸진 문자열 중복 처리 방지 (lines 78-83, 104-109)
  • Import/Export 문 및 객체 키 제외 (lines 96-100)
  • 템플릿 리터럴의 placeholder 이름 충돌 해결 (lines 153-161)

186-198: LGTM!

useTranslation import 추가 로직이 적절합니다. 한글 키가 있고 기존 import가 없을 때만 추가합니다.


232-266: LGTM!

문자열 변환 로직이 올바르게 구현되었습니다:

  • 템플릿 리터럴의 interpolation 객체 처리 (lines 233-246)
  • JSX 컨텍스트에서 JSXExpressionContainer로 적절히 래핑 (lines 257-261)

274-280: LGTM!

코드 생성 설정이 적절합니다. retainLines: true로 원본 구조를 유지하고, minimal: true로 가독성을 보장합니다.

package.json (2)

19-19: LGTM!

i18n 변환 스크립트가 명확한 이름으로 적절히 추가되었습니다.


27-35: 의존성 버전 업데이트 및 react-i18next 메이저 버전 검토 필요합니다.

현재 설정된 i18n 라이브러리와 개발 도구 버전을 확인한 결과:

  • i18next: ^25.5.2 → 최신 25.6.3 업데이트 가능 (마이너 버전, 안전)
  • react-i18next: ^15.7.3 → 최신 16.3.5 (메이저 버전 차이) — 호환성 검토 필수
  • i18next-browser-languagedetector, i18next-http-backend: 최신 버전과 일치
  • 보안: 현재 버전들은 알려진 취약점 없음 (구버전 CVE 제외)

조치 사항:

  1. i18next를 25.6.3으로 마이너 업데이트 검토
  2. react-i18next 메이저 버전 업그레이드 전 변경 사항 검토 필수 (breaking changes 확인)
  3. 기타 Babel, glob, ts-node 패키지도 필요시 업데이트 검토

"build-storybook": "storybook build",
"test": "vitest",
"chromatic": "npx chromatic --project-token=chpt_a6dc39eba6488b2"
"chromatic": "npx chromatic --project-token=chpt_a6dc39eba6488b2",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Chromatic 토큰을 환경 변수로 이동하세요.

Line 18에 Chromatic 프로젝트 토큰이 평문으로 노출되어 있습니다. 버전 관리 시스템에 토큰을 커밋하면 보안 위험이 발생할 수 있습니다.

다음과 같이 환경 변수를 사용하도록 수정하세요:

-    "chromatic": "npx chromatic --project-token=chpt_a6dc39eba6488b2",
+    "chromatic": "npx chromatic",

그리고 프로젝트 루트에 .env 파일을 생성하여 토큰을 저장하세요:

CHROMATIC_PROJECT_TOKEN=chpt_a6dc39eba6488b2

.env 파일은 .gitignore에 추가되어야 합니다.

또는 Chromatic CLI가 CHROMATIC_PROJECT_TOKEN 환경 변수를 자동으로 인식하므로 CI/CD 환경에 환경 변수로 설정할 수 있습니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"chromatic": "npx chromatic --project-token=chpt_a6dc39eba6488b2",
"chromatic": "npx chromatic",
🧰 Tools
🪛 Gitleaks (8.29.0)

[high] 18-18: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In package.json around line 18, the Chromatic project token is hard-coded;
update the npm script to call chromatic without the literal token so it reads
the token from the CHROMATIC_PROJECT_TOKEN environment variable instead. Remove
the plain token from the repo, add a .env file at project root containing
CHROMATIC_PROJECT_TOKEN=your_token (and add .env to .gitignore), and ensure
CI/CD sets CHROMATIC_PROJECT_TOKEN in its environment variables (or export it
locally) so the Chromatic CLI picks it up automatically.

Comment on lines +26 to +30
function isReactComponentFunction(path: NodePath): boolean {
// 함수 선언문
if (path.isFunctionDeclaration()) {
return path.node.id?.name?.[0] === path.node.id?.name?.[0]?.toUpperCase();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

함수 선언문 컴포넌트 판별 로직 오류를 수정하세요.

Line 29의 조건문 path.node.id?.name?.[0] === path.node.id?.name?.[0]?.toUpperCase()는 첫 글자를 자기 자신과 비교하여 항상 true를 반환합니다. 이는 React 컴포넌트가 아닌 일반 함수도 컴포넌트로 잘못 인식할 수 있습니다.

다음과 같이 수정하세요:

   // 함수 선언문
   if (path.isFunctionDeclaration()) {
-    return path.node.id?.name?.[0] === path.node.id?.name?.[0]?.toUpperCase();
+    return /^[A-Z]/.test(path.node.id?.name || '');
   }

또는:

-    return path.node.id?.name?.[0] === path.node.id?.name?.[0]?.toUpperCase();
+    const name = path.node.id?.name;
+    return name ? name[0] === name[0].toUpperCase() : false;
🤖 Prompt for AI Agents
In scripts/utils/astUtils.ts around lines 26 to 30, the function-declaration
check compares the first character to itself which is always true; replace that
condition by first extracting the identifier name (e.g., const name =
path.node.id?.name), ensure it exists, then compare its first character to its
uppercase form (e.g., const first = name[0]; return first ===
first.toUpperCase() && first !== first.toLowerCase()) so only identifiers
starting with an uppercase letter (actual alphabetic characters) are treated as
React components.

console.log(`[writeFile] 파일 쓰기 시작: ${filePath}`);
const dir = path.dirname(filePath);

await fsp.mkdir(dir, { recursive: true }).catch(() => {});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

에러 처리를 개선하세요.

현재 mkdir의 모든 에러를 무시하고 있습니다. 디렉토리가 이미 존재하는 경우(EEXIST)는 무시해도 되지만, 권한 문제나 디스크 공간 부족 등의 다른 에러도 무시되어 문제를 진단하기 어려울 수 있습니다.

다음과 같이 개선을 제안합니다:

-  await fsp.mkdir(dir, { recursive: true }).catch(() => {});
+  await fsp.mkdir(dir, { recursive: true }).catch((err) => {
+    if (err.code !== 'EEXIST') {
+      console.warn(`디렉토리 생성 경고: ${dir}`, err.message);
+    }
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await fsp.mkdir(dir, { recursive: true }).catch(() => {});
await fsp.mkdir(dir, { recursive: true }).catch((err) => {
if (err.code !== 'EEXIST') {
console.warn(`디렉토리 생성 경고: ${dir}`, err.message);
}
});
🤖 Prompt for AI Agents
In scripts/utils/fileUtils.ts around line 41, the current await fsp.mkdir(dir, {
recursive: true }).catch(() => {}); swallows all errors; change it to catch the
error and only ignore the "directory exists" case while rethrowing or logging
other errors: await the mkdir inside a try/catch, and in the catch check
err.code === 'EEXIST' (or err && err.code === 'EEXIST') to ignore that specific
case, otherwise rethrow the error (or log and throw) so permission/disk errors
surface.

Copy link
Contributor

@jaeml06 jaeml06 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node_moudules 지우고 npm i로 다신 패키지 다운후, 주신 명령어인
npm run i18n:transform
을 실행시켰는데 아래와 같은에러가 발생하는 것 같은데 제 환경에서만 발생하는 문제인지 추가적인 설정이 더 필요한 것인지 궁금합니다.
image

Copy link
Contributor

@i-meant-to-be i-meant-to-be left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스크립트 제외 코드 변경 사항 자체는 많지 않기도 하고 딱히 변경할 것은 없어 보입니다!

다만 저도 치코랑 동일한 문제가 발생해요. 저는 npm i로 썬데이가 새로 추가한 패키지 설치하고 npm run i118:transform 실행했습니다.

@useon
Copy link
Contributor Author

useon commented Dec 2, 2025

제 환경에서는 잘 동작이 됐는데 왜 이런 문제가 발생하나 찾아보았습니다.

저희 프론트 프로젝트 설정은
package.json에서 "type": "module" → 프로젝트 전체 ESM 모드이고,
tsconfig.*.json에서 "module": "ESNext", "moduleResolution": "bundler"로
ESM + Vite 기준 설정입니다.

하지만 ts-node는 CJS 기반으로 설계된 실행기라 ESM 환경 지원이 완벽하지 않다고 합니다. 따라서 ts-node가 모든 모듈을 제대로 잡아 트랜스파일하지 못해 일부 .ts 파일이 Node ESM 로더로 넘어가 Node에서 .ts 확장자를 실행할 수 없기 때문에 TypeError: Unkown file extension ".ts"에러가 발생합니다!!

따라서 스크립트 실행기를 ts-node를 유지하고 tsconfig에 설정을 더 넣는 것보다 .ts 파일을 EMS/TS 환경에 더 적합한 tsx 실행기로 변경하는 것이 더 좋겠다고 판단하여 수정했습니다.

혹시이 ,, 이제는 제대로 실행이 되는지 한번 확인 부탁드립니다 !!!!!!

Copy link
Contributor

@jaeml06 jaeml06 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 변환되는 것 확인했습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ff5e40 and f4ee51c.

📒 Files selected for processing (1)
  • src/routes/routes.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/routes/routes.tsx (2)
src/routes/ProtectedRoute.tsx (1)
  • ProtectedRoute (6-17)
src/routes/LanguageWrapper.tsx (1)
  • LanguageWrapper (7-20)
🔇 Additional comments (1)
src/routes/routes.tsx (1)

22-93: 라우트 구성이 잘 정리되었습니다.

상대 경로로의 전환과 requiresAuth 플래그를 통한 인증 요구사항 명시가 깔끔합니다. path: ''를 사용한 인덱스 라우트 정의도 적절합니다.

Comment on lines +95 to +103
// 인증 보호 로직을 적용한 라우트
const protectedAppRoutes = appRoutes.map((route) => ({
...route,
element: route.requiresAuth ? (
<ProtectedRoute>{route.element}</ProtectedRoute>
) : (
route.element
),
}));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "ProtectedRoute.tsx" -o -name "ProtectedRoute.ts" -o -name "ProtectedRoute.jsx"

Repository: debate-timer/debate-timer-fe

Length of output: 103


🏁 Script executed:

cat -n ./src/routes/ProtectedRoute.tsx

Repository: debate-timer/debate-timer-fe

Length of output: 655


🏁 Script executed:

# Verify the router structure to understand how language routing works
sed -n '105,126p' src/routes/routes.tsx

Repository: debate-timer/debate-timer-fe

Length of output: 506


ProtectedRoute의 리다이렉트 경로가 언어 라우팅 구조와 호환되지 않습니다.

ProtectedRoute가 인증되지 않은 사용자를 절대 경로 '/home'으로 리다이렉트하고 있습니다. 현재 라우팅 구조에서 보호된 라우트는 /:lang 패턴 하에 중첩되어 있으므로, 사용자가 /en/composition에 있을 때 인증 실패로 /home으로 리다이렉트되면 언어 컨텍스트가 손실됩니다.

ProtectedRoute에서 useLocation()으로 현재 경로에서 언어를 추출하고, 해당 언어로 리다이렉트 URL을 구성하도록 수정하세요. 예: 현재 경로가 /en/...이면 /:lang/home로 리다이렉트.

🤖 Prompt for AI Agents
In src/routes/routes.tsx around lines 95–103, ProtectedRoute currently redirects
unauthenticated users to the absolute path '/home', which loses the current
language segment (/:lang) for nested routes; update ProtectedRoute to read the
current location via useLocation(), parse the leading language segment (e.g.,
from pathname.match(/^\/([^/]+)/)), build the redirect path by prefixing that
lang to '/home' (fall back to a default lang if none), and redirect to
`/${lang}/home` instead of '/home' so language context is preserved.

Comment on lines +113 to +124
children: [
{
path: '/',
element: <LanguageWrapper />,
children: protectedAppRoutes, // 기본 언어(ko) 라우트
},
{
path: ':lang', // 다른 언어 라우트
element: <LanguageWrapper />,
children: protectedAppRoutes,
},
],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

:lang 동적 세그먼트가 실제 라우트 경로와 충돌할 수 있습니다.

:lang 동적 파라미터는 모든 단일 경로 세그먼트와 매칭됩니다. 예를 들어 /home 접근 시:

  • '/' 라우트의 자식 'home'으로 매칭될 수 있고
  • ':lang' 라우트에서 lang='home'으로 매칭될 수도 있습니다

React Router v7에서는 정적 세그먼트가 동적 세그먼트보다 우선하지만, 의도치 않은 동작이 발생할 수 있습니다. 지원하는 언어 코드를 명시적으로 제한하는 것이 더 안전합니다.

     children: [
       {
         path: '/',
         element: <LanguageWrapper />,
         children: protectedAppRoutes, // 기본 언어(ko) 라우트
       },
       {
-        path: ':lang', // 다른 언어 라우트
+        path: ':lang(en|zh)', // 지원하는 언어만 명시적으로 매칭
         element: <LanguageWrapper />,
         children: protectedAppRoutes,
       },
     ],

또는 별도의 정적 경로로 분리하는 방안을 고려해 주세요:

+      {
+        path: 'en',
+        element: <LanguageWrapper />,
+        children: protectedAppRoutes,
+      },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
children: [
{
path: '/',
element: <LanguageWrapper />,
children: protectedAppRoutes, // 기본 언어(ko) 라우트
},
{
path: ':lang', // 다른 언어 라우트
element: <LanguageWrapper />,
children: protectedAppRoutes,
},
],
children: [
{
path: '/',
element: <LanguageWrapper />,
children: protectedAppRoutes, // 기본 언어(ko) 라우트
},
{
path: ':lang(en|zh)', // 지원하는 언어만 명시적으로 매칭
element: <LanguageWrapper />,
children: protectedAppRoutes,
},
],

Copy link
Contributor

@i-meant-to-be i-meant-to-be left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

잘 동작하는 거 확인했습니다! 승인 남겨요. 고생하셨어요!

@useon useon merged commit 3ad9bc8 into develop Dec 9, 2025
2 checks passed
@useon useon deleted the feat/#382 branch December 9, 2025 10:33
@useon useon deployed to DEPLOY_DEV December 9, 2025 10:33 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[FEAT] 다국어 t함수 래핑 스크립트

4 participants