Conceptly
← 전체 목록
🔀

Pattern Matching

기반 원칙값의 모양을 보고 분기하며 내부 값을 꺼내는 방식

패턴 매칭은 값의 실제 모양을 보고 분기하면서, 그 안에 들어 있는 데이터까지 한 번에 꺼내 쓰는 방식입니다. 단순히 if (tag === "success")처럼 조건만 검사하는 데서 끝나지 않고, Success data라면 그 자리에서 data를 바인딩해 바로 다음 계산으로 넘길 수 있습니다. 함수형 프로그래밍에서는 값을 ADT로 닫아 두고, 패턴 매칭으로 그 값을 소비하는 조합이 정말 자주 반복됩니다. 그래서 패턴 매칭은 단순 제어문이라기보다 데이터 모델과 맞물린 분기 도구에 가깝습니다.

아키텍처 다이어그램

🔄 프로세스 다이어그램

점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다

왜 필요한가요?

태그를 따로 확인한 다음, 다음 줄에서 payload를 다시 꺼내 쓰는 코드는 생각보다 자주 어긋납니다. 특정 경우를 빼먹어도 런타임 전까지는 드러나지 않고, 필드 이름을 잘못 읽어도 모델과 분기가 느슨하게 연결돼 있어 실수가 숨어들기 쉽습니다. 값의 모양에 따라 다르게 행동하는 코드가 많아질수록 이런 균열은 더 커집니다. 분기 기준과 데이터 추출을 한 문법으로 묶고, 가능한 경우를 빠짐없이 처리하게 도와주는 도구가 필요할 때 패턴 매칭이 힘을 발휘합니다.

왜 이런 방식이 등장했나요?

패턴 매칭은 ML 계열 언어와 Haskell, Erlang, Elixir, Scala, Rust 같은 함수형·다중 패러다임 언어에서 오래전부터 핵심 기능이었습니다. 이 언어들이 강조한 것은 '프로그램은 값을 읽는 방식만으로도 안전성이 달라진다'는 점입니다. ADT가 널리 쓰이기 시작하면서, 그 값을 안전하게 소비하는 문법인 패턴 매칭도 함께 중요해졌습니다. 최근 TypeScript의 discriminated union, Swift의 switch, Rust의 match가 주목받는 이유도 같습니다. 단순한 조건문보다, 타입과 직접 연결된 분기가 더 안전하고 읽기 쉽기 때문입니다.

내부적으로 어떻게 동작하나요?

패턴 매칭은 보통 세 단계로 생각할 수 있습니다. 첫째, 대상 값의 변형을 검사합니다. 예를 들어 Some x인지 None인지, Ok value인지 Err error인지 확인합니다. 둘째, 일치한 변형이 들고 있는 payload를 지역 변수로 바인딩합니다. Ok user라면 user라는 이름이 바로 생깁니다. 셋째, 각 분기 안에서 그 값에 맞는 계산을 실행합니다. 언어가 exhaustiveness checking을 지원하면 가능한 변형을 전부 다뤘는지도 함께 검사합니다. 덕분에 패턴 매칭은 '분기'와 '분해(destructuring)'를 동시에 수행하는 도구가 됩니다.

코드로 보면

Result의 모양을 보고 분기하기

type Result<T, E> =
  | { kind: "ok"; value: T }
  | { kind: "err"; error: E };

function assertNever(value: never): never {
  throw new Error(`처리되지 않은 분기: ${JSON.stringify(value)}`);
}

function toMessage(result: Result<{ name: string }, string>): string {
  switch (result.kind) {
    case "ok":
      return `${result.value.name}님을 불러왔습니다.`;
    case "err":
      return `오류: ${result.error}`;
    default:
      return assertNever(result);
  }
}

`kind`를 보고 분기하는 순간 각 case에서 접근 가능한 필드가 달라집니다. `default`의 `never` 검사는 새 변형이 추가됐는데 분기를 빼먹은 경우를 드러내는 장치입니다.

AST 노드 타입별로 해석 로직 나누기

type Expr =
  | { type: "number"; value: number }
  | { type: "add"; left: Expr; right: Expr }
  | { type: "negate"; value: Expr };

function assertNever(value: never): never {
  throw new Error(`처리되지 않은 노드: ${JSON.stringify(value)}`);
}

function evaluate(expr: Expr): number {
  switch (expr.type) {
    case "number":
      return expr.value;
    case "add":
      return evaluate(expr.left) + evaluate(expr.right);
    case "negate":
      return -evaluate(expr.value);
    default:
      return assertNever(expr);
  }
}

분기 기준과 payload 사용이 한 문장 안에 묶여 있으므로, `add`일 때만 `left/right`를 읽고 `number`일 때만 숫자 값을 읽게 됩니다. 이것이 패턴 매칭이 모델과 로직을 같이 붙잡는 방식입니다.

경계와 구분

패턴 매칭과 ADT는 함께 움직이지만 같은 것이 아닙니다. ADT는 값이 가질 수 있는 형태를 정의하는 데이터 모델이고, 패턴 매칭은 그 형태를 읽어 행동을 나누는 제어 방식입니다. 모델이 없으면 패턴 매칭도 강점을 잃고, 패턴 매칭이 없으면 ADT의 장점이 코드로 잘 드러나지 않습니다. if/else나 switch와도 겹치는 부분이 있습니다. 단순한 boolean 검사나 범위 비교라면 if/else가 충분합니다. 하지만 값의 태그와 내부 payload를 함께 다뤄야 하는 순간, 패턴 매칭이 훨씬 직접적입니다. 같은 분기라도 '무엇을 검사하고 어떤 필드가 안전하게 존재하는가'가 한 문장에 묶이기 때문입니다.

언제 쓰나요?

패턴 매칭은 Option, Result, 비동기 상태, 트리 노드, 이벤트 메시지처럼 여러 변형을 가진 데이터를 소비할 때 거의 표준처럼 등장합니다. API 응답을 받아 로딩/성공/오류 UI를 나누거나, 파서가 만든 AST를 순회하거나, 이벤트 타입별 핸들러를 가를 때가 대표적입니다. 좋은 신호는 '태그를 검사한 직후 payload를 바로 꺼내 쓰는 코드가 반복된다'는 것입니다. 그럴 때 패턴 매칭으로 바꾸면 분기 누락이 줄고, 각 분기의 책임도 훨씬 선명해집니다.

Result 처리UI 렌더링AST 해석옵셔널 값 처리