useReducer
useReducer의 역할
useReducer는 State(상태)를 관리하고 업데이트하는 Hook인 useState를 대체할 수 있는 Hook이다. 다시 말해, useReducer는 useState처럼 State를 관리하고 업데이트 할 수 있는 Hook이다.
useReducer는 한 컴포넌트 내에서 State를 업데이트하는 로직 부분을 그 컴포넌트로부터 분리시키는 것을 가능하게 해준다는 것이다.
그렇게 useReducer는 State 업데이트 로직을 분리하여 컴포넌트의 외부에 작성하는 것을 가능하게 함으로써 코드의 최적화를 이루게 해준다. 아래의 두 코드를 비교해보자(State 업데이트 로직이 어디에 위치했는지 큰 구조만 살펴볼 것)
- useState를 사용한 경우 : 컴포넌트 내부에 State 업데이트 로직이 존재
- useReducer를 사용한 경우 : 컴포넌트 외부에 State 업데이트 로직이 존재
앞서 말한 분리시키는 것을 가능하게 해준다는 것은 state 업데이트 로직을 다른 파일에 작성해서(분리) 분리된 파일을 불러와서 사용하는 것도 가능하다는 뜻도 된다.
이렇게 useReducer를 사용하면 컴포넌트의 최적화를 이룰 수 있다.
useReducer 기본 구조와 요소
샘플 코드
React 튜토리얼에서 흔히 볼 수 있는 Counter 앱이다.
import React, { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "decrement":
// action의 type이 "decrement"일 때, 현재 state 객체의 count에서 1을 뺀 값을 반환함
return { count: state.count - 1 };
case "increment":
// action의 type이 "increment"일 때, 현재 state 객체의 count에서 1을 더한 값을 반환함
return { count: state.count + 1 };
default:
// 정의되지 않은 action type이 넘어왔을 때는 에러를 발생시킴
throw new Error("Unsupported action type:", action.type);
}
}
function Counter() {
const [number, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
{/* 현재 카운트 값은 state인 number 객체의 count로부터 읽어옴 */}
<h1>Count: {number.count}</h1>
{/* 카운트 값의 변경을 위해 각 버튼이 클릭되면 dispatch 함수가 발동되면서 reducer 함수가 실행됨.
dispatch 함수의 인자로, action 객체가 설정되었는데,
action 객체의 type에는 어떤 버튼을 클릭하였는지에 따라
"decrement" 또는 "increment"가 들어감
*/}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
export default Counter;
useReducer 함수
useReducer 함수는 첫번째 인자인 reducer 함수의 리턴 값으로 state를 갱신하는 역할을 한다.
기본적으로 useReducer는 다음과 같은 형태로 사용한다.
const [state, dispatch] = useReducer(reducer, initializerArg, initializer);
- state: 컴포넌트에서 사용할 state(상태)
- dispatch: reducer 함수를 실행시키며, 컴포넌트 내에서 state의 업데이트를 일으키기 위해서 사용하는 함수
- reducer: 컴포넌트 외부에서 state를 업데이트하는 로직을 담당하는 함수
현재의 state와 action 객체를 인자로 받아서 기존의 state를 대체(replace)할 새로운 State를 리턴하는 함수 - initializerArg: 초기 state
- initializer: 초기 함수
위 샘플코드에서는 useReducer 함수를 다음과 같이 작성해주었다.
const [number, dispatch] = useReducer(reducer, 0);
action
action은 업데이트를 위한 정보를 가지고 있는 것이며 dispatch의 인자가 되며 reducer 함수의 두번째 인자인 action에 할당된다.
action은 따로 정해진 형태는 없으나 아래의 코드와 같이 주로 type이라는 값을 지닌 객체 형태로 사용된다고 한다.
위 샘플 코드에서는 { type: "decrement" } 이 부분이 action이다.
dispatch({ type: "decrement" })
dispatch 함수
dispatch 함수는 reducer 함수를 실행시킨다.
dispatch 함수의 인자로써 업데이트를 위한 정보를 가진 action를 이용하여 컴포넌트 내에서 state의 업데이트를 일으키기 위해서 사용된다. dispatch 함수의 인자인 action은 reducer 함수의 두번째 인자인 action에 할당된다.
function Counter() {
const [number, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
{/* 현재 카운트 값은 state인 number 객체의 count로부터 읽어옴 */}
<h1>Count: {number.count}</h1>
{/* 카운트 값의 변경을 위해 각 버튼이 클릭되면 dispatch 함수가 발동되면서 reducer 함수가 실행됨.
dispatch 함수의 인자로, action 객체가 설정되었는데,
action 객체의 type에는 어떤 버튼을 클릭하였는지에 따라
"decrement" 또는 "increment"가 들어감
*/}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
reducer 함수
reducer 함수는 dispatch 함수에 의해 실행되며 컴포넌트 외부에서 state를 업데이트하는 로직을 담당한다.
useReducer 함수의 첫번째 파라미터로 입력된 reducer 함수는 현재의 state와 action을 인자로 받게 되는데 이 action의 값에 근거하여 기존의 state를 대체(replace)할 새로운 state를 리턴한다.
아래 reducer 함수의 코드에서는, 간편하게 switch문을 이용하여 action의 값이 무엇인지에 따라 새로운 state를 리턴하고 있다.
switch문을 사용하든 if문을 사용하든 방법은 자유지만, if문을 사용하면 action의 값에 맞춘 조건으로써 else if로 계속 처리를 만들어줘야하고 연산자도 써해서 보통 switch문을 사용한다.
function reducer(state, action) {
switch (action.type) {
case "decrement":
// action의 type이 "decrement"일 때, 현재 state에서 1을 뺀 값을 반환함
return state - 1;
case "increment":
// action의 type이 "increment"일 때, 현재 state에서 1을 더한 값을 반환함
return state + 1;
default:
// 정의되지 않은 action type이 넘어왔을 때는 에러를 발생시킴
throw new Error("Unsupported action type:", action.type);
}
}
주의할 점
reducer 함수는 기존의 state를 새로운 state로 대체(replace)한다는 것이다.
기존의 state를 변경(change)하거나 추가(add)하거나 덮어쓰기(overwrite)를 하면 안된다.
얕은 복사를 통해 새로운 state를 생성하고 그 state에 변경 및 추가 등을 하면 된다.
useReducer & useState 무엇을 사용해야 하는가
useReducer useState 둘다 State를 변경하고 관리할 때 사용할 수 있다.
그럼 언제 useReducer를 사용하며 언제 useState를 사용해야 하는지 찾아봤다.
- useState
- 관리해야 할 State가 1개일 경우
- 그 State가 단순한 숫자, 문자열 또는 Boolean 값일 경우 - useReducer
- 관리해야 할 State가 1개 이상, 복수일 경우
- 혹은 현재는 단일 State 값만 관리하지만 추후 유동적일 가능성이 있는 경우
- 스케일이 큰 프로젝트의 경우
- State의 구조가 복잡해질 것으로 보이는 경우