뭔 개소리 하고 자빠졌냐고?
너희들 대체적으로 공식 사이트에 소개된 예시대로 쓰고있잖아.
import { useReducer } from 'react';
const reducer = (state, action) => {
switch (action.type) {
case 'INCREASE':
return state + 1;
case 'DECREASE':
return state - 1;
}
};
export default function App() {
const [state, dispatch] = useReducer(reducer, 0);
return (
<>
<h3>{state}</h3>
<button
onClick={() => {
dispatch({ type: 'INCREASE' });
}}
>
Increase Counter
</button>
<button
onClick={() => {
dispatch({ type: 'DECREASE' });
}}
>
Decrease Counter
</button>
</>
);
}
그렇다고 내가 저렇게 쓰는게 잘못된 사용법이라고 한 적은 없다.
단지, 이녀석이 위 패턴 이상의 가능성을 소개하기 위해 글을 쓰는 것 뿐이지.
언론사들도 그렇고 너희들도 블로그 쓸 때 제목이 얼마나 중요한지는 알고 있지?
근데 개인적으로 위같은 redux 패턴 자체가 마음에 들지 않을 뿐.
솔직히 내가 싫어하는 패턴이라 redux 가 별로인데, 이건 개인취향이니 넘어가도록 하자.
dispatch
함수는 리렌더링하지 않는다.아는사람들은 아는 사실이지만, useState
의 반환값 두번째 함수와, useReducer
의 반환값 두번째 함수인 dispatch
함수는 렌더링이 바뀌어도 기존 함수 그대로 유지하는 뚝심을 자랑한다. 이건 리액트 공식이 보증한 팩트다.
따라서 useEffect
등의 종속성이 요구되는 훅에서 안심하고 상태 바뀌든 말든 걱정 없이 상태를 바꿀 수 있다.
useEffect(() => {
if (isNotEmpty) {
setMyState('YAY!');
}
}, []); // 아무런 종속성이 없는데도 이 코드는 동작이 정상이다.
따라서 dispatch
함수가 렌더링 시 대응할 필요 따위가 없다.
이건 작은 팁이니 이제 넘어가도록 하겠다.
보통 useState
사용 시, 두번째 dispatch
함수를 하기 위해 보통 이벤트단에서 이고생을 할 것이다. 특히 객체 변경 시 말이지. 객체 변경을 예로 들면,
const [obj, setObj] = useState({ foo: 'bar', bar: 'baz' });
const handleChange1 = (e) => {
const { value } = e.currentTarget;
setObj(obj => ({ ...obj, foo: e.currentTarget.value }));
}
const handleChange2 = (e) => {
const { value } = e.currentTarget;
setObj(obj => ({ ...obj, bar: value }));
}
const handleChangeSome = (e) => {
const { name, value } = e.currentTarget;
setObj(obj => ({ ...obj, [name]: value }));
}
보기만 해도 고구마 3개 다 입에다 우겨넣은 느낌이 들지 않는가?
useRecuder
로 이걸 리팩토링하면 끗.
const objectReducer = (prevObj, newObj) => {
return { ...prevObj, ...newObj };
}
const [obj, setObj] = useReducer(objectReducer, { foo: 'bar', bar: 'baz' });
const handleChange1 = (e) => {
const { value } = e.currentTarget;
setObj({ foo: e.currentTarget.value });
}
const handleChange2 = (e) => {
const { value } = e.currentTarget;
setObj({ bar: value });
}
const handleChangeSome = (e) => {
const { name, value } = e.currentTarget;
setObj({ [name]: value });
}
한 번 공통로직을 reducer
함수에 넣고 사용할 때 깜박하고 객체 병합하지 않는 실수를 범할 필요 없으니 얼마나 깔끔한가!
reducer
함수는 인자 없어도 된다. 따라서 이런 식이 가능하다.
const [toggle, setToggle] = useReducer((prev) => !prev, false);
const [count, increment] = useReducer((prev) => prev + 1, 0);
return <>
<button onClick={setToggle}>Toggle: {toggle}</button>
<button onClick={increment}>Count: {count}</button>
</>;
reducer
함수에 두번째 인자를 아예 없애버려서 인자 없는 함수로 하면 TypeScript 에서 이벤트 함수에 바로 넣어도 타입 오류 없이 정상 동작되는 광경도 볼 수 있고, 그냥 함수만 참조하면 되니 편리하다.
reducer
함수의 자유로움이 이렇게 좋을 수가 있을까? 공통 방어로직을 넣어서 상태 변경하는 로직마다 일일이 방어로직을 넣을 필요 없이 한 번에 해결하도록 하자.
예를 들어, 카운트다운 시 음수가 될 일 없는 로직을 만들어보자.
const [count, decrement] = useReducer((prev) => Math.max(--prev, 0), 10);
return <button onClick={decrement}>Countdown: {count}</button>
누르다 보면 수가 줄어들겠지만, 0 미만 음수가 떨어지지 않는 상태관리가 완성된다.
혹은 위에 내가 소개한 객체와의 조합도 가능하다. 예를 들면, 빈 문자열을 허용하지 않는 양식 기입도 된다.
const validationReducer = (prevObj, newObj) => {
for (const [key, value] of Object.entries(newObj)) {
if (!value?.trim()) {
alert(`${key} 값은 필수 입력 사항!`);
return prevObj; // 이전값 그대로 뱉으면 상태변경될 일이 없다.
}
}
return { ...prevObj, ...newObj };
}
const [obj, setObj] = useReducer(validationReducer, { foo: '', bar: '' });
const handleChange1 = (e) => {
const { value } = e.currentTarget;
setObj({ foo: e.currentTarget.value });
}
const handleChange2 = (e) => {
const { value } = e.currentTarget;
setObj({ bar: value });
}
const handleChangeSome = (e) => {
const { name, value } = e.currentTarget;
setObj({ [name]: value });
}
이렇게 해서 기본적인 유효성 검사도 넣어서 올바른 객체 병합을 유도하고 일일이 상태 변경 시마다 특별한 방어로직 아니면 기본적인 공통 방어로직을 넣어 해결할 수 있는 좋은 방법이 있다. 여기에 zod
같은 유효성 검사 라이브러리까지 조합하면? 캬아!
reducer
함수는 밖으로 빼야 좋다당연하겠지만 리액트 함수 컴포넌트의 아킬레스건인 뭐만 하면 리렌더링되고 리렌더링되면 별도 상태관리 하지 않는 모든 객체는 다 재생성된다는 점, 다들 알고 있을 것이다.
이는 useState
함수의 초기값 대신 초기화 함수 넣을 때도 마찬가지고,
const [val, setVal] = useState(() => (console.log(+new Date()), Date.now()));
reducer 함수 또한 얄짤없다.
const [val, setVal] = useReducer(() => (console.log(+new Date()), Date.now()), 0);
따라서 만약 컴포넌트 내 변수나 상수를 꼭 써야하지 않으면 안되는 상황 아니면 밖으로 빼는 것이 좋으며,
꼭 컴포넌트 내 자원을 활용해야 한다면, useCallback
같은 메모이제이션으로 감싼 함수가 도움이 될 것이다.
물론 함수 재생성 자체는 그렇게 크게 성능이 저하되는 문제는 아니다. 그래서 함수가 간단하다면 굳이 안해도 상관없기는 하다. 근데 클로저 개념을 배웠다면 바로 이해갈 텐데, 여러 종속성이 추가되는 등 함수 본문이 복잡해지면 얘기가 달라진다. 이건 직접 해보면 안다.
const [name, setName] = useState('누');
const myReducer = useCallback((prev, next) => `${name}님은 ${prev}였는데 지금은 ${next} 군요`, [name]);
const [hello, setHello] = seReducer(myReducer, '천재');
return <>
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={() => setHello('바보')}>지금의 {name}님은?</button>
<p>{hello}</p>
</>;
dispatch({ type: '이_업무는_저장이다' })
이런 고상한 패턴에서 벗어나라고.
마치 전자정부 차용한 공공 사이트들 다 확장자가 .do
인 것처럼 말이야.
(참고로 .do
안쓴다는 잘라빠진 사유로 계약위반 소송 걸 뻔한 사례가 있다)
이미 Redux 쓰고 있어서 이 패턴 벗어나면 안돼? 그럼 인정. 그쪽동네는 그렇게 써야 하니.
Zustand가 Redux 대체제로 열광하는 이유? 써보면 안다.
내가 이미 저렇게 쓰고 있냐고? 나한테도 흔한 패턴은 아니더라고...
끗.