개발새발 로그

React - useReducer에 대하여 본문

React

React - useReducer에 대하여

이즈흐 2023. 12. 13. 19:35

useReducer

이제까지 state를 생성하고 관리하는 useState를 사용했다.

하지만 리액트에서 state를 관리하기 위한 hook이 또 있는데 

그것이 바로 useReducer가 있다.

 

useState처럼 state를 생성하고 관리한다.

 

대체 언제 사용할까?

여러개의 하위 값을 포함하는 복잡한 state를 다뤄야할 때 

useState대신 useReducer를 사용하면 코드를 깔끔하게 쓸 수 있고, 유지 보수도 편리해진다.

 

useReducer는 Reducer, Dispatch, Action 3가지로 이루어져있다.

 

사용자가 state를 변경하려고 Reducer에게 요구할 때 -> Dispatch

사용자가 state를 어떻게 변경하려는지 요구안에 그 내용을 담는다. -> Action

Reducer는 해당 요구를 받아서 state를 변경한다.

 

코드로 보면 아래와 같다.

Dispatch(Action) ---> Reduecer(State, Action) ---> state 변경

 

 

사용방법

const [state, dispatch] = useReducer(reducer, initialArg, init?)

 

반환 값

1. state : 현재 상태입니다. 첫 번째 렌더링 시에는 init(initialArg ) 또는 초기화되지 않은 경우 initialArg로 설정됩니다.

2. dispatch : 상태를 다른 값으로 업데이트하고 렌더링을 다시 트리거할 수 있는 디스패치 함수입니다.

 

매개변수

1. redeucer : 상태가 업데이트되는 방식을 지정하는 reducer 함수입니다. 

2. initialArg : 초기 상태가 계산되는 값입니다. 모든 유형의 값이 될 수 있습니다. 초기 상태가 계산되는 방식은 다음 init 인수에 따라 달라집니다.

3. option init? : 초기 상태를 반환해야 하는 이니셜라이저 함수입니다. 지정하지 않으면 초기 상태가 initialArg로 설정됩니다. 그렇지 않으면 초기 상태는 init(initialArg)를 호출한 결과로 설정됩니다.

 

 

reducer함수 생성

import { useReducer } from 'react';

function reducer(state, action) {
  // ...
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });
  // ...

 

 

실제 사용 방법

import { useReducer } from 'react';


// reducer함수
function reducer(state, action) {
 //type 값에 따라 분기처리
  if (action.type === 'incremented_age') {
    return {
      age: state.age + 1
    };
  }
  throw Error('Unknown action.');
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });

  return (
    <>
      <button onClick={() => {
        dispatch({ type: 'incremented_age' })// type을 dispatch
      }}>
        Increment age
      </button>
      <p>Hello! You are {state.age}.</p>
    </>
  );
}

 

보통은 아래와 같이 switch문을 활용한다.

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

 

 

액션은 어떤 형태든 가질 수 있습니다.

function Form() {
  const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
  
  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    });
  }
  // ...

일반적으로 액션을 식별하는 type 속성을 가진 객체를 전달하는 것이 일반적입니다.

여기에는 reducer가 다음 상태를 계산하는 데 필요한 최소한의 필수 정보가 포함되어야 합니다.

 

 

주의점!

상태는 읽기 전용입니다. 상태의 개체나 배열을 수정하지 마세요

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // 🚩 이와 같은 상태의 개체는 변경하지 마세요:
      state.age = state.age + 1;
      return state;
    }

 

대신 항상 reducer에서 새 객체를 반환하세요

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // ✅ 대신 새 객체를 반환합니다.
      return {
        ...state,
        age: state.age + 1
      };
    }

자세한 내용은 상태 오브젝트 업데이트하기상태 배열 업데이트하기를 참조하세요.

 

 

 


사실 위 코드들은 너무 간단해서 useReducer보단 useState를 쓰는 것이 더 낫다.

하지만 복잡한 상태를 관리할 떄는 useReducer를 쓰면 더욱 효과적일 것이다.

 

아래 예시를 보자

아래 코드는 최상위 컴포넌트에서 reducer가 어떻게 사용되었는지의 예시다.

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Visit Kafka Museum', done: true },
  { id: 1, text: 'Watch a puppet show', done: false },
  { id: 2, text: 'Lennon Wall pic', done: false }
];

위 처럼 복잡한 state를 관리해야할 때 코드를 깔끔하게 작성할 수 있다.

 

 

초기상태를 만드는 함수를 최적화하기

React는 보통 초기 상태를 한 번 저장하고 다음 렌더링에서 이를 무시합니다.

 

하지만 아래 코드에서 createInitialState(username) 의 결과는 초기 렌더링에만 사용되지만,

여전히 모든 렌더링에서 이 함수를 호출하게 됩니다.

이는 큰 배열을 만들거나 값비싼 계산을 수행하는 경우 낭비가 될 수 있습니다.

function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, createInitialState(username));
  // ...

이 문제를 해결하려면 이니셜라이저 함수로 전달하여 대신 세 번째 인수로 Reducer를 사용할 수 있습니다

function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, username, createInitialState);
  // ...

함수를 호출한 결과인 createInitialState()가 아니라 함수 자체인 createInitialState를 전달하고 있다는 점을 봐야합니다.

이렇게 하면 초기화 후 초기 상태가 다시 생성되지 않습니다.

위의 예에서 createInitialState는 usename을 인수를 받습니다..(두 번째 인수)

이니셜라이저가 초기 상태를 계산하는 데 아무런 정보가 필요하지 않은 경우,

useReducer의 두 번째 인수로 null을 전달할 수 있습니다.

728x90
반응형
LIST