본문 바로가기
framework_library/react

상태는 어디에 둬야 하는가

by 죄니안죄니 2026. 1. 9.
application architectureapplication architectureapplication architecture
 
 
 

상태는 어디에 둬야 하는가

React 설계의 80%를 결정하는 단 하나의 판단

React에서 제일 어려운 질문은 이거다.

이 상태를
이 컴포넌트에 둘 것인가,
부모로 올릴 것인가,
아니면 전역으로 뺄 것인가

이 결정을 잘못하면

  • useEffect가 늘어나고
  • props가 꼬이고
  • 전역 상태가 쓰레기장이 된다

이번 글은 명확한 판단 기준을 남기는 게 목적이다.


1️⃣ 상태의 “주인”이라는 개념부터 잡자

React에서 state는 소유자(owner) 가 있어야 한다.

그 상태를 “변경할 책임”이 있는 컴포넌트

이 기준이 없으면
상태는 떠돌아다니기 시작한다.


2️⃣ 가장 먼저 고려할 선택지: 로컬 상태

 
function SearchInput() {
  const [value, setValue] = useState("");

  return (
    <input
      value={value}
      onChange={e => setValue(e.target.value)}
    />
  );
}

이 상태는 명확하다.

  • 입력값은 이 컴포넌트 안에서만 쓰인다
  • 외부에서 알 필요도 없다
  • 외부에서 바꾸면 오히려 이상하다

👉 이게 로컬 상태의 교과서적인 예시

형제 컴포넌트가 같은 값을 필요로 할 때

판단 기준 1:

이 상태를 이 컴포넌트 말고
누가 필요로 하는가?
→ “아무도 없음”이면 여기 둔다


3️⃣ 상태를 끌어올려야 하는 순간 (lift state up) 

이제 이 상황을 보자.
형제 컴포넌트가 같은 값을 필요로 할 때

function Parent() {
  return (
    <>
      <SearchInput /> //Input의 state를 Result에서 쓰고 싶은데 props로 못받음 useEffect로 바로 올리는건 위험의 시작
      <SearchResult />
    </>
  );
}

SearchInput의 값으로
SearchResult가 결과를 보여줘야 한다면?

👉 상태는 Input의 것이 아니다

 
function Parent() {
  const [keyword, setKeyword] = useState("");

  return (
    <>
      <SearchInput value={keyword} onChange={setKeyword} />
      <SearchResult keyword={keyword} />
    </>
  );
}

판단 기준 2:

여러 컴포넌트가
같은 상태를 읽거나 변경해야 하는가?
→ 그렇다면 공통 부모가 주인이다

이게 “상태 끌어올리기”의 전부다.
기술이 아니라 책임 분리 문제다.


4️⃣ props drilling은 언제 문제가 되는가

 
<Layout>
  <Page>
    <Section>
      <SearchInput
        value={keyword}
        onChange={setKeyword}
      />
    </Section>
  </Page>
</Layout>

이걸 보고 흔히 이런 말을 한다.

“props drilling이니까 전역으로 빼자”

대부분 너무 빠른 결론이다.

function ChildButton() {
  const [loggedIn, setLoggedIn] = useState(false);

  return (
    <button onClick={() => setLoggedIn(true)}>
      로그인
    </button>
  );
}
// 로그인 상태를 다른 컴포넌트에서 알 수 있는 방법은..? 
// (헤더, 라우터, 메뉴, 권한체크...? )
// 알 수 없음. 자식 안에 숨어있으니까. 이 구조는 이벤트도 자식, 결과도 자식이라
// 확장 불가능하다

props drilling은 “문제”가 아니라 “증상”이다

어떤 상태/행동을
실제로 쓰지도 않는 중간 컴포넌트들이
그저 전달만 하기 위해 props를 “뚫고 지나가는” 구조. (좋지 않은 것)

  • 중간 컴포넌트가 props를 사용하지 않는다
  • 단순 전달만 한다

이게 불편한 이유는 하나다.

👉 상태의 주인이 너무 멀리 있다

해결책은 전역 상태가 아니라
컴포넌트 경계 재설계인 경우가 많다.

application architecture

컴포넌트 재설계

function Parent() {
  const [loggedIn, setLoggedIn] = useState(false);

  function handleLogin() {
    setLoggedIn(true);
  }

  return <ChildButton onClick={handleLogin} />;
}

function ChildButton({ onClick }) {
  return <button onClick={onClick}>로그인</button>;
}
// 이 구조는 
// 클릭은 자식에서 발생
// 로그인 상태 변경은 부모가 책임 
// 즉, 자식만 알아도 되는 자식상태라면 그대로 두고 
// 형제/부모/라우터가 알아야 하는 상태라면 부모상태로 뺀다

** props drilling이지만 좋은 구조도 있다. 공통 컴포넌트는 props drilling이 기본 설계다.

더보기

다만 “아무 생각 없이 내려보낸 props drilling”과
“의도를 가진 props 전달”은 완전히 다른 물건이다.
차근차근 분해해보자.


1️⃣ 먼저 정리부터 — props drilling이 왜 욕을 먹나

props drilling이 싫어지는 순간은 보통 이때다.

 
A → B → C → D
  • B, C는 값을 쓰지도 않는데
  • 그냥 D에 전달하기 위해 존재함
  • 구조를 바꾸면 연쇄 수정 발생

이건 drilling 자체가 아니라 설계 부재가 문제다.


2️⃣ props drilling이지만 “좋은 설계”인 핵심 조건

좋은 props drilling에는 공통된 특징이 있다.

✅ 1. “데이터 흐름”이 명확하다

누가 소유하고, 누가 소비하는지가 선명함

 
<App> <Page user={user}> <UserProfile user={user} /> </Page> </App>
  • user의 소유자: App
  • Page는 레이아웃 역할
  • UserProfile이 실제 소비자

Page는 “파이프”라는 역할이 분명하다.

📌 이건 정보 손실 없는 전달이다.


✅ 2. 중간 컴포넌트가 의미를 가진다

 
<Form value={form}> <FormSection value={form}> <FormInput value={form.name} /> </FormSection> </Form>

여기서

  • Form
  • FormSection

둘 다 도메인 개념이다.
“그냥 통과 노드”가 아니다.

👉 이런 drilling은 구조를 설명하는 역할을 한다.


✅ 3. 데이터 수명이 짧다 (페이지 스코프)

아래 케이스는 거의 정석이다.

 
<Page> <Header title={title} /> <Content data={data}> <List items={items} /> </Content> </Page>
  • 페이지 벗어나면 데이터 소멸
  • 전역으로 올릴 이유 없음
  • Context 쓰면 오히려 추적이 어려워짐

📌 “한 화면에서만 쓰는 데이터”는 drilling이 맞다.


3️⃣ 실무에서 아주 흔한 “좋은 drilling” 예

🔹 레이아웃 + 슬롯 구조

 
<Layout user={user}> <Sidebar user={user} /> <Main user={user} /> </Layout>
  • Layout은 user를 쓰지 않아도
  • “이 레이아웃은 user 컨텍스트를 전제로 한다”는 의미 전달

이건 UI 계약(contract) 이다.


🔹 테이블 / 리스트 계열 컴포넌트

 
<DataTable columns={columns} rows={rows} onRowClick={handleClick} />

안 내려보내면?

  • Context 남발
  • 재사용성 붕괴
  • 테스트 난이도 폭증

📌 공통 컴포넌트는 props drilling이 기본 설계다.


🔹 이벤트 콜백 전달

 
<Popup onConfirm={handleConfirm} onCancel={handleCancel} />

이건 drilling이 아니라 의사결정권 위임이다.

“결과는 부모가 책임진다”

이 구조를 깨면

  • 팝업이 비즈니스 로직을 갖기 시작한다
  • 재사용 불가해진다

4️⃣ 그럼 언제 나쁜 drilling이 되는가 (경계선)

아래 중 하나라도 걸리면 위험 신호다.

  • 중간 컴포넌트가 의미 없이 전달만 한다
  • props 이름이 data, value, info처럼 추상적
  • 동일한 props를 5단계 이상 전달
  • 중간에서 타입이 바뀌거나 가공됨
  • “왜 여기까지 내려왔는지” 설명이 안 됨

이때는:

  • Context
  • 합성 컴포넌트
  • children 패턴
  • 상태 소유 위치 재조정

중 하나를 고려해야 한다.


5️⃣ 중요한 관점 하나

props drilling은 기술 문제가 아니다
👉 의사소통 문제다

좋은 drilling은 이런 문장이 성립한다.

“이 값은 여기서 만들어졌고
이 구조 전체에서 의미가 있다”

나쁜 drilling은 설명이 안 된다.

“그냥 필요해서 내려보냈어요…”


6️⃣ 한 줄 결론

  • props drilling은 악이 아니다
  • 의도 없는 drilling만 나쁘다
  • 구조를 설명하는 전달은 오히려 가장 안전한 설계

비유로 설명하면 (가장 잘 와닿는다)

주문 버튼 예시

  • 자식: “주문하기” 버튼
  • 이벤트: 버튼 클릭
  • 부모: 주문 목록, 결제 상태, 재고

버튼이 이런 걸 직접 하면 이상하다.

 
버튼이 재고를 줄이고 버튼이 결제를 만들고 버튼이 화면을 전환함

👉 버튼은 의사결정권자가 아니다.

버튼은 말만 한다.

“사용자가 나 눌렀어요”

결정은 부모가 한다.

“사용자의 행동은
가장 가까운 컴포넌트에서 감지하고,
그 행동의 의미와 결과는
더 큰 맥락을 아는 컴포넌트가 결정한다”

application architecture

 


5️⃣ 전역 상태는 언제 써야 하나 (정말로)

전역 상태를 써도 되는 조건은 명확하다.

 
1. 앱 전반에서 필요하고
2. 여러 화면에서 공유되고
3. 생명주기가 길고
4. 누가 바꿔도 의미가 같은 상태

대표적인 예:

  • 로그인 사용자 정보
  • 테마 / 다크모드
  • 언어 설정
  • 알림 카운트
 
const UserContext = createContext(null);

👉 이건 UI 상태가 아니라 앱 상태다.


6️⃣ 전역 상태가 지옥이 되는 순간

아래 상태들은 전역으로 두는 순간 사고가 난다.

  • 모달 열림 여부
  • 특정 화면의 검색 조건
  • 특정 탭의 선택 상태

이유는 간단하다.

화면이 사라져도
상태는 살아남기 때문

그 결과:

  • 이전 화면의 상태가 남아있고
  • 새 화면에서 “왜 이 값이?”가 된다

7️⃣ 실무에서 쓰는 최종 판단 체크리스트

상태를 만들기 전에 이 순서로 묻는다.

  1. 이 컴포넌트만 쓰는가? → 로컬
  2. 형제/부모/자식이 같이 쓰는가? → 공통 부모
  3. 화면 여러 개에서 쓰는가? → 상위 페이지
  4. 앱 전체에서 쓰는가? → 전역
  5. 서버에서 내려온 데이터인가? → state일 수도 아닐 수도

여기서 4번은 마지막 선택지다.


8️⃣ 기억해야 할 한 문장

이 문장은 React 설계에서 계속 써먹게 된다.

상태는 “가장 적게 필요로 하는 위치”에 둔다
위로 올리는 건 쉽지만
내려오는 건 항상 고통이다


다음 글 예고

다음 글에서는 이걸 이어간다.

👉 “전역 상태는 왜 마지막 카드인가”

  • Context / Redux / Zustand를 언제 쓰는지
  • 왜 전역 상태는 디버깅을 망가뜨리는지
  • 실무에서 전역 상태가 늘어나는 진짜 이유

React 카테고리의 중앙 분기점이 될 글이다.

댓글