[TIL] 2022-07-06 / 78일차 (useReducer)

미음제

·

2022. 7. 6. 16:25

 

Context API에 대한 강의에서 ContextProvider 컴포넌트에서 로컬 state를 관리하기 위해 useReducer hook을 사용했었다. 얼핏 기억하기로는 그때 당시 강의에서 useState와 유사한 역할을 한다고 간단한 설명을 하고 적용했던 것으로 기억한다. useState와 유사하다니 그런 줄만 알고 넘어갔었는데, Context API를 직접 다시 사용해보니 useState를 사용할 때와 다르게 사용되는 것을 보고 useReducer는 useState와 어떻게 다른지 알아야 할 것 같다는 필요성을 느끼게 되었고 간단하게 정리해보았다.

 

useReducer

 

우선 useState와 유사한데 굳이 사용하는 이유는 무엇일까? useReducer를 많이 사용해보지 않아 정확하게 사용하는 이유를 정의하기 어렵지만 찾아본 내용으로 추려보면 다음과 같다.

 

  • 1. useState보다 복잡한 state를 사용하는 경우에 useReducer를 사용한다
  • 2. nextState가 prevState와 의존적인 경우에 useReducer를 사용한다
  • 3. state 변경 로직을 분리할 수 있다.

 

간단한 형태의 state를 사용할 땐 useState를 사용하는 것이 바람직하고, 보다 복잡한 state를 사용하는 경우 useReducer를 사용하는 것이 바람직하다. 간단한 문자열 혹은 boolean 형태의 state를 사용할 땐 useState를 배열이나 오브젝트와 같은 보다 복잡한 state를 사용할 땐 useReducer를 사용하는 것이 좋다. 정답은 없지만 관리할 state가 많아지고 state간 복잡하게 얽혀 있을수록 useReducer를 사용하는 것이 편할 수 있다. 

 

useReducer 구조

 

useReducer 구조

 

크게 Reducer, Action, Dispatch로 이루어져 있다. 사용자가 event를 발생시키면, event에 맞는 요청을 Reducer로 보내게 된다. Reducer는 요청에 맞는 처리를 하여 state를 업데이트하는 구조이다.

 

예를들어 사용자가 현재 state 값을 1 증가시키는 event를 발생시켰다고 생각해보자. 사용자는 event를 통해 요청을 보내게 되는데 여기서의 요청을 Dispatch라고 한다. Dispatch는 이 요청을 처리하기 위해 요청을 처리할 Action을 Reducer로 보내게 된다. Reducer에서는 state를 처리하기 위한 Action을 실행하게 되고, Action에 따라 state가 업데이트되는 구조이다.

 

useReducer 사용 방법

 

useReducer hook을 사용하기 위해 useReducer를 import하고 useState와 같이 사용할 state를 정의해준다.

 

import styled from "@emotion/styled";
import { useReducer } from "react"; // useReducer hook import

const AppContainer = styled.div`
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 50px 0;
  font-size: 32px;
  gap: 1rem;
`;

function App() {
  const [state, dispatch] = useReducer(null, 0); // state
  return (
    <AppContainer>
      <div>현재 state 값 : {state}</div>
      <div>
        <button style={{ fontSize: "32px" }}>증가</button>
        <button style={{ fontSize: "32px" }}>감소</button>
      </div>
    </AppContainer>
  );
}

export default App;

 

const [state, dispatch] = useReducer(null, 0);

 

useState와 사용법이 동일하다. state 값과, state를 변경하기 위해 dispatch 함수를 배열 형태로 선언한다. useReducer hook의 첫 번째 인자는 reducer 함수를 두 번째 인자는 state의 초기 값을 지정한다. 현재 reducer 함수를 선언하지 않았기 때문에 null로 설정해 두었다.

 

다음으로는 state를 업데이트하기 위한 reducer 함수를 선언한다. reducer 함수는 dispatch로 넘어오는 인자를 통해 함수 로직을 처리한다. dispatch함수는 객체 형태로 데이터를 전달한다.

 

const reducer = (state, action) => {
  switch (action.type) {
    case "PLUS":
      return state + action.payload;
    case "MINUS":
      return state - action.payload;
    default:
      return 0;
  }
};

 

reducer 함수의 첫 번째 인자는 event가 발생해 state 업데이트 요청을 했을 당시의 state 값이고, 두 번째 인자는 dispatch로 넘어오는 데이터이다. 보통 dispatch에서 객체 형태로 state를 업데이트하기 위한 type과 값을 넘겨준다.

 

reducer는 보통 switch-case 혹은 if-else if 구조로 작성된다. dispatch로 넘어온 객체에서 action type에 따라 state 업데이트 로직을 구분하고, 기존 state 값과 payload를 통해 state를 업데이트한다. reducer 함수가 return 하는 값이 업데이트된 state가 되는 것이다.

 

reducer를 선언했으니, App 컴포넌트의 useReducer hook의 첫 번째 인자로 reducer를 작성해 준다.

 

const [state, dispatch] = useReducer(reducer, 0);

 

useReducer hook도 import 했고, reducer까지 정의했으니 state 업데이트 요청을 위한 event에 요청을 보낼 수 있는 useReducer의 dispatch 함수를 걸어준다.

 

function App() {
  const [state, dispatch] = useReducer(reducer, 0);
  const value = useRef(1);

  return (
    <AppContainer>
      <div>현재 state 값 : {state}</div>
      <div>
        <button
          style={{ fontSize: "32px" }}
          onClick={() => dispatch({ type: "PLUS", payload: value.current })}
        >
          증가
        </button>
        <button
          style={{ fontSize: "32px" }}
          onClick={() => dispatch({ type: "MINUS", payload: value.current })}
        >
          감소
        </button>
      </div>
    </AppContainer>
  );
}

 

'증가' 버튼을 클릭하여 클릭 이벤트가 발생하면 dispatch 함수가 실행된다. dispatch 함수는 reducer로 객체를 넘겨주게 된다. 해당 객체에는 state를 증가시키기 위한 action의 type과 payload가 넘어가게 된다. '감소' 버튼의 경우도 마찬가지이다. action type만 state를 감소시키기 위한 값을 넘기게 된다.

 

 

초기 상태에서 증가 버튼을 두번 누르고, 감소 버튼을 세 번 누르면 다음과 같은 결과가 나타난다.

 

 

정리

 

useReducer의 실행 흐름을 간단하게 다시 정리해보자면 다음과 같다.

 

  • event 발생
  • dispatch로 action을 reducer로 전달
  • redcuer가 action을 처리
  • state 업데이트

 

useState vs useReducer

 

가장 큰 차이는 Reducer를 사용하는 차이가 있다. useReducer를 사용하면 state를 변경하는 로직이 Reducer에서 이루어지게 된다.

 

useReducer를 사용하는 이유

 

1. 복잡한 로직을 처리하지 않아도 된다.

 

<button
  style={{ fontSize: "32px" }}
  onClick={() => dispatch({ type: "PLUS", payload: value.current })}
>
  증가
</button>
<button
  style={{ fontSize: "32px" }}
  onClick={() => dispatch({ type: "MINUS", payload: value.current })}
>
  감소
</button>

 

클릭 이벤트가 발생하면 type과 payload만 넘겨주는 dispatch 함수를 실행하면 된다. state를 처리하는 로직은 모두 reducer가 담당하게 된다.

 

const reducer = (state, action) => {
  switch (action.type) {
    case "PLUS":
      return state + action.payload;
    case "MINUS":
      return state - action.payload;
    default:
      return 0;
  }
};

 

2. 이전의 state가 업데이트 할 state와 연관이 깊은 경우에 사용하게 된다. 

 


오늘의 회고

 

useReducer에 대해 간단하게 정리하고 사용법을 익혔다. 처음 강의를 접했을 때 Context API에 대해 이해하는데 어려운 와중에 useReducer까지 써버리니 이해하기 어려웠었다. 조금 시간이 지났지만 둘을 따로 천천히 보니 어느 정도 이해할 수 있게 된 것 같다. 그리고 Context API를 설명할 때 '굳이 useReducer를 왜 사용하는 것일까?' 하는 의문이 있었는데, useReducer와 useContext를 조합하면 redux처럼 사용할 수 있어, 추후에 redux를 배우거나 사용할 때 조금 익숙해 지라고 강사님이 의도한 구조인 것 같다.

 

 

 

Hooks API Reference – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

리액트 공식 문서에서 useReducer hook에 대한 설명을 보면 다음과 같이 적혀 있는 것을 볼 수 있다.

 

useState의 대체 함수입니다. (state, action) => newState의 형태로 reducer를 받고 dispatch 메서드와 짝의 형태로 현재 state를 반환합니다. (Redux에 익숙하다면 이것이 어떻게 동작하는지 여러분은 이미 알고 있을 것입니다.)

출처 : 리액트 공식 문서

 


Conext API와 useReducer 강의를 복습하고 다시 정리를 해보니 왜 이렇게 사용하는지 어느정도 감이 잡힌 것 같고, redux 강의에서 todo app을 redux로 리팩터링 한 구조가 어느 정도 눈에 들어온 것 같다. 아직 redux를 잘 몰라 헷갈리긴 하지만 큰 구조는 ContextAPI에 useReducer를 접목한 것과 비슷하게 보인다.

 

redux store를 생성
action: action 타입과 payload를 지정한다.

types : redux type을 지정하는 곳이다.

reducer :  실제 state를 변화 시키는 순수 함수가 존재하는 곳이다.

index : reducer, actions를 export하는 곳이다.

redux/index : reducer를 합친 store를 만드는 곳이다.

생성된 store는 최상위 index에서 provider 형태로 내려준다.

state를 변화시킬 때에는 dispatch를 사용하고, state를 가져와 사용할때는 useSelector를 사용한다.

 

redux로 todo app을 리팩터링 할 때 commit message이다. reducer, action의 관심사가 무엇인지 이제 보이는 것 같다. 

 

다음 최종 프로젝트 까지 대략 2주 정도가 남았다. 지금 2차 팀원이 최종 때까지 유지될지 모르지만(아마 그럴 듯싶다), 최근에 나온 얘기로 "최종 때 사용해보고 싶은 프레임워크나 라이브러리가 있는지?"가 있었다. 타입스크립트, next, redux 혹은 redux toolkit 추가로 github actions 등 배운 것도(배웠다기 보단 이런 게 있으니 한 번 봐라 느낌) 많은데 다 해보고 싶지만, 중간 프로젝트 때도 Context API를 잘 몰라 헤맸던 것을 보면 욕심을 부릴 수 없다고 생각한다. 욕심부려봤자 내가 벅찰 것이니까 최대한 프로젝트에 내가 적용해서 사용할 수 있는 내용들 위주로 공부할 계획이다. 가장 도입해서 사용하고 싶은 것이 redux 혹은 redux toolkit이다. redux를 공부하기 위해 Context API와 useReducer를 공부했다. 이제 redux를 공부할 계획인데 이번 주 내로 강의를 다시 듣고 정리하고자 한다. 잘 될지는 모르겠다.

 

추가로 컴포넌트를 잘 작성해보고 싶은 욕심이 있다. 이전에는 완성이 급급했다면 이번엔 이런 부분을 신경써 보고 싶다. 중간 프로젝트가 끝나고 멘토님과 커피 챗을 하며 나왔던 내용이다.

 

페이지 단에서는 렌더링만 신경써보고 하위 컴포넌트에서 관리하는 로직은 따른 customhook(state를 관리하는)으로 분리?
hook은 페이지 안에서 처리할 로직들을 모아둔 곳 이렇게 분리를 하면 하위의 하위로 들어가더라도 hook만 가져다 쓰면 되는 이점이 있다.
어쩌면 url 마다 hook이 생성될 수도?

 

중간 프로젝트에서는 페이지 단에서 처리해야 할 로직이 다 적혀 있어(공통으로 사용하는 커스텀 hook을 제외하면) 작은 규모의 프로젝트였음에도 꽤나 페이지 하나에 많은 내용이 적혀 있었고, 컴포넌트 분리도 잘 되어 있지 않아 비대해졌다. 컴포넌트를 분리하고, 최대한 렌더링만 하는 구조로 신경 쓸 수 있도록 공부하고 적용해보고 싶은 욕심이 있다.

반응형