React

ts-pattern으로 선언적으로 분기처리하기

teveloper 2024. 7. 16. 21:35
반응형

 

 

저는 선언적인 프로그래밍을 좋아합니다. 프론트엔드 개발 초기시절 styled-components로 공통화되지 않은 div를 찍어내다가 Flutter를 처음 접했을때 충격을 잊을수가 없죠. Scaffold와 같은 위젯에서 옵션을 제공해주는데 어찌나 편리하던지 ! 심지어 자동완성까지 지원해주는데 저는 그저 필요한 속성들만 쏙쏙 골라 사용하면 됐습니다.

 

 

만약 분기처리도 명령형이 아닌 선언형으로 작성하면 어떨까요 ? 복잡한 if/else와 switch문에서 조금은 구원받을 수 있을텐데요.

 

 

최근에 ts-pattern 라이브러리를 재미있게 보고 있습니다. JS에서 아직 공식적으로 제공하지 않는 패턴매칭을 도와주는 패키지인데 복잡한 데이터나 API를 선언적으로 분기처리할 때 유용할 것 같습니다. 패턴매칭의 용어에 대해 생소할 수 있는데 개념을 먼저 살펴보겠습니다.

 

패턴매칭

 

 

 

패턴매칭의 사전적 의미를 찾아보면 데이터를 검색할 때 특정 패턴이 출현하는지, 또한 어디에 출현하는지 등을 특정하는 방법의 일종이다 라고 나타내고 있습니다. JS에서 좀 생소할 수 있는데 정규표현식(regEx)을 통해 특정 패턴을 찾는것과 비슷하다고 볼수도 있겠네요.

 

함수형 언어와 파이썬을 비롯해 이미 많은 언어에서는 패턴매칭을 지원하고 있습니다. 우리에게 익숙한 파이썬에서는 어떻게 패턴매칭을 활용하는지 살펴봅시다.

 

익숙한 if, else문
패턴매칭 적용

 

 

상단의 코드가 우리에게 익숙한 if, else문 입니다. if문을 여러번 사용해서 분기 처리를 하고 있네요. 간단한 규칙 혹은 데이터일 경우에는 크게 if, switch문으로 처리할 수 있겠지만 조금만 복잡해져도 코드의 가독성이 떨어집니다.

 

 

반면에 하단의 코드는 파이썬의 dictionary 구조를 활용해 분기처리를 진행하고 있습니다. Dictionary 자료 구조의 값에 따라 어떻게 처리하는지 쉽게 알아볼 수 있고 중첩된 코드도 없어 코드를 따라 가독성도 좋습니다.

 

 

이처럼 복잡한 분기 조건문 대신 간결한 표현식을 이용해 범위를 좁혀가는 방식을 패턴매칭이라고 부르는 것 같습니다. JS의 if, switch문에 익숙한 저도 처음에는 이걸 굳이 왜써 ? 하면서 의아해하기도 했는데, 복잡한 API를 처리했던 기억을 되살려보니 잘만 쓰면 가독성과 유지보수에 좋을것 같다는 생각이 들었습니다.

 

 

현대 프론트엔드의 데이터 처리는 상세페이지 하나에만 여러개의 API를 요청할 경우가 많습니다. 예전에 피부시술 이벤트 페이지 하나에만 이벤트 정보, 문의, 추천 상품, 후기 등 여러 데이터를 함께 조합했던 기억이 나네요. 현재 '상태'에 따라 문구에 대한 분기처리도 굉장히 많이 했는데 패턴매칭에 대해 미리 알았더라면 좀 더 쉽게 개발했을것 같습니다. 

 

예시로 가져온 29CM API. 이런 API를 몇개 더 호출한다면 ?

 

 

그렇다면 리액트 혹은 타입스크립트에서는 어떻게 패턴매칭을 활용할 수 있을까요 ? 그것을 도와주는 ts-pattern 라이브러리에 대해 알아보겠습니다.

 

ts-pattern

 

 

ts-pattern은 리액트와 타입스크립트 환경에서 패턴매칭을 구현하게 해주는 라이브러리입니다. exhaustive library라는 표현에 걸맞게 10k가 넘는 높은 인기를 보여주고 있죠. 패턴매칭의 공식지원에 대한 수요는 꾸준히 있지만 아직 JS에서 정식적으로 지원하지는 않고 현재는 ECMA 표준이 되기위해 초안단계에 머룰러 있습니다.

 

 

그렇다면 ts-pattern은 어떤 방식으로 동작할까요 ? 예시코드를 통해 API와 문법을 간단히 살펴보겠습니다.

 

import { match, P } from 'ts-pattern';

type Data =
  | { type: 'text'; content: string }
  | { type: 'img'; src: string };

type Result =
  | { type: 'ok'; data: Data }
  | { type: 'error'; error: Error };

const result: Result = ...;

const html = match(result)
  .with({ type: 'error' }, () => <p>Oups! An error occured</p>)
  .with({ type: 'ok', data: { type: 'text' } }, (res) => <p>{res.data.content}</p>)
  .with({ type: 'ok', data: { type: 'img', src: P.select() } }, (src) => <img src={src} />)
  .exhaustive();

 

해당 코드는 README에서 가장 먼저 찾아볼 수 있는 예시 코드입니다. ts-pattern에서 가장 빈번하게 사용하는 match API를 사용해 분기처리를 진행하는 코드네요.

 

 

먼저 어떤 객체를 매칭의 대상으로 삼을지 match에 선언합니다. 이는 switch문과 비슷합니다. 이후 result의 결과값을 토대로 'error', 'ok'에 따라 선언적으로 분기처리를 하고 data를 정상적으로 받을 경우 'text'와 'img' 값을 리턴해주고 있습니다. 마지막으로 exhaustive를 통해 가능한 모든 경우를 다루고 있음을 컴파일러에 알립니다. 이를통해 타입 안정성을 보장해주고 있습니다. 

 

 

언듯 보기에는 낯설어 보여도 크게 어렵지 않게 이해하실 수 있을겁니다. 이번에는 실제 사용하고 있는 코드를 ts-pattern으로 리팩토링 해보면 어떨까요 ?

 

 

 

ts-pattern으로 코드 개선해보기

 

const getStatusData = (data:StatusData)=>{
  switch (data.status) {
    case RESERVATION_WAIT: // 예약 확인중
    case RESERVATION_COMPLETE: // 예약 확정
    case RESERVATION_DELAYED: // 예약 대기
      return <ReservationComponent />;

    case VISIT_EXPECTED_TODAY: // 오늘 방문 예정
      return <VisitExpectComponent />;

    case WAIT_VISIT_COMPLETE: // 방문 확정
      return <VisitCompleteComponent />;
      
    default:
        return <DefualtComponent/>
    };
}

 

예약기능이 존재하는 상품의 로직을 단순화하여 sudo code로 작성해봤습니다. 상품의 현재 예약 '상황'에 따라 컴포넌트를 분기처리해서 리턴해주는 로직이네요. 이를 ts-pattern으로 바꿔보겠습니다.

 

const getStatusData = (data: StatusData) => {
  return match(data.status)
    .with(RESERVATION_WAIT, RESERVATION_COMPLETE, RESERVATION_DELAYED, () => {
      return <ReservationComponent />;
    })
    .with(VISIT_EXPECTED_TODAY, () => {
      return <VisitExpectComponent />;
    })
    .with(WAIT_VISIT_COMPLETE, () => {
      return <VisitCompleteComponent />;
    })
    .otherwise(() => <DefaultComponent />);
    .exhaustive();
};

 

코드의 양이 크게 차이나지는 않지만 가독성이 조금 더 좋아졌습니다. 가독성을 제외하고도 switch문과 비교할때 더 개선된점은 타입안정성이 확보된다는 것입니다. exhaustive를 통해 status의 모든 경우의 수를 처리하도록 강제하고 있기 때문에, 추후 상태값이 추가되거나 변경될 때 반드시 처리해줘야합니다.

 

 

data가 현재보다 더 복잡해지고 비대해질 경우 ts-pattern를 더욱 유용하게 사용할 수 있습니다. 개발자는 그저 인터페이스와 타입만 잘 선언해두고 가지치기하듯이 상태에 따른 분기 처리를 수행하면 됩니다. ts-pattern은 match 외에도 다양한 API를 제공해주고 있으니 한번 살펴보면 좋을것 같습니다.

 

 

https://github.com/gvergnaud/ts-pattern

 

GitHub - gvergnaud/ts-pattern: 🎨 The exhaustive Pattern Matching library for TypeScript, with smart type inference.

🎨 The exhaustive Pattern Matching library for TypeScript, with smart type inference. - gvergnaud/ts-pattern

github.com

 

 

마치며

ts-pattern을 통해 패턴매칭이란 개념을 알게되었고, 익숙한 if와 switch문이 아닌 함수형 방식의 분기처리를 통해 굉장히 유익한 경험을 했습니다. 패턴매칭의 개념을 통해 가독성있고 아름다운 코드들을 접할 수 있었죠. 잘만 익혀두면 점점 더 복잡해지는 프론트엔드의 API 처리에 큰 도움이 될 것 같습니다.

 

 

다만 높은 학습비용은 기술스택 도입에 큰 걸림돌로 작용할 것 같습니다. 일단 ts-pattern이 제공하는 기능들을 새로 익혀야 하고 구성원들의 공감대도 형성해야 하죠. 이미 if/switch로 잘 쓰고 있는데 왜 ? 라고하면 할 말이 없어집니다. 복잡하게 분기처리할만한 API가 아니라면 굳이 도입할 필요도 없을것 같습니다. 외부 종속성이 내 코드와 결합될 가능성도 높아집니다.

 

 

그럼에도 ts-pattern은 굉장히 매력적인 라이브러리라 생각합니다. 충분히 복잡한 데이터를 처리하고 있다면 선언적인 분기처리를 통해 안전하게 API를 처리할 수 있을것 같습니다. 여러 API를 호출하고 있다면 BFF(Backend For Frontend)에서도 유용하게 사용될 것 같네요.

 

 

여러분도 한번쯤 써보길 추천드립니다.

 

 

 

 

반응형