Inflearn Community Q&A
useReducer가 race condition을 해소하는 예시
Resolved
Written on
·
30
0
안녕하세요 강사님! 강의 잘 보고 있습니다.
학습을 위해 useReducer가 race condition 문제를 해소하는 예시 코드를 받아볼 수 있을까요?
useReducer의 장점으로 디버깅 용이성이나 테스트 용이성, UI와 데이터 로직의 분리 등의 장점이 있다지만 이것만으로는 그냥 취향 차이라고 느껴져서요.
명확한 문제를 해결할 수 있는 대안 중 하나로써 한번 공부해보고 싶습니다.
리액트 공식 문서에서도
"만약 일부 컴포넌트에서 잘못된 방식으로 State를 업데이트하는 것으로 인한 버그가 자주 발생하거나 해당 코드에 더 많은 구조를 도입하고 싶다면 Reducer 사용을 권장합니다"
위 같이 언급하고 있는데, 잘못된 방식으로 State를 업데이트해 버그가 발생하는 예시들을 알고 싶습니다.
Answer 1
1
안녕하세요 TAESUN님! 굉장히 깊이 있고 좋은 질문입니다.
사실 useReducer와 useState의 차이를 처음 접할 때, 이것이 그저 코딩 스타일이나 취향 차이가 아닌가 하고 느끼는 것은 저를 포함해 실무를 하는 프론트엔드 개발자들도 흔히 겪는 매우 자연스러운 과정입니다. 단순한 카운터나 토글 버튼 정도의 로직에서는 useState가 압도적으로 편하고 직관적입니다.
하지만 서비스가 복잡해지고 여러 상태가 서로 얽혀 있을 때, 혹은 비동기 이벤트가 동시다발적으로 발생할 때 useState가 가진 구조적 한계가 명확히 드러나게 됩니다. 리액트 공식 문서에서 말하는 '잘못된 방식의 업데이트'와 '경쟁 상태(Race Condition)'를 실제 실무에서 자주 마주치는 상황으로 구성하여 하나씩 이야기해 보겠습니다.
먼저 여러 상태를 응집력 있게 업데이트해야 하는 경우를 살펴보겠습니다. 실무에서 파일 업로드 컴포넌트를 개발하며 현재 상태, 업로드된 바이트, 그리고 진행률 이렇게 세 가지를 동시에 관리해야 하는 상황을 가정해 보겠습니다. useState를 사용하면 다음과 같은 코드를 작성하게 됩니다.
const [status, setStatus] = useState('idle');
const [loadedBytes, setLoadedBytes] = useState(0);
const [progress, setProgress] = useState(0);
const handleProgress = (event) => {
setStatus('uploading');
setLoadedBytes(event.loaded);
setProgress(Math.round((event.loaded / event.total) * 100));
};
이 코드는 동작은 하지만, 논리적으로 하나의 사건, 즉 파일 업로드 진행이라는 단일 이벤트에 의해 세 개의 상태가 개별적으로 업데이트되고 있습니다. 만약 로직이 복잡해져서 어느 한 곳에서 setProgress 호출을 누락하거나 순서를 헷갈린다면 바로 UI 버그로 이어지게 됩니다. 반면 이 상황에 useReducer를 적용하면 상태를 어떻게 바꿀지가 아니라 무슨 일이 일어났는지를 의미하는 액션(Action)에만 집중할 수 있게 됩니다.
function uploadReducer(state, action) {
switch (action.type) {
case 'PROGRESS':
return {
...state,
status: 'uploading',
loadedBytes: action.payload.loaded,
progress: Math.round((action.payload.loaded / action.payload.total) * 100),
};
// ... error 처리 등
}
}
// 컴포넌트 내부
const handleProgress = (event) => {
// 이벤트 객체 전체가 아닌, 리듀서에 필요한 순수 데이터만 추출하여 전달합니다.
dispatch({
type: 'PROGRESS',
payload: { loaded: event.loaded, total: event.total }
});
};
이렇게 useReducer를 사용하면 관련된 상태들이 언제나 일관성 있게 함께 업데이트되는 것을 보장할 수 있습니다. 즉, 여러 상태가 강하게 결합되어 있을 때 발생할 수 있는 휴먼 에러를 구조적으로 차단해 주는 것입니다.
이러한 상태 일관성 문제 외에도, useReducer는 비동기 통신의 레이스 컨디션 문제를 방어하는 데에도 매우 유용하게 쓰입니다. 레이스 컨디션 문제는 사용자의 액션 순서와 서버의 응답 순서가 엇갈릴 때 주로 발생합니다. 예를 들어 사용자가 스포츠 탭을 클릭했다가 데이터를 채 불러오기도 전에 연예 탭을 클릭했다고 가정해 보겠습니다.
이때 네트워크 사정으로 나중에 요청한 연예 데이터가 먼저 도착하고, 뒤늦게 처음에 요청했던 스포츠 데이터가 도착한다면 화면은 연예 탭인데 내용은 스포츠 기사가 뜨는 심각한 버그가 발생합니다. 이러한 상황에서 useReducer를 사용하면 현재 활성화된 탭과 요청한 데이터의 카테고리를 비교하여 상태 일관성을 보장할 수 있습니다.
function dataFetchReducer(state, action) {
switch (action.type) {
case 'FETCH_INIT':
return { ...state, isLoading: true, activeTab: action.payload };
case 'FETCH_SUCCESS':
// 핵심 방어 로직: 현재 탭과 도착한 데이터의 카테고리가 다르면 무시합니다.
if (state.activeTab !== action.payload.category) {
return state;
}
return { ...state, isLoading: false, data: action.payload.data };
default:
return state;
}
}
이 코드에서 볼 수 있듯이 네트워크 응답이 뒤늦게 도착하여 FETCH_SUCCESS 액션을 발생시키더라도, 리듀서 내부의 조건문이 훌륭한 방어막 역할을 해줍니다. 현재 활성화된 탭과 도착한 데이터의 카테고리가 다르면 무시하도록 처리하여 엇갈린 과거의 응답을 안전하게 폐기하기 때문에 레이스 컨디션을 선언적으로 해소할 수 있게 되는 것입니다.
실무에서는 여기에 useEffect의 클린업 함수나 AbortController를 더해 이전 네트워크 요청 자체를 취소하는 방식을 함께 사용하면 더욱 완벽하게 사이드 이펙트를 제어할 수 있습니다.
이러한 아키텍처적 이점과 더불어 UI와 비즈니스 로직의 완벽한 분리, 그리고 테스트 용이성 측면도 빼놓을 수 없습니다. 과거에는 useState를 여러 번 호출하면 화면이 여러 번 렌더링되는 성능 이슈가 있었지만, React 18부터는 자동 배칭(Automatic Batching)이 도입되어 리액트가 알아서 한 번의 렌더링으로 최적화해 줍니다. 그럼에도 불구하고 리액트 팀이 복잡한 상황에서 useReducer를 권장하는 가장 큰 이유는 바로 비즈니스 로직을 리액트 컴포넌트 생명주기에서 완전히 분리할 수 있기 때문입니다. 컴포넌트 내부에 길게 늘어진 상태 변경 로직들은 단위 테스트(Unit Test)를 하기가 매우 까다롭습니다. 하지만 리듀서는 리액트라는 라이브러리에 종속되지 않은 완벽한 순수 함수(Pure Function)입니다. 브라우저를 띄우거나 컴포넌트를 렌더링할 필요 없이, 상태 객체와 액션 객체만 넣으면 결과가 제대로 나오는지 아주 쉽고 빠르고 안정적으로 테스트할 수 있다는 점이 실무에서 엄청난 장점으로 다가옵니다.
결론적으로 오늘 말씀드린 핵심 내용을 요약해 보겠습니다. 첫째는 상태의 응집도를 높여준다는 점입니다. 여러 개의 연관된 상태를 하나의 트랜잭션처럼 묶어 업데이트하여 개발자의 휴먼 에러를 방지해 줍니다. 둘째는 상태 검증을 통한 안정성 확보입니다. 비동기 네트워크 통신 시 응답 순서가 꼬이더라도 리듀서 내부의 방어 로직을 통해 레이스 컨디션을 안전하게 걸러낼 수 있습니다. 마지막으로 셋째는 로직 분리와 테스트의 용이성입니다. UI 역할을 하는 컴포넌트와 비즈니스 로직을 담당하는 리듀서를 완벽히 분리하여 아키텍처를 깔끔하게 유지하고 로직 테스트를 매우 쉽게 만들어줍니다. 이번 답변이 TAESUN님께서 useReducer의 본질적인 존재 이유를 깊이 이해하시고, 앞으로 더 견고한 프론트엔드 애플리케이션을 설계해 나가시는 데 좋은 밑거름이 되기를 바랍니다. 파이팅입니다!
참고해주세요 😄😄




