Post

React state 설계와 파생 state 줄이기

state/props 소유권, 파생 state 줄이기, 상태 끌어올림 기준을 정리

React state 설계와 파생 state 줄이기

React에서 state는 가장 강력한 도구이자, 가장 많은 버그를 만드는 원인이다. 문제는 문법이 아니라 “무엇을 state로 둘 것인가”에 대한 판단 기준이다. state를 많이 안다고 React를 잘 쓰는 게 아니다. state를 안 쓰는 선택을 할 줄 아는지가 실력을 가른다.


state vs props: “소유권”의 문제

개념

  • props: 외부에서 주어지는 입력값 (read-only)
  • state: 컴포넌트가 스스로 소유하고 변경하는 값

핵심 차이는 단순히 “바뀌나 안 바뀌나”가 아니라 누가 그 값을 책임지는가(소유권)다.

props → 외부에서 주입됨 (통제권: 부모)
state → 내부에서 관리됨 (통제권: 나 자신)

왜 중요한가

값의 소유권이 명확해야 다음이 정리된다.

  • 어디서 변경되는가
  • 누가 그 변경의 결과를 책임지는가
  • 변경 시 어떤 컴포넌트들이 영향을 받는가

props와 state를 섞어 쓰면, 변경 경로 추적이 불가능해진다.

언제 문제가 되는가

1) props를 state로 복사하는 패턴

1
2
3
function Profile({ user }) {
  const [name, setName] = useState(user.name);
}

이 순간부터 문제가 시작된다.

  • 부모에서 user.name이 바뀌어도 반영되지 않음
  • “초기값인지, 동기화된 값인지” 애매해짐
  • 나중에 useEffect로 억지 동기화 → 버그 증가

원칙

props로 충분하면 state를 만들지 마라.


파생 state(Derived State)의 문제점

개념

파생 state란 다른 state/props로부터 계산 가능한 값을 굳이 state로 저장한 것이다.

1
2
const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0); // 파생 state

itemCountitems.length로 계산 가능하다.

왜 문제가 되는가

파생 state는 항상 동기화 문제를 만든다.

  • 업데이트를 한 군데라도 빼먹으면 값이 어긋남
  • “왜 이 값이 틀렸지?”의 원인을 추적하기 어렵다
  • 상태 수가 늘어날수록 조합 폭발

언제 문제가 되는가 (실무 증상)

  • 버튼 비활성화 조건이 가끔 틀린다
  • 리스트 개수 표시가 실제와 다르다
  • 특정 순서의 액션에서만 버그 발생

대안: 계산된 값으로 유지

1
const itemCount = items.length;

React는 렌더링 시 값을 다시 계산하는 비용을 감당할 수 있도록 설계되어 있다. 값을 저장하는 비용보다 동기화 비용이 훨씬 크다.


계산된 값 vs state: 구분 기준

판단 기준 (중요)

아래 질문에 “예”가 하나라도 나오면 state가 아닐 가능성이 높다.

  • 다른 state/props로부터 항상 계산 가능한가?
  • 사용자가 직접 변경하지 않는가?
  • 저장하지 않아도 매 렌더마다 계산해도 되는가?

예시 비교

❌ 잘못된 state

1
const [isEmpty, setIsEmpty] = useState(items.length === 0);

✅ 계산된 값

1
const isEmpty = items.length === 0;

왜 React는 이걸 허용하는가

React의 렌더링 모델은:

  • “값을 저장해서 덜 계산하자”가 아니라
  • “계산을 단순하게 만들고, 변경은 예측 가능하게 하자”에 가깝다

그래서 계산된 값은 state로 저장하지 않는 것이 기본 전략이다.


상태 끌어올리기(Lifting State Up)의 실제 기준

개념

상태 끌어올리기란, 여러 컴포넌트가 동일한 상태를 필요로 할 때 그 상태를 공통 부모로 이동시키는 것이다.

왜 중요한가

상태는 “쓰는 곳”이 아니라 **“결정되는 곳”에 있어야 한다.

  • A, B 컴포넌트가 같은 상태를 봐야 한다
  • A에서 변경된 결과가 B에 즉시 반영돼야 한다 → 상태는 A, B의 최소 공통 부모에 있어야 한다

실제 판단 기준 (암기용)

다음 중 하나라도 해당하면 끌어올림 후보:

  • 두 컴포넌트가 같은 값을 각자 state로 들고 있다
  • 한쪽에서 변경했는데 다른 쪽이 반응해야 한다
  • props로 내려보내는 값이 점점 늘어난다

언제 문제가 되는가

1) 너무 빨리 끌어올림

  • 아직 공유 요구가 명확하지 않은 상태를 상위로 이동
  • 부모 컴포넌트가 상태 저장소처럼 비대해짐
  • 책임이 모호해짐

2) 끌어올려야 하는데 안 끌어올림

  • 동일 개념의 state가 여러 군데 흩어짐
  • 동기화 로직 등장 (useEffect 지옥)
  • “왜 얘는 바뀌었는데 쟤는 안 바뀌지?” 발생

state를 많이 쓰는 것이 왜 위험한가

1) 상태 수 = 동기화 포인트 수

state가 늘어날수록:

  • 업데이트 타이밍
  • 업데이트 순서
  • 의존 관계 를 관리해야 한다.

이건 선형 증가가 아니라 조합 증가다.

2) 렌더링 예측이 어려워진다

  • 어떤 state 변경이 어떤 렌더를 유발하는지 파악 어려움
  • useEffect 의존성 관리 난이도 급상승

3) “상태가 곧 로직”이 되어버린다

  • 조건 분기 대부분이 state에 의존
  • 코드 읽을 때 “현재 가능한 상태 조합”을 머릿속에서 시뮬레이션해야 함

“state를 만들지 않는 선택”이 더 나은 경우

대표적인 케이스들

1) UI 파생 값

  • 버튼 활성화 여부
  • 에러 메시지 표시 여부
  • 리스트 empty 상태

→ 계산으로 충분

2) 일회성 입력 처리

  • submit 시점에만 필요한 값
  • 즉시 서버로 보내고 버리는 값

→ ref 또는 이벤트 핸들러 내부 변수

3) 외부에서 이미 관리되는 값

  • URL 파라미터
  • 서버 상태
  • 전역 상태 라이브러리에서 오는 값

→ 중복 state 금지


정리: 좋은 state 설계는 “적을수록 좋다”

  • state는 소유권이 명확한 값만
  • 파생 가능한 값은 계산으로
  • 공유가 필요하면 최소 공통 부모로
  • 의심되면 먼저 state를 만들지 말고 시작

React 실력은 결국 “이걸 state로 둘 이유가 있는가?”를 스스로 반박할 수 있는지에서 드러난다.


This post is licensed under CC BY 4.0 by the author.