IT의 IT 블로그

React 상태 끌어올리기(state lifting)와 props 흐름 정리 본문

React • TypeScript 학습 기록

React 상태 끌어올리기(state lifting)와 props 흐름 정리

IT의 IT 블로그 2025. 12. 18. 21:51

 

리액트를 처음 배우거나, 어느 정도 익숙해진 뒤에도 계속 헷갈리는 개념들이 있습니다.

  • 부모 컴포넌트 / 자식 컴포넌트
  • props와 state의 역할
  • state를 언제 끌어올려야 하는지
  • 왜 “올린다(lifting)”고 부르는지
  • Context는 언제 써야 하는지

이 글은렌더링 구조와 상태의 ‘소유권’ 이라는 관점으로
이 개념들을 한 번에 정리하는 것이 목표입니다.

 

1. 부모 컴포넌트란 무엇인가

리액트에서의 정확한 정의

JSX에서 다른 컴포넌트를 직접 렌더링하는 컴포넌트

 
function Parent() { return <Child />; }
  • Parent → 부모 컴포넌트
  • Child → 자식 컴포넌트

이 관계의 기준은 오직 JSX 구조입니다.


2. “상위 / 하위”의 기준

다음은 부모·자식 판단 기준이 아닙니다.

  • 파일 위치
  • 폴더 구조
  • import 순서
  • 컴포넌트 이름

부모/자식의 기준은 단 하나입니다.

“누가 JSX에서 누구를 렌더링했는가”


3. JSX 트리로 보는 상위/ 하위 기준

function A() {
  return <B />;
}

function B() {
  return <C />;
}

function C() {
  return <div>Hello</div>;
}

JSX 트리 구조는 다음과 같습니다.

 
A (부모)
└─ B (자식, 동시에 C의 부모)
   └─ C (자식)
  • A는 C를 직접 import하지 않아도
  • JSX 트리 기준으로는 C의 조상 컴포넌트

 **부모 컴포넌트란 “렌더링하는 위치”**로 이해하면 됩니다.


4. 컴포넌트 이름은 반드시 대문자로 시작

React는 JSX에서

  • 소문자 시작 → HTML 태그
  • 대문자 시작 → 사용자 정의 컴포넌트

로 인식합니다.

 
 

위 코드는 컴포넌트가 아니라
<nameButton>이라는 알 수 없는 DOM 태그로 처리됩니다.

컴포넌트는 반드시 대문자로 시작


5. state와 props 기본 개념

state란?

컴포넌트가 “소유”하고 관리하는 데이터

  • 바뀔 수 있음
  • UI 변경의 원인
  • useState로 선언
 
const [count, setCount] = useState<number>(0);

 

사실, React의 useState는 state 값 하나만 반환하는 게 아니라, 배열을 반환합니다.

위의 코드는 내부적으로 이런 형태입니다.

const stateArray = useState<number>(0);
// stateArray[0] → 현재 상태 값
// stateArray[1] → 상태를 변경하는 함수

그래서 우리는 구조 분해 할당을 사용해

  • 첫 번째 값 → state
  • 두 번째 값 → setState 함수

로 나누어 쓰는 것입니다.


왜 배열일까?

React는 반환 순서가 보장되는 구조를 필요로 합니다.

  • 객체 → 키 이름 충돌, 구조 변경 위험
  • 배열 → 항상 [값, 변경 함수] 순서 고정

그래서 useState는 배열을 반환하도록 설계되었습니다.


한 줄 정리

useState는
[현재 상태 값, 상태를 변경하는 함수] 형태의 배열을 반환한다.


props란?

부모가 자식에게 “전달”하는 데이터

  • 읽기 전용
  • 자식은 수정 불가
  • 부모가 값의 주인
 
type HelloProps = {
  name: string;
};

function Hello({ name }: HelloProps) {
  return <h2>Hello, {name}</h2>;
}

즉, 아래와 같습니다

  • state = 관리
  • props = 전달
  • 컴포넌트 = 표현

6. 그냥 props로 내려주는 경우 (state 이동 없는 상황)

상황

부모가 이미 데이터를 가지고 있고,
자식들은 그걸 표시만 한다.

import { useState } from "react";

type NameProps = {
  name: string;
};

function Hello({ name }: NameProps) {
  return <h2>Hello, {name}</h2>;
}

function Badge({ name }: NameProps) {
  return <p>Badge: {name}</p>;
}

export default function App() {
  const [name] = useState<string>("RIT");

  return (
    <div>
      <Hello name={name} />
      <Badge name={name} />
    </div>
  );
}
  • state는 처음부터 App이 소유
  • 자식은 props로 받아서 표시만 함

이것은 상태 끌어올리기가 (state lifting) 아닙니다.


7. 상태 끌어올리기(lifting state up)

문제 상황 (Before)

 
function NameDisplay() {
  return <h2>현재 이름: ???</h2>;
}

function NameEditor() {
  const [name, setName] = useState<string>("RIT");

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

export default function App() {
  return (
    <>
      <NameDisplay />
      <NameEditor />
    </>
  );
}
  • name은 NameEditor만 알고 있음
  • 형제 컴포넌트와 공유 불가
  • 같은 의미의 상태가 분리됨

해결 (After: state를 부모로 이동)

type NameDisplayProps = {
  name: string;
};

function NameDisplay({ name }: NameDisplayProps) {
  return <h2>현재 이름: {name}</h2>;
}

type NameEditorProps = {
  name: string;
  onChangeName: (next: string) => void;
};

function NameEditor({ name, onChangeName }: NameEditorProps) {
  return (
    <input
      value={name}
      onChange={(e) => onChangeName(e.target.value)}
    />
  );
}

export default function App() {
  const [name, setName] = useState<string>("RIT");

  return (
    <>
      <NameDisplay name={name} />
      <NameEditor name={name} onChangeName={setName} />
    </>
  );
}

 

이 방식의 핵심은 state의 소유권을 공통 부모로 이동시킨 것입니다.

기존에는 name 상태를 NameEditor만 알고 있었기 때문에,
형제 컴포넌트인 NameDisplay는 같은 데이터를 사용할 수 없었습니다.

이를 해결하기 위해 다음과 같이 구조를 변경했습니다.

  • name 상태를 공통 부모 컴포넌트(App) 로 이동하고
  • 부모가 해당 상태를 한 곳에서만 관리하는 기준값으로 소유하며
  • 자식 컴포넌트들은
    • 값은 props로 전달받아 사용하고
    • 변경은 콜백 함수를 통해 부모에게 “요청”하도록 구조를 변경했습니다.
const [name, setName] = useState<string>("RIT");

이제 App이 상태의 주인이 되고,

  • NameDisplay는 name을 읽기 전용으로 사용
  • NameEditor는 입력 이벤트 발생 시
    setName을 호출해 부모의 상태 변경을 요청

하게 됩니다.

이 구조의 장점

  • 같은 의미의 상태가 한 곳에서만 관리됨
  • 형제 컴포넌트 간 데이터 불일치 문제 해결
  • 데이터 흐름이 항상
    부모 → 자식 (props)
    자식 → 부모 (callback)
    형태로 유지됨

즉, 이 구조가 바로
React가 의도한 단방향 데이터 흐름을 따르는 설계입니다.


8. 왜 “올린다(lifting)”고 부를까?

헷갈리는 이유는 대부분 이것입니다.

“props는 아래로 내려가는데 왜 올린다고 하지?”

lifting은 렌더링 방향이 아니라
state의 ‘소유 위치’ 기준
입니다.

 
자식 → 부모
 

state의 위치가 위로 이동했기 때문에
lift(끌어올리다) 라고 부릅니다.


9. state lifting의 정확한 정의

같은 의미의 상태를 여러 컴포넌트가 필요로 할 때,
그 상태를 가장 가까운 공통 부모가 소유하도록 위치를 옮기는 설계 패턴


10. 언제 state lifting을 하면 안 되는가

① 한 컴포넌트 내부 UI 상태

 
const [isOpen, setIsOpen] = useState<boolean>(false);
  • 모달 열림/닫힘
  • 드롭다운 토글
  • 포커스 여부

공유 필요 없음 → 그 컴포넌트가 주인


② 부모가 의미를 모르는 상태

 
<Input />
  • 부모는 배치만 함
  • 값의 의미, 로직에 관심 없음

이런 상태까지 올리면 설계가 망가집니다


11. React는 단방향 데이터 흐름이다

리액트의 핵심 원칙 중 하나는 이것입니다.

데이터는 항상 위 → 아래로만 흐른다

  • 부모 → 자식 (props)
  • 자식 → 부모 (직접 전달 하지않고 콜백으로 요청)
 
onChangeName(nextValue);

자식은 부모의 state를 직접 바꾸지 못하고,
부모가 내려준 함수만 호출합니다.

이 단방향 구조 덕분에

  • 데이터 흐름이 예측 가능해지고
  • 디버깅이 쉬워지며
  • UI와 상태의 관계가 명확해집니다

12. Context는 언제 쓰는가

기준 한 줄

공유 범위가 넓어질수록 Context
공유 범위가 좁으면 lifting

Context가 어울리는 예

  • 로그인 사용자 정보
  • 테마 (다크/라이트)
  • 언어 설정

최종 정리 한 문장

부모 컴포넌트는 렌더링하는 위치이고,
state lifting은 상태의 소유권을 올바른 위치로 옮기는 설계 판단이다.

 

이상으로 부모 컴포넌트, props, 그리고 state lifting의 개념을
렌더링 구조와 상태 소유권 관점에서 정리해보았습니다.

'React • TypeScript 학습 기록' 카테고리의 다른 글

리액트 렌더링 과정 정리  (1) 2026.01.12