Q8. 조건부 렌더링에서 never 타입 — 어떻게 좁힐까?
크루의 질문
“리액트 자체의 사용에는 어려움은 없었지만 타입을 쓰는 과정에서 에러를 많이 만났습니다. 제일 최근에는 예외 처리를 하는 과정에서
errormode라는 상태를 만들어서 상태에 따라서 에러 메시지를 띄우려고 했는데, 타입을 너무 넓게 잡아버리는 바람에 조건부 렌더링을 걸어서 에러메시지가 있는 span 태그를 렌더링 할 때 never 타입이 뜨는 문제가 있었습니다. 이러한 비슷한 결의 타입 좁히기 문제들을 해결하는 과정에서 제일 시간이 많이 들었습니다.” — 루멘·디움 페어
AS-IS 코드
🔍 읽기 전에
if (!mode) return null아래에서 mode의 타입은 무엇인가요?좁힐수록 타입이 어디로 가고 있는지 따라가봅니다.
// 루멘·디움 페어의 시나리오를 재구성한 코드
type ErrorMode = 'cardNumber' | 'expiry' | 'cvc' | null;
type ErrorMessages = {
cardNumber?: string;
expiry?: string;
cvc?: string;
};
function ErrorBanner({ mode, messages }: { mode: ErrorMode; messages: ErrorMessages }) {
// mode가 null이면 렌더할 게 없다
if (!mode) return null;
// 여기서 mode는 'cardNumber' | 'expiry' | 'cvc'로 좁혀짐.
// messages[mode]는 string | undefined가 됨 (각 필드가 optional이라서).
return <span>{messages[mode]}</span>;
}
// switch에서 모든 케이스를 처리한 뒤 default 분기 — never의 전형적인 자리
function getLabel(mode: ErrorMode) {
switch (mode) {
case 'cardNumber': return '카드 번호';
case 'expiry': return '유효기간';
case 'cvc': return 'CVC';
case null: return null;
default: {
// 이 자리에서 mode는 never. 새 union 멤버를 추가했는데
// 케이스를 빠뜨리면 여기서 컴파일 에러가 나서 누락을 알려준다.
const _exhaustive: never = mode;
return _exhaustive;
}
}
}루멘·디움이 만난 never는 보통 두 상황에서 옵니다:
- 모든 분기를 다 좁힌 뒤 — switch/if로 union의 모든 케이스를 처리한 뒤의 default 분기에서 변수 타입이 never가 됩니다. 이건 버그 신호가 아니라 exhaustiveness check를 위한 의도된 자리입니다.
- 유니온 정의가 잘못된 채로 좁혀나간 경우 —
mode === 'cardNumber' && mode === 'cvc'처럼 동시에 두 값일 수 없는 조건을 만들면 그 분기 안 변수는 never. 조건의 논리나 유니온 정의를 다시 봐야 합니다.
공식문서 단서 — TypeScript Handbook · Narrowing
이 카드는 React 영역이 아닌 TypeScript 영역입니다. 핵심 두 페이지를 봅니다.
TypeScript Handbook — Narrowing
- 원문: typescriptlang.org/docs/handbook/2/narrowing.html
- 핵심:
typeof,instanceof,in, truthiness, equality, type predicate (사용자 정의 가드), discriminated union — 7가지 narrowing 기법. 루멘·디움이 만난 문제는 보통 discriminated union 또는 type predicate로 풀 수 있다.
TypeScript Handbook — The never type
- 원문: typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type
- 핵심:
never는 불가능한 경우를 표현한다. switch에서 모든 케이스를 다룬 뒤의 default 분기, 또는 과도하게 좁힌 변수의 타입. never가 보이면 보통 타입을 좁히는 순서가 잘못되었거나, 유니온 정의를 다시 봐야 한다.
React + TypeScript 공식 문서 — Useful Hooks
- 원문: react.dev/learn/typescript
- 핵심:
useState는 초기값으로 타입을 추론하므로, union state라면 타입 인자를 명시하는 편이 안전하다. 루멘·디움의 errormode 시나리오는useState<ErrorMode | null>(null)처럼 가능한 상태 전체를 타입에 드러내면 이후 조건부 렌더링에서 좁히기 흐름을 추적하기 쉽다.
선배 PR 읽기 가이드
선배 PR 읽기 가이드 — 펼쳐보기
선배 PR — TypeScript 사용 PR (errormode 또는 status union)
PR #211 · 인라인 리뷰 모음 → , PR #189
읽는 관점: “PR #211(코난·jariita)의 inline review에 isolatedModules와 re-export 시 값/타입 구분 코멘트가 있습니다. TypeScript의 컴파일 단위가 narrowing에 어떻게 영향을 주는지가 드러납니다. PR #189는 errormode 비슷한 union을 어떻게 다뤘는지 코드에서 직접 확인하세요.”
선배 PR — 최근 8기 직전 기수의 TS 사용 패턴
PR #439 · 인라인 리뷰 모음 → , PR #361
읽는 관점: “두 PR 모두 TS로 작성. useState<T | null> 또는 discriminated union을 어떻게 사용하는지 코드를 직접 따라가세요. 또 PR #439의 src/components/common/Input/Input.tsx:12 인라인 →에서 React.InputHTMLAttributes 타입 확장에 대한 리뷰가 있는데, 이는 공통 input의 타입을 어떻게 넓히고 좁히는가에 대한 단서입니다.”
추가 읽을거리 — Dan Abramov · Kent C. Dodds
외부 글 모음 — 펼쳐보기
Dan Abramov — How Are Function Components Different from Classes?
- 원문: overreacted.io/how-are-function-components-different-from-classes
- 한 줄 요약: 직접 narrowing 글은 아니지만, 함수 컴포넌트가 렌더 시점의 props/state 값을 클로저로 캡처한다는 감각을 설명한다. errormode 같은 값이 render 시점과 event 시점에 어떻게 다르게 보일 수 있는지 이해하는 데 보조 자료로 읽을 수 있다.
Kent C. Dodds — Don’t Sync State. Derive It!
- 원문: kentcdodds.com/blog/dont-sync-state-derive-it
- 한 줄 요약: errormode를 별도 state로 두지 말고 입력값과 검증 결과로부터 파생시키면, 좁히기 자체가 필요 없어진다. 타입 좁히기 문제의 우회 답안.
Matt Pocock — ts-reset (라이브러리)
- 원문: github.com/total-typescript/ts-reset
- 한 줄 요약: 표준 TS의 너무 넓은 타입 (예:
JSON.parse→any,Array.prototype.includes의 인자 타입)을 더 좁게 재정의하는 라이브러리. 루멘·디움이 만난 “타입을 너무 넓게 잡아버린” 함정의 한 갈래 해결책. Total TypeScript 의 Matt Pocock이 만들었습니다.
연관 PR 더 보기
이 주제는 TS 사용 PR에 한정되어 있어 부록에 별도 매핑되어 있지 않습니다. TypeScript를 사용한 PR들(2022년 이후 대부분)은 부록에서 변경 파일에 .tsx가 보이는 PR을 골라 따라갈 수 있습니다.
루멘·디움 페어가 시간을 가장 많이 쓴 문제가 타입 좁히기였다는 사실 자체가 학습 포인트입니다 — errormode를 별도 state로 두는 결정 자체가 Q5 검증·에러 책임과 만납니다. 한 페어의 페인 포인트가 두 카드의 교집합에서 풀릴 수 있습니다.





