iBetter Books
수정

퀴즈 데이터 모델링

타입이 준비됐습니다. 이제 그 타입에 실제 데이터를 채울 차례입니다. src/data.ts에 퀴즈 문제와 초기 상태를 만듭니다.

퀴즈 문제 데이터

TypeScript에 관한 5개의 질문을 만들어봅니다. Question 인터페이스를 따르는 배열을 만들면, 컴파일러가 각 항목이 올바른 형식인지 즉시 검사합니다.

// 새 파일: src/data.tsimport type { Question, QuizState } from "./types";export const questions: Question[] = [  {    id: 1,    text: "TypeScript에서 변수의 타입을 명시적으로 선언할 때 쓰는 문법은?",    choices: [      "let name = string;",      "let name: string;",      "string name;",      "var name as string;"    ],    answerIndex: 1  },  {    id: 2,    text: "다음 중 TypeScript에만 있고 JavaScript에는 없는 것은?",    choices: [      "화살표 함수",      "Promise",      "인터페이스(interface)",      "배열 구조 분해"    ],    answerIndex: 2  },  {    id: 3,    text: "제네릭 함수 `function identity<T>(arg: T): T`에서 T가 의미하는 것은?",    choices: [      "항상 string 타입",      "호출 시 결정되는 타입 매개변수",      "타입 에러를 무시하는 키워드",      "TypeScript 버전 번호"    ],    answerIndex: 1  },  {    id: 4,    text: "`string | null` 타입의 변수를 string으로 안전하게 쓰려면?",    choices: [      "타입 단언 `as string`만 쓰면 된다",      "그냥 쓰면 된다, TypeScript가 알아서 처리한다",      "null 체크 후 사용하거나 옵셔널 체이닝을 쓴다",      "any로 캐스팅하면 된다"    ],    answerIndex: 2  },  {    id: 5,    text: "TypeScript의 `strict` 모드를 켜면 추가되는 검사가 아닌 것은?",    choices: [      "strictNullChecks",      "noImplicitAny",      "strictFunctionTypes",      "noUnusedLocals"    ],    answerIndex: 3  }];

noUnusedLocalsstrict 모드에 포함되지 않고 별도 옵션입니다. 5번 문제의 정답입니다.

초기 상태 만들기

퀴즈를 시작할 때의 상태를 createInitialState 함수로 만듭니다. 함수로 만드는 이유는 "다시 시작" 기능 때문입니다. 퀴즈를 다시 시작하면 이 함수를 호출해서 깨끗한 초기 상태를 얻을 수 있습니다.

// 수정: src/data.tsimport type { Question, QuizState } from "./types";export const questions: Question[] = [  {    id: 1,    text: "TypeScript에서 변수의 타입을 명시적으로 선언할 때 쓰는 문법은?",    choices: [      "let name = string;",      "let name: string;",      "string name;",      "var name as string;"    ],    answerIndex: 1  },  {    id: 2,    text: "다음 중 TypeScript에만 있고 JavaScript에는 없는 것은?",    choices: [      "화살표 함수",      "Promise",      "인터페이스(interface)",      "배열 구조 분해"    ],    answerIndex: 2  },  {    id: 3,    text: "제네릭 함수 `function identity<T>(arg: T): T`에서 T가 의미하는 것은?",    choices: [      "항상 string 타입",      "호출 시 결정되는 타입 매개변수",      "타입 에러를 무시하는 키워드",      "TypeScript 버전 번호"    ],    answerIndex: 1  },  {    id: 4,    text: "`string | null` 타입의 변수를 string으로 안전하게 쓰려면?",    choices: [      "타입 단언 `as string`만 쓰면 된다",      "그냥 쓰면 된다, TypeScript가 알아서 처리한다",      "null 체크 후 사용하거나 옵셔널 체이닝을 쓴다",      "any로 캐스팅하면 된다"    ],    answerIndex: 2  },  {    id: 5,    text: "TypeScript의 `strict` 모드를 켜면 추가되는 검사가 아닌 것은?",    choices: [      "strictNullChecks",      "noImplicitAny",      "strictFunctionTypes",      "noUnusedLocals"    ],    answerIndex: 3  }];export function createInitialState(): QuizState {  return {    questions,    currentIndex: 0,    selectedAnswers: new Array(questions.length).fill(null),    phase: { kind: "playing" }  };}

new Array(questions.length).fill(null)은 질문 수만큼 null로 채워진 배열을 만듭니다. 질문이 5개라면 [null, null, null, null, null]이 됩니다. 아직 아무 답도 선택하지 않은 초기 상태입니다.

타입이 데이터를 보호한다

데이터를 작성하면서 타입의 도움을 직접 체험할 수 있습니다. 예를 들어 answerIndex를 잘못 입력하면 바로 오류가 납니다.

// 이렇게 쓰면 에러는 나지 않지만 논리 오류{  id: 1,  text: "...",  choices: ["A", "B", "C", "D"],  answerIndex: 5  // 인덱스 범위 초과! — 타입으로는 막기 어렵지만}

answerIndex의 유효 범위(0~3)를 타입으로 좁히고 싶다면 choices.length - 1과 연동해야 해서 복잡해집니다. 지금은 number로 두고, 데이터를 직접 주의 깊게 작성하는 것으로 충분합니다.

반면에 필드 이름을 잘못 쓰거나, 필수 필드를 빠뜨리면 즉시 오류가 납니다.

// 컴파일 오류: 'answerIndex' 필드가 없습니다const bad: Question = {  id: 1,  text: "질문",  choices: ["A", "B", "C", "D"]  // answerIndex 누락!};

이것만으로도 데이터 작성 실수를 상당히 줄일 수 있습니다.

데이터 검증 유틸리티

데이터가 올바른지 확인하는 간단한 검증 함수를 추가합니다.

// 수정: src/data.tsimport type { Question, QuizState } from "./types";export const questions: Question[] = [  {    id: 1,    text: "TypeScript에서 변수의 타입을 명시적으로 선언할 때 쓰는 문법은?",    choices: [      "let name = string;",      "let name: string;",      "string name;",      "var name as string;"    ],    answerIndex: 1  },  {    id: 2,    text: "다음 중 TypeScript에만 있고 JavaScript에는 없는 것은?",    choices: [      "화살표 함수",      "Promise",      "인터페이스(interface)",      "배열 구조 분해"    ],    answerIndex: 2  },  {    id: 3,    text: "제네릭 함수 `function identity<T>(arg: T): T`에서 T가 의미하는 것은?",    choices: [      "항상 string 타입",      "호출 시 결정되는 타입 매개변수",      "타입 에러를 무시하는 키워드",      "TypeScript 버전 번호"    ],    answerIndex: 1  },  {    id: 4,    text: "`string | null` 타입의 변수를 string으로 안전하게 쓰려면?",    choices: [      "타입 단언 `as string`만 쓰면 된다",      "그냥 쓰면 된다, TypeScript가 알아서 처리한다",      "null 체크 후 사용하거나 옵셔널 체이닝을 쓴다",      "any로 캐스팅하면 된다"    ],    answerIndex: 2  },  {    id: 5,    text: "TypeScript의 `strict` 모드를 켜면 추가되는 검사가 아닌 것은?",    choices: [      "strictNullChecks",      "noImplicitAny",      "strictFunctionTypes",      "noUnusedLocals"    ],    answerIndex: 3  }];export function createInitialState(): QuizState {  return {    questions,    currentIndex: 0,    selectedAnswers: new Array(questions.length).fill(null),    phase: { kind: "playing" }  };}// 개발 시 데이터 무결성 검사export function validateQuestions(qs: Question[]): boolean {  return qs.every((q) => {    const hasCorrectChoiceCount = q.choices.length === 4;    const hasValidAnswerIndex =      q.answerIndex >= 0 && q.answerIndex < q.choices.length;    return hasCorrectChoiceCount && hasValidAnswerIndex;  });}// 앱 시작 시 한 번 검사 (개발 환경에서만)if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {  if (!validateQuestions(questions)) {    console.error("퀴즈 데이터에 오류가 있습니다.");  }}

validateQuestions는 각 문제가 선택지 4개를 갖고 있는지, answerIndex가 유효한 범위인지 확인합니다. 타입으로 잡을 수 없는 논리적 오류를 런타임에서 잡는 보완책입니다.

typeof process !== "undefined" 검사를 추가하여 브라우저 환경에서도 안전하게 동작합니다. Node.js에서는 process 전역 객체가 있지만, 브라우저에서는 없을 수 있기 때문입니다.

이것으로 src/types.tssrc/data.ts가 완성됐습니다. 다음 챕터에서는 이 데이터를 가지고 게임 로직을 구현합니다.