useReducer와 커스텀훅
안녕하세요 강사님,
useReducer 파트의 강의를 보며 계속 useReducer의 필요성에 대해 생각해보고 있습니다.
협업을 여럿 진행하며 useReducer가 쓰인 코드를 많이 못봐서 그런지 강의해주신 장점들의 상당 부분을 커스텀 훅 로직으로 대체할 수 있을 거라는 생각이 들었습니다.
UI 컴포넌트와 여러 상태를 응집력있게 업데이트 해야 하는 경우
단순 휴먼 에러를 방지하고자 한다면, 커스텀 훅 내에 여러 setState()를 포함하는 함수를 만들 수 있을 것 같습니다. 리액트가 배칭 기능도 제공하니까 성능 면에서도 차이가 없지않나 생각이 들었습니다.
UI 역할을 하는 컴포넌트와 비즈니스 로직을 담당하는 리듀서를 완벽히 분리
마찬가지로 커스텀 훅을 통해 UI단과 비즈니스로직을 분리할 수 있을 것 같습니다.
테스트에서 장점이 있겠으나 테스트 만으로 useReducer를 사용해야 하는건지 아직 경험이 부족해 잘 모르겠네요. 강사님께서 혹시 useReduer만의 장점을 알려주실 수 있을까요? 아님 취향차이일까요?
Answer 1
1
안녕하세요 TAESUN님. 질문해주신 내용을 보니 리액트의 상태 관리와 아키텍처, 그리고 그 기반이 되는 원리까지 깊이 있게 고민하고 계신 것 같습니다. 결론부터 말씀드리면 제시해주신 의견이 정확히 맞습니다. 현대 리액트 개발 환경에서 useReducer가 제공하는 장점 중 상당 부분은 커스텀 훅으로 충분히 대체가 가능하며 때로는 더 직관적이기도 합니다. 리액트 18부터 도입된 자동 배칭(Automatic Batching) 기능 덕분에 여러 개의 setState를 연달아 호출해도 성능상의 불이익이나 불필요한 렌더링 폭포 현상이 발생하지 않기 때문입니다.
그럼에도 불구하고 실무에서 useReducer가 갖는 고유한 입지를 이해하려면 먼저 상태를 변경하는 방식, 즉 '어떻게(How)'와 '무엇을(What)'의 차이인 명령적 접근과 선언적 접근의 차이를 단계적으로 살펴봐야 합니다. 커스텀 훅 내부에 여러 setState를 묶어두는 방식은 철저히 명령적인 접근에 가깝습니다.
// 커스텀 훅을 활용한 명령적 접근의 예시
const useDataFetch = () => {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const handleSuccess = (fetchedData) => {
setIsLoading(false); // 1. 로딩 상태를 false로 변경해라
setData(fetchedData); // 2. 데이터를 업데이트해라
setError(null); // 3. 에러를 초기화해라
};
return { isLoading, data, error, handleSuccess };
}
위의 코드처럼 setLoading(true), setData(data), setError(null)과 같이 상태를 어떻게 바꾸어야 하는지 컴포넌트단에서 구체적인 지시를 내리는 방식이 우리가 자주 사용하는 패턴입니다. 반면 useReducer는 철저히 선언적인 접근을 지향합니다.
// useReducer를 활용한 선언적 접근의 예시
const [state, dispatch] = useReducer(reducer, initialState);
// 컴포넌트는 단지 '무슨 일이 일어났는지'만 알립니다.
dispatch({ type: 'FETCH_SUCCESS', payload: fetchedData });
데이터 로딩 성공이라는 사건이 발생했다는 의미로 위와 같이 디스패치를 호출하면, 상태가 어떻게 변할지는 컴포넌트 외부의 리듀서가 결정하게 됩니다. 실무에서 비즈니스 로직이 고도로 복잡해지면 컴포넌트 내부에서는 어떤 일이 일어났는지라는 액션만 신경 쓰고, 실제 상태의 변환은 리듀서라는 순수 함수에 온전히 위임하는 것이 유지보수에 훨씬 유리할 수 있습니다.
이와 더불어 참조 동등성과 성능 최적화 관점에서도 중요한 차이가 존재합니다. 커스텀 훅 안에서 상태를 업데이트하는 여러 함수를 만들어 하위 컴포넌트로 전달할 때는 렌더링될 때마다 함수가 재생성되는 것을 막기 위해 많은 useCallback과 의존성 배열 관리가 필요해집니다. 반면 useReducer가 반환하는 dispatch 함수는 리액트 컴포넌트의 생명주기 동안 메모리 참조가 절대 변하지 않음을 리액트 자체적으로 보장받습니다. 그렇기 때문에 dispatch를 Context API에 담아 깊은 하위 컴포넌트로 전달하더라도 불필요한 리렌더링이나 의존성 배열의 지옥에 빠지지 않고 매우 안정적인 아키텍처를 구성할 수 있습니다.
협업 프로젝트에서 useReducer를 많이 보지 못하신 것은 현재 프론트엔드 생태계의 현실을 그대로 반영하고 있으며, 여기에는 실무를 관통하는 명확하고 현실적인 이유들이 있습니다. 현업에서는 로컬 상태가 useReducer를 도입해야 할 만큼 복잡해지기 전에 이미 이를 훨씬 더 잘 다루는 전문 라이브러리들에게 역할을 위임하는 것이 압도적으로 유리하기 때문입니다.
이를 구체적인 실무 팁과 함께 상세히 나누어 살펴보자면, 첫째로 서버 상태(Server State) 관리 패러다임의 완벽한 전환입니다. 과거에는 API 통신 시 발생하는 로딩, 데이터, 에러 상태를 FETCH_START, FETCH_SUCCESS, FETCH_ERROR 등의 액션으로 나누어 useReducer로 관리하는 것이 정석이었습니다. 하지만 실제 서비스에서는 단순한 상태 저장을 넘어 캐싱, 포커스 시 자동 재요청, 낙관적 업데이트(Optimistic Update), 에러 재시도(Retry) 같은 매우 고도화된 기능이 필수적입니다. 이 엄청난 양의 보일러플레이트를 useReducer로 직접 구현하는 것은 실무 관점에서 명백한 인력 낭비이며, 현재는 TanStack Query(React Query)나 SWR 같은 라이브러리가 단 한두 줄의 코드로 이 모든 것을 완벽하게 대체하고 서비스의 안정성을 크게 높여줍니다.
둘째는 전역 상태(Global State) 관리 도구들의 경량화와 최적화입니다. useReducer를 Context API와 결합하면 훌륭한 상태 관리가 가능하지만, 실무에서는 상태가 조금만 변해도 Context를 구독하는 모든 하위 컴포넌트가 리렌더링되는 성능 이슈에 직면하게 됩니다. 이를 막기 위해 상태용 Context와 디스패치용 Context를 쪼개고 React.memo를 곳곳에 씌워야 하는 수고로움이 따릅니다. 반면 현대 실무의 표준으로 자리 잡은 Zustand나 Jotai, Redux Toolkit 같은 라이브러리들은 보일러플레이트가 거의 없으면서도 리액트의 렌더링 사이클 외부에서 상태를 효율적으로 관리하여 이런 성능 최적화 고민을 원천적으로 해결해 줍니다. 그래서 상태가 조금이라도 복잡해질 기미가 보이면 useReducer를 쓰기보다 곧바로 Zustand 같은 전역 스토어로 로직을 올려버리는 팀이 대다수입니다.
셋째는 폼(Form) 상태 관리의 위임입니다. 회원가입이나 상품 등록 같은 복잡한 입력 폼은 입력값, 에러 상태, 터치 여부, 유효성 검사 등 관리해야 할 상태가 방대하여 과거 useReducer의 단골 소재였습니다. 그러나 매 타이핑마다 무거운 리듀서 로직이 실행되고 컴포넌트가 리렌더링되는 것은 사용자 경험(UX)에 치명적일 수 있습니다. 현재는 React Hook Form 같은 라이브러리가 비제어(Uncontrolled) 컴포넌트 기반으로 렌더링을 최소화하며 이 역할을 훨씬 우수한 성능과 개발자 경험으로 손쉽게 해결해 주고 있습니다. 결국 실무에서 useReducer의 포지션은, 외부 라이브러리를 도입하기에는 과하고 useState만 쓰기에는 상태 간의 꼬임이 심각하게 우려되는 아주 좁고 특수한 로컬 컴포넌트 영역으로 축소된 것이 자연스러운 현실입니다.
이러한 상황에서도 현업에서 useReducer를 전략적으로 선택하는 구체적인 예외 케이스들이 분명 존재합니다. 먼저 상태 간의 의존성이 극도로 높은 경우를 들 수 있습니다. 예를 들어 A 상태가 바뀔 때 반드시 B와 C 상태도 이전 상태값을 참조하여 정교하게 동기화되어 바뀌어야 하는 복잡한 데이터 그리드나 드래그 앤 드롭 엔진, 혹은 노션 같은 리치 텍스트 에디터의 로컬 UI 로직을 설계할 때 그 진가를 발휘합니다. 다음으로 상태 머신 패턴을 구현할 때입니다.
// 상태 머신 패턴을 활용한 견고한 로직 예시
function dragReducer(state, action) {
switch (state.status) {
case 'IDLE':
if (action.type === 'START') return { status: 'DRAGGING', position: action.payload };
break;
case 'DRAGGING':
if (action.type === 'MOVE') return { ...state, position: action.payload };
if (action.type === 'DROP') return { status: 'DROPPED', position: action.payload };
break;
case 'DROPPED':
if (action.type === 'RESET') return { status: 'IDLE', position: { x: 0, y: 0 } };
break;
}
return state; // 현재 상태에서 허용되지 않은 액션은 무시되어 버그를 원천 차단함
}
이 코드처럼 UI가 가질 수 있는 상태가 IDLE, DRAGGING, DROPPED와 같이 명확히 정해져 있고, 특정 상태에서만 특정 액션이 허용되어야 하는 견고한 로직을 짤 때는 switch 문을 활용하는 리듀서가 가장 안전한 선택지가 됩니다. 물론 이조차도 로직이 거대해지면 XState 같은 전문 상태 머신 라이브러리로 대체되곤 합니다.
실무에서 직접적인 사용 빈도가 이렇게 줄어들었음에도 불구하고 우리가 useReducer를 반드시 깊이 있게 학습하고 이해해야 하는 명확한 이유가 있습니다. 가장 큰 이유는 useReducer가 현대 프론트엔드 상태 관리의 근간이 되는 '단방향 데이터 흐름(Unidirectional Data Flow)'과 '플럭스(Flux) 아키텍처'의 핵심을 리액트 내장 훅으로 완벽하게 구현해 둔 훌륭한 교보재이기 때문입니다. 앞서 말씀드린 Zustand나 Redux 같은 전역 상태 관리 라이브러리들은 내부적으로 형태만 조금 다를 뿐, 결국 '액션(Action)'을 발생시키고 이를 중앙에서 받아 '순수 함수(Reducer)'를 통해 예측 가능한 새로운 상태를 반환한다는 useReducer의 철학과 패턴을 그대로 계승하고 있습니다. 따라서 useReducer의 동작 원리를 명확히 이해하고 있다는 것은 단순히 훅 하나를 더 아는 것을 넘어, 상태가 어떻게 예측 가능하게 전이되어야 하는지, 그리고 UI 컴포넌트와 비즈니스 로직을 어떻게 완벽히 분리할 수 있는지에 대한 깊은 아키텍처적 통찰력을 갖추게 됨을 의미합니다. 이는 향후 거대한 프로젝트의 상태 관리 시스템을 직접 설계하거나 복잡한 오픈소스 코드를 분석할 때 가장 강력한 무기가 됩니다.
여기에 프로젝트 설계를 위한 또 다른 현실적인 팁을 덧붙이자면, 프로젝트 초기 설계 단계부터 이 로직은 복잡하니까 무조건 useReducer를 써야지라고 결정하기보다는, 일단 가장 익숙한 useState와 커스텀 훅의 조합으로 속도감 있게 개발을 시작하시는 것을 적극 권장합니다. 코드를 작성해 나가다 상태 업데이트 함수 내부에 if문이 너무 많아지고 여러 상태값들이 서로 얽혀서 사소한 수정에도 버그가 발생하기 쉬운 임계점이 오면, 그때 해당 컴포넌트의 비즈니스 로직만 추출하여 useReducer로 리팩토링하거나 앞서 말씀드린 Zustand 등의 라이브러리로 승격시키는 것이 실무에서 겪을 수 있는 가장 유연하고 이상적인 흐름입니다.
결론적으로 대부분의 일상적인 개발에서는 생각하신 대로 커스텀 훅과 useState의 조합으로 충분하며 실질적으로는 취향 차이의 영역이 맞으므로 억지로 useReducer를 도입할 필요는 없습니다. 다만 앞서 설명해 드린 대로 상태 변경 로직을 UI와 완벽히 격리하고 액션과 상태 전이를 분리한다는 이 근본적인 설계 철학을 깊이 이해하고 체화하신다면, 앞으로 어떤 상태 관리 도구를 마주하더라도 컴포넌트가 무분별하게 비대해지는 것을 막는 탄탄한 밑거름이 될 것입니다. 강의를 들으시면서 또 궁금한 점이나 현업의 시각이 필요한 부분이 있다면 언제든 질문을 남겨주시기 바랍니다.
감사합니다!
1
상태 관련 로직이 복잡해졌을때 전역 상태 관리 라이브러리의 도입이나 useReducer + context api 사용을 고려해볼 수 있을 것 같네요. 답변이 학습에 큰 도움이 되었네요. 감사합니다!
vercel new project 에 노출되지 않으면 어떻게 해야 할까요?
0
17
1
소스코드 어디서 다운받아요?
0
13
5
동영상 끊김 ( 섹션 2 )
0
15
2
supabase 다른 프로젝트 적용 관련 질문드려요.
0
21
1
시스템관리자가 앱을 차단했다고 뜹니다.
0
14
1
프로젝트 폴더 복사 후 사용 관련
0
19
2
기술스택 강의 관련해서 질문드려요.
0
22
2
강의 자료를 찾을 수 없습니다ㅠㅠ
0
22
2
서브에이전트 문의
0
22
2
노션 강의 화면과 실제 화면과 너무 달라서 수업 진행이 안 됩니다
0
29
1
파워셀에서 claude 코드의 버전확인이 않됩니다.
0
25
2
윈도우에서 설정화면이 다릅니다.클로드코드 환경변수 설정
0
23
2
React 와 Virtual DOM 의 이야기 영상 실행이 안됩니다.
0
19
1
깃허브 Publish 질문
0
29
2
클로드 코드 프로 사용자인데..
0
35
2
강의는 순서대로 들어야 할까요??
0
35
1
supabase 사용 관련.
0
32
2
상태(State) 가 "시간이 지남~" 에 대해 질문 있습니다.
0
24
2
문서 업데이트
0
37
2
미션 14에서 StockButton의 memo는 어떤 역할인가요
0
33
2
실습 가이드: 16강 에서 useMemo의 역할은 무엇인가요?
0
43
3
useReducer가 race condition을 해소하는 예시
0
99
1
useRef를 활용한 이전 상태 추적 시 발생하는 ESLint 에러(react-hooks/refs)에 대해 질문드립니다.
0
153
1
미션18
0
73
2

