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

 

반환값

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

 

사용 시 주의할 점

  • 훅(Hook)이므로 컴포넌트의 최상위 또는 커스텀 훅에서만 호출할 수 있다.
    • 반복문이나 조건문에서는 사용할 수 없다.
  • Strict Mode 에서는 reducerinit 함수를 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 재생성 방지하기

  • useReducer3번째 인수 초기화 함수를 전달하여 초기 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);

 

참고 사항

  • useReduceruseState와 매우 유사하지만, state 업데이트 로직을 이벤트 핸들러에서 컴포넌트 외부의 단일 함수로 분리할 수 있다는 차이점이 있다.
  • dispatchaction을 호출해도 오래된 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
728x90

useReducer 훅들어가며useReducer 개념(참고) 리듀스(Reduce, reduce())사용 방법매개변수반환값 사용 시 주의할 점 dispatch 함수action사용시 주의할 점reducer 함수 작성하기초기 staet 재생성 방지하기참고 사항예제 코드공식 문서 예제카운터 예제참고 사이트