iBetter Books
수정

프로젝트 설계와 타입 정의

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적 사고방식의 핵심입니다.

다음 챕터에서는 이 타입에 실제 데이터를 채웁니다.