useReducer는 useState의 대체 함수 입니다.
const [state, dispatch] = useReducer(reducer, initialState, init)
(state, action) => newState의 형태로 reducer를 입력받고 dispatch 메서드와 짝의 형태로 현재 state를 반환해 줍니다.
자세한 내용은 코드를 직접 살펴보겠습니다.
react 공식문서에서는 state로직이 복잡해지면 리듀서의 사용, 커스텀 hook을 고려하라고 안내하고 있습니다.
아래에서는 state의 복잡한 상황을 정의해 보고 개선하는 과정을 살펴보겠습니다.
개선된 코드가 개선되지 않았다고 느낄수도 있습니다.
모든 상황에서 장단점이 존재하기 때문입니다.
정답을 하나로 규정하기 보다는 팀의 컨벤션에 맞게, 자신의 스타일에 맞게 코드를 짜는것을 추천드립니다.
또 그에따른 유연한 태도를 겸비해야 하지 않을까 생각합니다.
총 4개의 state 정의
1. component에서 4개의 useState로 관리
2. 하나의 state로 관리
3. useReducer 이용
(세 컴포넌트 모두 동작은 정상적으로 수행합니다)
위의 1 -> 2 -> 3 번의 순서대로 코드를 살펴보고 각각의 장단점을 살펴보겠습니다.
1. 4개의 state로 관리
import { useState } from "react";
const CounterWithoutReducer = () => {
const [countOne, setCountOne] = useState(0)
const [countTwo, setCountTwo] = useState(0)
const [countThree, setCountThree] = useState(0);
const [booleanValue, setBooleanValue] = useState(false)
const numberHandler = (type, setter) => {
if (type === 'plus') {
setter(prev => prev + 1)
}
if (type === 'minus') {
setter((prev) => prev - 1);
}
}
const booleanHandler = () => {
setBooleanValue(prev => !prev)
}
return (
<>
<div>
CountOne: {countOne}
<button onClick={() => numberHandler("plus", setCountOne)}>+</button>
<button onClick={() => numberHandler("minus", setCountOne)}>-</button>
</div>
<div>
CountTwo: {countTwo}
<button onClick={() => numberHandler("plus", setCountTwo)}>+</button>
<button onClick={() => numberHandler("minus", setCountTwo)}>-</button>
</div>
<div>
CountThree: {countThree}
<button onClick={() => numberHandler("plus", setCountThree)}>+</button>
<button onClick={() => numberHandler("minus", setCountThree)}>-</button>
</div>
<div>
Boolean: {booleanValue === false ? 'false' : 'true'}
<button onClick={() => booleanHandler()}>toggle</button>
</div>
</>
);
};
export default CounterWithoutReducer;
4개의 state로 관리했을때의 코드입니다.
state의 성격에 따라 3,1 로 나눌수 있을것 같습니다.
각각의 성격에 따라 handler를 정의할 수 있지만
공통으로 사용하는 handler에서 매번 setter를 보내주어야 합니다.
만약에 기존의 state들과 다른 성격의 state가 추가되면 어떻게 될까요??
useState, handler를 새로 정의해주어야 합니다.
현재 상태에서 customHook으로의 정의또한 어려워 보입니다.
각각의 state를 별개로 관리해야하는 현재의 상황이고 이렇게 되면 customHook은 4개가 생깁니다.
customHook을 활용하기는 어려울것만 같습니다.(물론 상황에 따라 다를 수 있습니다. 그러므로 현재의 상황으로만 가정하겠습니다.)
현재는 코드의 난이도가 높지 않아 문제가 없지만 복잡해졌을때는 다수의 state가 정의된 곳을 파악해야 하고 state의 확장에 장애물이 있을것 같습니다.
즉 유지보수의 비용이 상대적으로 증가될것 같습니다.
하지만 팀의 컨벤션이 위와 같다면 팀의 컨벤션을 따르거나 리팩토링을 타협해야 할것만 같네요
2. 1개의 state로 관리
import { useState } from "react";
const CounterWithState = () => {
const [state, setState] = useState({ countOne: 0, countTwo: 0, countThree: 0, booleanValue: 0 })
const numberHandler = (type) => {
if (type === "plusOne") {
setState(prev => ({
...prev,
countOne: prev.countOne + 1,
}))
}
if (type === "minusOne") {
setState(prev => ({
...prev,
countOne: prev.countOne - 1,
}))
}
if (type === "plusTwo") {
setState((prev) => ({
...prev,
countTwo: prev.countTwo + 1,
}));
}
if (type === "minusTwo") {
setState((prev) => ({
...prev,
countTwo: prev.countTwo - 1,
}));
}
if (type === "plusThree") {
setState((prev) => ({
...prev,
countThree: prev.countThree + 1,
}));
}
if (type === "minusThree") {
setState((prev) => ({
...prev,
countThree: prev.countThree - 1,
}));
}
};
const booleanHandler = () => {
setState(prev => ({
...prev,
booleanValue: !prev.booleanValue,
}))
};
return (
<>
<div>
CountOne: {state.countOne}
<button onClick={() => numberHandler("plusOne")}>+</button>
<button onClick={() => numberHandler("minusOne")}>-</button>
</div>
<div>
CountTwo: {state.countTwo}
<button onClick={() => numberHandler("plusTwo")}>+</button>
<button onClick={() => numberHandler("minusTwp")}>-</button>
</div>
<div>
CountThree: {state.countThree}
<button onClick={() => numberHandler("plusThree")}>+</button>
<button onClick={() => numberHandler("minusThree")}>-</button>
</div>
<div>
Boolean: {state.booleanValue === false ? "false" : "true"}
<button onClick={() => booleanHandler()}>toggle</button>
</div>
</>
);
};
export default CounterWithState;
4개의 state를 key, value 로 하나의 객체에 정의해 주었고 handler를 성격에 맞게 정의해 주었습니다.
따라서 2개의 handler를 정의해 주었습니다. (물론 위의 코드보다 더 효율적으로 작성할 수 있습니다!!)
하나의 useState hook에서 관리하므로 state간의 관계 파악이 1번 보다는 수월할 것 같습니다.
state의 성격에 따라 알맞은 개수의 state hook으로 생성하면 더 좋은 방법일것 같네요
2번의 코드에서는 handler만 util 함수로 이동시키면 컴포넌트 내부의 코드를 파악하기도 수월해질것 같습니다.
하지만 저는 코드를 파악해야 할때 여러 파일들을 탐색해야 하는 것을 선호하지 않습니다.
그런 의미에서 위에서 언급한 컴포넌트에서 사용하는 util이 외부에 있는 것을 선호하지 않습니다.
컴포넌트 내부에 handler를 정의하는 것이 코드의 파악에 있어 더 수월할 것이기 때문입니다.
메모리에는 지역성 이란 개념이 있습니다.
시간 지역성, 공간 지역성이 있는데
시간 지역성은 최근에 참조된 영역은 곧 다시 참조될 가능성이 크고
공간 지역성은 최근에 참조된 영역의 주위가 다시 참조될 가능성이 크다는 이야기 입니다.
(개념이 틀리다면 말씀해주세요!! 잘못 알고 있을수도 있습니다.)
코드에서도 이 원리가 적용된다고 생각합니다.
하나의 코드를 수정하면 그 근처의 코드들이 수정되기 마련입니다.
이런 원리에 근거할때 수정되야 하는 코드가 가까이에 위치한다면 유지보수에 긍정적으로 작용할 것 같습니다.
다시 본론으로 돌아와서 2번의 hook을 customHook으로 분리할 수는 있을까요??
customHook으로의 분리는 가능할것 같네요
customHook 으로 분리한 코드
import { useState } from 'react';
const useMultipleState = () => {
const [state, setState] = useState({
countOne: 0,
countTwo: 0,
countThree: 0,
booleanValue: 0,
});
const numberHandler = (type) => {
if (type === "plusOne") {
setState((prev) => ({
...prev,
countOne: prev.countOne + 1,
}));
}
if (type === "minusOne") {
setState((prev) => ({
...prev,
countOne: prev.countOne - 1,
}));
}
if (type === "plusTwo") {
setState((prev) => ({
...prev,
countTwo: prev.countTwo + 1,
}));
}
if (type === "minusTwo") {
setState((prev) => ({
...prev,
countTwo: prev.countTwo - 1,
}));
}
if (type === "plusThree") {
setState((prev) => ({
...prev,
countThree: prev.countThree + 1,
}));
}
if (type === "minusThree") {
setState((prev) => ({
...prev,
countThree: prev.countThree - 1,
}));
}
};
const booleanHandler = () => {
setState((prev) => ({
...prev,
booleanValue: !prev.booleanValue,
}));
};
return {
state,
numberHandler,
booleanHandler,
}
}
export default useMultipleState;
import useMultipleState from "./hooks/useMultipleState";
const CounterWithState = () => {
const {state, numberHandler, booleanHandler } = useMultipleState();
return (
<>
<div>
CountOne: {state.countOne}
<button onClick={() => numberHandler("plusOne")}>+</button>
<button onClick={() => numberHandler("minusOne")}>-</button>
</div>
<div>
CountTwo: {state.countTwo}
<button onClick={() => numberHandler("plusTwo")}>+</button>
<button onClick={() => numberHandler("minusTwp")}>-</button>
</div>
<div>
CountThree: {state.countThree}
<button onClick={() => numberHandler("plusThree")}>+</button>
<button onClick={() => numberHandler("minusThree")}>-</button>
</div>
<div>
Boolean: {state.booleanValue === false ? "false" : "true"}
<button onClick={() => booleanHandler()}>toggle</button>
</div>
</>
);
};
export default CounterWithState;
3. useReducer 이용
import useCounter from "./hooks/useCounter";
const CounterTest = () => {
const {state, action} = useCounter();
return (
<>
<div>
CountOne: {state.countOne}
<button onClick={() => action("plusCountOne")}>+</button>
<button onClick={() => action("minusCountOne")}>-</button>
</div>
<div>
CountTwo: {state.countTwo}
<button onClick={() => action("plusCountTwo")}>+</button>
<button onClick={() => action("minusCountTwo")}>-</button>
</div>
<div>
CountThree: {state.countThree}
<button onClick={() => action("plusCountThree")}>+</button>
<button onClick={() => action("minusCountThree")}>-</button>
</div>
<div>
Boolean: {state.booleanValue === false ? "false" : "true"}
<button onClick={() => action("toggleBoolean")}>toggle</button>
</div>
</>
);
};
export default CounterTest;
import { useReducer } from 'react';
const useCounter = () => {
const reducer = (state, action) => {
if (action === "plusCountOne") {
state = {
...state,
countOne: state.countOne + 1,
};
}
if (action === "minusCountOne") {
state = {
...state,
countOne: state.countOne - 1,
};
}
if (action === "plusCountTwo") {
state = {
...state,
countTwo: state.countTwo + 1,
};
}
if (action === "minusCountTwo") {
state = {
...state,
countTwo: state.countTwo - 1,
};
}
if (action === "plusCountThree") {
state = {
...state,
countThree: state.countThree + 1,
};
}
if (action === "minusCountThree") {
state = {
...state,
countThree: state.countThree - 1,
};
}
if (action === "toggleBoolean") {
state = {
...state,
booleanValue: !state.booleanValue,
};
}
return state;
};
const initialState = {
countOne: 0,
countTwo: 0,
countThree: 0,
booleanValue: false,
};
const [state, dispatch] = useReducer(reducer, initialState);
return {
state,
action: dispatch
};
}
export default useCounter;
useReducer를 적용한 코드 입니다.
reducer내부의 코드가 길어지는 것은 단점이라 생각합니다.
하지만 customHook으로 분리가 가능한 점과
state의 확장에 유연하며
rendering 부분에서도 코드의 파악에 좀 더 용이하여 유지보수에 도움이 된다고 생각합니다.
하나의 컴포넌트에서 관리해야 하는 state가 복수개 있을때 저는 리듀서의 적용을 선호합니다.
여러개 컴포넌트를 관리해야할때 1번과 같은 방법보다는 각각 customHook으로 관리하여 좀 더 코드의 파악에 도움이 되는 것이 좋다고 생각합니다.
물론 적용은 각자가 선호하는 방식대로 하는 것이 좋지 않을까 생각합니다.
출처 : https://ko.reactjs.org/docs/hooks-reference.html#usereducer
'Frontend > React' 카테고리의 다른 글
React Derived State 🥲 (0) | 2022.04.02 |
---|---|
React props, state 🏔 (0) | 2022.04.02 |
React typescript 에서 dataset 이용하기 😢 (0) | 2022.04.01 |
React setState 🌱 (0) | 2022.03.27 |
React.memo 🌱 (0) | 2022.03.27 |