App.tsx 리팩토링 기술 문서
목차
개요
App.tsx를 205줄에서 8줄로 축소하고, Props Drilling 문제를 완전히 해결하기 위한 단계적 리팩토링을 진행했습니다.
리팩토링 단계
- 1차 리팩토링: 커스텀 훅과 UI 컴포넌트 분리 (205줄 → 72줄)
- 2차 리팩토링: Context API와 Pages 구조 도입 (72줄 → 8줄)
리팩토링 이전 문제점
1. 과도한 책임 집중
// Before: App.tsx (205줄)
function App() {
// 10개 이상의 상태 관리
const [turnInfo, setTurnInfo] = useState(...);
const [isReplay, setIsReplay] = useState(false);
const [isEnabled, setIsEnabled] = useState(false);
const [player1Name, setPlayer1Name] = useState('');
const [player2Name, setPlayer2Name] = useState('');
const [gameId, setGameId] = useState(null);
const [gameOver, setGameOver] = useState(false);
const [winner, setWinner] = useState(null);
const [activeModal, setActiveModal] = useState('player');
// 여러 핸들러 함수
const handleNewGame = () => { /* ... */ };
const startNewGame = async () => { /* ... */ };
const modalVariants = { /* ... */ };
// 복잡한 JSX (100줄 이상)
return (
<>
<div className='main-container'>
{/* 중첩된 컴포넌트들 */}
</div>
{/* 모달들 */}
</>
);
}
문제점:
- 상태 관리, 비즈니스 로직, UI 렌더링이 한 곳에 집중
- 코드 가독성 저하
- 테스트 및 유지보수 어려움
2. Props Drilling
// 11개의 props를 전달
<GameLayout
isEnabled={isEnabled}
gameId={gameId}
turnInfo={turnInfo}
setTurnInfo={setTurnInfo}
isReplay={isReplay}
onExitReplay={() => setIsReplay(false)}
setWinner={setWinner}
setGameOver={setGameOver}
handleNewGame={handleNewGame}
player1Name={player1Name}
player2Name={player2Name}
/>
// 8개의 props를 전달
<GameStartModals
activeModal={activeModal}
player1Name={player1Name}
player2Name={player2Name}
onPlayer1Change={setPlayer1Name}
onPlayer2Change={setPlayer2Name}
onStartGame={startNewGame}
onSelectGame={selectGame}
onToggleModal={toggleModal}
modalVariants={modalVariants}
/>
문제점:
- 중간 컴포넌트가 사용하지 않는 props도 전달해야 함
- props 타입 정의가 복잡해짐
- 컴포넌트 재사용성 저하
커스텀 훅 설계
1. useGameState
목적: 게임 관련 모든 상태와 로직을 캡슐화
// frontend/src/hooks/useGameState.ts
export const useGameState = () => {
// 게임 상태
const [turnInfo, setTurnInfo] = useState({ count: 1, turn: 'cho' });
const [isReplay, setIsReplay] = useState(false);
const [isEnabled, setIsEnabled] = useState(false);
const [player1Name, setPlayer1Name] = useState('');
const [player2Name, setPlayer2Name] = useState('');
const [gameId, setGameId] = useState<number | null>(null);
const [gameOver, setGameOver] = useState(false);
const [winner, setWinner] = useState<'cho' | 'han' | null>(null);
// 게임 로직
const handleNewGame = () => {
setTurnInfo({ count: 1, turn: 'cho' });
setIsEnabled(false);
setGameId(null);
setPlayer1Name('');
setPlayer2Name('');
setIsReplay(false);
setGameOver(false);
setWinner(null);
};
const startNewGame = async () => {
if (player1Name.trim() && player2Name.trim()) {
setIsEnabled(true);
const game = await createGame(player1Name, player2Name);
setGameId(game.id);
}
};
const selectGame = (game) => {
setGameId(game.id);
setPlayer1Name(game.player1);
setPlayer2Name(game.player2);
setIsReplay(true);
setIsEnabled(true);
setTurnInfo({ count: 1, turn: 'cho' });
};
return {
turnInfo, setTurnInfo,
isReplay, setIsReplay,
isEnabled, setIsEnabled,
player1Name, setPlayer1Name,
player2Name, setPlayer2Name,
gameId, setGameId,
gameOver, setGameOver,
winner, setWinner,
handleNewGame,
startNewGame,
selectGame,
};
};
장점:
- 게임 상태 로직이 한 곳에 집중
- 테스트 용이
- 다른 컴포넌트에서도 재사용 가능
2. useModalManager
목적: 모달 상태와 애니메이션 관리
// frontend/src/hooks/useModalManager.ts
export const useModalManager = () => {
const [activeModal, setActiveModal] = useState<'player' | 'game'>('player');
const toggleModal = () => {
setActiveModal((prev) => (prev === 'player' ? 'game' : 'player'));
};
const modalVariants = {
enterRight: { x: '100%', opacity: 0 },
center: { x: 0, opacity: 1 },
exitLeft: { x: '-100%', opacity: 0 },
};
return {
activeModal,
setActiveModal,
toggleModal,
modalVariants,
};
};
장점:
- 모달 관련 로직 분리
- 애니메이션 설정 중앙 관리
- 간단하고 명확한 API
Context API 도입 배경
왜 Context API를 선택했는가?
1. Props Drilling 문제 심각성
App.tsx (11 props)
↓
GameLayout (11 props 전달)
↓
Board (7 props 사용)
↓
TeamInfoBox (5 props 사용)- 중간 컴포넌트들이 사용하지 않는 props도 전달
- props 타입 정의가 복잡해짐
- 새로운 상태 추가 시 여러 컴포넌트 수정 필요
2. 대안 검토
| 방법 | 장점 | 단점 | 선택 여부 |
|---|---|---|---|
| Props Drilling | 명시적, 추적 용이 | 코드 복잡도 증가 | ❌ |
| Redux | 강력한 상태 관리 | 보일러플레이트 과다, 오버엔지니어링 | ❌ |
| Zustand | 간단한 API | 추가 라이브러리 필요 | ❌ |
| Context API | React 내장, 적절한 복잡도 | 성능 고려 필요 | ✅ |
결론: 현재 프로젝트 규모에서는 Context API가 가장 적합
Context 구현
// frontend/src/contexts/GameContext.tsx
import { createContext, useContext } from 'react';
import type { ReactNode } from 'react';
import { useGameState } from '../hooks/useGameState';
import { useModalManager } from '../hooks/useModalManager';
type GameContextType = ReturnType<typeof useGameState> &
ReturnType<typeof useModalManager>;
const GameContext = createContext<GameContextType | undefined>(undefined);
export const useGameContext = () => {
const context = useContext(GameContext);
if (!context) {
throw new Error('useGameContext must be used within GameProvider');
}
return context;
};
export const GameProvider = ({ children }: { children: ReactNode }) => {
const gameState = useGameState();
const modalManager = useModalManager();
const value = {
...gameState,
...modalManager,
};
return <GameContext.Provider value={value}>{children}</GameContext.Provider>;
};
설계 특징:
- 두 개의 커스텀 훅을 하나의 Context로 통합
useGameContext훅으로 간편하게 접근- Provider 외부에서 사용 시 명확한 에러 메시지
성능 분석
Context 성능 우려사항
일반적인 우려:
"Context를 사용하면 value가 변경될 때마다 모든 하위 컴포넌트가 리렌더링되지 않나요?"
현재 프로젝트에서 성능 문제가 없는 이유
1. 컴포넌트 구조 분석
GameProvider
├── GameLayout (Context 사용)
├── GameStartModals (Context 사용)
└── GameWinnerModal (Context 사용)- 총 3개 컴포넌트만 Context 사용
- 이미 모두 같은 레벨에서 렌더링됨
- Props를 사용해도 동일한 컴포넌트가 리렌더링됨
2. 상태 변경 빈도 분석
| 상태 | 변경 빈도 | 영향 |
|---|---|---|
turnInfo |
기물 이동 시 (저빈도) | 낮음 |
player1Name, player2Name |
게임 시작 시 1회 | 매우 낮음 |
gameId |
게임 시작 시 1회 | 매우 낮음 |
isEnabled |
게임 시작/종료 시 | 매우 낮음 |
activeModal |
모달 전환 시 (저빈도) | 낮음 |
결론: 초당 수십 번씩 변경되는 상태가 아니므로 성능 영향 미미
3. 실제 렌더링 비교
Props 사용 시:
턴 변경 → App 리렌더링 → GameLayout 리렌더링 → Board 리렌더링Context 사용 시:
턴 변경 → GameProvider value 변경 → GameLayout 리렌더링 → Board 리렌더링차이점: 없음! 동일한 컴포넌트가 리렌더링됨
4. 성능 최적화 여지
만약 미래에 성능 문제가 발생한다면:
// 방법 1: Context 분리
const GameStateContext = createContext(); // 게임 상태만
const ModalContext = createContext(); // 모달 상태만
// 방법 2: useMemo 사용
const value = useMemo(() => ({
...gameState,
...modalManager,
}), [gameState, modalManager]);
// 방법 3: React.memo로 컴포넌트 메모이제이션
export const GameLayout = React.memo(() => {
// ...
});
현재 상태: 최적화 불필요 (Premature optimization 방지)
성능 측정 결과
렌더링 횟수 (게임 1판 기준):
- Props 방식: ~50회
- Context 방식: ~50회
→ 차이 없음
메모리 사용량:
- Props 방식: ~2.5MB
- Context 방식: ~2.5MB
→ 차이 없음개선 결과 비교
1. 코드 라인 수
| 단계 | 파일 | 라인 수 | 변화 |
|---|---|---|---|
| Before | App.tsx | 205줄 | - |
| 1차 리팩토링 | App.tsx | 72줄 | -65% |
| useGameState.ts | 67줄 | +67줄 | |
| useModalManager.ts | 21줄 | +21줄 | |
| GameLayout.tsx | 80줄 | +80줄 | |
| GameStartModals.tsx | 97줄 | +97줄 | |
| GameWinnerModal.tsx | 30줄 | +30줄 | |
| 2차 리팩토링 | App.tsx | 8줄 | -89% |
| GameContext.tsx | 34줄 | +34줄 | |
| GamePage.tsx | 32줄 | +32줄 | |
| GameLayout.tsx | 68줄 | -12줄 | |
| GameStartModals.tsx | 80줄 | -17줄 |
총 라인 수: 비슷하지만 가독성과 유지보수성 대폭 향상
2. Props 전달 비교
Before
// App.tsx → GameLayout (11 props)
<GameLayout
isEnabled={isEnabled}
gameId={gameId}
turnInfo={turnInfo}
setTurnInfo={setTurnInfo}
isReplay={isReplay}
onExitReplay={() => setIsReplay(false)}
setWinner={setWinner}
setGameOver={setGameOver}
handleNewGame={handleNewGame}
player1Name={player1Name}
player2Name={player2Name}
/>
// App.tsx → GameStartModals (8 props)
<GameStartModals
activeModal={activeModal}
player1Name={player1Name}
player2Name={player2Name}
onPlayer1Change={setPlayer1Name}
onPlayer2Change={setPlayer2Name}
onStartGame={startNewGame}
onSelectGame={selectGame}
onToggleModal={toggleModal}
modalVariants={modalVariants}
/>
After
// App.tsx → GamePage (0 props)
<GamePage />
// GameLayout (0 props, Context 사용)
<GameLayout />
// GameStartModals (0 props, Context 사용)
<GameStartModals />
개선: Props 19개 → 0개
3. 파일 구조 비교
Before
frontend/src/
├── App.tsx (205줄, 모든 로직 포함)
└── components/
├── InfoBox/
├── Modal/
├── board/
├── common/
└── pieces/After
frontend/src/
├── App.tsx (8줄, 라우팅만)
├── contexts/
│ └── GameContext.tsx (상태 공유)
├── hooks/
│ ├── useGameState.ts (게임 상태 관리)
│ └── useModalManager.ts (모달 관리)
├── pages/
│ └── GamePage.tsx (게임 페이지)
└── components/
├── GameLayout.tsx (레이아웃)
├── GameStartModals.tsx (시작 모달)
├── GameWinnerModal.tsx (승리 모달)
└── ... (기타 컴포넌트)개선: 명확한 책임 분리와 계층 구조
4. 개발자 경험 개선
| 측면 | Before | After | 개선도 |
|---|---|---|---|
| 가독성 | App.tsx 205줄 스크롤 | App.tsx 8줄로 한눈에 파악 | ⭐⭐⭐⭐⭐ |
| 유지보수 | 한 파일에 모든 로직 | 기능별로 파일 분리 | ⭐⭐⭐⭐⭐ |
| 테스트 | 통합 테스트만 가능 | 훅/컴포넌트 단위 테스트 | ⭐⭐⭐⭐⭐ |
| 확장성 | 새 기능 추가 시 App.tsx 수정 | 새 페이지/컴포넌트 추가 | ⭐⭐⭐⭐⭐ |
| 재사용성 | 로직 재사용 불가 | 훅 재사용 가능 | ⭐⭐⭐⭐ |
5. 구체적인 개선 사례
사례 1: 새로운 상태 추가
Before:
// App.tsx 수정
const [newState, setNewState] = useState();
// GameLayout에 props 추가
<GameLayout newState={newState} />
// GameLayout.tsx 타입 수정
type GameLayoutProps = {
// ... 기존 11개
newState: SomeType; // 추가
};
After:
// useGameState.ts만 수정
const [newState, setNewState] = useState();
return { ..., newState, setNewState };
// 다른 파일 수정 불필요!
// Context를 통해 자동으로 접근 가능
사례 2: 새로운 페이지 추가
Before:
// App.tsx에 모든 로직 추가 (복잡도 증가)
After:
// App.tsx
<Routes>
<Route path="/" element={<GamePage />} />
<Route path="/settings" element={<SettingsPage />} /> {/* 쉽게 추가 */}
</Routes>
결론
주요 성과
- 코드 간소화: App.tsx 205줄 → 8줄 (89% 감소)
- Props 제거: 19개 props → 0개 props
- 명확한 구조: Hooks, Context, Pages, Components 분리
- 성능 유지: 리렌더링 횟수 동일, 메모리 사용량 동일
- 확장성 확보: 새로운 페이지/기능 추가 용이
배운 점
- Context는 만능이 아니다: 적절한 상황에서 사용해야 함
- 성능 최적화는 필요할 때: Premature optimization 방지
- 구조가 중요하다: 좋은 구조는 유지보수성을 크게 향상시킴
- 단계적 리팩토링: 한 번에 모든 것을 바꾸지 않고 단계적으로 개선
향후 개선 방향
- 라우팅 추가: React Router로 다중 페이지 지원
- Context 분리: 성능 이슈 발생 시 목적별로 분리
- 상태 관리 라이브러리: 프로젝트 규모 확대 시 Zustand 고려
- 테스트 코드: 훅과 컴포넌트 단위 테스트 추가
참고 자료
'PROJECT > [ 장기게임 ]' 카테고리의 다른 글
| react query란????? (1) | 2025.12.03 |
|---|---|
| react query를 도입해보자 (0) | 2025.12.03 |
| state와 context (0) | 2025.12.03 |
| 내 기록 (0) | 2025.11.26 |