티스토리 뷰

Web/React

[React] switch문 심화 정리

HAN_PY 2023. 5. 22. 00:39
반응형

JavaScript에서 if문과 비슷한 switch 문에 대해 적어보려 한다. 우리는 switch문을 언제 사용하고, 잘 사용하고 있는지에 대해 고민해 보면 좋을 것 같다. 기본적인 문법 설명은 생략한다. eslint관점에서 우선 설명을 풀어본다. 그리고 TypeScript에서 SRP(Single Responsibility Principle) 원칙으로 switch를 어떻게 사용해야 할지에 대한 고민을 함께 해본 후, 추가로  React에서 활용해 보도록 하자.

 

 

일반적으로 우리는 switch 문에서 default값을 꼭 넣어줘야 에러가 발생하지 않는다고 생각한다. 왜냐하면, 개발자도 사람이기 때문에 실수로 모든 조건을 넣지 않아 예외가 발생 할 수 도 있기 때문이다. 또한, eslint 관점에서 default문을 넣어주지 않는, 아래 코드에서는 eslint에서 에러를 출력한다.

 

switch (a) {
    case 1:
        /* code */
        break;
}

 

에러를 출력 막기위해서는 아래와 같이 default를 넣어주기도 하고, 의도적으로 생략을 명시하기도 한다. eslint에서는 아래의 경우에는 에러 출력을 하지 않는다.

 

// default 추가
switch (a) {
    case 1:
        /* code */
        break;
        
    case 2:
        /* code */
        break;
        
    default:
    // do nothing
}


// 물론 아래와 같이 의도적으로 생략을 명시하는 것도 좋다.
switch (a) {
    case 1:
        /* code */
        break;
        
    case 2:
        /* code */
        break;
        
    // no default
}

 

 

사실 eslint를 끈다면, default 값을 적지 않아도 application은 잘 돌아간다. 그렇다면, 왜 default값을 굳이 적어야 할까?

 

단지 개발자의 실수를 막기 위해서 default값을 넣으라고 eslint에서 default-case로 막아주는 것일까? 여기서 조금 더 생각이 필요하다. 개발자의 목표는 우리가 만드는 application이 유지보수가 쉽게 가능하도록 만드는 것에 있다. 그런데 위의 코드는 확장성의 측면에서는 전혀 좋은 코드가 아니다. 그 이유는 바로 코드 자체에서 우리가 예상하는 결과가 아닌 값이 들어올 수도 있기 때문이다. 만약, switch문에 들어온 a가 1이나 2가 아니라면, 예측 불가능한 코드가 된다. 이는 매우 크리티컬 한 Error를 야기할 수 있다. 그렇다면, 이러한 코드는 좋은 코드라 할 수 있을까? 

 

조금 더 생각을 해보자. 다른 서버에서 API에서 값을 받아서 switch 로직을 거쳐 비즈니스 로직을 실행한다고 해보자. 갑자기 예상하지 못한 새로운 값이 추가된다면 어떻게 대응하여 코드를 짜야할까? Typescript를 통한 예시를 보고 조금 더 고민해 보자.

 

 

Typescript에서의 switch 적용

위의 코드에서는 단순하게 default를 switch에 넣었다.  default를 넣는 순간 case에서 정의되지 않는 입력 값은 default로 가게 된다. 이러한 코드는 괜찮아 보일지도 모른다. 하지만, javascript가 아닌 다른 언어에서는 pattern matching이라는 개념이 나온다. 간단히 말하면, pattern matching이란, 다른 인풋값에 따라 matching 되는 다른 아웃풋 값을 연결한다는 개념이다. 이는 switch 문에서 default를 사용하지 않고 모든 경우의 수를 case에 넣어주는 것과 사실 같다. pattern matching 측면에서 switch문을 작성을 한다면, co-lacation를 해치지 않는 선에서 유지보수가 가능한 코드가 된다고 할 수 있다. 이러한 방법은 사실 TypeScript에서 Union type으로 작성이 가능하다. 그렇다면 아래의 코드를 보면서 조금 더 이해도를 높여보자.

 

아래의 코드는 typescript 공식문서에서 자주 볼 수 있는 코드이다. 일반적인 shape에 따른 string 값을 리턴하는 함수가 있다. 아래의 코드는 어떤 Error를 TypeScript에서 발생시킬까?

 

type Circle = {
  kind: 'circle'
  radius: number
}

type Rectangle = {
  kind: 'rectangle'
  width: number
  height: number
}

type Shape = Circle | Rectangle

const renderShape = (shape: Shape): string => {
  switch (shape.kind) {
    case 'circle':
      return 'I am a circle'
  }
}

 

위 코드에서 Union type으로 Shape를 지정했기 때문에 아래의 에러가 발생하는 것을 확인할 수 있다.

기본적으로 typescript의 모든 경우의 수의 return 값을 테스트한다. 위에서 발생한 Error는 Rectangle의 경우에 return값이 없다는 경우에 대한 Error라고 할 수 있겠다. 이러한 TypeScript Error는 우리가 위에서 언급했던, pattern matching관점에서의 개발자의 실수를 방지해 준다고 할 수 있다.

 

그러나 이러한 애러가 발생했을 때, 여기서 중요한 것은 default를 switch 문에 추가하면 안 된다는 것이다. TypeScript는 인풋 값에 대한 경우한 판단하지, 비즈니스 로직 측면에서, 특정 경우에 대한 case가 빠졌다고 말해주지 않는다. 즉, pattern matching에 관점에서는 Shape는 Circle과 Rectangle으로 2가지가 가능하기 때문에, default값을 switch문에 추가하는 것이 아니라, case 부분에 rectangle부분을 추가해 줘야 한다는 것이다. case가 많아질수록 이러한 실수는 치명적인 Error를 발생할 수 있다. 또한, Error를 찾는데 더 많은 시간이 걸리 수도 있다. 위의 renderShape 함수를 수정해 보자.

 

const renderShape = (shape: Shape): string => {
  switch (shape.kind) {
    case 'circle':
      return 'I am a circle'
    case 'rectangle':
      return 'I am a rectangle'
  }
}

 

 기본적으로 typescript에서는 예측 불가능한 값을 자동으로 타입을 만들어 주지 않음을 이해하자. 그렇기 때문에 default 값을 가능하면 사용하지 않는 것이 좋다. 더욱 직관적으로 말하면, default를 사용하는 것은, any 쓰는 것과 같다고 할 수 있을 것 같다. 그러면 default를 그냥 사용하면 안 되는 것일까? 그건 아니다. default도 ErrorBoundary와 비슷한 개념으로 예외처리 시 유용하게 사용하기도 한다. 아래 글을 조금 더 보자.

 

 

[심화] React에 적용하기

사실 위의 switch가 적용된 함수는 TypeScript에서는 더 이상 수정 할 부분이 없다. 실제 현업에서는 javaScript로 작성이 되어 있거나, API를 받아서 사용하는 경우에는, input 값을 typescript로 통제할 수 없는 경우가 많다. 즉, 여기서는 TypeScript이 런타임 환경은 고려하지 않는다는 관점에서 글을 풀어보려 한다.

 

export const App = () => {
  const [shapes, setShapes] = useState({})

  React.useEffect(() => {
    getAPIShapes().then(setShapes)
  }, [])

  if (!shapes) {
    return <Loading />
  }

  return (
    <section>
      {shapes.map((shape) => (
        <Shape {...shape} />
      ))}
    </section>
  )
};

type Circle = {
  kind: 'circle'
  radius: number
};

type Rectangle = {
  kind: 'rectangle'
  width: number
  height: number
};

type Shape = Circle | Rectangle;

const Shape = (props: Shape): JSX.Element => {
  switch (props.kind) {
    case 'circle':
      return <Circle radius={props.radius} />
    case 'rectangle':
      return <Rectangle width={props.width} height={props.height} />
  }
};

 

우선 위의 코드는 별 문제가 없어 보인다. 그러나, 이는 Circle / Rectangle라는 코드만 받을 때만에 한정해서 문제가 없는 react 코드이다. 만약 backend 팀에서 Circle / Rectangle 가 아니라 갑자기 새로운 Square을 만들어서 호출한다면 어떻게 될까? 위의 로직에서는 undefined를 return 하게 되어 웹이 터지거나 예상할 수 없는 오류가 발생한다.(물론 react 18부터는 undefined를 return 에도 null로 판단하여 터지진 않는다..) 이러한 경우를 위해서 default가 필요한 것이다. 사실상 예측할 수 없는 부분을 handling 하는 측면에서 default가 필요하다고 볼 수 있는 것이다. default 부분에 Typescript의 never을 이 부분에 활용한다면 예측 불가능한 부분에 대한 handling이 가능하다. 우선은 TypeScript의 never에 대해 간단히 알아보자.

 

 

TypeScript : never type

never 타입은 빈 타입으로 발생할 수 없는 타입을 나타내며, 기능 제약을 수행한다.

 

  • 허용할 수 없는 매개변수에 제한을 할 수 있다. : never 사용
  • 아무 값이나 전달하거나 아무 값도 전달하지 않으면 타입 에러가 발생한다. : never 사용

never 타입의 사용하는 시기는 여러 가지가 있지만, 여기서는 else와 같은 도달할 수 없는 조건 타입에 사용이 된다.

 

function isCheck(arg: string | number): boolean {
  if (typeof arg === "string") {
    return true;
  } else if (typeof arg === "number") {
    return false;
  }
  return fail("Not Allow!"); //Error
}
function fail(message: string): void { throw new Error(message); }

 

위의 예제에서 isCheck 함수는 boolean을 리턴하도록 설정되어 있다. 그러나, fail에서 void로 리턴 없음을 설정하면, Error가 발상한다. (void형식에 boolean 형식을 할 당 할 수 없다.) 이러한 경우에 아래와 같이 never을 사용하여 해결 가능하다.

 

function isCheck(arg: string | number): boolean {
  if (typeof arg === "string") {
    return true;
  } else if (typeof arg === "number") {
    return false;
  }
  return fail("Not Allow!");
}
function fail(message: string): never { throw new Error(message); }

 

 

 

React 예외 API 처리하기

위의 리엑트 코드는 circle과 rectangle 부분의 case만 나눠서 Union type으로 지정되어 있다. 그 외의 경우는 never type을 활용하여 default에 넣어보면 아래와 같다.

 

const UnknownShape = ({ shape }: { shape: never }) => (
  <Button>Retray Render</Button>
)

const Shape = (props: Shape): JSX.Element => {
  switch (props.kind) {
    case 'circle':
      return <Circle radius={props.radius} />
    case 'rectangle':
      return <Rectangle width={props.width} height={props.height} />
    default:
      return <UnknownShape shape={props} />
  }
}

 

위와 같은 코드는 기본적으로 서비스 장애를 발생시키지 않는다. 그리고 Frontend 개발자는 logo을 확인하여, 문제을 인지하여 빠르게 대응이 가능하다. 만약 Square부분이 추가된다고 하면 위의 코드에서 case를 추가해 주면 간단히 가능하다.

 

관련 코드를 정리해 보면, 아래와 같다.

 

import * as React from 'react'

type BaseShape = {
  id: number;
};
type Circle = BaseShape & {
  kind: "circle";
  radius: number;
};
type Rectangle = BaseShape & {
  kind: "rectangle";
  width: number;
  height: number;
};
type Square = BaseShape & {
    kind: "square",
    length: 7
}
type Shape = Circle | Rectangle | Square;

const Circle = ({ radius }: Omit<Circle, "kind" | "id">) => (
  <div>I'm a circle with radius {radius}</div>
);

const Rectangle = ({ width, height }: Omit<Rectangle, "kind" | "id">) => (
  <div>
    I'm a rectangle with width {width} and height {height}
  </div>
);

const Square = ({ length }: Omit<Square, "kind" | "id">) => (
  <div>
    I'm a rectangle with width {length} and height 
  </div>
);

const UnknownShape = ({ shape }: { shape: never }) => <div>Unknown Shape</div>

const Shape = (props: Shape): JSX.Element => {
    switch (props.kind) {
        case 'circle':
            return <Circle radius={props.radius} />
        case 'rectangle':
            return <Rectangle width={props.width} height={props.height} />
        case 'square':
            return <Square length={props.length} />
        default:
            return <UnknownShape shape={props} />
    }
}

 

 

정리하면, 입력이 완전히 제어가 가능하다면, switch에서 default를 제외하는 코드를 작성하면 된다. 그리고 입력을 완전히 제어가 불가능하면, Typescript의 런타임 시 발생할 예외를 default를 추가하여 작성해 주면 된다.

 

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함