퀴즈 데이터 모델링
타입이 준비됐습니다. 이제 그 타입에 실제 데이터를 채울 차례입니다. 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 }];
noUnusedLocals는 strict 모드에 포함되지 않고 별도 옵션입니다. 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.ts와 src/data.ts가 완성됐습니다. 다음 챕터에서는 이 데이터를 가지고 게임 로직을 구현합니다.