Post

TypeScript를 실무에 적용하는 방법 – DOM/React/Node에서 타입이 깨지는 지점 정리

DOM·React·Node 경계에서 타입이 깨지는 지점을 짚고 union 상태, 런타임 검증, 외부 입력 처리 전략을 다룸

TypeScript를 실무에 적용하는 방법 – DOM/React/Node에서 타입이 깨지는 지점 정리

결론: 타입이 깨지는 지점은 항상 “경계면”이며, 여기서 설계가 갈린다

TypeScript는 내부 로직을 안전하게 만든다. 하지만 DOM·React props·HTTP 요청·외부 라이브러리처럼 시스템 밖에서 값이 들어오는 순간, 타입은 쉽게 무너진다. 실무에서 중요한 건 “타입을 쓰느냐”가 아니라 어디서 타입을 믿지 말아야 하는지를 아는 것이다.


DOM을 다룰 때 타입 단언이 늘어나는 이유는 “선택 결과가 불확실”하기 때문이다

결론: DOM API는 실패 가능성이 기본값이므로, 타입 단언 대신 좁히기 패턴을 써야 한다.

왜 문제가 생기나

  • querySelector는 항상 Element | null
  • HTML 구조는 런타임에 바뀔 수 있음
  • TS는 DOM 구조를 확정할 수 없음

흔한 실수

1
2
const input = document.querySelector("#email") as HTMLInputElement;
input.value = "test"; // ❌ 런타임에서 null이면 즉시 터짐

안전 패턴 (type guard 포함)

1
2
3
4
5
6
7
8
9
10
11
function isInput(el: Element | null): el is HTMLInputElement {
  return el instanceof HTMLInputElement;
}

const el = document.querySelector("#email");

if (!isInput(el)) {
  throw new Error("email input not found");
}

el.value = "test"; // ✅ 안전

판단 기준

  • DOM 접근은 항상 실패 가능
  • 단언(as)은 “구조가 고정된 템플릿”에서만 허용

상태 관리에서 타입의 가치는 “불가능한 상태를 제거”하는 데 있다

결론: 타입이 있으면 상태 조합 중 애초에 존재하면 안 되는 케이스를 제거할 수 있다.

문제

  • boolean/nullable 상태가 늘어날수록 분기 폭발
  • “이 상태에서 이 값이 있을 수 있나?”를 사람이 기억해야 함

개선: Discriminated Union 기반 상태

1
2
3
4
5
type LoadState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string }
  | { status: "error"; error: Error };
1
2
3
4
5
function render(state: LoadState) {
  if (state.status === "success") {
    state.data; // string 보장
  }
}

이점

  • 불가능한 접근이 컴파일 단계에서 차단
  • UI/스토어 로직 단순화

React에서 타입 설계의 핵심은 “props·이벤트·ref는 경계면”이라는 인식이다

결론: React 타입 문제의 대부분은 “외부에서 주입되는 값”을 과신해서 발생한다.

React 컴포넌트 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Props = {
  onSubmit: (value: string) => void;
};

function Form({ onSubmit }: Props) {
  const inputRef = React.useRef<HTMLInputElement>(null);

  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (!inputRef.current) return;
    onSubmit(inputRef.current.value);
  };

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>Submit</button>
    </>
  );
}

흔한 실수

  • useRef<any> 사용 → DOM 타입 정보 상실
  • 이벤트 타입을 any로 처리
  • props를 너무 느슨하게(string | undefined) 설계

판단 기준

  • props: 외부 계약 → 최대한 명확히
  • state: 내부 → union으로 상태 제한
  • ref: null 가능성 항상 처리

Node/Express에서 타입이 깨지는 지점은 req/res “입구”다

결론: 서버에서는 요청 본문을 절대 타입으로 믿지 마라.

문제

  • req.body는 런타임 데이터
  • 타입 선언만으로는 실제 구조 보장 불가

Express 컨트롤러 + 미들웨어 타입 흐름

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Request, Response, NextFunction } from "express";

type CreateUserBody = {
  email: string;
};

function validateCreateUser(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const body = req.body as Partial<CreateUserBody>;
  if (typeof body.email !== "string") {
    return res.status(400).send("Invalid body");
  }
  next();
}

function createUser(req: Request<{}, {}, CreateUserBody>, res: Response) {
  req.body.email; // string 보장
  res.send("ok");
}

핵심

  • 미들웨어에서 런타임 검증
  • 검증 이후에만 “강한 타입” 부여

외부 JS 라이브러리 통합의 핵심은 “declare는 최후의 수단”이다

결론: 타입이 없을 때는 “정확성 < 안전성”을 선택하라.

우선순위

  1. 공식 타입 포함 여부 확인
  2. @types/* 존재 여부 확인
  3. 최소 선언으로 declare 사용

최소 declare 예제

1
2
3
4
// globals.d.ts
declare module "legacy-lib" {
  export function run(value: string): void;
}

위험 경고

  • 실제 API와 불일치해도 컴파일 통과
  • 라이브러리 업데이트 시 조용히 깨짐

원칙

  • declare는 표면적 타입만
  • 내부 구현 추정 금지

“타입만 믿고 런타임 검증을 안 해서” 터지는 대표 사례

결론: 타입은 약속이지, 사실이 아니다.

사례

  • 프론트: API 응답을 interface로만 믿음
  • 백엔드: req.body 타입 선언만 하고 검증 없음
  • 결과: 런타임 undefined 접근, 장애 발생

방어 전략

  • 모든 경계면에서 unknown → 좁히기
  • 프론트/백엔드 공통으로 런타임 검증 단계 유지
  • 타입은 검증 이후에만 강하게

실무 체크리스트: TS 도입/강화 단계별 순서

결론: 한 번에 완벽하게 하려다 실패한다.

  1. strict ON (또는 점진 강화)
  2. 외부 입력을 unknown으로 받기
  3. DOM/req.body/ref 경계면부터 정리
  4. 상태를 union으로 재설계
  5. 외부 라이브러리 타입 정비

체크리스트

  • DOM 접근에서 as를 무조건 쓰고 있지 않은가?
  • 상태 모델에 불가능한 조합이 남아 있지 않은가?
  • React props가 계약으로서 충분히 명확한가?
  • 서버에서 요청 본문을 런타임 검증하는가?
  • declare로 “추측 타입”을 쓰고 있지 않은가?

요약 5줄

  1. 타입이 깨지는 지점은 항상 시스템 경계면이다.
  2. DOM과 req.body는 실패 가능성을 전제로 설계해야 한다.
  3. 상태는 union으로 모델링해야 불가능한 케이스가 제거된다.
  4. React와 Express에서 타입은 “검증 이후”에만 신뢰 가능하다.
  5. declare는 최후의 수단이며, 정확성보다 안전성이 우선이다.

자기점검 질문 5개

  1. 내 코드에서 가장 위험한 경계면은 어디인가?
  2. 타입 단언(as)을 제거할 수 있는 곳은 없는가?
  3. 상태 모델에 논리적으로 불가능한 조합은 없는가?
  4. 서버 요청 검증을 타입 선언으로 대체하고 있지 않은가?
  5. 외부 라이브러리 타입을 실제 API와 비교해본 적이 있는가?

실전 미션 3개

  1. DOM 접근 코드 하나를 type guard 기반으로 리팩터링하라.
  2. React 상태 하나를 discriminated union으로 재설계하라.
  3. Express 엔드포인트 하나에 런타임 검증 미들웨어를 추가하라.

This post is licensed under CC BY 4.0 by the author.