[CRUD 실습 - React + Vite] 리팩토링 (1) - 문제 분석과 해결 전략

Chan의 기술 블로그·2025년 12월 14일

산출물 링크
- GitHub
- 배포 페이지

지난 포스팅까지 DELETE 기능을 끝으로 기본적인 CRUD 구현을 모두 마쳤다. 이번 단계부터는 작성한 코드를 엔지니어링 관점에서 분석하고 개선하는 리팩토링 과정을 기록하고자 한다.


리팩토링 배경: 기존 코드의 3가지 문제점

본격적인 코드 수정에 앞서, 현재 프로젝트가 가진 구조적 한계와 성능 이슈를 분석했다. 유지보수성과 렌더링 최적화 관점에서 발견된 치명적인 문제점은 크게 3가지다.

1. 거대해진 UsersProvider (관심사 분리 실패)

가장 큰 문제는 UsersProvider가 소위 'God Object(전지전능한 객체)'가 되어버렸다는 점이다. 하나의 Provider가 성격이 다른 너무 많은 책임을 동시에 지고 있다.

  • 데이터 페칭: API 호출 및 서버 상태 관리 (useUsers)
  • 폼 상태 관리: 전체 사용자 목록의 수정 데이터(input value) 관리 (builtAllUsersValue)
  • UI 상태 제어: 체크박스 선택, 모달 노출, 삭제 모드 토글 등
  • 사이드 이펙트: alert, confirm 등 사용자 인터랙션 처리

비즈니스 로직과 UI 로직이 한곳에 뒤섞이면서 코드의 응집도는 낮아지고 결합도는 높아졌다. 이로 인해 기능 수정 시 사이드 이펙트를 예측하기 어렵고, 유지보수가 힘든 상태가 되었다.

2. 잦은 전체 리렌더링 (상태 위치시키기 부재)

입력 필드(input)의 상태가 최상위 컴포넌트인 UsersProvider에 위치함으로써 심각한 렌더링 비효율이 발생했다.

  • 문제 상황: 개별 유저의 input 값이 단 한 글자만 바뀌어도, 상태를 가진 UsersProvider가 업데이트되면서 하위의 모든 컴포넌트(리스트 전체)가 강제로 리렌더링 된다.
  • 성능 이슈: 사용자 리스트가 100명, 1,000명으로 늘어날 경우, 단순한 타이핑 이벤트가 애플리케이션 전체의 프레임 드랍을 유발할 수 있는 구조다.
  • 해결책: 상태를 해당 상태를 실제로 사용하는 컴포넌트 내부로 내리는 'State Colocation(상태 위치시키기)' 작업이 시급하다.

3. useRef와 useEffect를 이용한 안티 패턴

useCallback 등의 의존성 배열 관리 문제를 회피하기 위해, stateref를 억지로 동기화하는 기형적인 패턴을 사용하고 있었다.

// 의존성 회피를 위한 수동 동기화
const [newUserValue, setNewUserValue] = useState<PayloadNewUser>(INIT_NEW_USER_VALUE);
const newUserValueRef = useRef<PayloadNewUser>(newUserValue);

useEffect(() => {
  newUserValueRef.current = newUserValue; // State 변경 시 Ref에 강제 주입
}, [newUserValue]);

해결 전략: useReducer 도입

위에서 분석한 문제점들, 특히 '복잡한 상태 로직'과 '최적화' 문제를 해결하기 위해 useReducer를 도입하기로 결정했다. 도입 이유는 다음과 같다.

1. 상태 업데이트 로직의 분리

useReducer를 사용하면 컴포넌트 내부에 흩어져 있던 상태 변경 로직(setState들의 나열)을 reducer라는 외부 함수로 분리할 수 있다. 컴포넌트는 "무엇을 할지"만 요청하고, 구체적인 "어떻게 변경할지"는 리듀서가 담당하게 하여 관심사를 명확히 분리할 수 있다.

2. dispatch의 안정성

useReducer가 제공하는 dispatch 함수는 React로부터 "객체의 주소값이 영원히 변하지 않음"을 보장받는다.

// 부모 컴포넌트
const Parent = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    // dispatch는 리렌더링이 되어도 주소값이 변하지 않는다.
    // 따라서 ChildComponent에 props로 넘겨도 불필요한 리렌더링을 유발하지 않는다.
    <ChildComponent dispatch={dispatch} />
  );
};

useState의 핸들러 함수를 자식에게 넘길 때는 useCallback 처리가 필수적이었지만, dispatch는 그 자체로 최적화되어 있어 자식 컴포넌트의 불필요한 렌더링을 방지하는 데 유리하다.

useReducer는 단순히 렌더링 횟수를 줄이는 도구가 아니라, '서로 연관된 복잡한 상태들'을 '안전하고 예측 가능하게' 관리하기 위한 설계 도구다.


리팩토링 계획

한 번에 모든 코드를 뜯어고치는 것은 위험 부담이 크다. 따라서 수정하고자 하는 영역을 분활하여 리펙토링을 진행할 것이다.

가장 먼저 UsersNewForm (신규 유저 추가 영역)부터 리팩토링을 진행한다. 전체 수정 로직에 비해 상대적으로 범위가 작아, useReducer 패턴을 실험하고 적용하기에 적합하기 때문이다.

profile
퍼블리셔에서 프론트앤드로 전향하기

0개의 댓글