본문 바로가기

React

[ React ] useReducer를 알아보자

반응형

 

이 글은 리액트 공식 문서와 이 영상을 참고해서 정리한 글이다. 영상이 아주 잘 정리되어 있어서 보는 것을 강력히 추천한다.

(영상제작자에게 허락 받았습니다)

 

useReducer의 세 요소

useReducer의 구문을 먼저 살펴보자.

 

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

 

useReducer는 useState와 같이 상태 관리를 할때 사용하는 훅이다. useState와 육안으로 구분되는 점은 dispatchreducer다. 이 둘은 함수인데, 각각 형태는 아래와 같다.

 

dispatch(action)

reducer(state, action)

 

여기서 주목해야할 점은 dispatch 내부의 action이 reducer의 두 번째 인자로 간다는 점이다. reducer의 첫번째 인자(state)는 관리의 대상이 되는 '상태 객체'다. dispatch는 이미 구현 완료된 함수이기 때문에 action에 적절한 값을 넣어주기만 하면 되지만, reducer는 직접 구현해줘야 한다. 예제를 보면 이해할 수 있을 것이다. 

 

useReducer의 내부 로직을 이해하기 위해서는 useReducer의 세 요소를 알고 있어야 한다. 세 요소는 Dispatch, Action, Reducer이며, 각각 역할은 아래와 같다. 

reducer → state를 update 해준다.
dispatch Reducer에게 요구하는 행위
action Dispatch(요구) 내용

 

state를 update하는 주체는 reducer 임을 꼭 기억하자! 그리고 dispatch(actoin) 은 세트다. dispatch 호출시 reducer가 trigger 된다.

 

예제1 - 카운터

먼저 공식문서의 예제를 살펴보자. 코드는 간단하다. 

 

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

 

reducer, dispatch, action 위주로 분석해보자. 우선, 상태를 업데이트 하는 주체는 reducer이고, 이는 직접 구현해줘야 하는 부분이다. 상태가 변하는 시점은 버튼을 클릭했을 때다.

 

1) 플러스 버튼 클릭시

- dispatch가 reducer에게 action을 요구한다. (actoin = {type: 'increment'})

- action 객체의 type 속성값은 'increment'이므로, state는 1 증가한다. 

 

2) 마이너스 버튼 클릭시

- dispatch가 reducer에게 action을 요구한다. (actoin = {type: 'decrement'})

- action 객체의 type 속성값은 'decrement'이므로, state는 1 감소한다. 

 

 

예제2 - TodoList

약간 코드가 복잡하지만 로직 자체는 간단하다. 전체 코드는 접은글을 펼치면 확인할 수 있다. UI는 아래와 같다.

 

 

더보기

App.jsx

import React, { useState, useReducer } from 'react';
import { List } from './List'

const reducer = (state, action) => {
  switch(action.type) {
    case 'add-list':
      const title = action.payload.title;
      const newList = {
        id: Date.now(),
        title,
        isHere: false
      }
      return {
        count: state.count + 1,
        lists: [...state.lists, newList],
      };
    case 'delete-list':
      return {
        count: state.count - 1,
        lists: state.lists.filter(list => list.id !== action.payload.id)
      }
    case 'mark-list':
      return {
        count: state.count,
        lists: state.lists.map((list) => {
          if (list.id === action.payload.id) {
            return {...list, isHere: !list.isHere};
          }
          return list;
        })
      }
    default:
      return state;
  }
}

const initialState = {
  count: 1,
  lists: [
    {
      id: Date.now(),
      title: 'todolist',
      isHere: false,
    },
  ],
}

const App = () => {
  const [title, settitle] = useState('');
  const [listsInfo, dispatch] = useReducer(reducer, initialState);

  return (
    <div style={{ padding: '40px' }}>
      <h1>TodoList</h1>
      <p>List Count: {listsInfo.count}</p>
      <input
        type='text'
        placeholder='할일을 입력해주세요'
        value={title}
        onChange={((e) => settitle(e.target.value))}
      />

      <button onClick={() => {
        if (title !== '') {
          dispatch({type: 'add-list', payload: {title}})
          settitle('')
        }
      }}>
        추가
      </button>

      {listsInfo.lists.map((list) => {
        return (
        <List 
        key={list.id} 
        title={list.title} 
        dispatch={dispatch}
        id={list.id}
        isHere={list.isHere}
        />
        )
      })}
    </div>
  )
}

 

List.js

import React from "react";

export const List = ({ title, dispatch, id, isHere }) => {
    const action = {
        type: 'delete-list',
        payload: { id }
    }
    return (
        <div>
            <span
             style={{
                display: 'inline-block',
                margin: '5px 0px',
                textDecoration: isHere ? 'line-through' : 'none',
                color: isHere ? 'gray' : 'black',
                cursor: 'pointer'
             }}
             onClick={() => {
                dispatch({type: 'mark-list', payload: { id }})
             }}
            >
                {title}
            </span>

            <button
             onClick={() => {
                dispatch(action)
             }}
            >
                삭제
            </button>
        </div>
    )
}

 

reducer, dispatch, action 위주로 분석해보자. 상태를 업데이트 하는 주체는 reducer이고, 이는 직접 구현해줘야 하는 부분이다. 상태가 변하는 시점은 아래 세 가지 케이스로 나눌 수 있다. 참고로 입력란에 글을 작성하는 상태 변화는 제외시켰다.

 

1) 추가 버튼 클릭시

- dispatch가 reducer에게 action을 요구한다. (action = { type: 'add-list', payload: { title } })

- action.type으로 reducer 내부 switch문 어느 한 곳의 코드를 실행시킨다.

- action.payload를 참고해서 상태를 업데이트한다. 

 

2) 삭제 버튼 클릭시

- dispatch가 reducer에게 action을 요구한다. (action = { type: 'delete-list', payload: { id } })

- action.type으로 reducer 내부 switch문 어느 한 블록을 실행시킨다.

- action.payload를 참고해서 상태를 업데이트한다. 

 

3) 텍스트 클릭시

- dispatch가 reducer에게 action을 요구한다. (action = { type: 'mark-list', payload: { id } })

- action.type으로 reducer 내부 switch문 어느 한 곳의 코드를 실행시킨다.

- action.payload를 참고해서 상태를 업데이트한다. 

 

흐름은 간단하다. 상태 변화 이벤트가 발생하면 dispatch → reducer 순서로 호출되고, reducer는 action을 참고해서 상태값을 변화시킨다.

 

 

아래와 같은 패턴은 자주 사용되니 참고하자!

 

const reducer = (state, action) => {
  switch(action.type) {
    case 'add-list':
      // action.payload.data1 를 활용해서 상태 update
    case 'delete-list':
      // action.payload.data2 를 활용해서 상태 update
    case 'mark-list':
      // action.payload.data3 를 활용해서 상태 update
    default:
      return state;
  }
}

  

 

 

<참조>

https://ko.reactjs.org/docs/hooks-reference.html#usereducer   

https://www.youtube.com/watch?v=tdORpiegLg0&list=PLZ5oZ2KmQEYjwhSxjB_74PoU6pmFzgVMO&index=8    

 

반응형