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@
- 상태가 어떻게 업데이트 되는지 지정하는 (리듀서) 함수
- 반드시 순수 함수여야 하며, @state@와 @action@을 인수로 받아야 하고, 다음 @state@를 반환해야 한다.
- @state@와 @action@에는 모든 데이터 타입이 할당될 수 있다.
- @initialArg@
- 초기 @state@가 계산되는 값
- 모든 데이터 타입이 할당될 수 있다.
- 초기 @state@가 어떻게 계산되는지는 다음 @init@ 인수에 따라 달라진다.
- @init@ (선택)
- 초기 @state@를 반환하는 초기화 함수
- 함수에 이 인수가 할당되지 않으면 초기 @state@는 @initialArg@로 설정된다.
- 할당되었다면 초기 @state@는 @init(initialArg)@를 호출한 결과가 할당된다.
반환값
- 2개의 요소로 구성된 배열을 반환한다.
- 현재 @state@
- 첫 번째 렌더링에서의 @state@는 @init(initialArg)@ 또는 @initialArg@로 설정된다.
- @init@이 없을 경우 @initialArg@로 설정된다.
- 첫 번째 렌더링에서의 @state@는 @init(initialArg)@ 또는 @initialArg@로 설정된다.
- @dispatch@ 함수
- @staet@를 새로운 값으로 업데이트하고 리렌더링을 일으킨다.
- 현재 @state@
사용 시 주의할 점
- 훅(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;
참고 사이트
728x90
728x90
'Programming > React' 카테고리의 다른 글
[React.js] 리액트에서의 상태 관리 방법 (0) | 2024.06.30 |
---|---|
[React.js] <form> 요소에서 제출(Submit) 버튼 사용 시 기본 동작 막는 방법 (0) | 2024.06.28 |
[React.js] memo() 사용할 때 주의할 점 (0) | 2024.06.25 |
[React.js] useEffect와 useCallback (0) | 2024.06.24 |
[React.js] map 함수를 사용할 때 중괄호({})와 소괄호(()) (0) | 2024.05.29 |
[React.js] 토스트 메시지 띄우기 간단 예제 (ReactDOM.createPortal) (0) | 2024.05.21 |
[React.js] useImperativeHandle과 forwardRef (0) | 2024.05.21 |
[React.js] 부모 컴포넌트에서 자식 컴포넌트로 요소 넘기는 방법 (0) | 2024.05.14 |