Skip to content

feat(HK-173): i18n 초기 설정 및 언어 변경 모달 기능 추가#76

Merged
soooheeee merged 5 commits intomainfrom
feature/HK-173-i18n-setup
Dec 3, 2025
Merged

feat(HK-173): i18n 초기 설정 및 언어 변경 모달 기능 추가#76
soooheeee merged 5 commits intomainfrom
feature/HK-173-i18n-setup

Conversation

@soooheeee
Copy link
Copy Markdown
Contributor

@soooheeee soooheeee commented Nov 30, 2025

Motivation

  • 다국어 지원을 위함
  • 사용자가 쉽게 언어를 전환할 수 있는 UI를 제공하고자 함

도파민 다국어 설정

Problem Solving

  • react-i18next, i18next 기반 i18n 초기 설정 진행(a72d10d)
    • i18n 초기화 파일 생성(i18n.ts)
    • 기본 언어 리소스 구성(ko / en/ zh)
  • Header 내 언어 변경 모달 컴포넌트 구현(a72d10d)
    • 언어 선택 UI 추가
    • i18n.changeLanguage() 호출 시 즉시 반영되도록 상태 연동
    • 모달 열림 / 닫힘 처리 및 클릭 이벤트 버블링 제어

To Reviewer

  • i18next, react-i18next 설치 과정에서 TypeScript 버전 불일치 문제가 발생했습니다.
    • 현재 프로젝트 TypeScript 버전: 4.x.x
    • react-i18next의 peerDependencies 요구사항: TypeScript5 이상
  • 가능한 해결 방안 3가지
    • 프로젝트 TypeScript 버전을 5 이상으로 업그레이드
    • peerDependencies 무시하고 설치(강제 설치)
    • react-i18next 버전을 낮춰 TS4와 호환되는 옛 버전 사용
  • 최종 선택한 문제 해결 방식
    • i18next의 버전을 다운그레이드 했습니다.
  • 이전 머지 충돌은 TypeScript와 i18next 버전 호환 문제로 발생한 이슈였습니다.
    이를 해결하기 위해 i18next 버전을 TS 4.x에 맞게 다운그레이드했습니다.

자세한 내용은 Jira에서 보시면 됩니다! : https://team-dopamine.atlassian.net/browse/HK-173

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 다국어 지원(한국어, 영어, 중국어) 기능 추가
    • 헤더에 언어 선택 버튼 추가 - 클릭 시 언어 선택 모달 표시
    • 앱이 브라우저 언어 설정을 자동으로 감지하여 초기 언어 설정
  • Bug Fixes

    • 로그아웃 처리 시 오류 처리 및 유효성 검사 개선

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 30, 2025

Walkthrough

이 PR은 i18next 기반의 국제화(i18n) 기능을 도입합니다. 패키지 의존성에 i18next, i18next-browser-languagedetector, react-i18next를 추가하고, 한/영/중 세 언어의 번역 파일을 추가했습니다. 헤더 컴포넌트에 언어 선택 UI와 모달을 구현하고, useModal 훅에 toggleModal 함수를 추가했습니다. 기존 콘텐츠 컴포넌트들을 i18n과 연동하여 하드코딩된 텍스트를 useTranslation 훅 기반으로 변경했습니다.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35-45분

추가 검토 필요 영역:

  • 번역 파일의 콘텐츠 정확성 (src/locales/*/common.json)

    • 한/영/중 세 언어의 번역 키가 동일하게 구성되었는지 확인 필요
    • 번역 텍스트의 맥락 및 톤이 일관성 있게 유지되는지 검토 권장
    • 누락된 번역 키가 있는지 확인
  • i18n 설정의 완성도 (src/i18n/index.ts)

    • fallbackLng가 'ko'로 설정되었는데, 이것이 의도한 기본 언어인지 확인
    • LanguageDetector 설정이 브라우저 언어 감지를 올바르게 수행하는지 검증
    • 번역 키가 동적으로 로드되는 경우 추가 설정 필요 여부 검토
  • useModal 훅의 공개 API 변경 (src/hooks/useModal/index.tsx)

    • toggleModal 반환값 추가가 기존 코드 호환성에 영향을 미치는지 확인
    • 훅을 사용하는 다른 컴포넌트에서 예상치 못한 동작이 발생하지 않는지 검증
  • 모달 접근성 및 상호작용

    • 오버레이 클릭 시 모달이 닫히는 동작이 예상대로 작동하는지 확인
    • 언어 선택 후 모달이 정상적으로 종료되는지 검증
    • 키보드 네비게이션 지원 여부 검토 (ESC 키로 모달 종료 등)
  • 국제화 상태 관리

    • 언어 변경 시 전체 애플리케이션의 상태 동기화 확인
    • 새로 고침 후 선택한 언어가 유지되는지 확인 (localStorage 등의 영속성)

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly describes the main changes: i18n initial setup and language-switching modal feature, which aligns perfectly with the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 feature/HK-173-i18n-setup

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

@soooheeee soooheeee requested review from Jsplix and juiuj November 30, 2025 09:26
@soooheeee soooheeee added the enhancement New feature or request label Nov 30, 2025
@soooheeee soooheeee self-assigned this Nov 30, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 30, 2025

Caution

Docstrings generation - FAILED

No docstrings were generated.

Copy link
Copy Markdown

@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: 5

🧹 Nitpick comments (6)
src/locales/zh/common.json (1)

1-5: P4: 중국어 번역 검증을 고려해주세요.

JSON 구조와 키 네이밍은 정확합니다. 다만, 번역의 정확성과 자연스러움을 중국어 원어민에게 검토받는 것을 권장드립니다.

참고:

  • 현재 번역: "HUSK 是一项基于网页的终端服务。用户可以通过终端使用各种功能。"
  • 기술적인 용어와 서비스 설명이 자연스러운지 확인이 필요합니다
src/i18n/index.ts (1)

9-26: Pn4: LanguageDetector와 Lang 타입을 맞추기 위한 supportedLngs 설정 제안

현재 resources에는 'ko' | 'en' | 'zh'만 정의되어 있는데, i18next-browser-languagedetector는 기본 설정에서 en-US, ko-KR 같은 locale 코드를 반환할 수 있습니다. 이 경우 resolvedLanguage 값과 실제 사용 가능한 리소스 키가 달라져서, 모달에서 관리하는 Lang 타입('ko' | 'en' | 'zh')과 미묘하게 어긋날 수 있습니다.

  • 문제점
    • Detector가 en-US를 반환하면, 내부적으로는 en 리소스를 쓰더라도 resolvedLanguageen-US가 되어 UI 레벨에서 언어 하이라이트 상태가 어색해질 수 있습니다.
  • 개선 방안
    • supportedLngs를 명시해서 Detector가 리소스에 정의된 언어 코드만 사용하도록 제한하면, 모달의 Lang 타입과도 자연스럽게 일관성이 맞습니다.
    • (선택) debug는 주석 처리하신 것처럼 NODE_ENV 기반 조건부 사용을 유지하면 좋습니다.

예시:

 i18n
   .use(LanguageDetector)
   .use(initReactI18next)
   .init({
     resources,
     fallbackLng: 'ko',
+    supportedLngs: ['ko', 'en', 'zh'],
     interpolation: {
       escapeValue: false,
     },
-    // debug: process.env.NODE_ENV === 'development',
-    debug: false,
+    debug: process.env.NODE_ENV === 'development',
   });

이 패턴은 i18next 공식 문서의 LanguageDetector 설정 예시(supportedLngs, fallbackLng)와도 일치하는 구조입니다.

src/components/header/index.tsx (1)

32-35: Pn5: onclickFunction 없이 toggleModal 직접 전달로 간결화 가능

onclickFunction은 단순히 toggleModal을 한 번 감싸서 호출만 하고 있어, 중간 래퍼 함수 없이 바로 이벤트 핸들러로 넘겨도 됩니다.

  • 현재:
const { isOpen, toggleModal, closeModal } = useModal();
const onclickFunction = () => {
  toggleModal();
};
// ...
<LanguageButton onClick={onclickFunction} />
  • 제안:
const { isOpen, toggleModal, closeModal } = useModal();
// ...
<LanguageButton onClick={toggleModal} />

이렇게 하면 불필요한 클로저 생성을 줄이고, React 공식 문서의 “이벤트 핸들러에 함수 참조를 직접 전달하는 패턴”과도 더 잘 맞습니다. (물론 현재 코드도 기능적으로는 문제 없습니다.)

src/components/header/modal/index.tsx (2)

15-27: Pn2: Lang 타입과 i18n.resolvedLanguage 값 간 정합성 및 상태 중복 개선 제안

지금 구조에서는 초기 선택 언어 계산 시 약간 헷갈릴 수 있는 부분이 있습니다.

  • 문제점

    1. i18n.resolvedLanguageen-US, ko-KR처럼 region 코드까지 포함된 값을 반환할 수 있습니다. 그런데 이를 그대로 as Lang으로 캐스팅하면, 실제 값은 'en-US'인데 타입만 'en'으로 간주하게 되어, selectedLanguage === 'en' 비교가 항상 false가 되고 활성 하이라이트가 안 보일 수 있습니다.
    2. 언어 상태를 i18n이 이미 관리하고 있는데, 컴포넌트에서 selectedLanguage를 별도 state로 다시 관리하여 소스 오브 트루스가 두 군데로 나뉩니다. changeLanguage 이후 i18n 쪽과 로컬 state가 잠깐이라도 어긋날 여지가 있습니다.
  • 개선 방안 1: 언어 코드 정규화 유틸 추가
    Detector가 반환하는 값을 ko/en/zh로 정규화해서 사용하면 일관성이 좋아집니다.

type Lang = 'ko' | 'en' | 'zh';

const normalizeLang = (lng?: string): Lang => {
  const base = (lng ?? 'ko').split('-')[0]; // en-US -> en
  if (base === 'ko' || base === 'en' || base === 'zh') return base;
  return 'ko';
};

const LanguageModal = ({ onClose }: LanguageModalProps) => {
  const { i18n } = useTranslation();
  const [selectedLanguage, setSelectedLanguage] = useState<Lang>(() => normalizeLang(i18n.resolvedLanguage));

  useEffect(() => {
    setSelectedLanguage(normalizeLang(i18n.resolvedLanguage));
  }, [i18n.resolvedLanguage]);

  const selectLanguage = (lang: Lang) => {
    i18n.changeLanguage(lang);
    onClose();
  };

  // ...
};
  • 개선 방안 2: 로컬 state 제거 (더 단순한 패턴)
    react-i18next 공식 문서에서도 i18n.language/resolvedLanguage를 바로 UI에 사용하는 패턴을 많이 보여줍니다. 하이라이트만 필요하다면 state 없이도 가능합니다.
const LanguageModal = ({ onClose }: LanguageModalProps) => {
  const { i18n } = useTranslation();
  const selectedLanguage = normalizeLang(i18n.resolvedLanguage);

  const selectLanguage = (lang: Lang) => {
    i18n.changeLanguage(lang);
    onClose();
  };

  // selectedLanguage를 그대로 $active 비교에 사용
};
  • 장점
    • Detector가 en-US 등을 반환하는 경우에도 항상 ko/en/zh 중 하나로 매핑되어, 버튼 활성 상태와 실제 언어 설정이 일치합니다.
    • 언어 상태의 소스 오브 트루스를 i18n 한 곳으로 통일해 유지보수가 쉬워집니다.
    • React Hooks 의존성 배열도 i18n.resolvedLanguage만 정확히 바라보게 되어, 공식 문서에서 권장하는 “의존성에 사용한 값을 그대로 넣는 패턴”과 잘 맞습니다.

29-54: Pn4: createPortal(document.body) 사용 시 SSR/테스트 환경 고려 (선택 사항)

현재는 브라우저 환경만 가정하고 document.body에 바로 포탈을 생성하고 있습니다. 앱이 CSR 전용이라면 문제 없지만, 나중에:

  • Next.js나 Remix 등 SSR 환경으로 옮기거나
  • Jest/RTL 테스트에서 document가 완전히 세팅되지 않은 상태로 이 컴포넌트를 렌더링

하는 상황이 생기면 document is not defined 에러가 날 수 있습니다.

선택적으로 아래와 같이 가드 코드를 두어도 좋겠습니다.

if (typeof document === 'undefined') {
  return null;
}

return createPortal(
  // ...
  document.body,
);

이 패턴은 ReactDOM createPortal 공식 문서에서도, 브라우저 객체가 없는 환경에서의 사용 시 주의사항과 같은 맥락으로 자주 언급되는 방식입니다. 지금 당장 필수는 아니지만, 장기적으로 SSR 가능성을 염두에 두고 있다면 참고해 주세요.

src/components/header/modal/index.style.tsx (1)

4-65: Pn5: 모달 스타일 전반은 괜찮고, 오버레이 배경만 향후 UX에 맞춰 조정 가능

구조적으로는 잘 잡혀 있습니다.

  • ModalOverlayModalContainerposition: fixed + z-index 레이어링도 적절하고,
  • 언어 리스트 버튼의 $active 상태 표현도 깔끔합니다.

향후 UX 측면에서 한 가지만 선택적으로 고려해볼 수 있습니다.

  • 현재 ModalOverlaybackground: transparent;라서, 클릭 영역을 막는 용도로만 동작하고 시각적인 “모달이 떴다”는 느낌은 약합니다.
  • Material Design / Fluent UI 등 모달 가이드를 보면 일반적으로 rgba(0,0,0,0.2~0.4) 정도로 배경을 dimming 해서 포커스를 모달로 모아주는 패턴을 많이 사용합니다.

예를 들면:

-export const ModalOverlay = styled.div`
+export const ModalOverlay = styled.div`
   position: fixed;
   inset: 0;
-  background: transparent;
+  background: rgba(0, 0, 0, 0.2);
   z-index: 9998;
 `;

지금도 기능적으로 문제는 없고, 디자인 방향에 따라 선택적으로 적용하시면 될 것 같습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4e88d2 and a72d10d.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • src/assets/Language.svg is excluded by !**/*.svg
📒 Files selected for processing (12)
  • package.json (1 hunks)
  • src/App.tsx (1 hunks)
  • src/components/header/index.style.tsx (2 hunks)
  • src/components/header/index.tsx (2 hunks)
  • src/components/header/modal/index.style.tsx (1 hunks)
  • src/components/header/modal/index.tsx (1 hunks)
  • src/hooks/useModal/index.tsx (1 hunks)
  • src/i18n/index.ts (1 hunks)
  • src/locales/en/common.json (1 hunks)
  • src/locales/ko/common.json (1 hunks)
  • src/locales/zh/common.json (1 hunks)
  • src/pages/main/content/index.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
src/pages/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (README.md)

Store routable page components (homepage, login page, profile page, etc.) in the pages directory as top-level components for each route

Files:

  • src/pages/main/content/index.tsx
src/**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

src/**/*.{ts,tsx}: 1. 코드에서의 오타, 불필요한 로직, 중복 코드, 서비스/도메인 설계, 예외 처리를 확인해주세요. 2. 많은 양의 코드리뷰는 피로감을 줄 수 있으므로 꼭 필요한 부분에만 집중해주고, 나머지는 캡션으로 설명해주세요. 3. Pn룰을 사용합니다. 모든 코멘트에는 Pn:을 붙여주세요. 중요도에 따라 숫자를 반영해주세요. (P1: 꼭 반영해주세요 (Request changes) / P2: 적극적으로 고려해주세요 (Request changes) / P3: 웬만하면 반영해 주세요 (Comment) / P4: 반영해도 좋고 넘어가도 좋습니다 (Approve) / P5: 그냥 사소한 의견입니다 (Approve)) 4. 코드에 대한 리뷰를 남길 때는 해당 라인 범위에 코멘트를 작성해주세요. 5. 리뷰 포인트별로 문제점, 대안, 장단점을 논리적이고 명확하게 제시하고 경우에 따라 단순하지만 효율적인 예시 코드도 추가해주세요. 6. 타입, 컴포넌트, 클래스명은 PascalCase로 작성합니다. 7. 멤버, 함수명은 camelCase로 작성합니다. 8. React + TypeScript 코드의 경우, 불필요한 리렌더링, 훅 의존성 배열, 과도한 any 사용 여부도 함께 확인해주세요.

Files:

  • src/pages/main/content/index.tsx
  • src/components/header/modal/index.tsx
  • src/hooks/useModal/index.tsx
  • src/components/header/index.style.tsx
  • src/components/header/index.tsx
  • src/i18n/index.ts
  • src/components/header/modal/index.style.tsx
  • src/App.tsx
src/components/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (README.md)

Store reusable UI components (buttons, input fields, cards, etc.) in the components directory

Files:

  • src/components/header/modal/index.tsx
  • src/components/header/index.style.tsx
  • src/components/header/index.tsx
  • src/components/header/modal/index.style.tsx
src/hooks/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (README.md)

Store custom React hooks for state management and side effect handling in the hooks directory

Files:

  • src/hooks/useModal/index.tsx
🧬 Code graph analysis (3)
src/pages/main/content/index.tsx (1)
src/pages/main/content/index.style.tsx (1)
  • Description (42-48)
src/components/header/modal/index.tsx (1)
src/components/header/modal/index.style.tsx (5)
  • ModalOverlay (4-9)
  • ModalContainer (11-35)
  • LanguageList (37-41)
  • LanguageListItem (43-45)
  • LanguageOptionButton (47-65)
src/components/header/index.tsx (3)
src/api/context/auth-context/index.tsx (1)
  • AuthContext (12-12)
src/components/header/index.style.tsx (5)
  • HeaderWrapper (5-19)
  • LogoWrapper (82-88)
  • Logo (21-33)
  • LanguageWrapper (90-93)
  • LanguageButton (43-54)
src/components/dashboard/layout/dashboard-header/index.style.tsx (3)
  • HeaderWrapper (5-19)
  • LogoWrapper (21-27)
  • Logo (29-40)
🔇 Additional comments (3)
src/hooks/useModal/index.tsx (1)

8-10: P5: toggleModal 추가가 깔끔합니다!

모달 토글 기능을 추가하신 것이 좋습니다. 기존 openModal, closeModal과 일관된 패턴으로 구현되었고, useCallback과 이전 상태 패턴 (prev) => !prev을 사용하여 최적화도 잘 되어 있습니다.

src/components/header/index.style.tsx (1)

43-54: P5: 언어 선택 UI 스타일링이 깔끔합니다!

LanguageButtonLanguageWrapper의 구현이 잘 되었습니다.

좋은 점:

  • LanguageButton: 브라우저 기본 스타일을 제대로 리셋했습니다 (background, border, outline, padding)
  • LanguageWrapper: relative positioning으로 내부 모달의 absolute positioning을 지원합니다
  • 기존 코드 스타일과 일관성을 유지했습니다

참고:

  • margin-right: 20px로 적절한 간격을 확보했습니다
  • focus outline 제거 시 키보드 접근성을 위한 대체 포커스 표시를 고려해주세요 (모달 오픈 시 포커스 트랩 등)

Also applies to: 90-93

src/App.tsx (1)

20-20: The import and i18n initialization are correctly configured.

The './i18n' side-effect import in App.tsx is the proper pattern. The src/i18n/index.ts file exists and includes all necessary configurations: initReactI18next integration, LanguageDetector setup, proper resource definitions for ko/en/zh languages, fallbackLng set to 'ko', and correct interpolation settings. No changes needed.

@aws-amplify-ap-northeast-2
Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-76.d15ua1f8x2sllg.amplifyapp.com

Copy link
Copy Markdown

@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: 0

🧹 Nitpick comments (10)
src/components/header/index.style.tsx (1)

43-54: P4: 스타일링이 기능적으로 적절하나, 디자인 토큰 활용을 고려해보세요.

LanguageButton의 기본 버튼 리셋 스타일링은 잘 작성되었습니다. 다만 몇 가지 개선 제안을 드립니다:

현재 구현의 장점:

  • 불필요한 기본 스타일 제거가 깔끔합니다
  • cursor: pointer로 상호작용 명확

개선 제안:

export const LanguageButton = styled.button`
  background: none;
  border: none;
  outline: none;
  cursor: pointer;
  padding: 0;
  margin-right: ${({ theme }) => theme.spacing.md || '20px'}; // 테마 시스템 활용
  
  &:focus-visible { // 접근성 개선
    outline: 2px solid ${({ theme }) => theme.colors.focus || '#4A90E2'};
    outline-offset: 2px;
  }
`;

이유:

  1. 테마/디자인 토큰: 하드코딩된 20px 대신 테마 시스템을 활용하면 일관성 유지가 쉽습니다
  2. 접근성: :focus-visible을 사용하면 키보드 사용자에게 포커스 표시를 제공하면서 마우스 클릭 시 outline은 표시하지 않습니다

참고 문서:

현재 구조로도 충분히 동작하므로 선택적으로 반영하시면 됩니다.

src/pages/main/content/index.tsx (1)

35-60: P3: i18n 통합이 잘 되었으나, 메인 타이틀도 국제화를 고려해보세요.

이전 리뷰에서 지적된 하드코딩 텍스트들이 모두 t() 함수로 대체되어 완전한 다국어 지원이 구현되었습니다. 훌륭합니다! 👍

확인된 변경사항:

  • ✅ Line 35: t('home.description')
  • ✅ Line 39: t('home.feature_button')
  • ✅ Line 43: t('home.start_button')
  • ✅ Line 52-53: KeyChain 섹션 국제화
  • ✅ Line 59-60: SSH 섹션 국제화

추가 고려사항:
Line 33의 "HUSK: Help Use Shell Kindly" 타이틀이 여전히 하드코딩되어 있습니다.

<Title>HUSK: Help Use Shell Kindly</Title>

질문:

  1. 이것이 브랜드 네임으로 모든 언어에서 영문 유지가 의도된 것인가요?
  2. 아니면 각 언어별로 번역이 필요한가요?

만약 번역이 필요하다면:

// locale 파일에 추가
{
  "home": {
    "title": "HUSK: Help Use Shell Kindly", // en
    "title": "HUSK: 쉽고 친절한 쉘 사용", // ko
    "title": "HUSK: 帮助友好使用Shell", // zh
    ...
  }
}

// 컴포넌트에서 사용
<Title>{t('home.title')}</Title>

만약 영문 유지가 의도라면:
주석으로 명시하면 향후 유지보수 시 혼란을 방지할 수 있습니다:

{/* Brand name - intentionally kept in English across all languages */}
<Title>HUSK: Help Use Shell Kindly</Title>

현재 구현도 충분히 훌륭하므로, 이는 제품 기획에 따라 결정하시면 됩니다.

src/i18n/index.ts (1)

15-26: P4: i18n 초기화 설정이 견고하나, LanguageDetector 옵션 명시를 고려해보세요.

i18n 초기화 설정이 전반적으로 잘 되어 있습니다.

잘된 점:

  1. LanguageDetector 사용: 사용자 브라우저 언어 자동 감지
  2. fallbackLng: 'ko': 기본 언어 지정
  3. interpolation.escapeValue: false: React는 자체적으로 XSS 방어하므로 올바른 설정
  4. debug: false: 프로덕션 성능 고려

개선 제안 (선택사항):
LanguageDetector의 감지 순서를 명시적으로 지정하면 더 예측 가능합니다:

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    fallbackLng: 'ko',
    detection: {
      // 감지 순서 명시: localStorage → querystring → navigator
      order: ['localStorage', 'querystring', 'navigator'],
      caches: ['localStorage'], // 사용자 선택 언어 저장
      lookupQuerystring: 'lang', // ?lang=en 같은 쿼리 파라미터
    },
    interpolation: {
      escapeValue: false,
    },
    debug: false,
  });

이유:

  • 사용자가 언어를 변경하면 localStorage에 저장되어 다음 방문 시 유지
  • URL 쿼리로 언어 테스트 가능 (예: ?lang=en)
  • 명시적 순서로 동작 예측성 향상

참고 문서:

현재 설정으로도 충분히 동작하므로 선택적으로 반영하세요.

src/components/header/modal/index.tsx (2)

15-27: P3: selectedLanguage 로컬 상태가 중복됩니다.

i18n.resolvedLanguage 또는 i18n.language를 직접 사용하면 로컬 상태를 제거하고 불필요한 리렌더링을 방지할 수 있습니다.

현재 구현에서는:

  1. selectedLanguage 상태 관리
  2. useEffecti18n.resolvedLanguage 동기화
  3. selectLanguage에서 다시 setSelectedLanguage 호출

이 모든 것이 i18n.language를 직접 참조하면 불필요해집니다.

 const LanguageModal = ({ onClose }: LanguageModalProps) => {
   const { i18n } = useTranslation();
-  const [selectedLanguage, setSelectedLanguage] = useState<Lang>('ko');
-
-  useEffect(() => {
-    if (i18n.resolvedLanguage) {
-      setSelectedLanguage(i18n.resolvedLanguage as Lang);
-    }
-  }, [i18n.resolvedLanguage]);
+  const currentLanguage = (i18n.resolvedLanguage || 'ko') as Lang;

   const selectLanguage = (lang: Lang) => {
     i18n.changeLanguage(lang);
-    setSelectedLanguage(lang);
     onClose();
   };

이후 JSX에서 selectedLanguage 대신 currentLanguage를 사용하세요.

참고: React 공식 문서 - State를 최소화하세요


17-21: P4: 타입 캐스팅 안전성 검토가 필요합니다.

i18n.resolvedLanguage as Lang 캐스팅은 브라우저 언어 감지 등으로 예상치 못한 언어 코드가 반환될 경우 런타임에 문제가 될 수 있습니다.

+const isValidLang = (lang: string | undefined): lang is Lang => {
+  return lang === 'ko' || lang === 'en' || lang === 'zh';
+};
+
 useEffect(() => {
-  if (i18n.resolvedLanguage) {
-    setSelectedLanguage(i18n.resolvedLanguage as Lang);
+  if (isValidLang(i18n.resolvedLanguage)) {
+    setSelectedLanguage(i18n.resolvedLanguage);
   }
 }, [i18n.resolvedLanguage]);

또는 앞선 리팩터링 제안을 적용할 경우:

const currentLanguage: Lang = isValidLang(i18n.resolvedLanguage) 
  ? i18n.resolvedLanguage 
  : 'ko';

참고: TypeScript Handbook - Type Guards

src/components/header/index.tsx (2)

18-18: P3: useContext 비-널 단언(!)은 런타임 에러 위험이 있습니다.

AuthContext가 Provider 외부에서 사용되면 undefined가 되어 앱이 크래시됩니다. 방어적인 처리를 권장합니다.

-const { logout, isLoggedIn, accessToken, refreshToken } = useContext(AuthContext)!;
+const authContext = useContext(AuthContext);
+
+if (!authContext) {
+  throw new Error('Header must be used within AuthProvider');
+}
+
+const { logout, isLoggedIn, accessToken, refreshToken } = authContext;

또는 커스텀 훅으로 추출:

// useAuth.ts
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

참고: React Context 공식 문서


33-35: P5: onclickFunction 래퍼 함수가 불필요합니다.

단순히 toggleModal()을 호출하는 래퍼 함수는 직접 참조로 대체할 수 있습니다.

 const { isOpen, toggleModal, closeModal } = useModal();
-const onclickFunction = () => {
-  toggleModal();
-};
 
 // JSX에서:
-<LanguageButton onClick={onclickFunction} aria-label="언어 변경">
+<LanguageButton onClick={toggleModal} aria-label="언어 변경">

함수명도 onclickFunction보다 명확한 이름(예: handleLanguageButtonClick)이 권장되지만, 직접 toggleModal을 사용하면 더 간결합니다.

src/components/header/modal/index.style.tsx (3)

1-1: P5: 파일명 주석이 실제 파일명과 다릅니다.

주석에 index.style.ts로 되어 있지만 실제 파일명은 index.style.tsx입니다.

-// index.style.ts
+// index.style.tsx

19-21: P4: 하드코딩된 위치 값은 유지보수성을 저해할 수 있습니다.

top: 70px, right: 54px는 현재 헤더 높이(80px)와 레이아웃에 의존합니다. 헤더 스타일이 변경되면 모달 위치가 어긋날 수 있습니다.

가능한 대안:

  1. CSS 변수를 사용하여 헤더 높이를 공유
  2. 모달을 LanguageWrapper 내부에 상대 위치로 배치
/* 예시: CSS 변수 활용 */
:root {
  --header-height: 80px;
}

/* ModalContainer에서 */
top: calc(var(--header-height) - 10px);

현재 구현도 동작하므로, 향후 리팩터링 시 고려해 주세요.


47-65: P3: 키보드 접근성을 위한 포커스 스타일이 누락되었습니다.

:hover 스타일은 있지만 :focus 스타일이 없어 키보드 사용자가 현재 포커스된 옵션을 시각적으로 확인하기 어렵습니다.

 export const LanguageOptionButton = styled.button<{ $active: boolean }>`
   /* ... existing styles ... */

   &:hover {
     border-radius: 5px;
     background-color: #f3f4f6;
   }
+
+  &:focus {
+    outline: 2px solid #3b82f6;
+    outline-offset: -2px;
+    border-radius: 5px;
+  }
+
+  &:focus:not(:focus-visible) {
+    outline: none;
+  }
 `;

:focus-visible을 사용하면 마우스 클릭 시에는 outline이 보이지 않고, 키보드 탐색 시에만 표시됩니다.

참고: WCAG 2.4.7 Focus Visible

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4e88d2 and 1686751.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • src/assets/Language.svg is excluded by !**/*.svg
📒 Files selected for processing (12)
  • package.json (1 hunks)
  • src/App.tsx (1 hunks)
  • src/components/header/index.style.tsx (2 hunks)
  • src/components/header/index.tsx (1 hunks)
  • src/components/header/modal/index.style.tsx (1 hunks)
  • src/components/header/modal/index.tsx (1 hunks)
  • src/hooks/useModal/index.tsx (1 hunks)
  • src/i18n/index.ts (1 hunks)
  • src/locales/en/common.json (1 hunks)
  • src/locales/ko/common.json (1 hunks)
  • src/locales/zh/common.json (1 hunks)
  • src/pages/main/content/index.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

src/**/*.{ts,tsx}: 1. 코드에서의 오타, 불필요한 로직, 중복 코드, 서비스/도메인 설계, 예외 처리를 확인해주세요. 2. 많은 양의 코드리뷰는 피로감을 줄 수 있으므로 꼭 필요한 부분에만 집중해주고, 나머지는 캡션으로 설명해주세요. 3. Pn룰을 사용합니다. 모든 코멘트에는 Pn:을 붙여주세요. 중요도에 따라 숫자를 반영해주세요. (P1: 꼭 반영해주세요 (Request changes) / P2: 적극적으로 고려해주세요 (Request changes) / P3: 웬만하면 반영해 주세요 (Comment) / P4: 반영해도 좋고 넘어가도 좋습니다 (Approve) / P5: 그냥 사소한 의견입니다 (Approve)) 4. 코드에 대한 리뷰를 남길 때는 해당 라인 범위에 코멘트를 작성해주세요. 5. 리뷰 포인트별로 문제점, 대안, 장단점을 논리적이고 명확하게 제시하고 경우에 따라 단순하지만 효율적인 예시 코드도 추가해주세요. 6. 타입, 컴포넌트, 클래스명은 PascalCase로 작성합니다. 7. 멤버, 함수명은 camelCase로 작성합니다. 8. React + TypeScript 코드의 경우, 불필요한 리렌더링, 훅 의존성 배열, 과도한 any 사용 여부도 함께 확인해주세요.

Files:

  • src/i18n/index.ts
  • src/App.tsx
  • src/components/header/index.style.tsx
  • src/components/header/modal/index.tsx
  • src/pages/main/content/index.tsx
  • src/components/header/index.tsx
  • src/components/header/modal/index.style.tsx
  • src/hooks/useModal/index.tsx
src/components/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (README.md)

Store reusable UI components (buttons, input fields, cards, etc.) in the components directory

Files:

  • src/components/header/index.style.tsx
  • src/components/header/modal/index.tsx
  • src/components/header/index.tsx
  • src/components/header/modal/index.style.tsx
src/pages/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (README.md)

Store routable page components (homepage, login page, profile page, etc.) in the pages directory as top-level components for each route

Files:

  • src/pages/main/content/index.tsx
src/hooks/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (README.md)

Store custom React hooks for state management and side effect handling in the hooks directory

Files:

  • src/hooks/useModal/index.tsx
🧬 Code graph analysis (3)
src/components/header/modal/index.tsx (1)
src/components/header/modal/index.style.tsx (5)
  • ModalOverlay (4-9)
  • ModalContainer (11-35)
  • LanguageList (37-41)
  • LanguageListItem (43-45)
  • LanguageOptionButton (47-65)
src/pages/main/content/index.tsx (2)
src/api/context/use-auth/index.tsx (1)
  • useAuth (4-10)
src/pages/main/content/index.style.tsx (12)
  • Banner (19-32)
  • Title (34-40)
  • Description (42-48)
  • ButtonContainer (50-53)
  • FunctionButton (55-59)
  • StartButton (61-70)
  • MainContent (72-79)
  • ContentContainer (89-97)
  • ContentImage (81-87)
  • TextContainer (99-103)
  • MainTitle (105-109)
  • MainDescription (111-116)
src/components/header/index.tsx (4)
src/api/context/auth-context/index.tsx (1)
  • AuthContext (12-12)
src/components/dashboard/modals/register-modal/index.tsx (1)
  • useModal (61-61)
src/components/dashboard/modals/update-password/index.tsx (1)
  • useModal (104-104)
src/components/header/index.style.tsx (6)
  • HeaderWrapper (5-19)
  • LogoWrapper (82-88)
  • Logo (21-33)
  • LanguageWrapper (90-93)
  • LanguageButton (43-54)
  • LogoutButton (56-66)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: AWS Amplify Console Web Preview
🔇 Additional comments (12)
src/locales/ko/common.json (1)

1-18: P5: 번역 키 구조가 완성되었습니다! 👍

이전 리뷰에서 요청된 추가 번역 키들이 모두 반영되었습니다. home.feature_button, home.start_button, home.keychain.*, home.ssh.* 등이 적절하게 추가되어 src/pages/main/content/index.tsx에서 사용하는 모든 키를 커버하고 있습니다.

작은 제안:
향후 번역 파일이 커질 것을 대비해 다음을 고려해보세요:

  • 각 섹션별로 파일을 분리 (예: home.json, features.json)
  • 또는 네임스페이스 활용 (i18next의 namespace 기능)

하지만 현재 규모에서는 단일 파일 구조가 충분히 관리 가능합니다.

src/locales/en/common.json (1)

1-18: P5: 영문 번역이 자연스럽고 완성도가 높습니다.

한국어 locale과 동일한 키 구조를 유지하고 있어 일관성이 좋습니다. 영문 표현도 자연스럽고 전문적입니다.

참고:

  • Line 3의 "web-based terminal service" 표현이 명확합니다.
  • Line 10의 "View your key list at a glance" 같은 표현이 사용자 친화적입니다.

세 언어(ko, en, zh) 간 키 일치도를 유지하는 것이 중요한데, 현재 잘 지켜지고 있습니다.

src/components/header/index.style.tsx (1)

90-93: P5: LanguageWrapper의 포지셔닝이 적절합니다.

position: relativedisplay: inline-block 조합은 언어 선택 모달을 absolute 포지셔닝할 수 있는 기준점을 제공합니다. 일반적인 드롭다운 패턴에 부합하는 구조입니다.

src/App.tsx (1)

20-20: P5: i18n 초기화 방식이 적절합니다.

import './i18n'; 사이드 이펙트 임포트는 i18n 초기화에 권장되는 패턴입니다.

이 방식의 장점:

  1. 명시적 초기화: 앱 진입점에서 i18n이 초기화됨을 명확히 표현
  2. 실행 순서 보장: 컴포넌트 렌더링 전에 i18n 설정 완료
  3. Tree shaking 방지: 번들러가 i18n 코드를 제거하지 않음

공식 문서 참고:

이런 초기화 패턴은 i18next 공식 문서에서도 권장하는 방식입니다.

src/locales/zh/common.json (1)

1-18: P5: 중국어 번역이 완성되어 세 언어 지원이 일관되게 구현되었습니다.

한국어, 영어와 동일한 키 구조를 유지하여 locale 간 일관성이 확보되었습니다.

확인된 장점:

  • 모든 locale 파일(ko, en, zh)이 동일한 키 세트를 가짐
  • 중국어 간체 번역이 자연스러움 (Line 3: "基于网页的终端服务")
  • 기술 용어(KeyChain, SSH)는 영문 유지로 일관성 확보

유지보수 팁:
향후 새로운 번역 키를 추가할 때는 세 파일을 동시에 업데이트하여 누락을 방지하세요. TypeScript의 타입 안전성을 활용하면 이를 강제할 수 있습니다:

// i18n/index.ts에서 타입 정의 활용
type TranslationKeys = typeof ko;
// 모든 locale이 동일한 키를 갖도록 타입 체크
src/pages/main/content/index.tsx (1)

6-6: P5: useTranslation 훅 사용이 적절합니다.

react-i18nextuseTranslation 훅을 올바르게 import하고 사용하고 있습니다.

이 패턴의 장점:

  • 함수형 컴포넌트에서 권장되는 i18n 사용법
  • 타입 안전성 확보 가능 (향후 개선 시)
  • 언어 변경 시 자동 리렌더링

Also applies to: 12-12

src/i18n/index.ts (2)

1-13: P5: i18n 리소스 설정이 타입 안전하게 구성되었습니다.

as const 어설션을 사용하여 타입 안전성을 확보한 점이 좋습니다.

현재 구조의 장점:

const resources = {
  ko: { translation: ko },
  en: { translation: en },
  zh: { translation: zh },
} as const;
  • as const: 리터럴 타입 추론으로 타입 안전성 향상
  • 일관된 구조: 모든 locale이 'translation' 네임스페이스 사용

향후 확장 시 고려사항:
프로젝트가 커지면 네임스페이스 분리를 고려해보세요:

const resources = {
  ko: { 
    common: ko,
    dashboard: koDashboard,
    auth: koAuth 
  },
  en: { 
    common: en,
    dashboard: enDashboard,
    auth: enAuth 
  },
} as const;

하지만 현재 규모에서는 단일 네임스페이스로 충분합니다.


24-25: P5: 디버그 설정 관리가 좋습니다.

주석 처리된 개발 환경 디버그 옵션과 명시적인 debug: false 설정이 좋습니다.

작은 팁:
개발 시 번역 누락을 빠르게 발견하려면 주석을 해제하면 됩니다:

debug: process.env.NODE_ENV === 'development',

이렇게 하면 개발 환경에서만 디버그 로그가 출력되어 번역 키 오타나 누락을 쉽게 찾을 수 있습니다.

package.json (1)

22-23: Current i18n dependency versions are compatible.

The version combination in package.json is compatible:

  • i18next@^23.16.8 satisfies react-i18next@^13.5.0 peer dependency requirement (which specifies i18next >= 23.2.3)
  • No TypeScript peer dependency constraints are declared by these packages, so typescript@^4.9.5 poses no issues
  • i18next-browser-languagedetector@^8.2.0 has no conflicting peer dependencies

No changes needed for version compatibility.

Likely an incorrect or invalid review comment.

src/hooks/useModal/index.tsx (1)

8-10: P5: LGTM! toggleModal 구현이 올바릅니다.

toggleModal 함수가 함수형 업데이트 패턴 (prev) => !prev를 사용하여 이전 상태를 기반으로 토글하고 있습니다. useCallback의 의존성 배열도 비어있어 적절합니다.

src/components/header/modal/index.tsx (1)

29-54: P5: Portal 및 이벤트 처리 구현이 적절합니다.

  • createPortal을 사용하여 모달을 document.body에 렌더링
  • ModalOverlay 클릭 시 onClose 호출
  • ModalContainer에서 e.stopPropagation()으로 버블링 방지

모달 패턴이 올바르게 구현되어 있습니다.

src/components/header/index.tsx (1)

44-49: P5: 이전 리뷰 피드백이 잘 반영되었습니다.

  • onClick이 버튼 요소에 직접 연결됨
  • aria-label="언어 변경"으로 접근성 개선
  • SVG에서 인라인 cursor: 'point' 오타 제거됨

키보드 및 스크린 리더 사용자 경험이 개선되었습니다. 👍

Copy link
Copy Markdown
Member

@juiuj juiuj left a comment

Choose a reason for hiding this comment

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

수정사항 확인했습니당 참고해서 작업하겠습니다.!

@soooheeee soooheeee merged commit c48beb3 into main Dec 3, 2025
3 checks passed
@soooheeee soooheeee changed the title feat: i18n 초기 설정 및 언어 변경 모달 기능 추가 feat(HK-173): i18n 초기 설정 및 언어 변경 모달 기능 추가 Jan 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants