상세 컨텐츠

본문 제목

[React.js] UseReducer와 상태 관리 -최적화

25-26/React.js

by jheartit 2025. 11. 28. 10:00

본문

728x90

UseReducer와 상태 관리

상태 관리 : 앱의 복잡성을 줄이고, 사용자 인터페이스의 일관성을 유지하는데 중요한 역할.

state : “시간이 지나면서 바뀌는 데이터”. 버튼 클릭 횟수, 입력 폼 값 등등..

 

UseState와 UseReducer

useState - 상태값과 해당 값을 업데이트하는 함수를 반환한다. 상태가 단순할 때 유용, 기본적인 상태 관리 방식

useReducer - 상태 업데이트 로직을 컴포넌트 외부로 추출하여 더 구조화된 형태로 관리할 수 있다. 복잡할 때 유용하다.

const [state, dispatch] = useReducer(reducer, initialState);

 

     - state: 현재 상태 값

     - dispatch(action): 상태를 바꾸기 위해 “액션”을 보낸다.

     - reducer(state, action): 액션에 따라 새로운 상태를 반환하는 함수

 

유용한 상황

     - 상태가 여러 필드를 가진 객체, 배열 등일 때

     - 상태를 바꾸는 액션 종류가 많을 때 (ADD / REMOVE / UPDATE / TOGGLE …)

     - “상태 변경 로직”을 한 곳에 모아두고 싶을 때  →  유지보수, 테스트, 리팩터링에 유리

 

useState 버전

const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);

async function handleSave() {
  setIsSaving(true);
  setError(null);
  try {
    await savePost({ title, content });
    setIsSaving(false);
  } catch (e) {
    setError(e.message);
    setIsSaving(false);
  }
}

useReducer 버전

const initialState = {
  title: '',
  content: '',
  isSaving: false,
  error: null,
};

function reducer(state, action) {
  switch (action.type) {
    case 'CHANGE_FIELD':
      return { ...state, [action.name]: action.value };
    case 'SAVE_START':
      return { ...state, isSaving: true, error: null };
    case 'SAVE_SUCCESS':
      return { ...state, isSaving: false };
    case 'SAVE_ERROR':
      return { ...state, isSaving: false, error: action.error };
    default:
      return state;
  }
}

const [state, dispatch] = useReducer(reducer, initialState);

async function handleSave() {
  dispatch({ type: 'SAVE_START' });
  try {
    await savePost({ title: state.title, content: state.content });
    dispatch({ type: 'SAVE_SUCCESS' });
  } catch (e) {
    dispatch({ type: 'SAVE_ERROR', error: e.message });
  }
}

 

 

상태 관리의 기본 원칙

1. 필요한 최소한의 상태만 둔다.

     계산으로 얻을 수 있는 값은 state에 따로 저장하지 않는다. (리스트 길이, 필터링된 결과 등)

2. 중복 상태를 피한다.

     싱크가 안 맞으면 버그가 될 수 있으므로, 같은 정보를 여러 곳에 state로 저장하지 않는다.

3. 항상 같이 바뀌는 값은 하나의 state로 합친다.

     예를 들어, firstName과 lastName을 항상 같이 다룬다면 user객체로 묶는다.

4. 상태 올리기

     둘 이상의 컴포넌트가 같은 state를 필요로 한다면 —> 그 둘의 공통 부모로 state를 올리고, props로 내려준다.

5. 상태는 불변성을 유지하기

     배열은 push 대신 concat, filter, map을 사용하고, 

     객체는 직접 수정이 아닌, { ...obj, something: newValue } 같이 복사해서 새 객체로 반환한다.

 

useReducer + Context로 “전역” 느낌 내기

여러 컴포넌트에서 활용하고 싶을 때

“useReducer로 상태·로직 관리 + Context”로 전역에서 활용할 수 있다.

import { createContext, useContext, useReducer } from "react";

const CounterStateContext = createContext(null);
const CounterDispatchContext = createContext(null);

function counterReducer(state, action) {
  switch (action.type) {
    case "INC":
      return state + 1;
    case "DEC":
      return state - 1;
    default:
      return state;
  }
}

export function CounterProvider({ children }) {
  const [count, dispatch] = useReducer(counterReducer, 0);

  return (
    <CounterStateContext.Provider value={count}>
      <CounterDispatchContext.Provider value={dispatch}>
        {children}
      </CounterDispatchContext.Provider>
    </CounterStateContext.Provider>
  );
}

export function useCounterState() {
  return useContext(CounterStateContext);
}

export function useCounterDispatch() {
  return useContext(CounterDispatchContext);
}

이것을

import { useCounterState, useCounterDispatch } from "./CounterContext";

function SomeComponent() {
  const count = useCounterState();
  const dispatch = useCounterDispatch();

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch({ type: "INC" })}>+1</button>
    </div>
  );
}

이렇게 하면 redux 느낌의 구조를 리액트 내장 기능만으로 만들 수 있다.

 

useState, useReducer 포함 모든 훅에는 공통 규칙이 있다.

  1.  컴포넌트 최상단에서만 호출하기

       if, for, 함수 안의 함수 이런 곳에서는 호출 X

       항상 컴포넌트 함수의 최상단에서 순서가 변하지 않게

  2.  리액트 함수 컴포넌트나 커스텀 훅 내부에서만 호출

       일반 자바스크립트 함수에서 Hooks 사용 X

 

최적화

최적화란

    - 빠르게 뜨는 서비스

첫 화면이 빨리 보이는지 (First Paint / LCP)

사용자가 클릭했을 때 바로바로 반응하는지 (TTI, 응답성)

   - 가볍게 돌아가는 서비스

불필요한 렌더링이 없는지

자바스크립트 번들이 너무 크지 않은지

네트워크 요청이 최소한인지

   - 사용자 환경에 맞는 서비스

느린 네트워크/저사양 기기에서도 적당히 쓸 만한지

  - 서버 리소스도 효율적으로

필요 없는 API 호출 줄이기

캐싱, CDN 등 활용

 

동일한 기능을 제공하되, 더 빠르고 더 가볍고, 더 적은 리소스로 동작하게 만드는 것 → 이걸 도와주는 것이 “최적화 도구들

 

리액트에서 자주 쓰는 최적화 도구와 패턴

1. React.memo

똑같은 props로 렌더링하면, 이전 결과를 재사용해서 다시 렌더링하지 않는다.

import { memo } from 'react';

const TodoItem = memo(function TodoItem({ todo, onToggle }) {
  console.log('렌더링', todo.id);
  return (
    <li onClick={() => onToggle(todo.id)}>
      {todo.text}
    </li>
  );
});

불필요한 렌더링을 줄이기 위해 사용한다.

같은 props가 들어오면, 이전 렌더링 결과를 재사용하고 다시 렌더링하지 않는다.

부모가 자주 렌더링되는데, 자식은 바뀔 일이 적을때 / 리스트 아이템 에 자주 사용한다.

 

2. useMemo

“무거운 계산”을 캐싱해서, 의존성(deps)이 바뀔 때만 다시 계산한다. 

const filteredTodos = useMemo(() => {
  return todos.filter(todo => todo.text.includes(keyword));
}, [todos, keyword]);

정렬, 필터링, 큰 배열을 계산할 때 사용한다.

 

3. useCallback

매 렌더마다 새로 만들어지는 함수를 “같은 레퍼런스”로 유지해준다.

const handleToggle = useCallback((id) => {
  setTodos(todos => 
    todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    )
  );
}, []);

 

자식이 React.memo로 감싸져 있고, 그 자식에 콜백 함수를 props로 내려줄 때 유용하다.

 

4. useTransition / useDeferredValue – 입력 랙 줄이기

“급한 업데이트(입력/클릭)”와 “덜 급한 업데이트(무거운 렌더링)”를 나눠 UI 버벅임을 줄인다.

useTransition 예시

const [isPending, startTransition] = useTransition();

function handleKeywordChange(e) {
  const next = e.target.value;
  setKeyword(next); // 입력 필드는 즉시 반응 (급한 업데이트)

  startTransition(() => {
    setFilteredTodos(filterBigList(allTodos, next)); // 무거운 작업
  });
}

 

useDeferredValue 예시 (느리게 따라가는 값)

const deferredKeyword = useDeferredValue(keyword);

const filteredTodos = useMemo(
  () => filterBigList(allTodos, deferredKeyword),
  [allTodos, deferredKeyword]
);
사용자가 빠르게 타이핑해도, 리스트는 “좀 느리게” 따라와서 렉이 줄어든다.
 

5. React.lazy + <Suspense> – 코드 스플리팅

지금 당장 필요 없는 컴포넌트는 나중에 네트워크로 받아오게 만들어서, 초기에 받는 JS 양을 줄인다. 

const Chart = React.lazy(() => import('./Chart'));

function Page() {
  return (
    <Suspense fallback={<p>차트 불러오는 중...</p>}>
      <Chart />
    </Suspense>
  );
}

 

상태 관리와 최적화의 관계성

상태 구조가 최적화의 시작이다. 너무 많은 것을 하나의 state로 묶어두면, 조금만 바뀌어도 전체가 다시 렌더링된다.

반대로, 잘게 나누고 필요한 곳에만 전달하면 불필요한 렌더링이 줄어든다.

그러나, 너무 잘게 쪼개면 네트워크 요청이 너무 많아져서 오히려 느려질 수 있다는 것을 유의해야 한다.

 

 


 

Corner React.js
Editor: J

 

728x90

관련글 더보기