ts-pattern은 더 멋진 if문이 아니다
복잡한 분기와 타입 추론의 고통
개발을 하다 보면, 조건에 따라 서로 다른 로직을 실행해야 할 때가 많아요. 이러한 상황에서 우리는 흔히 if
문을 사용하죠. 하지만 복잡한 조건들이 겹쳐질수록 분기문은 점점 난잡해지고, 이를 유지 보수하는 것이 어려워집니다. 특히, 타입스크립트와 같은 강력한 타입 시스템에서는 타입이 추가될 때마다 모든 분기 조건을 다 신경 써야 하는데, 이를 놓치면 버그가 발생할 가능성이 커져요.
저 또한 과거 프로젝트에서 타입이 추가되었음에도, 기존 분기문에서 해당 타입을 고려하지 않아 발생한 버그를 경험한 적이 있어요. 이러한 문제는 분기문의 복잡성을 줄이고, 타입 추론을 더욱 쉽게 할 수 있는 도구가 필요하다는 고민을 불러일으켰습니다.
ts-pattern 소개
이러한 문제를 해결하기 위해 도입된 도구 중 하나가 바로 ts-pattern
입니다. 이 라이브러리는 TC39에서 제안한 패턴 매칭을 기반으로 하고 있으며, 복잡한 조건부 분기를 간결하게 작성할 수 있게 도와줘요.
과연 ts-pattern은 타입스크립트에서 if문을 대체할 수 있을까?
- 최근
ts-pattern
벤치마크에 따르면,ts-pattern
은 자바스크립트의 기본 제어 구조인if/else
,switch
, 삼항 연산자에 비해 성능이 현저히 떨어집니다. if/else
문은 초당 약 10억 회 이상의 연산을 처리할 수 있는 반면,ts-pattern
의.exhaustive()
,.otherwise()
,.run()
같은 메서드는 초당 130만 회 정도의 연산을 수행하며 99% 더 느립니다.
성능 차이 원인
ts-pattern
은 다양한 데이터 구조와 복잡한 타입 추론을 지원하기 위해, 입력 타입의 가능한 모든 경우를 사전에 계산하는데요. 내부 코드를 살펴보면 match.ts
파일에서는 클래스 기반으로 패턴 매칭을 구현하고, this를 사용해 상태를 함수 체이닝을 통해 공유합니다. 이러한 방식은 안전한 타입 매칭을 제공하지만, 자바스크립트 엔진이 최적화된 기본 if/else
와 비교하면 성능이 떨어질 수밖에 없어요.
이 성능 이슈는 복잡한 분기 로직에서 ts-pattern
을 무조건 사용해야 하는지에 대해 고민하게 만들었습니다.
우리는 실제로 ts-pattern을 어떻게 사용하고 있었나?
실제로 제가 ts-pattern
을 사용한 코드를 돌아봤을 때, 대부분 복잡하지 않은 코드에서 사용되고 있었어요. 복잡한 분기 로직이나 타입 체크가 필요한 상황에서는 ts-pattern
이 큰 도움이 되었지만, 그렇지 않은 경우에는 오히려 오버엔지니어링처럼 느껴졌고요.
특히 exhaustive()
문법을 사용해 조건에서 처리되지 않은 모든 경우를 타입스크립트 컴파일러가 감지하고 타입 체크를 할 수 있다는 점에서는 유용했지만, 간단한 조건 분기를 처리할 때는 기존 방식이 더 효율적일 수 있다는 생각이 들었어요.
또한 다시 한번 생각해봐야 할 질문은, “정말 ts-pattern
을 사용할 만큼 복잡한 분기가 자주 발생하는가?”입니다. 복잡한 분기가 필요할 때도 있지만, 그런 상황에서는 “오히려 코드를 더 간결하게 만들 수 있는 방법을 고민하는 것이 본질적인 해결책이 아닐까?” 라는 생각도 해보았어요.
복잡한 분기문을 간결하고 안전하게 처리하는 방법
1. 복잡한 분기문을 단순하고 안전하게 처리하기
복잡한 분기는 피할 수 있다면 if/else
나 switch
를 사용해 더 단순하게 표현하는 것이 좋아요. 특히 early return 패턴을 활용하면 조건을 명확하게 처리할 수 있어요.
type 가위바위보타입 = '가위' | '바위' | '보';
function 가위바위보처리(choice: 가위바위보타입) {
if (choice === '가위') {
return '✌️ 가위를 선택하셨습니다!';
}
if (choice === '바위') {
return '✊ 바위를 선택하셨습니다!';
}
if (choice === '보') {
return '🖐 보를 선택하셨습니다!';
}
// 필요하다면 여기에 적절한 에러 처리를 넣습니다.
return;
}
또한, switch 문을 사용해 더 선언적으로 분기 처리를 다룰 수 있습니다.
type 가위바위보타입 = '가위' | '바위' | '보';
function 가위바위보처리(union: 가위바위보타입) {
switch (union) {
case '가위':
console.log("✌️ 가위를 선택하셨습니다.");
break;
case '바위':
console.log("✊ 바위를 선택하셨습니다.");
break;
case '보':
console.log("🖐 보를 선택하셨습니다.");
break;
default:
// 타입스크립트가 모든 케이스를 처리했는지 확인하는 검증
union satisfies never;
neverCheck(union);
}
}
function neverCheck(v: never) {
// 여기에 적절한 에러 처리를 넣습니다.
}
여기서 satisfies
키워드는 TypeScript에서 타입이 특정 조건을 만족하는지 확인하는 데 사용됩니다. 이 문법을 통해 union
타입이 외에 처리되지 않은 다른 케이스가 없는지를 타입 시스템에서 강제할 수 있어요. 만약 새로운 타입이 추가되었는데 처리되지 않았다면, TypeScript는 경고를 발생시켜 코드의 안정성을 높일 수 있어요.
처리되지 않은 케이스가 있을 경우 예시로 작성한 neverCheck()
함수와 같은 코드를 통해 런타임 에러를 발생시킵니다. 이를 ErrorBoundary
나 Sentry
와 연동하면 더욱 안정적인 에러 처리와 모니터링이 가능해요.
2. JSX 코드에서의 선언적 분기 처리 (TypeScript)
ts-pattern
라이브러리를 사용하면 match 인터페이스와 체이닝을 통해 코드를 선언적으로 쓸 수 있다는 장점이 있습니다.
import { match } from 'ts-pattern';
type 가위바위보타입 = '가위' | '바위' | '보';
const 가위바위보 = ({ choice }: { choice: 가위바위보타입 }) => {
return (
<div>
{
match(choice)
.with('가위', () => <div>✌️ 가위를 선택하셨습니다!</div>)
.with('바위', () => <div>✊ 바위를 선택하셨습니다!</div>)
.with('보', () => <div>🖐 보를 선택하셨습니다!</div>)
.exhaustive() // 모든 가능한 값이 처리되었는지 체크
}
</div>
);
};
하지만 TypeScript
에서도 JSX
로직을 처리할 때, 기존의 조건분기문에서IIFE(즉시 실행 함수 표현)
를 통해서 조금 더 선언적인 코드를 작성할 수 있어요. 더 나아가, SwitchCase
컴포넌트 같은 추상화된 도구를 활용해보는 것도 좋아요.
IIFE와 기존 조건 분기문 사용 예시
type 가위바위보타입 = '가위' | '바위' | '보';
const 가위바위보 = ({ choice }: { choice: 가위바위보타입 }) => {
return (
<div>
{(() => {
switch (choice) {
case '가위': return <div>✌️ 가위를 선택하셨습니다!</div>;
case '바위': return <div>✊ 바위를 선택하셨습니다!</div>;
case '보': return <div>🖐 보를 선택하셨습니다!</div>;
default: return <div>선택이 없습니다.</div>;
}
})()}
</div>
);
};
SwitchCase 컴포넌트 사용 예시
import { SwitchCase } from '@tossteam/react';
type 가위바위보타입 = '가위' | '바위' | '보';
const 가위바위보 = ({ choice }: { choice: 가위바위보타입 }) => {
return (
<div>
<SwitchCase
value={choice}
caseBy={{
'가위': <div>✌️ 가위를 선택하셨습니다!</div>,
'바위': <div>✊ 바위를 선택하셨습니다!</div>,
'보': <div>🖐 보를 선택하셨습니다!</div>,
}}
defaultComponent={<div>선택이 없습니다.</div>}
/>
</div>
);
};
이 코드는 TypeScript
의 강력한 타입 시스템을 활용하여 각 분기에서 타입 안전성을 유지하면서, 선언적인 방식으로 코드를 작성할 수 있게 도와줍니다. 라이브러리의 도움을 받지 않더라도 가독성을 높이고, 모든 가능성을 안전하게 처리할 수 있는 구조를 고민해볼 수 있어요.
유연한 사고와 기술에 대한 의심
ts-pattern
은 복잡한 타입 추론과 패턴 매칭에서 유용하지만, 성능이 중요한 경우에는 기본 제어 구조인 if/else
와 switch
가 더 적합할 수 있어요. early return
과IIFE
같은 기법을 사용하여 복잡성을 줄이고 가독성을 높이는 방법을 고려해보시는 것도 해보시면 좋을 것 같아요.
최종적으로 중요한 것은 유연한 사고에요. 어떤 도구에만 의존하는 것이 아니라, 항상 더 나은 방법이 있는지 고민하고 탐구하는 자세가 필요해요. 저도 토스에서 일하면서 당연하다고 여겨지는 것조차 깊이 고민하고 더 나은 방법을 찾으려는 환경에서 많은 것을 배웠어요.
토스에는 이러한 고민을 당연하게 여기고, 끊임없이 더 나은 문제 해결 방식을 찾기 위해 노력하는 엔지니어들이 가득합니다. 만약 이런 환경에서 함께 문제를 해결하고, 깊이 고민하는 여정을 시작하고 싶다면 토스에 오시는 것을 추천드려요.