PROJECT/[ 장기게임 ]

state와 context

Lim임 2025. 12. 3. 16:54

상태 관리 아키텍처 상세 문서

목차

  1. 개요
  2. useGameState Hook 상세 분석
  3. useModalManager Hook 상세 분석
  4. GameContext 상세 분석
  5. 설계 의도 및 패턴

개요

이 프로젝트는 Context API + Custom Hooks 패턴을 사용하여 전역 상태를 관리합니다.

아키텍처 구조

GameProvider (Context)
    ├─ useGameState (게임 로직 상태)
    └─ useModalManager (UI 상태)

핵심 원칙:

  • 관심사의 분리(Separation of Concerns): 게임 로직과 UI 상태를 별도의 훅으로 분리
  • 단일 진입점(Single Source of Truth): GameContext를 통해 모든 상태에 접근
  • Props Drilling 방지: 깊은 컴포넌트 트리에서도 직접 상태 접근 가능

useGameState Hook 상세 분석

파일 위치: src/hooks/useGameState.ts

전체 구조 개요

이 훅은 게임의 핵심 비즈니스 로직을 담당합니다. 게임 진행 상태, 플레이어 정보, 게임 종료 조건 등을 관리합니다.


1. Import 구문

import { useState } from 'react';
import type { Team } from '../types/types';
import { createGame } from '../api/gameApi';

라인별 설명:

  • Line 1: React의 useState 훅을 가져옵니다. 이 훅은 컴포넌트의 상태를 관리하는 기본 도구입니다.
  • Line 2: Team 타입을 가져옵니다. type 키워드를 사용하여 타입만 import하므로, 런타임에는 포함되지 않습니다 (TypeScript의 타입 전용 import).
    • Team'cho' | 'han' 같은 유니온 타입으로, 장기의 두 진영을 나타냅니다.
  • Line 3: 백엔드 API와 통신하는 createGame 함수를 가져옵니다. 새 게임을 생성할 때 사용됩니다.

2. 상태 선언부

2-1. turnInfo (턴 정보)

const [turnInfo, setTurnInfo] = useState<{ count: number; turn: Team }>({
  count: 1,
  turn: 'cho',
});

목적: 현재 게임의 턴 정보를 추적합니다.

상세 설명:

  • 타입 정의: { count: number; turn: Team }
    • count: 현재 몇 번째 턴인지 (1부터 시작)
    • turn: 현재 누구의 차례인지 ('cho' 또는 'han')
  • 초기값: { count: 1, turn: 'cho' }
    • 장기는 항상 초(楚)가 먼저 시작하므로 turn: 'cho'
    • 첫 번째 턴이므로 count: 1

사용 시나리오:

  • Board 컴포넌트에서 현재 턴에 해당하는 기물만 선택 가능하도록 제한
  • InfoBox에서 "1턴 - 초 차례" 같은 정보 표시

2-2. isReplay (리플레이 모드 여부)

const [isReplay, setIsReplay] = useState(false);

목적: 현재 게임이 새 게임인지, 과거 게임을 다시 보는 리플레이인지 구분합니다.

상세 설명:

  • 타입: boolean
  • 초기값: false (기본적으로 새 게임 모드)
  • true일 때: 과거 게임 기록을 재생 중 (수정 불가, 읽기 전용)
  • false일 때: 실시간으로 진행 중인 게임 (수정 가능)

사용 시나리오:

  • 리플레이 모드에서는 기물 이동을 막고, 재생/일시정지 버튼만 활성화
  • 새 게임 모드에서는 기물 이동 가능

2-3. isEnabled (게임 활성화 여부)

const [isEnabled, setIsEnabled] = useState(false);

목적: 게임이 시작되었는지 여부를 나타냅니다.

상세 설명:

  • 타입: boolean
  • 초기값: false (앱 시작 시 게임이 시작되지 않은 상태)
  • true일 때: 플레이어 이름이 입력되고 게임이 시작됨 → Board 표시
  • false일 때: 아직 게임 시작 전 → 플레이어 입력 모달 표시

사용 시나리오:

  • GamePage에서 !isEnabled && <GameStartModals />로 조건부 렌더링
  • 게임 시작 전에는 보드를 숨기고 모달만 표시

2-4. player1Name, player2Name (플레이어 이름)

const [player1Name, setPlayer1Name] = useState('');
const [player2Name, setPlayer2Name] = useState('');

목적: 두 플레이어의 이름을 저장합니다.

상세 설명:

  • 타입: string
  • 초기값: '' (빈 문자열)
  • 사용처:
    • PlayerInputContent 모달에서 입력받음
    • InfoBox에서 "플레이어1 vs 플레이어2" 형태로 표시
    • 백엔드에 게임 생성 요청 시 전달

왜 두 개의 state로 분리했나?

  • 각 입력창이 독립적으로 제어되어야 하므로 (한 명의 이름을 바꿔도 다른 사람 이름은 유지)
  • 나중에 각 플레이어별 통계나 정보를 추가할 때 확장성이 좋음

2-5. gameId (게임 ID)

const [gameId, setGameId] = useState<number | null>(null);

목적: 백엔드 데이터베이스에 저장된 게임의 고유 ID를 저장합니다.

상세 설명:

  • 타입: number | null
    • number: 게임이 생성되면 백엔드에서 받은 ID
    • null: 아직 게임이 생성되지 않음
  • 초기값: null
  • 사용처:
    • 게임 기록을 저장할 때 어떤 게임인지 식별
    • 리플레이 모드에서 특정 게임의 기록을 불러올 때 사용

왜 null을 허용하나?

  • 게임 시작 전에는 ID가 없으므로 null이 자연스러운 초기 상태
  • undefined보다 null을 사용하는 이유: 명시적으로 "값이 없음"을 나타내기 위함

2-6. gameOver (게임 종료 여부)

const [gameOver, setGameOver] = useState(false);

목적: 게임이 끝났는지 여부를 나타냅니다.

상세 설명:

  • 타입: boolean
  • 초기값: false
  • true일 때: 승부가 결정됨 → GameWinnerModal 표시
  • false일 때: 게임 진행 중

사용 시나리오:

  • Board에서 왕이 잡히면 setGameOver(true) 호출
  • GamePage에서 {gameOver && <GameWinnerModal />}로 승리 모달 표시

2-7. winner (승자)

const [winner, setWinner] = useState<'cho' | 'han' | null>(null);

목적: 게임의 승자를 저장합니다.

상세 설명:

  • 타입: 'cho' | 'han' | null
    • 'cho': 초(楚) 진영 승리
    • 'han': 한(漢) 진영 승리
    • null: 아직 승자가 결정되지 않음
  • 초기값: null

사용 시나리오:

  • WinnerContent에서 "초 진영 승리!" 같은 메시지 표시
  • 승자에 따라 다른 색상이나 애니메이션 적용 가능

왜 gameOver와 winner를 분리했나?

  • gameOver는 "게임이 끝났다"는 사실만 표현
  • winner는 "누가 이겼는지"라는 추가 정보 제공
  • 나중에 무승부 기능을 추가하면 gameOver: true, winner: null 같은 상태 가능

3. 함수 정의부

3-1. handleNewGame (게임 초기화)

const handleNewGame = () => {
  setTurnInfo({ count: 1, turn: 'cho' });
  setIsEnabled(false);
  setGameId(null);
  setPlayer1Name('');
  setPlayer2Name('');
  setIsReplay(false);
  setGameOver(false);
  setWinner(null);
};

목적: 모든 게임 상태를 초기값으로 되돌립니다.

라인별 설명:

  • Line 19: 턴을 1턴, 초 차례로 초기화
  • Line 20: 게임을 비활성화 (다시 플레이어 입력 모달 표시)
  • Line 21: 게임 ID 제거 (새 게임이므로 이전 ID 무효화)
  • Line 22-23: 플레이어 이름 초기화 (새로 입력받기 위해)
  • Line 24: 리플레이 모드 해제
  • Line 25: 게임 종료 상태 해제
  • Line 26: 승자 정보 제거

사용 시나리오:

  • "새 게임" 버튼 클릭 시
  • 게임 종료 후 다시 시작할 때

왜 이 순서로 초기화하나?

  • 순서는 크게 중요하지 않지만, 논리적 흐름을 따름:
    1. 게임 진행 상태 초기화 (턴)
    2. 게임 활성화 해제
    3. 게임 메타데이터 초기화 (ID, 플레이어)
    4. 모드 및 결과 초기화

3-2. startNewGame (새 게임 시작)

const startNewGame = async () => {
  if (player1Name.trim() && player2Name.trim()) {
    setIsEnabled(true);
    const game = await createGame(player1Name, player2Name);
    setGameId(game.id);
  }
};

목적: 플레이어 이름을 검증하고, 백엔드에 게임을 생성한 후 게임을 시작합니다.

라인별 설명:

  • Line 29: async 함수로 선언 (백엔드 API 호출이 비동기이므로)
  • Line 30: 유효성 검사
    • player1Name.trim(): 앞뒤 공백 제거 후 빈 문자열이 아닌지 확인
    • &&: 두 이름이 모두 입력되었을 때만 진행
    • 왜 trim()을 사용하나?: 사용자가 실수로 공백만 입력하는 것을 방지
  • Line 31: 게임 활성화 (보드 표시 시작)
  • Line 32: 백엔드 API 호출
    • await: API 응답을 기다림 (비동기 처리)
    • createGame(player1Name, player2Name): 플레이어 정보를 서버에 전송
    • 서버는 새 게임을 DB에 저장하고 { id: number } 형태로 응답
  • Line 33: 받은 게임 ID를 상태에 저장

사용 시나리오:

  • PlayerInputContent에서 "게임 시작" 버튼 클릭 시
  • Enter 키로 입력 완료 시

왜 async/await를 사용하나?

  • 백엔드 API 호출은 시간이 걸리므로 비동기 처리 필요
  • await를 사용하면 응답을 받은 후에 다음 코드 실행 (순차적 흐름 보장)

에러 처리가 없는 이유:

  • 현재는 간단한 구현이지만, 실제 프로덕션에서는 try-catch로 에러 처리 필요
  • 예: 네트워크 오류 시 사용자에게 알림 표시

3-3. selectGame (리플레이 게임 선택)

const selectGame = (game: { id: number; player1: string; player2: string }) => {
  setGameId(game.id);
  setPlayer1Name(game.player1);
  setPlayer2Name(game.player2);
  setIsReplay(true);
  setIsEnabled(true);
  setTurnInfo({ count: 1, turn: 'cho' });
};

목적: 과거 게임 기록을 선택하여 리플레이 모드로 진입합니다.

라인별 설명:

  • Line 37: 매개변수로 게임 객체를 받음
    • id: 게임의 고유 ID
    • player1, player2: 해당 게임의 플레이어 이름
  • Line 38: 선택한 게임의 ID 저장
  • Line 39-40: 플레이어 이름 복원 (과거 게임의 플레이어 정보)
  • Line 41: 리플레이 모드 활성화 (수정 불가 모드)
  • Line 42: 게임 활성화 (보드 표시)
  • Line 43: 턴을 1턴으로 초기화 (리플레이는 처음부터 시작)

사용 시나리오:

  • GameSelectContent에서 게임 목록 중 하나를 클릭했을 때

왜 turnInfo를 초기화하나?

  • 리플레이는 항상 1턴부터 재생되므로
  • 사용자가 중간부터 보고 싶다면 재생 컨트롤로 조작

4. Return 구문

return {
  turnInfo,
  setTurnInfo,
  isReplay,
  setIsReplay,
  isEnabled,
  setIsEnabled,
  player1Name,
  setPlayer1Name,
  player2Name,
  setPlayer2Name,
  gameId,
  setGameId,
  gameOver,
  setGameOver,
  winner,
  setWinner,
  handleNewGame,
  startNewGame,
  selectGame,
  isDisabled: !player1Name.trim() || !player2Name.trim(),
};

목적: 훅의 모든 상태와 함수를 외부에 노출합니다.

구조 분석:

  1. 상태값 + Setter 쌍: 각 상태마다 읽기(turnInfo)와 쓰기(setTurnInfo) 모두 제공

    • 왜 Setter도 노출하나?: 컴포넌트에서 직접 상태를 변경할 수 있도록 (유연성)
    • 예: Board에서 턴이 바뀌면 setTurnInfo({ count: turnInfo.count + 1, turn: ... })
  2. 함수들: 복잡한 로직을 캡슐화한 헬퍼 함수들

    • handleNewGame, startNewGame, selectGame
    • 왜 함수로 분리했나?: 여러 상태를 동시에 변경하는 로직을 재사용 가능하게
  3. 계산된 값 (Computed Value):

    isDisabled: !player1Name.trim() || !player2Name.trim()
    • 목적: 버튼 비활성화 여부를 실시간으로 계산
    • 왜 별도 state가 아닌가?: 이미 player1Name, player2Name에서 파생되는 값이므로 중복 저장 불필요
    • 장점: 이름이 바뀔 때마다 자동으로 재계산됨 (동기화 문제 없음)

useModalManager Hook 상세 분석

파일 위치: src/hooks/useModalManager.ts

전체 구조 개요

이 훅은 UI 상태 관리를 담당합니다. 특히 게임 시작 시 표시되는 두 모달(플레이어 입력, 게임 선택) 간의 전환을 관리합니다.


1. activeModal (현재 활성 모달)

const [activeModal, setActiveModal] = useState<'player' | 'game'>('player');

목적: 현재 어떤 모달이 표시되고 있는지 추적합니다.

상세 설명:

  • 타입: 'player' | 'game' (리터럴 유니온 타입)
    • 'player': 플레이어 이름 입력 모달
    • 'game': 게임 선택 (리플레이) 모달
  • 초기값: 'player' (앱 시작 시 플레이어 입력부터)

사용 시나리오:

  • GameStartModals에서 activeModal === 'player'일 때 PlayerInputContent 표시
  • activeModal === 'game'일 때 GameSelectContent 표시

2. toggleModal (모달 전환)

const toggleModal = () => {
  setActiveModal((prev) => (prev === 'player' ? 'game' : 'player'));
};

목적: 두 모달 사이를 토글합니다.

상세 설명:

  • 함수형 업데이트 사용: setActiveModal((prev) => ...)
    • 왜 함수형 업데이트인가?: 이전 상태를 기반으로 새 상태를 계산하므로 안전
    • 일반 업데이트(setActiveModal('game'))와의 차이: 최신 상태를 보장
  • 삼항 연산자: prev === 'player' ? 'game' : 'player'
    • 현재 'player''game'으로, 'game'이면 'player'로 전환

사용 시나리오:

  • "리플레이" 버튼 클릭 시 (플레이어 입력 → 게임 선택)
  • "새 게임" 버튼 클릭 시 (게임 선택 → 플레이어 입력)

3. modalVariants (애니메이션 설정)

const modalVariants = {
  enterRight: { x: '100%', opacity: 0 },
  center: { x: 0, opacity: 1 },
  exitLeft: { x: '-100%', opacity: 0 },
};

목적: Framer Motion 라이브러리를 위한 애니메이션 변형(variants)을 정의합니다.

상세 설명:

  • enterRight: 오른쪽에서 들어올 때
    • x: '100%': 화면 오른쪽 밖에 위치 (100% 이동)
    • opacity: 0: 투명 (보이지 않음)
  • center: 중앙에 정착했을 때
    • x: 0: 원래 위치
    • opacity: 1: 완전히 보임
  • exitLeft: 왼쪽으로 나갈 때
    • x: '-100%': 화면 왼쪽 밖으로 이동
    • opacity: 0: 투명

사용 시나리오:

  • GameStartModals에서 모달 전환 시 슬라이드 애니메이션 적용
  • <motion.div variants={modalVariants} initial="enterRight" animate="center" exit="exitLeft">

왜 훅 안에 정의했나?

  • 애니메이션 설정도 모달 관리의 일부로 간주
  • 나중에 애니메이션 속도나 방향을 동적으로 변경할 수 있음

GameContext 상세 분석

파일 위치: src/contexts/GameContext.tsx

전체 구조 개요

GameContext두 개의 훅을 결합하여 단일 진입점을 제공합니다. 이를 통해 컴포넌트는 게임 상태와 모달 상태를 한 곳에서 가져올 수 있습니다.


1. Import 구문

import { createContext, useContext } from 'react';
import type { ReactNode } from 'react';
import { useGameState } from '../hooks/useGameState';
import { useModalManager } from '../hooks/useModalManager';

라인별 설명:

  • Line 1: React Context API의 핵심 함수들
    • createContext: Context 객체 생성
    • useContext: Context 값을 읽는 훅
  • Line 2: ReactNode 타입 (children prop의 타입)
  • Line 3-4: 앞서 정의한 두 커스텀 훅

2. 타입 정의

type GameContextType = ReturnType<typeof useGameState> &
  ReturnType<typeof useModalManager>;

목적: Context가 제공할 값의 타입을 정의합니다.

상세 설명:

  • ReturnType<typeof useGameState>: useGameState 훅이 반환하는 타입을 자동으로 추출
    • 왜 수동으로 타입을 안 쓰나?: 훅의 반환값이 바뀌면 자동으로 타입도 업데이트됨 (DRY 원칙)
    • 예: useGameState에 새 상태를 추가하면 타입도 자동 반영
  • & (Intersection Type): 두 타입을 합침
    • 결과: { turnInfo, setTurnInfo, ..., activeModal, toggleModal, ... }

장점:

  • 타입 안정성: 잘못된 속성 접근 시 컴파일 에러
  • 자동 완성: IDE에서 사용 가능한 모든 속성 표시

3. Context 생성

const GameContext = createContext<GameContextType | undefined>(undefined);

목적: Context 객체를 생성합니다.

상세 설명:

  • createContext<GameContextType | undefined>: 제네릭으로 타입 지정
    • GameContextType: Provider가 제공할 값의 타입
    • | undefined: 초기값이 undefined이므로 포함
  • 초기값 undefined: Provider 밖에서 사용하면 undefined

왜 undefined를 허용하나?

  • Provider 밖에서 Context를 사용하려는 실수를 감지하기 위함
  • 다음 섹션의 useGameContext에서 에러를 던져 개발자에게 알림

4. useGameContext (안전한 Context 접근)

export const useGameContext = () => {
  const context = useContext(GameContext);
  if (!context) {
    throw new Error('useGameContext must be used within GameProvider');
  }
  return context;
};

목적: Context를 안전하게 사용할 수 있는 커스텀 훅을 제공합니다.

라인별 설명:

  • Line 12: useContext(GameContext)로 Context 값을 읽음
  • Line 13-15: 가드 체크
    • if (!context): Context가 undefined인지 확인
    • 언제 undefined인가?: GameProvider 밖에서 사용했을 때
    • throw new Error(...): 명확한 에러 메시지로 개발자에게 알림
  • Line 16: 타입이 GameContextType으로 좁혀진 context 반환

왜 이렇게 하나?

  • 타입 안정성: 반환값이 GameContextType으로 보장됨 (undefined 제거)
  • 개발자 경험: 실수 시 즉시 명확한 에러 메시지 표시
  • 대안: 직접 useContext(GameContext)를 쓰면 매번 undefined 체크 필요

5. GameProvider (Context 제공자)

type GameProviderProps = {
  children: ReactNode;
};

export const GameProvider = ({ children }: GameProviderProps) => {
  const gameState = useGameState();
  const modalManager = useModalManager();

  const value = {
    ...gameState,
    ...modalManager,
  };

  return <GameContext.Provider value={value}>{children}</GameContext.Provider>;
};

목적: Context 값을 생성하고 하위 컴포넌트에 제공합니다.

라인별 설명:

  • Line 19-21: Props 타입 정의
    • children: Provider로 감쌀 컴포넌트들
  • Line 24: useGameState() 호출로 게임 상태 생성
  • Line 25: useModalManager() 호출로 모달 상태 생성
  • Line 27-30: 두 훅의 반환값을 하나의 객체로 합침
    • 스프레드 연산자 (...): 객체의 모든 속성을 펼침
    • 결과: { turnInfo, setTurnInfo, ..., activeModal, toggleModal, ... }
  • Line 32: GameContext.Provider로 값을 제공
    • value={value}: 위에서 만든 합쳐진 객체를 제공
    • {children}: 하위 컴포넌트들을 렌더링

사용 예시:

// App.tsx 또는 GamePage.tsx
<GameProvider>
  <GameLayout />
  <GameStartModals />
  <GameWinnerModal />
</GameProvider>

왜 두 훅을 합치나?

  • 단일 진입점: 컴포넌트에서 useGameContext() 하나로 모든 상태 접근
  • 관심사 분리 유지: 훅은 분리되어 있지만, 사용은 통합됨
  • 확장성: 나중에 useBoardState 같은 훅을 추가해도 쉽게 통합 가능

설계 의도 및 패턴

1. 왜 Context를 사용했나?

문제: Props Drilling

// Context 없이 Props로 전달하는 경우
<App turnInfo={turnInfo} setTurnInfo={setTurnInfo} player1Name={...} ...>
  <GamePage turnInfo={turnInfo} setTurnInfo={setTurnInfo} ...>
    <GameLayout turnInfo={turnInfo} ...>
      <InfoBox turnInfo={turnInfo} />  {/* 여기서 실제 사용 */}
      <Board setTurnInfo={setTurnInfo} />  {/* 여기서 실제 사용 */}
    </GameLayout>
  </GamePage>
</App>
  • GameLayoutturnInfo를 사용하지 않지만, 자식에게 전달하기 위해 받아야 함
  • Props가 많아질수록 관리 복잡도 증가

해결: Context

// Context 사용
<GameProvider>
  <GameLayout>
    <InfoBox />  {/* useGameContext()로 직접 접근 */}
    <Board />    {/* useGameContext()로 직접 접근 */}
  </GameLayout>
</GameProvider>
  • 중간 컴포넌트는 props를 받을 필요 없음
  • 필요한 컴포넌트만 useGameContext()로 접근

2. 왜 Custom Hook으로 분리했나?

장점:

  1. 재사용성: 다른 곳에서도 같은 로직 사용 가능
  2. 테스트 용이성: 훅만 따로 테스트 가능
  3. 관심사 분리: 게임 로직(useGameState)과 UI 로직(useModalManager) 분리
  4. 가독성: Context Provider가 간결해짐

대안 (모든 로직을 Provider 안에 작성):

// 나쁜 예
export const GameProvider = ({ children }) => {
  const [turnInfo, setTurnInfo] = useState(...);
  const [isReplay, setIsReplay] = useState(...);
  // ... 50줄의 상태와 로직

  return <GameContext.Provider value={{...}}>{children}</GameContext.Provider>;
};
  • Provider가 너무 비대해짐
  • 로직 재사용 불가
  • 테스트 어려움

3. 계산된 값(Computed Value) 패턴

isDisabled: !player1Name.trim() || !player2Name.trim()

왜 별도 state로 만들지 않았나?

나쁜 예 (중복 상태):

const [isDisabled, setIsDisabled] = useState(true);

// player1Name이 바뀔 때마다 수동으로 업데이트
const handlePlayer1Change = (name: string) => {
  setPlayer1Name(name);
  setIsDisabled(!name.trim() || !player2Name.trim());  // 동기화 필요
};
  • 동기화 문제: player1Name이 바뀌었는데 isDisabled를 업데이트하지 않으면 버그
  • 중복 저장: 이미 player1Name, player2Name에서 계산 가능한 값

좋은 예 (계산된 값):

isDisabled: !player1Name.trim() || !player2Name.trim()
  • 항상 최신 값 반영
  • 동기화 문제 없음
  • 메모리 절약

4. 함수형 업데이트 패턴

setActiveModal((prev) => (prev === 'player' ? 'game' : 'player'));

왜 사용하나?

문제 상황 (일반 업데이트):

// 나쁜 예
const toggleModal = () => {
  setActiveModal(activeModal === 'player' ? 'game' : 'player');
};

// 빠르게 두 번 클릭하면?
toggleModal();  // activeModal = 'player' 읽음 → 'game'으로 설정
toggleModal();  // 아직 activeModal = 'player' (리렌더링 전) → 또 'game'으로 설정
// 결과: 한 번만 토글됨 (버그!)

해결 (함수형 업데이트):

setActiveModal((prev) => (prev === 'player' ? 'game' : 'player'));

// 빠르게 두 번 클릭해도
toggleModal();  // prev = 'player' → 'game'
toggleModal();  // prev = 'game' (최신 값) → 'player'
// 결과: 정상적으로 두 번 토글됨

규칙:

  • 이전 상태를 기반으로 새 상태를 계산할 때는 항상 함수형 업데이트 사용

5. 타입 추론 패턴 (ReturnType)

type GameContextType = ReturnType<typeof useGameState> &
  ReturnType<typeof useModalManager>;

장점:

  • DRY (Don't Repeat Yourself): 타입을 두 번 정의하지 않음
  • 자동 동기화: 훅의 반환값이 바뀌면 타입도 자동 업데이트
  • 유지보수성: 한 곳만 수정하면 됨

대안 (수동 타입 정의):

// 나쁜 예
type GameContextType = {
  turnInfo: { count: number; turn: Team };
  setTurnInfo: (value: ...) => void;
  // ... 50개의 속성을 수동으로 작성
};
  • useGameState에 새 상태를 추가하면 여기도 수동으로 추가해야 함
  • 실수로 빠뜨리면 타입 불일치 발생

요약

핵심 개념

  1. Custom Hook: 로직을 재사용 가능한 단위로 분리
  2. Context API: Props Drilling 없이 전역 상태 공유
  3. 관심사 분리: 게임 로직과 UI 로직을 별도 훅으로 분리
  4. 타입 안정성: TypeScript로 런타임 에러 방지

데이터 흐름

useGameState() ──┐
                 ├─> GameProvider ──> useGameContext() ──> 컴포넌트
useModalManager()─┘

사용 예시

// 컴포넌트에서
const { turnInfo, setTurnInfo, activeModal, toggleModal } = useGameContext();

이 아키텍처는 확장 가능하고, 유지보수하기 쉬우며, 타입 안전합니다.

'PROJECT > [ 장기게임 ]' 카테고리의 다른 글

react query란?????  (1) 2025.12.03
react query를 도입해보자  (0) 2025.12.03
App.tsx 줄이기  (0) 2025.11.27
내 기록  (0) 2025.11.26