프로젝트 설계와 타입 정의
TypeScript 프로젝트를 시작하는 방법은 두 가지입니다. 첫 번째는 JavaScript처럼 일단 코드를 쓰고, 나중에 타입을 붙이는 방법입니다. 두 번째는 타입부터 설계하고, 그 타입에 맞춰 코드를 채워 나가는 방법입니다.
TypeScript를 제대로 쓴다는 것은 두 번째 방법을 택하는 것입니다. 이 챕터에서는 코드를 한 줄도 쓰기 전에 "무엇을 만들지"를 타입으로 표현하는 과정을 밟겠습니다.
요구사항 분석
퀴즈 앱이 해야 할 일을 먼저 정리합니다.
- 질문을 하나씩 화면에 표시합니다.
- 각 질문에는 4개의 선택지가 있습니다.
- 사용자가 선택지를 클릭하면 정답 여부를 즉시 보여줍니다.
- 5개의 질문이 끝나면 최종 점수와 결과를 보여줍니다.
- 다시 시작 버튼으로 처음부터 다시 할 수 있습니다.
이 요구사항에서 "데이터"와 "상태"를 찾아봅니다. 데이터는 변하지 않는 것이고, 상태는 시간에 따라 변하는 것입니다.
데이터 (변하지 않음)
- 질문 목록: 질문 텍스트, 선택지 4개, 정답 인덱스
상태 (시간에 따라 변함)
- 현재 몇 번째 질문인지
- 각 질문에 어떤 선택지를 골랐는지
- 퀴즈가 진행 중인지, 끝났는지
이것을 TypeScript 타입으로 옮기는 것이 첫 번째 작업입니다.
프로젝트 구조 만들기
먼저 프로젝트 폴더를 만들고 필요한 파일을 세팅합니다.
quiz-app/
├── src/
│ ├── types.ts ← 인터페이스/타입 정의
│ ├── data.ts ← 퀴즈 데이터
│ ├── engine.ts ← 게임 로직
│ └── app.ts ← DOM 연결 (엔트리 포인트)
├── index.html
├── tsconfig.json
└── package.json
터미널에서 다음 명령어로 초기 설정을 합니다.
mkdir quiz-appcd quiz-appnpm init -ynpm install -D typescriptnpx tsc --initmkdir src
tsconfig.json을 프로젝트에 맞게 수정합니다.
// 새 파일: tsconfig.json{ "compilerOptions": { "target": "ES2020", "module": "ES2020", "moduleResolution": "node16", "strict": true, "outDir": "./dist", "rootDir": "./src", "lib": ["ES2020", "DOM"] }, "include": ["src/**/*"]}
"lib": ["ES2020", "DOM"] 설정이 중요합니다. DOM을 포함해야 document, HTMLElement 같은 브라우저 타입을 쓸 수 있습니다.
package.json에 빌드 스크립트를 추가합니다.
// 수정: package.json{ "name": "quiz-app", "version": "1.0.0", "scripts": { "build": "tsc", "watch": "tsc --watch" }, "devDependencies": { "typescript": "^5.0.0" }}
타입 설계
이제 src/types.ts를 작성합니다. 이 파일이 프로젝트의 청사진입니다.
// 새 파일: src/types.ts// 하나의 질문을 표현하는 타입export interface Question { id: number; text: string; choices: string[]; // 4개의 선택지 answerIndex: number; // 정답 선택지의 인덱스 (0~3)}// 퀴즈 진행 상태를 표현하는 타입export interface QuizState { questions: Question[]; currentIndex: number; // 현재 몇 번째 질문인지 selectedAnswers: (number | null)[]; // 각 질문에서 선택한 인덱스 phase: QuizPhase; // 현재 단계}// 퀴즈의 단계를 나타내는 판별 유니온export type QuizPhase = | { kind: "playing"; } // 문제 풀이 중 | { kind: "answered"; correct: boolean; } // 답변 직후 (정답/오답 표시) | { kind: "finished"; score: number; total: number; }; // 완료 후 결과// 결과 화면에 필요한 요약 정보export interface QuizResult { score: number; total: number; percentage: number; grade: "excellent" | "good" | "average" | "poor";}
타입을 설계하면서 중요한 결정을 내렸습니다.
QuizPhase를 판별 유니온으로 만든 것입니다. 단순히 "playing" | "answered" | "finished" 문자열 리터럴로도 표현할 수 있었습니다. 하지만 answered 단계에서는 정답 여부가 필요하고, finished 단계에서는 점수가 필요합니다. 판별 유니온을 쓰면 각 단계에 필요한 데이터가 자연스럽게 붙어 다닙니다.
나중에 코드를 쓸 때 이렇게 됩니다.
// 타입 가드로 단계 구분if (state.phase.kind === "finished") { // 여기서 state.phase.score, state.phase.total을 사용 가능 console.log(`점수: ${state.phase.score}/${state.phase.total}`);}
컴파일러가 kind를 보고 각 단계에서 어떤 속성이 있는지 정확히 알아냅니다.
selectedAnswers를 (number | null)[]로 만든 것도 의도적인 선택입니다. 아직 풀지 않은 질문은 null, 풀었으면 선택한 인덱스 번호입니다. 이렇게 하면 "이 질문에 답을 했는가"를 null 체크만으로 확인할 수 있습니다.
타입 설계가 알려주는 것
src/types.ts만 보면 이 앱이 무엇인지 전부 이해할 수 있습니다. Question을 보면 퀴즈 문항의 구조가 보입니다. QuizState를 보면 앱이 어떤 상태를 관리하는지 보입니다. QuizPhase를 보면 앱의 화면이 몇 가지인지 보입니다.
코드를 읽지 않아도 타입만으로 앱의 설계도가 됩니다. 이것이 "타입부터 설계한다"는 TypeScript적 사고방식의 핵심입니다.
다음 챕터에서는 이 타입에 실제 데이터를 채웁니다.