본문 바로가기

업무

React hook 리팩토링 하기 (xstate)

 

최근 위와 같은 스타일의 페이지를 개발할 일이 아주 많았다.

 

첫 페이지 로드 시 A 화면, 3초 뒤에 B 화면, API 응답 성공 후에 C 화면, 다시 2초 뒤에 D 화면 ... 이런 식이다.

 

이런 유형의 페이지를 처음 작성할 땐 아래처럼 작성했다.

 

function useTransition() {
  const [isPending, setIsPending] = useState(true);
  const [isFinishSoon, setIsFinishSoon] = useState(false);
  const [isFinishAll, setIsFinishAll] = useState(false);


  useEffect(() => {
    const handle = setTimeout(() => setIsPending(false), 3000);
    return () => clearTimeout(handle);
  }, []);

  useEffect(() => {
    if (isFinishSoon) {
      const handle = setTimeout(() => setIsFinishAll(true), 2000);
      return () => clearTimeout(handle);
    }
  }, [isFinishSoon]);
  
}

화면 타입마다 useState를 선언해서 사용했는데

언제나 리팩토링이 그렇듯 어떻게든 돌아가지만 요구사항의 변경으로 코드를 수정할 때 문제가 많이 생겼다.

 

위 방식의 코딩의 문제점은 새로운 화면마다 state를 선언해야 하는 것이다.

또, 이 hook이 어떻게 돌아갈지 전혀 예측할 수 없기 때문에 재사용은 당연히 할 수 없을 것이고, 훅을 사용하는 의미가 없어진다.

 

처음엔 state가 많아질 때 액션 기반으로 묶기 위해 useReducer를 기계적으로 사용하려고 했으나

그렇게 하면 위 문제를 아무 것도 해결할 수 없다. 액션 기반으로 state가 변할테니 의미 파악 정도는 되겠지만.

 

나는 훅을 리팩토링 할 때는 먼저 훅을 그냥 필요한 컴포넌트에서 마치 구현이 된 것처럼 가장 편한 형태로 선언해본다.

그렇게 한 뒤에 다듬어 나가면 어려운 리팩토링에 점진적으로 접근할 수 있는 것 같다.

 

 


  const { currentState, transition } = useTimeoutTransitionState<'pending' | 'wait' | 'finishSoon' | 'finishAll'>({
    initialState: 'pending',
    states: {
      pending: {
        value: '잠시만 기다려주세요.',
        duration: 3000,
        onTimeout: transition => {
          setIsEnabledAPI(true);
          transition('wait');
        },
      },
      wait: {
        value: '약 1분 정도 걸릴 수 있어요',
        duration: 7000,
        onTimeout: () => {
          setIsEnabledAPI(false);
        },
      },
      finishSoon: {
        value: '거의 다 되어가요',
        duration: 2000,
        onTimeout: transition => {
          transition('finishAll');
        },
      },
      finishAll: {
        value: '완료되었습니다!',
      },
    },
  });
  
  return (
  	<p>{currentState}</p>
  )

 

이렇게 하면 훅을 선언하는 쪽에서 필요한 상태와 변화 조건들을 선언할 수 있으니

비슷한 화면에서 훅을 재사용할 수 있고, 상태 변화도 훨씬 명확하게 알 수 있다.

 

내부 구현은 다음과 같다.

 

type States<K extends string> = Record<K, State<K>>;
type State<K> = {
  value: string | ReactNode;
  duration?: number;
  onTimeout?: (transition: (name: K) => void) => void;
};
type CurrentStateValue<K extends string> = { name: K; state: State<K> };
interface Props<K extends string, T = States<K>> {
  states: T;
  initialState: K;
}

function useTimeoutTransitionState<K extends string>({ states, initialState }: Props<K>) {
  const [currentState, setCurrentState] = useState<CurrentStateValue<K>>({
    name: initialState,
    state: states[initialState],
  });
  const timeoutId = useRef<NodeJS.Timeout>();

  const transition = (name: K) => {
    const found = states[name];
    if (found) {
      setCurrentState({ name, state: found });
    }
  };

  useEffect(() => {
    const { onTimeout, duration = 0 } = currentState.state;

    timeoutId.current = setTimeout(() => {
      onTimeout?.(transition);
    }, duration);

    return () => timeoutId?.current && clearTimeout(timeoutId.current);
  }, [currentState]);

  return {
    transition,
    currentState,
  };

 

이렇게 구현을 다 해보고 코드 리뷰에서 동료가 알려준 것인데

xstate라는 라이브러리가 내가 한 리팩토링 형태와 아주 흡사하다고 알려줬다.

 

xstate 라이브러리로 필요한 기능을 구현해본 코드는 다음과 같다.

import { createMachine } from 'xstate';

const pendingMachine = createMachine({
  initial: 'pending',
  states: {
    pending: {
      after: {
        5000: { target: 'wait' },
      },
    },
    wait: {
      after: {
        1000: { target: 'finishSoon' },
      },
    },
    finishSoon: {
      after: {
        2000: { target: 'finishAll' },
      },
    },
    finishAll: {
      after: {},
    },
  },
});

이 라이브러리는 복잡한 상태 변화를 표현하는 화면에 아주 훌륭하고 기능도 내가 한 리팩토링 수준보다 훨씬 많다.

아주 가볍고 리액트 뿐만 아니라 자바스크립트가 지원되는 모든 프레임워크에 사용될 수 있어서 활용하기 좋아보인다.

하지만 내가 필요한 것보다 너무 많은 기능을 지원해서 우선은 내가 리팩토링한 훅으로 사용하기로 했다.

 

https://www.npmjs.com/package/xstate

 

xstate

Finite State Machines and Statecharts for the Modern Web.. Latest version: 4.37.0, last published: 7 days ago. Start using xstate in your project by running `npm i xstate`. There are 740 other projects in the npm registry using xstate.

www.npmjs.com