외부 API의 JSON 데이터를 unknown과 타입 가드로 어떻게 검증하나요?
API가 돌려준 JSON은 타입 이름을 붙였다고 검증되지 않습니다. unknown으로 받은 뒤 객체인지, id와 title이 필요한 타입인지 실제 실행 중에 검사하고 통과한 값만 사용합니다.
적용 조건·범위: 브라우저에서 const searchParams = new URLSearchParams(location.search);를 먼저 선언합니다. 아래 localStorage 조각은 브라우저 전용입니다. 서버 없이 실행하는 타입 가드 검증은 직접 확인 예제를 사용하세요.
본문 근거: TypeScript narrowing과 타입 가드
직접 확인: 오류에서 수정까지
TypeScript 외부 API 데이터 unknown과 type guard 안전 검증 · 독립 실습
얻을 결과: unknown 입력에서 객체·id·title을 순서대로 검사한 뒤에만 Task로 사용합니다.
예제 크게 보기 (새 탭)단독 검사는 tsconfig.json이 없는 새 폴더에서 example.ts 한 파일씩 교체하며 진행합니다. TypeScript가 없다면 설치 글에서 Node.js/npm·TypeScript 준비를 확인하세요. 타입 검사 성공은 코드 실행 성공과 구분합니다.
독자 질문: fetch JSON이 실제 Task인지 타입 단언 없이 어떻게 확인할까요?
얻을 결과: unknown 입력에서 객체·id·title을 순서대로 검사한 뒤에만 Task로 사용합니다.
다음 코드를 별도 example.ts에 복사합니다.
const data: unknown = JSON.parse('{"id":"1","title":"공부"}');
console.log(data.title);
예상 오류 (TS18046): unknown 값에서는 title 속성에 바로 접근할 수 없습니다. isTask로 실행 시 구조를 확인합니다.
오류 예제를 다음 코드로 교체합니다.
type Task = { id: string; title: string };
function isTask(value: unknown): value is Task {
return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string' && 'title' in value && typeof value.title === 'string';
}
const data: unknown = JSON.parse('{"id":"1","title":"공부"}');
if (isTask(data)) console.log(data.title);
npx tsc --strict --noEmit example.ts
기존 최소 예제 확인 기록: TypeScript 7.0.2의 별도 최소 예제에서 오류 TS18046, 수정 후 exit 0 확인. TaskFlow 전체 체크포인트는 미확인.
다음 학습: 17회차에서 검증한 Task 타입을 React 컴포넌트 props로 연결합니다.
잘못된 신뢰
아래 개념 조각에서 참조하는 학습용 타입입니다. 프로젝트 파일을 import하지 않습니다. 같은 이름을 다시 선언하는 비교 코드는 별도 파일에서 확인하세요.
type Task = { id: string; title: string; completed: boolean };
const data =
await response.json()
as Task[];
이 코드는 runtime 검증을 하지 않습니다.
서버가 다음 값을 보내도 TypeScript는 실행 중에 막아주지 않습니다.
[
{
"id": 1,
"title": null,
"completed": "no"
}
]
unknown으로 시작
const data:
unknown =
await response.json();
이제 사용 전에 검증해야 합니다.
isTask
export function isTask(
value: unknown,
): value is Task {
if (
typeof value !==
'object' ||
value === null
) {
return false;
}
if (
!('id' in value) ||
!('title' in value) ||
!('completed' in value)
) {
return false;
}
return (
typeof value.id ===
'string' &&
typeof value.title ===
'string' &&
typeof value.completed ===
'boolean'
);
}
true를 반환하면 TypeScript는 value를 Task로 좁힙니다.
배열 검증
export function isTaskArray(
value: unknown,
): value is Task[] {
return (
Array.isArray(value) &&
value.every(isTask)
);
}
fetch 함수
export async function fetchTasks():
Promise<Task[]> {
const response =
await fetch('/api/tasks');
if (!response.ok) {
throw new Error(
'요청 실패',
);
}
const data:
unknown =
await response.json();
if (!isTaskArray(data)) {
throw new Error(
'잘못된 작업 데이터',
);
}
return data;
}
이제 API가 예상 구조를 반환하지 않으면 명시적으로 오류 처리할 수 있습니다.
URLSearchParams도 외부 입력
브라우저에서 const searchParams = new URLSearchParams(location.search);를 먼저 선언합니다. 아래 localStorage 조각은 브라우저 전용입니다. 서버 없이 실행하는 타입 가드 검증은 직접 확인 예제를 사용하세요.
URL parameter는 string 또는 null입니다.
const id =
searchParams.get('id');
존재 여부와 형식 검증이 필요합니다.
localStorage도 마찬가지입니다.
const raw =
localStorage.getItem(
'tasks',
);
저장된 문자열을 JSON.parse한 결과도 unknown으로 검증할 수 있습니다.
schema validation library는 언제?
실제 서비스에서 API 구조가 크고 중첩되면 직접 type guard를 모두 작성하기 어려울 수 있습니다.
그때 Zod 같은 schema validation 도구를 검토할 수 있습니다.
하지만 먼저 TypeScript 타입과 runtime validation이 다른 문제라는 원리를 이해해야 합니다.
이 과정에서는 외부 라이브러리를 추가하지 않습니다.
JSON.parse는 JSON 문법이 틀린 문자열에서 먼저 예외를 던집니다. 파싱에 성공한 값의 구조를 확인하는 type guard와는 다른 실패이므로, 실제 입력을 다룰 때는 파싱 오류와 데이터 형식 오류를 구분해 처리합니다.
원리 확인: TypeScript narrowing과 타입 가드 (2026-10-12). 아래 기대 결과와 기존 실행 기록은 구분해서 읽습니다.
학습 완료 체크리스트
타입 검사를 통과하는 코드와 올바른 외부 데이터는 같은 의미일까요? title이 숫자인 입력에서 guard가 false가 되는 이유를 설명하면 컴파일 타입과 실행 중 검증의 경계를 구분한 것입니다.
작은 변형으로 확인하기
입력 JSON의 title을 123 또는 null로 바꾸면 isTask가 false여야 합니다. id 또는 title이 없는 객체도 통과하지 않는지 확인하세요.
실행 중 검증 확인: 수정한 example.ts를 같은 config 없는 연습 폴더에서 npx tsc --strict --target es2022 --module commonjs --outDir out example.ts로 변환한 뒤 node out/example.js를 실행합니다. 정상 입력은 공부를 출력하는 것이 기대 결과입니다. title을 숫자로 바꾸면 guard가 통과하지 않아 이 예제는 아무것도 출력하지 않습니다. noEmit 타입 검사만으로 이 실행 결과까지 확인한 것은 아닙니다. 이 편집에서 두 실행은 미확인입니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript 코드를 TypeScript로 확장합니다.
TaskFlow의 외부 작업 데이터 경계를 type guard로 안전하게 만듭니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
TypeScript 외부 API 데이터 unknown과 type guard 안전 검증 · 단계별 TaskFlow
타입을 적용한 TaskFlow에서 작업을 추가·완료 전환·삭제하세요. 타입만 바뀐 단계는 이전과 화면이 같습니다.
예제 크게 보기 (새 탭)시작·완료 파일과 적용 순서에서 이번 변경 파일의 전체 내용과 적용 순서를 확인합니다.
시작본과 이번 변경
TypeScript 고급 상태 모델링: 판별 유니온과 never 타입 테스트의 완료 체크포인트를 기준으로 이어갑니다. 변경하지 않는 파일은 그 시작 상태를 유지합니다.
src/guards/task.ts의 isTask와 isTaskArray를 만들고 src/api/tasks.ts에서 Task 및 isTaskArray를 import합니다. /api/tasks는 별도 서버가 제공하는 경로이므로 서버 없는 단독 검증은 아래 fixture를 사용합니다.
이전 체크포인트
typescript-step-15-state-modeling
이번 목표
TaskFlow의 외부 작업 데이터 경계를 type guard로 안전하게 만듭니다.
전체 파일·코드 보기
src/guards/task.ts
import type {
Task,
} from '../types/task';
export function isTask(
value: unknown,
): value is Task {
if (
typeof value !==
'object' ||
value === null
) {
return false;
}
return (
'id' in value &&
typeof value.id ===
'string' &&
'title' in value &&
typeof value.title ===
'string' &&
'completed' in value &&
typeof value.completed ===
'boolean'
);
}
export function isTaskArray(
value: unknown,
): value is Task[] {
return (
Array.isArray(value) &&
value.every(isTask)
);
}
src/api/tasks.ts
export async function getTasks():
Promise<Task[]> {
const response =
await fetch('/api/tasks');
if (!response.ok) {
throw new Error(
'작업 요청 실패',
);
}
const data:
unknown =
await response.json();
if (!isTaskArray(data)) {
throw new Error(
'작업 데이터 형식 오류',
);
}
return data;
}
테스트
정상 fixture와 잘못된 fixture를 각각 isTaskArray에 넣어 결과를 확인합니다.
npx tsc --noEmit
코어 TypeScript 과정은 여기까지입니다.
중간부터 시작하는 전체 파일과 적용 순서
시작 ZIP은 직전 프로젝트 단계 typescript-step-15-state-modeling의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.
- 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
- guards와 API 파일의 import를 모두 연결합니다. public/api/tasks에 로컬 JSON fixture를 제공해 /api/tasks를 실제 읽을 수 있게 합니다.
- 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
- 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.
추가·교체 파일
CHANGES.md— 전체 교체README.md— 전체 교체src/guards/task.ts— 추가src/api/tasks.ts— 추가public/api/tasks— 추가
실행과 확인
npm ci
npm run dev
npm run typecheck
npm test
npm run build
개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.
예상 결과: 정상 배열은 반환, HTTP 실패·객체·completed 문자열은 거부됩니다. 로컬 JSON은 읽기 전용이며 실제 저장 API가 아닙니다.
기존 ZIP 검증 기록: TypeScript strict 검사·긍정/부정 타입 계약·런타임 검사·Vite 빌드 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.
이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.
복사할 변경 파일 전체 코드
src/guards/task.ts — 전체 파일
import type {
Task,
} from '../types/task.js';
export function isTask(
value: unknown,
): value is Task {
if (
typeof value !==
'object' ||
value === null
) {
return false;
}
return (
'id' in value &&
typeof value.id ===
'string' &&
'title' in value &&
typeof value.title ===
'string' &&
'completed' in value &&
typeof value.completed ===
'boolean'
);
}
export function isTaskArray(
value: unknown,
): value is Task[] {
return (
Array.isArray(value) &&
value.every(isTask)
);
}
src/api/tasks.ts — 전체 파일
import type { Task } from '../types/task.js';
import { isTaskArray } from '../guards/task.js';
export async function getTasks():
Promise<Task[]> {
const response =
await fetch('/api/tasks');
if (!response.ok) {
throw new Error(
'작업 요청 실패',
);
}
const data:
unknown =
await response.json();
if (!isTaskArray(data)) {
throw new Error(
'작업 데이터 형식 오류',
);
}
return data;
}
public/api/tasks — 전체 파일
[{"id": "api-1", "title": "검증된 API 작업", "completed": false}]
완료 체크포인트
typescript-step-16-runtime-validation
다음 학습
다음 글부터 React 연결 구간입니다. 지금까지 만든 Task 타입을 .tsx 컴포넌트 props로 연결합니다.
다음 글: React TypeScript 기본 구조: tsx 파일과 props 타입 기준
글 소개·학습 준비·전체 목차

TypeScript · 16 / 22 · 필수 학습 · 외부 데이터 runtime 검증
외부 JSON이 실제로 Task인지 어떻게 확인할까요? unknown으로 받은 값이 객체인지, 필요한 속성이 올바른 타입인지 실행 중에 검사한 뒤 사용합니다. as Task는 이 검사를 대신하지 않습니다. 서버 없이도 정상·잘못된 JSON 입력으로 type guard의 허용 범위를 확인할 수 있습니다.
따라서 fetch로 받은 JSON에 Task 타입을 붙였다고 해서 서버가 실제로 그 구조를 반환했다는 보장은 없습니다.
이번 글에서 배우는 내용
- compile-time과 runtime 경계
- JSON response
- unknown
- type guard
- property 타입 검사
- 배열 검증
- type assertion의 위험
단독 학습 선수 지식
unknown·객체 검사·사용자 정의 type guard와 async/await을 알면 됩니다. 이전 TaskFlow 파일 없이 직접 확인 예제로 학습할 수 있습니다.
목차
이 글이 도움이 되었나요?
분류별 과정 이동 (선택)
읽음 기록
읽음 — / 전체 22개 · 필수 21개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.