TypeScript를 실무에 적용하는 방법 – DOM/React/Node에서 타입이 깨지는 지점 정리
DOM·React·Node 경계에서 타입이 깨지는 지점을 짚고 union 상태, 런타임 검증, 외부 입력 처리 전략을 다룸
Posted
By okorion
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는 최후의 수단”이다
결론: 타입이 없을 때는 “정확성 < 안전성”을 선택하라.
우선순위
- 공식 타입 포함 여부 확인
@types/*존재 여부 확인- 최소 선언으로
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 도입/강화 단계별 순서
결론: 한 번에 완벽하게 하려다 실패한다.
- strict ON (또는 점진 강화)
- 외부 입력을
unknown으로 받기 - DOM/req.body/ref 경계면부터 정리
- 상태를 union으로 재설계
- 외부 라이브러리 타입 정비
체크리스트
- DOM 접근에서
as를 무조건 쓰고 있지 않은가? - 상태 모델에 불가능한 조합이 남아 있지 않은가?
- React props가 계약으로서 충분히 명확한가?
- 서버에서 요청 본문을 런타임 검증하는가?
- declare로 “추측 타입”을 쓰고 있지 않은가?
요약 5줄
- 타입이 깨지는 지점은 항상 시스템 경계면이다.
- DOM과 req.body는 실패 가능성을 전제로 설계해야 한다.
- 상태는 union으로 모델링해야 불가능한 케이스가 제거된다.
- React와 Express에서 타입은 “검증 이후”에만 신뢰 가능하다.
- declare는 최후의 수단이며, 정확성보다 안전성이 우선이다.
자기점검 질문 5개
- 내 코드에서 가장 위험한 경계면은 어디인가?
- 타입 단언(
as)을 제거할 수 있는 곳은 없는가? - 상태 모델에 논리적으로 불가능한 조합은 없는가?
- 서버 요청 검증을 타입 선언으로 대체하고 있지 않은가?
- 외부 라이브러리 타입을 실제 API와 비교해본 적이 있는가?
실전 미션 3개
- DOM 접근 코드 하나를 type guard 기반으로 리팩터링하라.
- React 상태 하나를 discriminated union으로 재설계하라.
- Express 엔드포인트 하나에 런타임 검증 미들웨어를 추가하라.
This post is licensed under CC BY 4.0 by the author.
