TypeScript로 React 계약 표현하기
props·이벤트·상태를 타입으로 설명하고 외부 JSON을 런타임에서 검증하기
타입은 실행 전에 맺는 계약이다
JavaScript로 동작을 설명할 수 있게 된 뒤, TypeScript로 잘못된 조합을 일찍 찾습니다. 타입은 API 응답을 정제하거나 브라우저에서 검증해 주는 기능이 아닙니다. 코드를 실행하기 전에 확인하는 계약이며 빌드된 JavaScript에서는 지워집니다.
이 문서의 결과물은 타입이 있는 검색 목록입니다. 앞선 React 예제의 props·상태·이벤트를 설명하고 외부 JSON이 잘못되면 명확히 실패시키는 경계를 만듭니다.
먼저 알아둘 다섯 가지
| 표현 | 뜻 | 이 실습에서의 예 |
|---|---|---|
string, number | 기본 값의 종류 | 이름, ID |
type 또는 interface | 객체 구조 | Member |
| \`"frontend" \\ | "design"\` | 허용 값의 집합 |
name?: string | 속성이 없을 수도 있음 | 선택 속성 |
unknown | 확인 전에는 사용할 수 없는 값 | JSON 응답 |
any는 검사를 건너뛰게 합니다. 모르는 입력을 다룰 때는 unknown으로 받고 조건으로 좁힙니다. as Member[]는 개발자가 타입을 단언할 뿐 실제 값을 검사하지 않습니다.
실행 환경
기존 JavaScript 실습을 보관하고 별도의 브랜치나 연습 폴더에서 TypeScript 템플릿을 만듭니다.
npm create vite@latest lion-track-ts -- --template react-tscd lion-track-tsnpm installnpm run dev템플릿의 src/main.tsx는 유지하고 아래 파일들을 작성합니다. 초기 템플릿의 샘플 스타일 때문에 결과가 가려지면 src/index.css와 App의 기본 스타일 import를 정리합니다.
외부 데이터 검증과 타입 좁히기
src/members.ts입니다. 검증 함수가 성공해서 반환한 값만 Member로 사용합니다.
export type Member = { id: number; name: string; track: "frontend" | "design";};
export function parseMembers(value: unknown): Member[] { if (!Array.isArray(value)) throw new Error("목록 형식이 아닙니다."); const ids = new Set<number>(); return value.map((item: unknown, index) => { if (typeof item !== "object" || item === null || !("id" in item) || typeof item.id !== "number" || !Number.isSafeInteger(item.id) || item.id < 1 || !("name" in item) || typeof item.name !== "string" || item.name.trim().length === 0 || !("track" in item) || (item.track !== "frontend" && item.track !== "design")) { throw new Error("잘못된 구성원: " + (index + 1) + "번째"); } if (ids.has(item.id)) throw new Error("중복 ID: " + item.id); ids.add(item.id); return { id: item.id, name: item.name.trim(), track: item.track }; });}실습에서는 직접 검사해 narrowing을 관찰합니다. 여러 화면에서 복잡한 스키마를 공유하게 되면 팀이 이미 쓰는 검증 라이브러리로 옮길 수 있습니다.
props·이벤트·상태 연결하기
src/App.tsx 전체 예제입니다.
import { useState } from "react";import { parseMembers, type Member } from "./members";
const members = parseMembers([ { id: 1, name: "아기사자 A", track: "frontend" }, { id: 2, name: "아기사자 B", track: "design" },]);
function MemberCard({ member }: { member: Member }) { return <li>{member.name} · {member.track}</li>;}
export default function App() { const [query, setQuery] = useState(""); const filtered = members.filter(member => member.name.includes(query)); return ( <main> <h1>구성원 검색</h1> <label htmlFor="query">이름</label> <input id="query" value={query} onChange={event => setQuery(event.currentTarget.value)} /> <p aria-live="polite">{filtered.length}명</p> <ul>{filtered.map(member => <MemberCard key={member.id} member={member} />)}</ul> </main> );}문자열 초기값은 string으로 추론되므로 모든 useState에 제네릭을 붙일 필요가 없습니다. 빈 배열처럼 원소 타입을 추론할 근거가 없을 때는 useState<Member[]>([])로 의도를 지정합니다. JSX 안의 이벤트도 추론됩니다. 핸들러를 별도 함수로 뺄 때는 React의 이벤트 타입을 명시할 수 있습니다.
모순 없는 요청 상태
다음은 설계용 타입 예시이며 위 App에 추가할 필수 코드는 아닙니다.
type LoadState = | { status: "loading" } | { status: "error"; message: string } | { status: "success"; data: Member[] };loading인데 error message와 data가 동시에 남아 있는 조합을 없앱니다. status가 success인지 확인한 분기에서만 data를 읽게 하면 누락된 처리가 드러납니다. 빈 목록은 success의 data가 빈 배열인 경우입니다.
세션 실습과 확인
90분 동안 20분은 JavaScript 예제에 타입 달기, 30분은 실행과 props 변경, 25분은 나쁜 JSON 실험, 15분은 리뷰에 사용합니다.
- MemberCard에 문자열을 member로 넘겨 봅니다.
npm run build에서 오류가 나야 합니다. 확인 후 정상 코드로 되돌립니다. - 데이터의 id를
"1"로 바꿉니다. 런타임 검증이 거부하는 이유를 설명합니다. - 이름이 공백, 중복 ID, 알 수 없는 track, 빈 배열을 각각 확인합니다.
- optional 소개 필드를 추가하고 값이 없는 화면을 설계합니다. non-null assertion
!로 검사를 숨기지 않습니다.
완료 증거는 정상 빌드 결과, 잘못된 입력 세 가지의 오류 기록, “타입 오류”와 “런타임 검증 실패”를 구분한 PR 설명입니다.
더 읽어보기
연결된 PBL 미션과 VOD
| 주차·미션 | 참고 VOD 범위 |
|---|---|
| 8주 · TypeScript | 리액트 실무 6·7·8장 |
7–9주의 VOD 표시는 외부 상호작용·서버 연동·앱 제작 범위를 함께 안내합니다. Router·TypeScript·Supabase의 세부 API는 각 공식 문서로 보완합니다.
강좌 안내: 참고 강좌 1 · 참고 강좌 2. 이 문서는 영상 전체를 옮긴 전사 자료가 아닙니다. 세부 내용은 위 공식 문서에서 확인합니다.