useReducer를 써야겠다고 생각이 들때

다양한 상태관리 라이브러리를 공부하기 위해 간단한 프로젝트를 만드는 중이다.

상태관리가 필요한 상황을 생각해 봤는데

  1. prop을 깊게 전달해야 할 경우
  2. 페이지를 이동했다가 다시 돌아왔을 때 상태가 남길 원하는 경우
  3. api 통신으로 가져온 값이나 스토리지 값의 진입점이 필요한 경우
  4. 여러 개의 컴포넌트가 상태를 공유하는 경우

이런 상황들이 있지 않을까.

시작으로 어떤 상황이 좋을까 고민하다가 2번째 경우로 상태관리를 사용해 본 적이 잘 없어서 2번으로 결정.

스타트를 끊어야 속도가 붙기 때문에 어렵지 않지만 실력 향상에 도움을 줄 수 있는 덜 익숙한 기능을 만들어보고자 최근 검색어를 보여주는 페이지를 만들기로 했다.

디자인은 어렵다!

useReducer를 쓰게된 원인

일단 토대를 만든다 생각하고 Context API를 사용해서 구현하던 중, 상태를 업데이트하는 로직이 중복으로 사용되고 가독성도 떨어지다 보니 하나의 파일에서 관리해 보자는 생각이 들었다.

이럴때 useReducer를 써야하지 않을까.

바닐라JS로 프로젝트를 만들 때도 해당 서비스의 상태관련 로직을 하나의 객체 혹은 클래스로 묶어서 구현했기 때문에 리액트에서 이러한 묶는 행위를 할 땐 useReducer가 딱인 듯하다.

아래는 useReducer를 안쓴 코드들

export function SearchProvider({ children }: SearchProviderProps) {
  const [searchedList, setSearchedList] = useState<Searched[]>([]);

  const searchContextValue = useMemo(
    () => ({
      searchedList,
      setSearchedList,
    }),
    [searchedList, setSearchedList]
  );

  return (
    <SearchContext.Provider value={searchContextValue}>
      {children}
    </SearchContext.Provider>
  );
}
const { searchedList, setSearchedList } = useContext(SearchContext);

const handleSubmit: FormEventHandler = (event) => {
  // handleSelectClick과 중복되는 부분
  // ...
  const date = getCurrentDate();

  if (searchedList.find(({ keyword }) => keyword === currentKeyword)) {
    const removedList = searchedList.filter(({ keyword }) => keyword !== currentKeyword);

    setSearchedList([{ keyword: currentKeyword, date }, ...removedList]);
  } else {
    setSearchedList([{ keyword: currentKeyword, date }, ...searchedList]);
  }

  // ...
};
const { searchedList, setSearchedList } = useContext(SearchContext);

const handleResetClick = (event: React.MouseEvent) => {
  event.stopPropagation();

  if (confirm('최근검색어를 모두 삭제하시겠습니까?')) {
    setSearchedList([]);
  }
};

const handleDeleteClick = (event: React.MouseEvent, clickedKeyword: string) => {
  event.stopPropagation();

  const removedList = searchedList.filter(({ keyword }) => keyword !== clickedKeyword);

  setSearchedList(removedList);
};

const handleSelectClick = (clickedKeyword: string) => {
  // handleSubmit과 중복되는 부분
  const removedList = searchedList.filter(({ keyword }) => keyword !== clickedKeyword);
  const date = getCurrentDate();

  setSearchedList([{ keyword: clickedKeyword, date }, ...removedList]);

  // ...
};

코드를 보면 중복되는 부분을 확인할 수 있다. (handleSubmithandleSelectClick)
또한, 상태 업데이트 로직이 분산되어있다.

중복을 없애고 로직을 한곳에서 관리하기 위해 useReducer를 사용한 코드들

type Action = { type: 'added'; keyword: string; date: string } | { type: 'deleted'; keyword: string } | { type: 'reset' };

function searchReducer(state: Searched[], action: Action) {
  switch (action.type) {
    case 'added': {
      const { keyword, date } = action;

      if (state.find(({ keyword: prevKeyword }) => prevKeyword === keyword)) {
        const removedList = state.filter(({ keyword: prevKeyword }) => prevKeyword !== keyword);

        return [{ keyword, date }, ...removedList];
      }

      return [{ keyword, date }, ...state];
    }
    case 'deleted': {
      return state.filter(({ keyword }) => keyword !== action.keyword);
    }
    case 'reset': {
      return [];
    }
    default: {
      throw Error(`Unknown action: ${action}`);
    }
  }
}
export function SearchProvider({ children }: SearchProviderProps) {
  const [searchedList, dispatch] = useReducer(searchReducer, []);

  const searchContextValue = useMemo(
    () => ({
      searchedList,
      dispatch,
    }),
    [searchedList, dispatch]
  );

  return (
    <SearchContext.Provider value={searchContextValue}>
      {children}
    </SearchContext.Provider>
  );
}
const { dispatch } = useContext(SearchContext);

const handleSubmit: FormEventHandler = (event) => {
  // ...

  dispatch({
    type: 'added',
    keyword: currentKeyword,
    date: getCurrentDate()
  });

  // ...
};
const { searchedList, dispatch } = useContext(SearchContext);

const handleResetClick = (event: React.MouseEvent) => {
  event.stopPropagation();

  if (confirm('최근검색어를 모두 삭제하시겠습니까?')) {
    dispatch({ type: 'reset' });
  }
};

const handleDeleteClick = (event: React.MouseEvent, clickedKeyword: string) => {
  event.stopPropagation();

  dispatch({ type: 'deleted', keyword: clickedKeyword });
};

const handleSelectClick = (clickedKeyword: string) => {
  dispatch({
    type: 'added',
    keyword: clickedKeyword,
    date: getCurrentDate()
  });

  // ...
};

확실히 깔끔해졌다.
리듀서는 순수하게 상태를 업데이트해야 하기에 연관된 로직만 묶었다.

갑작스러운 결론

써야 할 때 써야 의미가 있고 배움을 얻을 수 있다.
useReducer는 중복을 줄이고 상태를 업데이트하는 관련 로직을 묶을 때 유용하다.