FEInterview Prep

Tistory

왜 타입스크립트는 당신을 구해주지 못하는가

TypeScript 의 초록색 체크는 *코드가 자기 자신과 일관됨* 을 의미할 뿐, *외부 데이터가 정말 그 모양인지* 는 보장하지 않는다. 안전성은 경계(boundary) 에서 검증되어야 한다.

2025-12-04·7분 읽기
TypeScript아키텍처
원문 보기 ↗

핵심 요약

안전한 프런트엔드 아키텍처는 (1) 인프라 레이어에서만 외부 데이터(API, localStorage, URL, 사용자 입력)를 받아 (2) Zod/io-ts/Effect 로 파싱하여 검증된 타입을 만든 뒤 (3) 도메인 레이어는 그 검증된 타입만 다루도록 분리한다. as 단언과 any벤치마크 안전망 이 없는 한 PR 에서 차단한다. 컴파일된다 ≠ 동작한다 는 사고를 팀의 디폴트로 만들고, 실패 경로 테스트 를 항상 함께 작성한다.

TypeScript 를 '런타임을 막아주는 보안 장치' 로 보는 시각에서 '내부 일관성을 보장하는 컴파일타임 린터' 로 시각을 바꾼다. 진짜 안전성은 어디까지가 외부 데이터인가 를 명확히 그어 인프라 레이어에서 파싱·검증 하고, 도메인 레이어로는 검증된 값만 흘려보내는 아키텍처에서 나온다.

프로덕션 버그의 큰 비중은 타입은 맞지만 값은 틀린 데이터에서 시작된다.

  • response.json() 은 시그니처상 Promise<any> 인데, 반환 타입에 User 만 적으면 TS 는 거짓말을 그대로 믿는다
  • as, as unknown as T, @ts-ignore, any 같은 escape hatch 가 코드베이스 어딘가에 하나만 있어도 그 지점부터는 안전하지 않다
  • useEffect 안에서 fetch + setState(data as User) 처럼 인프라/도메인이 한 컴포넌트에 뭉치면 검증할 위치 자체가 사라진다

면접에서 '타입스크립트로 어떻게 안전성을 보장하나' 라는 질문에 Zod 같은 런타임 파서로 경계에서 검증 하고 brand 타입 으로 검증된 값만 도메인에 들어오게 강제한다는 답을 할 수 있어야 한다.

학습 포인트

면접 답변으로 연결할 학습 포인트입니다.

반환 타입은 *암묵적 캐스팅* 이다

함수의 반환 타입을 적는 순간, 본문에서 어떤 값이 나오든 TS 는 그 타입 으로 믿는다. response.json() 처럼 실제로는 any 를 반환하는 경우 특히 위험하다.

// ❌ 거짓말 — fetch 응답이 어떤 모양이든 User 라고 단언
async function getUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json(); // any → User 로 암묵 캐스팅
}

// ✅ 경계에서 파싱
import { z } from 'zod';
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});
async function getUser(id: number): Promise<z.infer<typeof UserSchema>> {
  const res = await fetch(`/api/users/${id}`);
  return UserSchema.parse(await res.json()); // 런타임 검증
}
return typeany 전염Zod런타임 파싱
자주 하는 오해

Promise<User> 만 적고 안심하는 것. TS 는 fetch 응답이 진짜 User 인지 검사할 능력이 없다.

Escape hatch 는 *팀의 가장 약한 부분* 만큼 안전하다

any, @ts-ignore, as unknown as T 는 한 곳에서만 쓰여도 그 지점 이후 모든 안전성 가정을 무너뜨린다.

우회 경로위험도대응
any매우 높음noImplicitAny + ESLint no-explicit-any 로 차단
as User높음검증 함수(타입 가드) 로 대체
as unknown as T매우 높음PR 차단 + 코드리뷰 항목
@ts-ignore매우 높음@ts-expect-error + 만료 일자 코멘트

ESLint 규칙으로 자동 차단하지 않으면 대규모 코드베이스에서 누군가는 반드시 빠뜨린다.

any@ts-ignoreas unknown as타입 가드
자주 하는 오해

'테스트 코드에서만 any 쓰자' 같은 예외를 만드는 것. 한 번 허용하면 곧 도메인 코드로 번진다.

Brand 타입으로 *검증된 값만* 도메인에 들어오게

런타임 검증으로 만든 값에 컴파일타임 브랜드 를 붙이면 검증을 거치지 않은 raw 값을 도메인 함수에 못 넘긴다.

type Email = string & { readonly __brand: 'Email' };

function parseEmail(input: string): Email {
  if (!/^[^@]+@[^@]+$/.test(input)) throw new Error('invalid');
  return input as Email;
}

function sendWelcome(email: Email) { /* ... */ }

sendWelcome('plain string'); // ❌ 컴파일 에러
sendWelcome(parseEmail(form.email)); // ✅ 검증된 값만
brand 타입nominal typing타입 가드도메인 레이어
자주 하는 오해

Zod 로 검증만 해두고 결과를 평범한 string 으로 흘려보내는 것. 검증되지 않은 값과 구분되지 않으면 결국 검증을 잊는 곳이 생긴다.

읽는 순서

  1. 1이론

    Zod 의 .parse() vs .safeParse()z.infer<> 동작을 정독하고, brand 타입 패턴 한 페이지로 정리한다.

  2. 2구현

    기존 API 클라이언트의 fetch 함수 3개를 Zod 스키마로 감싸 본다. 응답 형태가 다른 케이스를 일부러 만들어 safeParse 가 어떻게 실패하는지 관찰한다.

  3. 3실무

    ESLint 에 no-explicit-any, no-non-null-assertion, consistent-type-assertions 를 켜고 한 PR 에서 위반을 모두 잡는다.

  4. 4설명

    팀에 '타입스크립트와 타입 안전성은 다르다' 라는 주제로 10분 발표를 준비. 데모로 as User 가 통과하는 코드 vs Zod 로 잡히는 코드를 비교.

면접 연결 질문

medium`as User` 와 `parseUser(data)` (타입 가드) 의 차이를 *런타임/컴파일타임* 관점에서 설명하고, 어떤 경우에 무엇을 써야 하는지 말해보세요.
힌트

[감점 답변] 'as 는 캐스팅, parse 는 검증' 만 말하기. [좋은 답변] as 는 컴파일타임에 타입을 강제 할 뿐 런타임에 아무 일도 일어나지 않는다 — 거짓말이 가능하다. 타입 가드는 런타임에 형태를 검사하고 반환값으로 narrowing 까지 제공한다. 외부 데이터에는 항상 가드/파서, 내부에서 이미 검증된 값 의 타입을 좁힐 때만 as.

hardZod 로 모든 fetch 응답을 검증하는 게 비싸지 않나요? 성능과 안전성의 트레이드오프를 어떻게 잡습니까?
힌트

[감점 답변] '안전이 최우선'. [좋은 답변] (1) 대부분의 페이로드는 KB 수준이라 파싱 비용은 ms 미만 — fetch 지연에 묻힌다. (2) 핫패스(거대 리스트) 만 strict 대신 passthrough + 필드 단위 검증으로 완화. (3) 백엔드 OpenAPI 에서 스키마를 생성하면 타입과 검증을 동시 에 얻어 유지보수 비용도 작다.

medium코드베이스에서 `any` 를 점진적으로 제거하기 위한 단계적 전략을 제시해보세요.
힌트

[감점 답변] 'any 를 다 unknown 으로 바꾼다'. [좋은 답변] (1) noImplicitAny 켜고 신규 코드부터 차단. (2) eslint-plugin-import + no-explicit-any경고 로 두고 기존 위반 카운트를 측정. (3) 가장 위험한 경계(API 클라이언트, localStorage, URL parser) 부터 Zod 로 교체. (4) @ts-expect-error 에 만료 일자를 코멘트로 강제하고, 만료 시 CI 에서 차단.

자기 점검

본인 프로젝트에서 *아무도 검증하지 않는* 외부 데이터 진입점을 3개 이상 찾아보세요.
API 응답localStorageURL paramsform input
자주 하는 오해

'백엔드가 검증했으니 프런트는 신뢰해도 된다'. 백엔드 변경/스키마 마이그레이션/캐시된 stale 응답에서 깨진다.

`Promise<User>` 라고 적힌 함수의 반환값이 실제로는 `null` 일 수 있는 시나리오 두 가지를 설명해보세요.
any 전염404스키마 변경
자주 하는 오해

'타입이 있으니 그대로 믿어도 된다'. 실제로는 검증 없는 타입 어노테이션은 주석에 가깝다.