728x90

useReducer 훅

들어가며

  • 리액트(React.js)에서 사용되는 @useReducer@ 훅에 대해 정리해본다.

 

@useReducer@ 

개념

  • 컴포넌트에 리듀서(Reducer)를 추가하는 리액트 훅
const [state, dispatch] = useReducer(reducer, initialArg, init?)

 

 

(참고) 리듀스(Reduce, @reduce()@)

  • 배열의 각 요소에 대해 제공된 함수(@callback@ 함수)를 실행하고 하나의 결과값을 반환하는 메서드
  • 배열의 각 요소를 누적(Accumulate)하여 단일 값으로 줄이는 역할을 한다.
  • 주로 합계, 평균, 최대값, 최소값, 객체 생성 등 다양한 경우에 유용하게 사용된다.
const array = [1, 2, 3, 4, 5];

const result = array.reduce((accumulator, currentValue) => {
  return accumulator + currentValue;
}, initialValue);

console.log(sum);   // 15

 

@accumulator@
  • 누적기(Accumulator)
  • 콜백 함수의 반환값이 다음 반복(Iteration)에서 @accumulator@로 사용된다.

 

@currentValue@
  • 현재 처리되고 있는 배열 요소

 

@initialValue@
  • 초기 누적값
  • 이 값이 생략되면 배열의 첫 번째 요소가 초기 누적값으로 사용된다.

 

사용 방법

useReducer(reducer, initialArg, init?)

 

  • 다른 훅과 마찬가지로 @useReducer@를 컴포넌트 최상단에 호출한다.
  • @reducer@를 이용해 상태(State)를 관리한다.
import { useReducer } from 'react';

// 상태 관리 함수
function reducer(state, action) {
  // ...
}

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

 

매개변수

  • @reducer@
    • 상태가 어떻게 업데이트 되는지 지정하는 (리듀서) 함수
    • 반드시 순수 함수여야 하며, @state@와 @action@을 인수로 받아야 하고, 다음 @state@를 반환해야 한다.
    • @state@와 @action@에는 모든 데이터 타입이 할당될 수 있다.
  • @initialArg@
    • 초기 @state2가 계산되는 값
    • 모든 데이터 타입이 할당될 수 있다.
    • 초기 @state@가 어떻게 계산되는지는 다음 @init@ 인수에 따라 달라진다.
  • @init@ (선택)
    • 초기 @state@를 반환하는 초기화 함수
    • 함수에 이 인수가 할당되지 않으면 초기 @state@는 @initialArg@로 설정된다.
    • 할당되었다면 초기 @state@는 @init(initialArg)@를 호출한 결과가 할당된다. 

 

반환값

  • 2개의 요소로 구성된 배열을 반환한다.
    • 현재 @state@
      • 첫 번째 렌더링에서의 @state@는 @init(initialArg)@ 또는 @initialArg@로 설정된다.
        • @init@이 없을 경우 @initialArg@로 설정된다.
    • @dispatch@ 함수
      • @staet@를 새로운 값으로 업데이트하고 리렌더링을 일으킨다.

 

사용 시 주의할 점

  • 훅(Hook)이므로 컴포넌트의 최상위 또는 커스텀 훅에서만 호출할 수 있다.
    • 반복문이나 조건문에서는 사용할 수 없다.
  • @Strict Mode@ 에서는 @reducer@와 @init@ 함수를 2번 호출한다.

 

 

@dispatch@ 함수

  • @state@를 새로운 값으로 업데이트하고 리렌더링을 일으킨다.
  • 유일한 인수로 @action@이 있다.
  • 어떤 값도 반환하지 않는다.
const [state, dispatch] = useReducer(reducer, { age: 42 });

function handleClick() {
  dispatch({ type: 'incremented_age' });   // action : { type: 'incremented_age' }
  // ...

 

@action@

  • 사용자에 의해 수행된 활동
  • 모든 데이터 타입이 할당될 수 있다.
  • 컨벤션으로 일반적으로 @action@을 정의하는 @type@ 프로퍼티와 추가적인 정보를 표현하는 기타 프로퍼티를 포함한 객체로 구성된다.
{
    type: action_type,
    prop1: value1,
    ...
    propn: valuen,
}

 

사용시 주의할 점

  • @dispatch@ 함수는 오직 다음 렌더링에 사용할 @state@ 변수만 업데이트 한다.
    • 만약 @dispatch@ 함수를 호출한 직후에 @state@ 변수를 읽는다면 호출 이전의 최신화 되지 않는 값이 참조 된다.
  • @Object.is@ 비교를 통해 새롭게 제공된 값과 현재 @state@를 비교한 값이 같을 경우, 컴포넌트와 해당 컴포넌트의 자식 요소들의 리렌더링이 발생하지 않는다.

 

@reducer@ 함수 작성하기

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

 

  • 보통 코드 컨벤션에 따라 @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);
}

 

  • @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
    });
  }
  // ...

 

  • @state@는 읽기 전용이므로 @state@의 객체나 배열을 변경하지 말자.
    • 그 대신에 @reducer@에서 새로운 객체를 반환한다.
// (1) 잘못된 사용 예
function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // 🚩 아래와 같이 객체를 변형시키지 않는다.
      state.age = state.age + 1;
      return state;
    }
}

// (2) 올바른 사용 예
function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // ✅ 그 대신에 새로운 객체를 반환한다.
      return {
        ...state,
        age: state.age + 1
      };
    }
}

 

초기 @staet@ 재생성 방지하기

  • @useReducer@의 3번째 인수 초기화 함수를 전달하여 초기 @state@ 재생성을 방지할 수 있다.
function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, username, createInitialState);   // 초기화 함수 전달
  // ...

 

 

createInitialState()처럼 함수를 호출해서 전달하는 것이 아니라, createInitialState 함수 자체를 전달한다.

 

 

  • 초기화 함수(@createInitialState@)가 @username@을 인수로 받는다.
  • 만약 초기화 함수가 초기 @state@를 계산할 때 어떤 @state@ 값을 인수로 필요하지 않는다면 2번째 인수에 @null@을 전달할 수 있다.
const [state, dispatch] = useReducer(reducer, null, createInitialState);

 

참고 사항

  • @useReducer@는 @useState@와 매우 유사하지만, @state@ 업데이트 로직을 이벤트 핸들러에서 컴포넌트 외부의 단일 함수로 분리할 수 있다는 차이점이 있다.
  • @dispatch@로 @action@을 호출해도 오래된 @state@ 값이 참조되는데, 이 문제를 해결하려면 @reducer@ 함수를 직접 호출해서 다음 값을 확인해보면 된다.
function handleClick() {
  console.log(state.age);  // 42

  dispatch({ type: 'incremented_age' }); // Request a re-render with 43
  console.log(state.age);  // Still 42!

  setTimeout(() => {
    console.log(state.age); // Also 42!
  }, 5000);
}

//
// To check the right estimatead value :
//

const action = { type: 'incremented_age' };
dispatch(action);

const nextState = reducer(state, action);
console.log(state);     // { age: 42 }
console.log(nextState); // { age: 43 }

 

 

  • 리액트에서는 이전 @state@와 다음 @state@를 비교했을 때, 값이 일치하면 업데이트가 되지 않는다.
    • 비교는 @Object.is@ 를 통해 이루어진다.
    • 리액트는 기존의 @staet@ 객체가 변형(Mutation)된 상태로 반환된다면 업데이트를 무시한다.
// 잘못된 사용 예
function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // 🚩 Wrong: mutating existing object
      state.age++;
      return state;
    }
    case 'changed_name': {
      // 🚩 Wrong: mutating existing object
      state.name = action.nextName;
      return state;
    }
    // ...
  }
}

// 올바른 사용 예
function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // ✅ Correct: creating a new object
      return {
        ...state,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      // ✅ Correct: creating a new object
      return {
        ...state,
        name: action.nextName
      };
    }
    // ...
  }
}

 

  • 각각의 `case`에서 새로운 @state@를 반환할 때 기존에 있는 필드를 모두 복사했는지 확인한다. 그렇게 하지 않을 경우, @state@ 객체의 일부가 @dispatch@된 후 @undefined@로 참조될 수 있다.
function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        ...state, // ✅ 기존에 있는 필드 모두 복사
        age: state.age + 1
      };
    }
    // ...

 

예제 코드

공식 문서 예제

import { useReducer } from 'react';

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);
}

const initialState = { name: 'Taylor', age: 42 };

export default function Form() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

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

  return (
    <>
      <input
        value={state.name}
        onChange={handleInputChange}
      />
      <button onClick={handleButtonClick}>
        Increment age
      </button>
      <p>Hello, {state.name}. You are {state.age}.</p>
    </>
  );
}

 

카운터 예제

import { useReducer } from 'react';

export function counterReducer(state, action) {
    if (action.type === "INCREMENT") {
        return ({
            ...state,
            count: state['count'] + 1
        });
    }
    else if (action.type === "DECREMENT") {
        return ({
            ...state,
            count: state['count'] - 1
        });
    }
    else if (action.type === "RESET") {
        return ({
            ...state,
            count: 0
        });
    }
    
    return state;
}

function App() {
    const [counterState, counterDispatch] = useReducer(counterReducer, {
        count: 0,
    })
    
    const handleIncrement = () => {
        counterDispatch({
            type: "INCREMENT"
        });
    }
    
    const handleDecrement = () => {
        counterDispatch({
            type: "DECREMENT"
        });
    }
    
    const handleReset = () => {
        counterDispatch({
            type: "RESET"
        });
    }
    
    const ctxValue = {
        count: counterState.count
    };
    
    return (
    <div id="app">
      <h1>The (Final?) Counter</h1>
      <p id="actions">
        <button onClick={() => handleIncrement()}>Increment</button>
        <button onClick={() => handleDecrement()}>Decrement</button>
        <button onClick={() => handleReset()}>Reset</button>
      </p>
      <p id="counter">{ctxValue.count}</p>
    </div>
    );
}

export default App;

 

 

고 사이트

 

useReducer – React

The library for web and native user interfaces

ko.react.dev

 

728x90