본문 바로가기
source-code/FrontEnd

유한 상태 기계를 통한 사용자 선택 UI 개발 (feat xState)

by mattew4483 2023. 10. 11.
728x90
반응형

프론트엔드 개발을 하다 보면, 다음과 같은 요구사항과 심심찮게 만날 수 있다.

 

1. 사용자는 여러 선택지 중 하나를 선택할 수 있다.
2. 선택된 요소는 스타일이 변경된다.
3. 선택된 요소를 다시 선택했을 경우, 해당 요소의 선택이 해제된다.

사실 여기까지는 별다른 문제 없이, 쉽게 구현이 가능하다.

 

현재 선택한 role 도망자 선택 수사관 선택
선택 안함 도망자 로 변경 수사관 으로 변경
도망자 선택 안함 으로 변경 선택 안함 으로 변경
수사관 도망자 로 변경 수사관 으로 변경

role을 선택하는 동작은 현재 선택된 role(상태)에 따라 각기 다른 동작을 수행하며,

 

만약 React로 이를 구현한다면...

import React, { useState } from "react";

const page = () => {
  const [selectedRole, setSelectedRole] = useState(null);

  const onClickFugitiveCard = () => {
    if (selectedRole === "도망자") {
      setSelectedRole(null);
    } else {
      setSelectedRole("도망자");
    }
  };

  const onClickMarshalCard = () => {
    if (selectedRole === "수사관") {
      setSelectedRole(null);
    } else {
      setSelectedRole("수사관");
    }
  };

  return (
    <div>
      <button onClick={onClickFugitiveCard}>도망자</button>
      <button onClick={onClickMarshalCard}>수사관</button>
    </div>
  );
};

각 선택지 요소의 onClick 함수로 해당 동작을 구현하면 될 테다.

 

하지만 위 코드엔 아래와 같은 문제점이 있다.

1) 선택된 role을 제어하는 로직이 page 컴포넌트와 강하게 결합

2) role에 따른 액션들이 추가될 수록, 코드의 복잡도 증가

 

1의 경우, role제어 로직과 UI 컴포넌트와의 결합을 제거해 해결할 수 있다.

React의 경우 useReducer를 통해 구현이 가능하겠다.

import React, { useReducer } from "react";

type role = "도망자" | "수사관" | "선택안함";
interface actionType {
  type: role;
}
const roleSelectReducer = (state: role, action: actionType) => {
  switch (action.type) {
    case "도망자":
      return "도망자";
    case "수사관":
      return "수사관";
    case "선택안함":
      return "선택안함";
    default:
      return state;
  }
};

const page = () => {
  const [role, dispatch] = useReducer(roleSelectReducer, "선택안함");

  const onClickFugitiveCard = () => {
    dispatch({ type: "도망자" });
  };

  const onClickMarshalCard = () => {
    dispatch({ type: "수사관" });
  };

  return (
    <div>
      <h1>{`현재 ${role}`}</h1>
      <button onClick={onClickFugitiveCard}>도망자</button>
      <button onClick={onClickMarshalCard}>수사관</button>
    </div>
  );
};

export default page;

role이라는 상태를 업데이트하는 로직을 reducer함수 내에 작성해,

page라는 컴포넌트와의 의존성을 분리해준 모습.

 

하지만 여전히 2) role에 따른 액션들이 추가될 수록, 코드의 복잡도 증가 란 문제는 존재한다.

당장 위의 reducer함수에 선택된 요소를 다시 선택했을 경우, 해당 요소의 선택이 해제된다 란 요구사항을 구현할 경우...

const roleSelectReducer = (state: role, action: actionType) => {
  switch (action.type) {
    case "도망자":
      if (state === "도망자") {
        return "선택안함";
      } else {
        return "도망자";
      }
    case "수사관":
      if (state === "수사관") {
        return "선택안함";
      } else {
        return "수사관";
      }
    case "선택안함":
      return "선택안함";
    default:
      return state;
  }
};

각 상태마다 분기가 하나씩 더 추가됨과 동시에, 코드의 복잡도가 눈에 띄게 증가한 모습!

위 로직에서 선택할 수 있는 role이 하나라도 더 추가된다면...

swich문 내부의 if절들이 기하급수적으로 늘어나면서, 로직이 더욱더 복잡해질 것이다.

 

그렇다면 이를 어떻게 해결할 수 있을까?

의도를 추상화하면, 사실 단순하다!

여러 상태 중 하나의 상태를 가질 수 있으며, 현재 상태에 따라 발생할 수 있는 액션(이벤트)이 달라진다.

유한 상태 기계(finite-state machine, FSM) 모델로 이를 구현할 수 있다!

 

https://fe-developers.kakaoent.com/2022/220922-make-cart-with-xstate/

 

자바스크립트로 만든 유한 상태 기계 XState | 카카오엔터테인먼트 FE 기술블로그

김성호(shiren) 하고 싶은 것은 많은데, 시간 탓만 하는 개발자입니다.

fe-developers.kakaoent.com

 

우리가 작성한 위 코드에 xState를 사용한 유한 상태 기계를 적용해보면...

import React from "react";
import { useMachine } from "@xstate/react";
import { createMachine } from "xstate";

export const roleSelectMachine = createMachine({
  id: "role-select",
  initial: "선택안함",
  states: {
    선택안함: {
      on: {
        SELECT_FUGITIVE: {
          target: "도망자",
        },
        SELECT_MARSHAL: {
          target: "수사관",
        },
      },
    },
    도망자: {
      on: {
        SELECT_FUGITIVE: {
          target: "선택안함",
        },
        SELECT_MARSHAL: {
          target: "수사관",
        },
      },
    },
    수사관: {
      on: {
        SELECT_FUGITIVE: {
          target: "도망자",
        },
        SELECT_MARSHAL: {
          target: "선택안함",
        },
      },
    },
  },
});

const page = () => {
  const [state, send] = useMachine(roleSelectMachine);

  const onClickFugitiveCard = () => {
    send("SELECT_FUGITIVE");
  };
  const onClickMarshalCard = () => {
    send("SELECT_MARSHAL");
  };

  return (
    <div>
      <h1>{`현재 ${state.value}`}</h1>
      <button onClick={onClickFugitiveCard}>도망자</button>
      <button onClick={onClickMarshalCard}>수사관</button>
    </div>
  );
};

export default page;

절대적인 코드량은 늘었지만, 상태 관리 로직이 선언적으로 작성되어 훨씬 이해하기 쉽다!

 

한가지 아쉬운 점은...

각 상태 및 액션에 대한 타입을 지정을 못해줬다는 점인데

(아마 방법이 있겠지? 추후 작성해보도록 하겠다. 하하!)

 

 

728x90
반응형