MAKE IT SIMPLE

프론트엔드가 복잡해질수록 먼저 무너지는 것: 상태 관리 본문

React

프론트엔드가 복잡해질수록 먼저 무너지는 것: 상태 관리

빈빠끄 2025. 12. 30. 18:01

프론트엔드 프로젝트가 일정 규모를 넘기 시작하면, 기능보다 먼저 문제가 되는 것은 상태(state)다. UI 버그, 불필요한 리렌더링, 어디서 바뀌는지 알 수 없는 값들. 대부분 “상태를 어디서, 어떻게 관리할지”에 대한 기준 부재에서 시작된다

이 글에서는 실제로 자주 마주치는 문제 상황을 기준으로
어떤 상태를 로컬로 두고, 언제 전역으로 끌어올리며, Context와 Recoil은 어디서 갈라지는지, URLSearchParams 은 언제 사용하면 좋은지를 단계적으로 정리한다.


문제 상황 1: “분명 값은 바뀌었는데 UI가 안 바뀐다”

증상

  • API 응답은 정상
  • 콘솔 로그에는 최신 값
  • 그런데 화면은 이전 상태 그대로

원인 분석

React는 상태 변화 → 리렌더링이라는 단순한 규칙 위에서 동작한다. 문제는 다음 중 하나다.

  1. 상태가 불변성을 깨고 직접 수정됨
  2. 상태가 **잘못된 위치(너무 상위 or 너무 전역)**에 있음
  3. 파생 가능한 값을 중복 상태로 관리하고 있음

특히 주니어 단계에서 가장 흔한 패턴은

“일단 전역 상태로 올려두자”

하지만 모든 상태를 전역으로 두는 순간, 데이터 흐름은 흐려지고 디버깅 비용은 급격히 증가한다

해결 1: 상태를 먼저 분류한다 (관리 도구를 고르기 전에)

상태 관리 도구 선택보다 중요한 것은 상태의 성격 분리다.

실무 기준 상태 분류

  • 로컬 상태: 단일 컴포넌트 UI 제어
  • 전역 상태: 여러 컴포넌트에서 동시에 필요
  • 서버 상태: API 기반, 캐싱/동기화 대상
  • URL 상태: 라우팅과 직접 연결
  • 폼 상태: 입력·검증 중심

이 기준만 명확해져도 “왜 여기서 이 상태를 쓰는가”가 설명 가능해진다

 


 

문제 상황 2: props drilling이 지옥이 되기 시작한다

증상

  • 상위 → 하위 → 손자 컴포넌트까지 props 전달
  • 중간 컴포넌트는 값도 안 쓰는데 전달만 함
  • 구조 변경 시 연쇄 수정 발생

1차 해결: Context API

Context는 설정 비용이 거의 없고, 작은 범위의 상태 공유에는 여전히 유효하다

Context API가 적합한 경우

  • 테마, 인증 정보
  • 변경 빈도 낮음
  • Provider 범위가 명확함

한계

  • Context value 변경 시 구독 컴포넌트 전부 리렌더링
  • 상태 의존성이 커질수록 추적 난이도 상승

이 지점에서 프로젝트는 보통 다음 단계로 넘어간다.

 


 

문제 상황 3: 전역 상태는 필요한데 Context로는 버겁다

증상

  • Context가 점점 비대해짐
  • selector 같은 파생 계산이 필요해짐
  • 렌더링 최적화가 어려워짐

해결: Recoil 도입 포인트

Recoil은 전역 상태를 atom 단위로 쪼개고, 필요한 컴포넌트만 정확히 구독하게 만든다

Recoil이 적합한 경우

  • 상태 변경 빈도가 높다
  • 파생 상태가 많다
  • 성능 최적화가 중요하다
  • props / Context 계층을 줄이고 싶다

특히 selector는

“이 값은 저 값으로부터 계산된다”
라는 의존성을 코드 레벨에서 고정시킨다는 점에서 강력하다

 


 

문제 상황 4: 새로고침하면 상태가 날아간다

증상

  • 검색어를 입력하고 페이지를 이동했는데 새로고침 시 초기화됨
  • 페이지네이션 후 뒤로 가기를 누르면 페이지 정보가 유지되지 않음
  • URL을 공유했지만 동일한 검색 결과가 재현되지 않음

이 상황에서 흔히 보이는 구현은 다음과 같다.

const [searchValue, setSearchValue] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [sort, setSort] = useState<"recent" | "favorite">("recent");

문제는 이 상태들이 모두 “쿼리로 표현되는 상태”라는 점이다.
즉, 컴포넌트 내부 상태(useState)로 관리하는 순간부터 구조적으로 한계를 갖게 된다

원인: 쿼리 상태를 컴포넌트 메모리에 두고 있다

useState로 관리된 상태는 다음 특성을 가진다.

  • 페이지 내에서만 유효
  • 새로고침 시 초기화
  • 브라우저 히스토리와 무관
  • URL 공유 불가능

검색어, 페이지, 정렬 조건은 본질적으로

“현재 화면을 설명하는 상태”
이며, 이 상태는 브라우저 주소창(URL)에 존재해야 일관성이 유지된다

해결: 검색 상태를 URL Query Parameter로 끌어올린다

핵심 전환

  • ❌ 검색 상태를 useState로 관리
  • ✅ 검색 상태를 **URL query string을 단일 진실 소스(Single Source of Truth)**로 사용

예시:

/items?page=3&category=health&sort=recent

이 URL 하나로 다음이 모두 가능해진다.

  • 새로고침 후 동일한 상태 복원
  • 뒤로 가기 / 앞으로 가기 동작 보장
  • URL 공유 시 동일한 결과 재현

실무 패턴: 관련된 쿼리는 “하나의 훅”으로 관리한다

중요한 포인트는 쿼리마다 개별 훅을 만들지 않고, 하나의 통합 훅으로 관리했다는 점이다

이유

  • 검색어 변경 → 페이지는 보통 1로 초기화됨
  • 정렬 변경 → 같은 검색어라도 결과 의미가 달라짐
  • 쿼리 간 의존성이 명확하게 존재

따라서 “독립 상태”가 아니라 하나의 검색 상태 묶음으로 다루는 것이 맞다.

Next.js App Router 기준 예시 구조

export function useProductQuery() {
  const router = useRouter();
  const params = useSearchParams();

  const query = useMemo(() => {
    return {
      page: Number(params.get("page")) || 1,
      search: params.get("search") || "",
      sort: (params.get("sort") as "recent" | "favorite") || "recent",
    };
  }, [params]);

  const updateQuery = useCallback(
    (updates) => {
      const newParams = new URLSearchParams(params.toString());

      Object.entries(updates).forEach(([key, value]) => {
        if (!value) newParams.delete(key);
        else newParams.set(key, String(value));
      });

      router.push(`?${newParams.toString()}`);
    },
    [params, router]
  );

  return {
    ...query,
    setPage: (page: number) => updateQuery({ page }),
    setSearch: (search: string) => updateQuery({ search, page: 1 }),
    setSort: (sort) => updateQuery({ sort }),
  };
}

이 구조의 핵심은

  • 컴포넌트는 상태를 “소유”하지 않는다
  • URL을 읽어 UI를 그릴 뿐이다
  • 상태 변경 = URL 변경 = 네비게이션

이렇게 기준으로 두고 판단해보면 좋다.

전역 상태를 써야 할때

  • 새로고침 시 초기화
  • 검색처럼 페이지 종속적인 상태에 비해 범위가 넓음
  • 라우팅과 상태가 분리됨

 URL 상태를 써야 할때

  • 새로고침 안전
  • 뒤로 가기 / 앞으로 가기 자연스러움
  • 공유·북마크 가능
  • 서버 컴포넌트 / SSR과도 궁합이 좋음

다음 질문에 2개 이상 YES라면 URL 상태가 맞다.

  • 이 상태는 특정 페이지를 설명하는가?
  • 사용자가 뒤로 가기를 기대하는가?
  • URL 공유 시 동일한 화면이 나와야 하는가?
  • 새로고침 이후에도 의미가 유지돼야 하는가?

검색, 필터, 정렬, 페이지네이션은 거의 항상 여기에 해당한다.

 

Comments