본문 바로가기
C.W.K.
Stream
Lesson 05 of 05 · published

제어 흐름 분석: 코드가 증명한 타입을 줄마다 추적해

~10 min · narrowing, control-flow-analysis, inference

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"좁히기 연산자가 낱말이라면, 제어 흐름 분석은 그 낱말을 문장으로 묶는 문법이야."

컴파일러가 경로를 따라간다

TypeScript는 if와 switch의 각 분기, 대입, return, throw를 따라가며 프로그램의 어느 지점에 어떤 값이 도달할 수 있는지 계산해. 변수의 선언 타입은 그대로지만 각 줄에서 사용할 수 있는 더 좁은 타입은 흐름에 따라 달라진다.

if (x === null) return 다음 줄에는 null인 경로가 도달할 수 없어. 그래서 함수 나머지에서 x는 자동으로 null이 제거된 타입이 돼. 사람이 코드를 읽으며 하는 추론을 컴파일러가 보수적으로 따라 하는 거야.

조기 반환이 중첩을 줄여

잘못된 경우와 빈 경우를 함수 위에서 먼저 반환하면 아래의 주요 흐름은 좁은 타입으로 평평하게 이어져. else 안에 핵심 로직을 깊게 넣는 것보다 읽기 쉽고, 타입 검사도 함수 끝까지 같은 증거를 유지한다.

대입도 가능한 타입을 바꾼다

let name: string | null이 null일 때 기본 문자열을 대입하면 그 뒤의 name은 string이야. 컴파일러는 단순 조건뿐 아니라 값이 실제로 다시 정해졌다는 사실도 반영해. 다만 변수의 선언 타입 자체가 바뀌는 것은 아니어서 이후 null을 다시 대입할 수는 있어.

객체 속성과 함수 호출은 더 보수적이야

지역 변수는 쓰기 지점을 모두 볼 수 있지만 객체 속성은 다른 함수가 바꿀 수 있어. box.value를 좁힌 뒤 box를 외부 함수에 넘기면, 호출이 속성을 변경했을 가능성 때문에 좁히기가 유지되지 않을 수 있다.

필요하다면 좁힌 속성을 지역 상수에 복사하거나, 변경하지 않는다는 계약을 타입과 설계로 분명히 해. 컴파일러가 괜히 의심하는 것이 아니라 별칭을 통한 변경 가능성을 인정하는 거야.

대입을 통한 좁히기는 재할당할 수 있는 let에서 작동하고 const에는 해당하지 않아. 또 선언 자체를 바꾸는 것이 아니라 현재 함수의 특정 지점에서만 가능한 타입을 줄이는 분석이야.

타입 오류를 단언으로 누르기 전에 코드 흐름이 충분한 증거를 주는지 봐. 조기 반환과 명확한 분기가 많은 단언을 없애 준다.

Code

CFA 3가지 형태 — early return, assignment, throw·typescript
// Early return 이 함수 나머지 narrow.
function process(x: User | null) {
  if (x === null) return;
  // x: User
  console.log(x.name);
}

// CFA 가 각 assignment 추적.
function normalize(x: string | null): string {
  if (x === null) {
    x = 'default';   // 함수 나머지 narrow
  }
  return x;          // x: string
}

// Throw 도 narrow (throw 분기 절대 return 안 함).
function must(x: User | null): User {
  if (x === null) {
    throw new Error('missing user');
  }
  return x;          // x: User
}
CFA 가 리셋할 때 — 함수 호출과 property·typescript
// 함수 호출이 property 의 narrowing 리셋 가능.
interface Box { value: string | null }

function maybeReassign(b: Box) {
  b.value = null;     // 함수가 이거 할 수 있어 — compiler 가 확신 못 함
}

function process(b: Box) {
  if (b.value !== null) {
    // b.value: string
    maybeReassign(b);
    // b.value 가 string | null 로 다시 widening — 함수 호출이 mutate 했을 수
    // b.value.toUpperCase();   // ❌ 이제 null 가능
  }
}

// Local 변수엔 (compiler 가 모든 write 볼 수 있는), narrowing 이
// 함수 호출 가로질러 보존. Object property 엔 (함수가 reassign 가능한),
// narrowing 이 리셋.

External links

Exercise

string | number | null을 받는 handle 함수를 작성해. null이면 'invalid'를 일찍 반환하고, 숫자면 형식화한 값을 일찍 반환한 뒤 마지막에 남은 문자열을 처리해.
Hint
null을 먼저 반환하면 남은 값은 문자열이나 숫자야. 숫자 분기까지 일찍 반환한 뒤에는 문자열만 남는다는 사실을 컴파일러가 추적해야 해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.