부모가 자식에게 데이터를 전달하는 가장 기본적인 방법인 Props, 과연 얼마나 깊이 이해하고 있을까요?

개념 정의

Props(Properties)는 부모 컴포넌트에서 자식 컴포넌트로 읽기 전용 데이터를 전달 하는 메커니즘입니다. 함수의 인자와 같은 역할이며, 컴포넌트를 순수 함수처럼 동작하게 만드는 핵심 요소입니다.

왜 필요한가

컴포넌트를 재사용 가능하게 만들려면, 내부에 데이터를 하드코딩하는 대신 외부에서 주입 받아야 합니다.

JSX
// ❌ 하드코딩 — 재사용 불가
function Greeting() {
  return <h1>안녕하세요, 홍길동님!</h1>;
}

// ✅ Props 사용 — 어떤 이름이든 가능
function Greeting({ name }) {
  return <h1>안녕하세요, {name}님!</h1>;
}

내부 동작

Props는 하나의 객체

여러 개의 props를 전달하더라도, React 내부에서는 단일 객체 로 묶여서 전달됩니다.

JSX
// 사용하는 쪽
<UserCard name="홍길동" age={25} active={true} />

// 받는 쪽 — props 객체를 구조 분해
function UserCard({ name, age, active }) {
  // props === { name: '홍길동', age: 25, active: true }
}

// 또는 props 객체 그대로 받기
function UserCard(props) {
  console.log(props.name); // '홍길동'
}

children — 특별한 prop

태그 사이에 넣은 콘텐츠는 자동으로 children prop이 됩니다.

JSX
// 이 두 표현은 동일합니다
<Card>카드 내용입니다</Card>
<Card children="카드 내용입니다" />

children의 타입은 상황에 따라 다릅니다.

JSX
// 문자열
<Card>텍스트</Card>                    // children: "텍스트"

// 단일 JSX Element
<Card><span>내용</span></Card>        // children: <span>내용</span>

// 여러 요소 (배열)
<Card>                                 // children: [<h2>제목</h2>, <p>내용</p>]
  <h2>제목</h2>
  <p>내용</p>
</Card>

// 함수 (render props)
<Card>{(data) => <span>{data}</span>}</Card>  // children: (data) => ...

children을 활용한 레이아웃 컴포넌트 패턴은 매우 강력합니다.

JSX
function PageLayout({ sidebar, children }) {
  return (
    <div className="layout">
      <aside className="sidebar">{sidebar}</aside>
      <main className="content">{children}</main>
    </div>
  );
}

// 사용
<PageLayout sidebar={<Navigation />}>
  <Article />
  <Comments />
</PageLayout>

기본값 처리

JSX
// 방법 1: 구조 분해 기본값 (권장)
function Button({ variant = 'primary', size = 'medium', children }) {
  return <button className={`btn-${variant} btn-${size}`}>{children}</button>;
}

// 방법 2: defaultProps (클래스형에서 사용, 함수형에서는 비권장)
Button.defaultProps = {
  variant: 'primary',
  size: 'medium',
};

defaultProps는 향후 제거될 수 있으므로, 구조 분해 기본값 을 사용하는 것이 좋습니다.

Props Drilling 문제

깊이 중첩된 컴포넌트에 데이터를 전달하려면 중간 컴포넌트들이 모두 해당 prop을 받아서 넘겨야 합니다.

JSX
// ❌ Props drilling — App → Layout → Sidebar → UserInfo → Avatar
function App() {
  const [user, setUser] = useState(null);
  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Sidebar user={user} />;  // Layout은 user를 사용하지 않음
}

function Sidebar({ user }) {
  return <UserInfo user={user} />;  // Sidebar도 사용하지 않음
}

function UserInfo({ user }) {
  return <Avatar src={user.avatar} />;  // 여기서만 사용
}

해결 방법들입니다.

  • 컴포넌트 합성: children을 활용하여 중간 전달 제거
  • Context API: 깊은 곳까지 직접 전달
  • 상태 관리 라이브러리: 전역 스토어 활용
JSX
// ✅ 컴포넌트 합성으로 해결
function App() {
  const [user, setUser] = useState(null);
  return (
    <Layout>
      <Sidebar>
        <UserInfo user={user} />
      </Sidebar>
    </Layout>
  );
}

function Layout({ children }) {
  return <div className="layout">{children}</div>;
}

Render Props 패턴

함수를 prop으로 전달하여 렌더링 결과를 호출자가 결정 하는 패턴입니다.

JSX
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  return render(position);
}

// 사용: 같은 로직, 다른 UI
<MouseTracker render={({ x, y }) => (
  <div>마우스 위치: {x}, {y}</div>
)} />

<MouseTracker render={({ x, y }) => (
  <div style={{ position: 'fixed', left: x, top: y }}>
    🎯
  </div>
)} />

children을 함수로 사용하는 변형도 흔합니다.

JSX
function DataFetcher({ url, children }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => { setData(data); setLoading(false); });
  }, [url]);

  return children({ data, loading });
}

// 사용
<DataFetcher url="/api/users">
  {({ data, loading }) =>
    loading ? <Spinner /> : <UserList users={data} />
  }
</DataFetcher>

오늘날에는 커스텀 훅이 render props의 대부분의 사용 사례를 대체하지만, 렌더링 제어를 외부에 위임 해야 할 때는 여전히 유용합니다.

주의할 점

Prop Spreading과 DOM 경고

{...props}를 DOM 요소에 직접 전달하면, React가 인식하지 못하는 비표준 속성이 DOM에 전달되어 콘솔 경고가 발생합니다.

JSX
// ⚠️ DOM 요소에 spreading — onCustomEvent 같은 속성이 DOM에 전달됨
function BadExample(props) {
  return <div {...props} />;
}

// ✅ 필요한 props만 분리하여 전달
function GoodExample({ className, onClick, children }) {
  return (
    <div className={className} onClick={onClick}>
      {children}
    </div>
  );
}

Props를 직접 수정하면 안 되는 이유

Props는 읽기 전용 입니다. props 객체를 직접 변경하면 부모 컴포넌트의 데이터가 예기치 않게 변경되어 추적하기 어려운 버그가 발생합니다. React는 props의 불변성을 전제로 리렌더링 최적화를 수행하기 때문입니다.

JSX
// ❌ props를 직접 수정 — 부모 데이터가 오염됨
function Wrong({ user }) {
  user.name = '변경';
}

// ✅ 새로운 값이 필요하면 지역 변수 사용
function Right({ user }) {
  const displayName = user.name.toUpperCase();
  return <span>{displayName}</span>;
}

defaultProps의 deprecation

함수형 컴포넌트에서 defaultProps는 React 19부터 deprecated입니다. 구조 분해 기본값을 사용해야 합니다.

JSX
// ❌ deprecated
Button.defaultProps = { variant: 'primary' };

// ✅ 구조 분해 기본값
function Button({ variant = 'primary', children }) {
  return <button className={`btn-${variant}`}>{children}</button>;
}

정리

항목설명
Props의 본질부모에서 자식으로 흐르는 읽기 전용 단일 객체
children태그 사이 콘텐츠가 자동으로 전달되는 특수 prop (ReactNode 타입)
Props Drilling합성(children 활용), Context, 상태 관리 라이브러리로 해결
Render Props함수를 prop으로 전달하여 렌더링을 위임하는 패턴 (커스텀 훅이 대부분 대체)
Prop Spreading편리하지만 DOM 요소에는 비표준 속성 전달 위험
불변성props 직접 수정 금지 — 새 변수나 state 사용

Props를 제대로 이해하면 컴포넌트 설계가 유연해집니다. 특히 children을 적극 활용하면 props drilling 없이도 깔끔한 구조를 만들 수 있습니다.

댓글 로딩 중...