에러 메시지 읽는 법 - "빨간 줄"에 압도되지 않기
공식문서 기반의 타입스크립트 입문기
들어가며
tsc를 실행하면 뜨는 빨간 줄은, 처음에는 “무슨 말인지 모르는 외계어”처럼 느껴지기 쉽습니다. 특히 TypeScript는 오류 메시지가 길게 늘어지는 편이라, 내용을 찬찬히 들여다보지 않으면 어디가 문제인지 감이 오지 않는 경우가 많았고요. 요즘은 AI에 메시지를 복사/붙여넣어 물어보면 빠르게 해결되지만, 언제나 그렇게 할 수 있는 것도 아니어서, 그래도 메시지 구성과 흐름은 스스로 익혀 두면 조금 더 빠르게 고칠 수 있다는 생각에 이 편을 준비했습니다. (스스로 수정할 수 있으면 AI 요청 토큰값도 아낄 수 있어요.)
TS2322: Type '{ id: number; nickname: string; profilePicture?: { url: string; }; }' is not assignable to type 'UserProfile'.
Property 'profilePicture' is missing in type '{ id: number; nickname: string; }' but required in type '{ url: string; }'.위 같은 메시지를 보면 “필드가 없대?”라고 뭉뚱그려 해석하게 되지만, 실제로는 UserProfile의 중첩된 profilePicture.url까지 내려가서 한정된 오류입니다. 여기서 중요한 건 “맨 앞줄”이 아니라, 계층적으로 내려가는 “에러 트리”를 따라가는 습관입니다.
이 편에서는 "TypeScript의 빨간 줄(에러 메시지)을 해석하는 법"을 알려드립니다. 복잡한 에러 메시지를 한 줄씩 읽고, 어떤 타입들이 문제인지 파악해 "이걸 고치면 어디가 안전해질까?"라고 생각할 수 있게 돕습니다.
TypeScript의 제안
TypeScript는 오류를 뱉을 때 다음과 같은 순서로 정보를 제공합니다.
- 기본 메시지 (TS코드): 어떤 종류의 오류인지 예:
TS2322: Type 'X' is not assignable to type 'Y'. - 세부 경로: 오류가 발생한 표현식의 위치(
src/app.ts:42)와 해당 표현식을 평가했을 때의 추론 결과. - 추론 체인: 내부적으로 어떤 타입 추론이 연결되었는지를 보여줍니다. (흔히 화살표 -> 형태로 안내)
- 제안/대체: 오류를 해소하려면 어떤 타입을 수정해야 하는지 힌트를 제공합니다.
이 구조를 머릿속에 넣고 “마침표에서 멈추지 말고, 화살표를 따라 내려간다”는 사고를 가지면 빨간 줄이 공포가 아니라 힌트가 됩니다.
심층 분석
1) 에러 트리를 따라 내려가기
Type 'A' is not assignable to type 'B'. ← 여기서 멈추지 마세요
Type 'A' is missing the following properties from type 'B': x, y ← 어떤 필드를 찾는지첫 줄만 보면 “할당이 안 돼” 정도만 알 수 있지만, 두 번째 줄부터는 “무슨 필드가 부족한지”를 알려줍니다. 그 필드를 하나씩 펼쳐보다 보면 결국 “여기서 막혀 있구나”라는 실마리를 잡을 수 있고, 그러면 빨간 줄이 “무엇이 부족한지 알려주는 안내”처럼 느껴집니다.
2) 제네릭이 섞인 에러 읽기
Type 'Promise<string>' is not assignable to type 'Promise<UserProfile>'.
Type 'string' is not assignable to type 'UserProfile'.제네릭 에러는 “먼저 어떤 제네릭을 쓰는지”를 보면 헷갈릴 수 있으니, 아래쪽(마지막 줄)의 타입부터 거꾸로 따라가세요. “어느 시점에서 string이 UserProfile이 아니라고 한정됐지?”를 질문하듯 따라가면, 제네릭이 어떻게 연결되어 있는지 감이 잡힙니다.
3) tsc --noEmit --pretty false와 --traceResolution
--pretty false를 붙이면 컴파일러가 내부에서 어떤 타입으로 추론했는지를 그대로 보여주고, --traceResolution은 “이 파일이 어디서 어떤 선언을 가져왔나”를 안내합니다. 에러 메시지를 통째로 복사해 공식문서나 검색에 붙여넣어 보면 “이건 전에 본 옵션 결과랑 같다”는 감이 생기고, 그만큼 원인을 좁히기 쉬워집니다.
실전 패턴 (In React)
Add: error → refactor loop
const profile = await fetchUser(id); // error TS2322
// ↳ fetchUser의 반환 타입을 `ProfileResponse`에서 `UserProfile`로 바꾸면 해결TypeScript가 빨간 줄을 띄울 때, “이 줄을 수동으로 고치기”보다 “해당 타입을 선언한 위치”로 이동하는 것이 중요합니다. 에러 메시지를 복사해서 파일 전체에서 ProfileResponse를 찾고, interface를 열어 profilePicture 여부를 확인하세요.
JS 대비: console.log vs tsc
JS에서는 console.log(profile.profilePicture?.url)을 여러 번 찍어가며 디버깅했지만, TypeScript는 “이 표현식에서 타입이 무엇인지”를 알려줍니다. 같은 워크플로우를 TypeScript 에러로 옮기는 방법은 다음과 같습니다.
- 에러 줄을 클릭 → 타입을 hover.
Go to definition으로 타입 선언으로 이동.- 타입 정의를 읽고,
as const/| undefined등으로 명시적인 조정.
실제 모듈 진단 워크플로우
에러가 특정 파일에서 반복될 때는 메시지 하나보다 “파일 전체가 무거운지”를 먼저 파악하는 것이 좋습니다. tsc --diagnostics로 타입 체크 시간이 길게 잡히는 파일을 우선 확인하고, tsc --showConfig로 적용 중인 tsconfig를 수시로 확인해 보세요. 그래도 감이 오지 않으면 tsc --explainFiles를 돌려 모듈 간 타입 캐시와 설명을 들여다보면 “어디서 예상치 못한 타입이 끼어들었는지”를 빠르게 찾을 수 있습니다.
에러 진단 흐름:
tsc --pretty false로 메시지를 얻고, 가장 구체적인 타입부터 거꾸로 따른다.tsc --diagnostics를 통해 체크 시간이 긴 파일부터 살펴보고, 해당 파일 안에서 에러 메시지를 중첩해서 읽는다.tsconfig의paths,baseUrl,skipLibCheck설정이 잘못된 타입을 참조하고 있지 않은지 확인한다.tsc --explainFiles로 어떤 파일이 어떤 선언을 가져오는지 확인하고, “타입이 어디에서 왔는지”라는 맥락을 머릿속에 둔다.Type 'X' is not assignable to type 'Y'처럼 메시지가 나온다면,Y의 선언 위치로 가extends/infer/Extract등을 사용해 임시 타입 별칭을 만들며 좁혀 본다.
에러 코드 대응 매트릭스
| 에러 코드 | JS에서 흔히 실수한 점 | TypeScript가 던지는 힌트 | 따라가야 할 순서 |
|---|---|---|---|
| TS2322 | 반환 타입을 바꿨는데 필드를 하나 놓쳤다 | “필드가 없다”는 메시지 뒤에 타입 트리를 따라 인터페이스 내부를 확인 | hover → go to definition → 빠진 필드 확인 |
| TS2345 | 잘못된 인수를 넣어도 런타임까지 파악하지 못함 | “Argument of type 'X' is not assignable…” 구문으로 호출자/피호출자 타입 비교 | 함수 정의 → 파라미터 제네릭/extends 확인 |
| TS18048 | lookup한 키가 keyof이나 lookup 대상에 없음 | “Type 'never'”은 lookup 실패 신호. Extract로 단계별 타입 별칭 만들어 좁히기 | 관련 타입을 Extract로 하나씩 좁혀 보기 |
이 표는 각각의 에러 코드가 “JS에서 어떤 실수를 했을 때” 뜨고, TypeScript가 어떤 말을 덧붙여 안내하는지를 대응시킨 것입니다. 해석 순서는 오른쪽 열을 따라 “먼저 해당 줄을 hover → 정의로 이동 → 부족한 필드/제네릭을 확인”처럼 실제로 따라갈 수 있는 단계를 제시합니다.
에러 추적 체크리스트
- 가장 마지막 줄(가장 구체적인 타입)을 먼저 읽는다.
- 해당 타입 정의로 이동해 필요한 필드/제네릭 조건을 확인하고, 필요한 경우
type Narrowed = Extract<...>처럼 별칭을 만든다. - 제네릭이 섞여 있다면
tsc --pretty false로 타입 이름을 명확히 하고,Infer/Extract로 단계별로 좁혀 본다. skipLibCheck/skipDefaultLibCheck가 켜져 있다면 끄고, 라이브러리가 숨기는 오류를 다시 보이게 한다.paths/baseUrl/isolatedModules가 타입을 엉뚱하게 가져오고 있지 않은지 확인한다.
체크리스트를 실제 작업 노트처럼 쓰면, 다음에도 같은 에러가 뜰 때 “여기까지 확인했고 여기서 더 깊이 보겠다”는 흐름이 명확해집니다.
케이스 스터디: useState의 nullable 타입
React에서 const [profile, setProfile] = useState(null);처럼 작성하면, setProfile(profile)에서 TS2322: Type 'null' is not assignable to type 'UserProfile' 오류가 나타나기 쉽습니다. JS에서는 “아직 값이 없다”고 그냥 넘기지만, TypeScript는 useState가 null을 제네릭으로 추론하기 때문에 이후 모든 호출이 null 타입에 묶입니다. 이 메시지를 읽을 때 다음 단계를 따릅니다.
hover로useState의SetStateAction제네릭을 확인하고, 내부에서null을 허용하는 플로우인지 판단.type NullableProfile = UserProfile | null같은 별칭을 만들어useState<NullableProfile>(null)로 명시하고 null 가능성을 선언.setProfile이 실행되는 곳마다profile?.대신profile &&처럼 조건을 명시하거나if (!profile) return으로 분기시켜, TS에게 “null을 다루겠습니다”라는 의도를 알려준다.
이 시나리오는 “메시지를 끝까지 따라가면 SetStateAction 내부 제네릭”까지 도달해야 하며, JS에서 null을 관대하게 넘기던 습관이 어떻게 빨간 줄로 나타나는지를 보여줍니다.
참고:
tsc --explainFiles를 활용하면 어떤 파일이 어떤 선언을 참조하는지 모듈 분석을 할 수 있고, 의도치 않은 타입 공유를 발견하는 데 도움이 됩니다.
함정
tsconfig에서skipLibCheck를 켜면 라이브러리 오류가 가려지지만, 실제 문제의 근원을 숨길 수 있습니다. 가능하면 끄고, 필요한 경우paths/baseUrl을 정리하죠.- 타입 트레이스가 너무 깊어 보이면,
--diagnostics를 켜서 각 파일의 타입 체크 시간을 확인하고, 진짜 문제가 있는 파일을 최우선으로 처리하세요.
예상 질문
Q1. 에러 코드가 너무 많아서 어디서부터 읽어야 하나요?
TypeScript는 가장 구체적인 코드 하나와, 그 기준으로 생성된 위쪽 메시지를 함께 보여줍니다. “강체 유형”을 먼저 보고, 그다음 실제 표현식에서 hover하여 추론된 타입을 확인하면 됩니다. 그리고 반드시 “빠진 필드”부터 확인하고, 그 필드가 무엇을 참조하는지 trace하면 마음이 정리됩니다.
Q2. Type 'X' is not assignable to type 'Y'에서 Y가 너무 복잡하면?
Y가 제네릭이거나 인터섹션이라면, T 키워드를 사용해 type Narrowed = Extract<Y, { kind: "xxx" }>처럼 한 단계씩 좁히는 별칭을 만들어서 컴파일러에게 “이것부터 보면 된다”고 알려주세요.
Q3. 에러 메시지가 영어라 읽기 어렵습니다.
선형적인 구조를 말로 옮기기 위해, 메시지 앞에 있는 TS#### 코드를 복사해서 공식 문서에서 검색하면 한글 설명을 살짝 덧붙인 자료를 찾을 수 있습니다. 가끔 tsc의 내부 lib.d.ts 주석이 힌트가 되기도 합니다.
요약
빨간 줄은 “여기서 뭔가 틀렸다”는 신호입니다. 그 신호를 듣고 곧바로 코드를 고치지 말고, 에러 메시지가 설명하는 타입 트리를 거꾸로 더듬어 보세요. inference가 복잡할수록, 에러 트리가 길수록, TypeScript는 실제로 우리가 건드리면 되는 필드를 안내해줍니다. 이 패턴을 익히면 JS 디버깅보다 한 줄 먼저 문제를 잡아낼 수 있습니다.
참조
- TypeScript 공식문서