이 글에서 정리하는 내용
props가 왜 필요한지부터 부모에서 자식으로 데이터를 전달하는 방식, 문자열·숫자·배열·객체·함수 props 전달, children, props drilling까지 한 흐름으로 정리합니다. 읽고 나면 React 컴포넌트를 어떻게 연결해야 하는지 구조가 훨씬 선명해집니다.
Props란 무엇인가

React에서 컴포넌트는 혼자 움직이지 않습니다. 화면을 작은 조각으로 나눈 뒤, 필요한 데이터를 서로 주고받으면서 하나의 UI를 만듭니다. 이때 부모 컴포넌트가 자식 컴포넌트에게 값을 내려주는 통로가 바로 props입니다.
핵심은 데이터 흐름이 기본적으로 위에서 아래라는 점입니다. 부모가 가진 값은 자식에게 전달할 수 있지만, 자식이 부모 값을 직접 바꾸는 구조로 이해하면 금방 꼬이기 시작합니다. 그래서 props를 제대로 이해해야 컴포넌트를 나눌 때도 덜 흔들립니다.
여기서 한 가지를 더 같이 잡아두면 좋습니다. props는 자식 컴포넌트 입장에서 읽기 전용 값처럼 다뤄야 합니다. 전달받은 값을 자식 안에서 직접 바꾸는 것이 아니라, 필요하면 부모가 새로운 값을 다시 내려주는 흐름으로 생각해야 React 구조가 안정적으로 유지됩니다.
가장 기본적인 props 전달
type ProfileCardProps = { name: string; age: number;
}; function ProfileCard({ name, age }: ProfileCardProps) { // 부모가 내려준 name, age를 화면에 출력합니다. return <div>{name} / {age}세</div>;
} export default function App() { // App이 부모이고, ProfileCard가 자식입니다. return <ProfileCard name="해비" age={29} />;
}
이 코드에서 부모는 App이고 자식은 ProfileCard입니다. 부모는 JSX 태그 속성처럼 값을 넘기고, 자식은 함수 매개변수에서 그 값을 받습니다. HTML 속성과 비슷하게 보이지만, 직접 만든 컴포넌트에는 원하는 이름의 props를 자유롭게 설계할 수 있다는 점이 중요합니다.
다양한 값은 어떻게 props로 전달할까
props는 문자열만 넘기는 기능이 아닙니다. 숫자, 배열, 객체, 함수까지 자바스크립트 값이라면 대부분 전달할 수 있습니다. 이 감각이 잡혀야 컴포넌트를 실무 구조로 연결할 수 있습니다.
문자열, 숫자, 배열, 객체 전달
type ProductListProps = { title: string; count: number; products: string[]; user: { nickname: string; level: number; };
}; function ProductList({ title, count, products, user }: ProductListProps) { // 문자열, 숫자, 배열, 객체를 각각 받아서 사용합니다. return ( <section> <h2>{title}</h2> <p>총 {count}개</p> <p>작성자: {user.nickname} / Lv.{user.level}</p> <ul> {products.map((product) => ( <li key={product}>{product}</li> ))} </ul> </section> );
} export default function App() { // 배열과 객체도 중괄호 안에서 그대로 전달할 수 있습니다. return ( <ProductList title="신상품 목록" count={3} products={["키보드", "마우스", "모니터"]} user={{ nickname: "haebi", level: 2 }} /> );
}
문자열은 따옴표로, 숫자·배열·객체처럼 자바스크립트 값은 중괄호로 감싸서 전달합니다. JSX에서는 이 중괄호가 매우 중요합니다. 중괄호를 기준으로 단순 문자열인지, 실제 JS 값인지가 나뉘기 때문입니다.
함수 props 전달과 이벤트 연결
type SaveButtonProps = { onSave: () => void;
}; function SaveButton({ onSave }: SaveButtonProps) { // 자식은 부모에게서 받은 함수를 클릭 이벤트에 연결합니다. return <button onClick={onSave}>저장</button>;
} export default function App() { const handleSave = () => { // 실제 저장 로직은 부모가 가지고 있습니다. console.log("저장 완료"); }; return <SaveButton onSave={handleSave} />;
}
함수를 props로 넘기는 순간 컴포넌트 연결이 한 단계 확장됩니다. 자식 컴포넌트는 버튼을 렌더링하는 역할만 맡고, 클릭했을 때 무엇을 할지는 부모가 정합니다. 그래서 화면 조각과 동작 제어를 분리하기가 쉬워집니다.
| 전달하는 값 | JSX 예시 |
|---|---|
| 문자열 | title=”상품 목록” |
| 숫자·배열·객체·함수 | count={3}, items={list}, user={user}, ={handleClick} |
children은 무엇이 다를까
children은 따로 새로운 문법처럼 보이지만, 사실은 props의 한 종류입니다. 차이는 값을 속성으로 직접 적는 대신 컴포넌트 태그 사이에 넣는다는 점입니다.
이 개념을 이해하면 레이아웃 박스, 카드, 모달, 공통 섹션 컴포넌트를 만들 때 훨씬 유연해집니다. 감싸는 틀은 컴포넌트가 맡고, 내부 내용은 사용하는 쪽에서 자유롭게 넣을 수 있기 때문입니다.
children으로 내부 콘텐츠 받기
import { ReactNode } from "react"; type CardProps = { children: ReactNode;
}; function Card({ children }: CardProps) { // 태그 사이에 넣은 JSX를 children으로 받아 감싸줍니다. return <div className="card">{children}</div>;
} export default function App() { return ( <Card> <h2>공지사항</h2> <p>오늘 점검은 오후 8시에 시작합니다.</p> </Card> );
}
여기서 Card는 내용을 직접 결정하지 않습니다. 대신 어떤 내용이 들어와도 감싸서 같은 스타일과 구조를 제공하는 역할을 합니다. 이런 패턴은 공통 UI를 재사용할 때 매우 자주 쓰입니다.
또한 children에는 꼭 태그만 들어가는 것이 아닙니다. 문자열, 숫자, 여러 개의 JSX 노드, 조건에 따라 사라지는 null 같은 값도 들어갈 수 있습니다. 그래서 children은 단순한 장식이 아니라, 공통 틀 컴포넌트를 유연하게 만드는 핵심 수단으로 이해하는 편이 좋습니다.
props drilling은 왜 생길까

처음에는 props가 아주 명확하고 편합니다. 그런데 컴포넌트 구조가 깊어지면, 어떤 값이 필요한 실제 자식까지 도달시키기 위해 중간 컴포넌트들이 계속 전달만 하게 되는 순간이 생깁니다. 이 상황을 props drilling이라고 부릅니다.
중요한 점은 깊게 전달된다고 해서 모두 props drilling은 아니라는 점입니다. 중간 컴포넌트도 그 값을 실제로 사용한다면 자연스러운 전달일 수 있습니다. 반대로 중간 컴포넌트가 값을 쓰지 않고 통과만 시키는 구조가 반복될 때 보통 불편함이 커집니다.
type ButtonProps = { theme: string;
}; function Button({ theme }: ButtonProps) { // 실제로 theme를 쓰는 곳은 가장 아래 Button입니다. return <button className={theme}>확인</button>;
} function Section({ theme }: { theme: string }) { // Section은 theme를 직접 쓰지 않고 아래로 넘기기만 합니다. return <Button theme={theme} />;
} function Page({ theme }: { theme: string }) { // Page도 마찬가지로 전달만 합니다. return <Section theme={theme} />;
} export default function App() { // theme는 App에서 시작했지만 실제 사용 위치는 훨씬 아래입니다. return <Page theme="primary" />;
}
이 예제에서 theme는 가장 아래의 Button에서 실제로 필요합니다. 하지만 그 사이에 있는 Page와 Section도 같은 props를 받아서 다시 넘겨야 합니다. 컴포넌트가 몇 단계 더 늘어나면 수정 포인트가 많아지고, 어떤 값이 어디서 왜 전달되는지 추적하기도 어려워집니다.
왜 구조가 꼬인다고 느껴질까
function Toolbar({ theme, user, onLogout }: { theme: string; user: { name: string }; onLogout: () => void;
}) { // 필요한 값이 여러 개로 늘어나면 전달 전용 컴포넌트가 빠르게 무거워집니다. return <Header theme={theme} user={user} onLogout={onLogout} />;
}
처음에는 theme 하나만 전달해서 괜찮아 보이지만, 나중에는 user, isAdmin, locale, onLogout 같은 값이 계속 늘어납니다. 그러면 중간 컴포넌트는 자기 역할보다 전달 역할이 더 커지고, 그 시점부터 구조가 불편하게 느껴집니다.
다만 여기서 바로 props가 나쁘다고 결론내릴 필요는 없습니다. 가까운 부모-자식 관계에서는 props가 가장 직관적입니다. 단지 깊게 반복 전달되는 상황이 생기면 Context 같은 다른 방법을 검토할 이유가 생긴다고 이해하면 충분합니다.
정리
props는 React 컴포넌트를 연결하는 가장 기본적인 데이터 전달 방식입니다. 부모가 자식에게 값을 내려주고, 자식은 그 값을 받아 화면을 그립니다. 문자열과 숫자만이 아니라 배열, 객체, 함수도 props로 전달할 수 있고, 태그 사이의 내용은 children으로 받습니다.
여기까지 이해하면 컴포넌트가 왜 분리되고, 데이터가 왜 위에서 아래로 흐르며, props drilling이 왜 생기는지 한 번에 연결됩니다. 이후 state, event, Context를 공부할 때도 기준점이 훨씬 명확해집니다.
props를 어디까지 넘길지 판단하려면 먼저 React 컴포넌트 분리 기준 글에서 컴포넌트 분리 기준을 함께 확인하는 것이 좋습니다.
많이 받는 질문
Q. props와 state는 무엇이 다른가요?
props는 부모가 자식에게 내려주는 값이고, state는 컴포넌트가 스스로 관리하는 값입니다. props는 외부에서 전달되고, state는 내부에서 바뀐다고 이해하면 시작점으로 충분합니다.
Q. props를 자식 컴포넌트에서 직접 바꿔도 되나요?
권장되지 않습니다. props는 부모가 내려준 값이므로 자식에서는 읽기 전용처럼 다루는 편이 맞습니다. 값 변경이 필요하면 부모 state를 바꾸거나, 부모가 새 값을 다시 전달하는 구조로 가야 합니다.
Q. children도 결국 props인가요?
맞습니다. children은 태그 사이에 넣은 내용을 전달받는 특별한 형태의 prop입니다. 그래서 일반 props처럼 자식 컴포넌트 안에서 사용할 수 있습니다.
Q. props drilling이 보이면 바로 Context를 써야 하나요?
항상 그런 것은 아닙니다. 전달 단계가 짧고 구조가 단순하면 props가 오히려 더 읽기 쉽습니다. 다만 여러 중간 컴포넌트가 같은 값을 계속 전달만 한다면 Context나 구조 분리를 검토할 시점입니다.
“React props 사용법: 부모에서 자식으로 데이터 전달하는 구조”에 대한 3개의 생각