props를 이용한 데이터 전달
부모가 자식 컴포넌트에 props와 children을 전달하는 흐름을 익히고 읽기 전용 데이터라는 제약을 이해합니다.
컴포넌트가 독립된 UI 조각이라는 것을 이해했다면, 이제 이 컴포넌트들이 서로 협력해 하나의 완전한 애플리케이션을 만드는 방법을 배울 차례입니다. 그 핵심 방법 가운데 하나가 props를 이용한 데이터 전달입니다. 이 글에서는 부모가 자식에게 값과 children을 넘기는 방법, props를 간결하게 읽는 구조 분해, 그리고 props를 읽기 전용으로 다뤄야 하는 이유를 살펴봅니다.
PROPS & CHILDREN
부모가 값과 children을 전달하고 자식은 읽어 표시합니다
UserProfile 예제는 프로필 데이터와 추가 JSX를 표시합니다. 아래 변경 요청 경로는 다음 state 학습으로 이어지는 별도 설계이며, props 객체를 직접 고치는 방법이 아닙니다.
App에서 전달
<UserProfile
name="최프로퍼티" age={25}
location="제주">
<p>특기: 농구</p>
</UserProfile>name·age·location은 속성, 태그 안 p는 children으로 전달합니다. 스프레드 {...user2}도 각 속성을 props로 전달합니다.
UserProfile에서 읽기
function UserProfile({
name, age, location, children
}) { /* 카드 JSX 반환 */ }비구조화는 받은 props에서 이름을 꺼내는 JS 문법입니다. props 안 객체·배열도 직접 변경하지 않습니다.
값 변경이 필요할 때
부모가 state와 변경 함수를 소유 → 값·콜백을 props로 전달 → 자식의 사용자 동작이 콜백 호출 → 부모가 setter로 갱신 → 새 props로 렌더. 콜백도 부모에서 내려온 props입니다.
초기값 복사와 타입 검사
props를 자식 state의 초기값으로 쓰면 이후 props 변경을 자동 동기화하는 것은 아닙니다. React 19는 propTypes 검사를 무시하며, 본문의 PropTypes 코드는 React 18 이하 자료입니다.
위 그림은 App이 UserProfile에 두 종류의 입력을 나란히 넘기는 모습을 보여 줍니다. 하나는 name, age, location 같은 일반 속성 값이고, 다른 하나는 태그 안에 넣은 children입니다. 그림의 예처럼 <UserProfile name="최프로퍼티" age={25} location="제주"> 안에 <p>특기: 농구</p>를 넣으면, 세 값은 속성으로, 태그 안의 p는 children으로 전달됩니다. children도 props의 하나이며, 스프레드 {...user2}로 넘겨도 객체의 각 속성이 하나씩 props로 전달됩니다. UserProfile은 이 입력을 구조 분해로 꺼내 카드를 그리고, props 안의 객체나 배열도 직접 고치지 않습니다.
그림 아래쪽의 값 변경 경로는 다음 글의 state로 이어지는 설계이며, 이 글의 UserProfile 코드에 들어 있는 기능은 아닙니다. 부모가 state와 변경 함수를 가지고 있다가 값과 콜백을 props로 넘기면, 자식은 사용자의 동작에 맞춰 그 콜백을 호출하고, 부모가 setter로 값을 갱신해 새 props로 다시 렌더링합니다. 콜백도 부모에게서 내려온 props라는 점이 핵심입니다. 그림의 마지막 칸은 이 글의 뒤에서 다룰 두 가지 주의점, 곧 props를 자식 state의 초기값으로 복사하면 이후의 변경이 따라오지 않는다는 점과 React 19가 propTypes 검사를 무시한다는 점을 미리 보여 줍니다.
props란 무엇인가?
props는 properties(속성)의 줄임말입니다. React에서 props는 부모 컴포넌트가 자식 컴포넌트에게 데이터를 넘기는 통로이며, HTML 태그에 속성을 주듯이 React 컴포넌트에도 속성을 주어 데이터를 전달합니다.
props에는 세 가지 특징이 있습니다. 첫째, props는 언제나 부모에서 자식으로 한 방향으로만 전달되며, 자식은 받은 props를 직접 바꿀 수 없는 읽기 전용입니다. 둘째, props는 항상 자바스크립트 객체의 형태로 전달됩니다. 셋째, 같은 컴포넌트라도 어떤 props를 넘기느냐에 따라 다양하게 바꿔 쓸 수 있어 재사용성이 크게 높아집니다.
레고 블록에 빗대 보면, 부모 컴포넌트가 큰 레고판이고 자식 컴포넌트는 그 위에 얹는 작은 블록입니다. props는 그 블록의 색깔과 크기, 모양을 정해 주는 설명서와 같아서, 블록은 설명서대로 조립되지만 블록 스스로 설명서를 고칠 수는 없습니다.
props 사용하기: 부모에서 자식으로 데이터 전달
가장 기본적인 예제로 부모가 자식에게 데이터를 어떻게 넘기는지 살펴봅니다. 먼저 src 폴더 안에 components 폴더를 만들고, 그 안에 UserProfile.jsx 파일을 만듭니다.
function UserProfile(props) { // (1) props는 함수의 인자로 전달됩니다.
console.log(props); // 전달받은 props 객체를 콘솔에 출력하여 확인해봅시다.
return (
<div style={{ border: '1px solid #ddd', padding: '15px', margin: '10px', borderRadius: '8px' }}>
<h3>이름: {props.name}</h3> {/* (2) props.속성명 으로 데이터에 접근 */}
<p>나이: {props.age}세</p>
<p>지역: {props.location}</p>
{props.children && ( // (3) children props가 있다면 렌더링
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px dashed #eee' }}>
<h4>추가 정보:</h4>
{props.children}
</div>
)}
</div>
);
}
export default UserProfile;함수 컴포넌트에서 props는 함수의 첫 번째 인자로 전달되며, props.속성명의 형태로 넘겨받은 데이터에 접근합니다. style 속성에서 중괄호를 두 겹으로 쓰는 이유는, 바깥 중괄호가 JSX에 자바스크립트 표현식을 넣는다는 표시이고 안쪽 중괄호가 CSS 속성을 담은 자바스크립트 객체이기 때문입니다. 이런 방식을 인라인 스타일이라고 합니다.
이제 src/App.jsx를 열어 기존 내용을 고치고, UserProfile 컴포넌트를 불러와 씁니다.
import './App.css';
import UserProfile from './components/UserProfile'; // UserProfile 컴포넌트 불러오기
function App() {
const user1 = {
name: '김리액트',
age: 28,
location: '서울'
};
const user2 = {
name: '박컴포넌트',
age: 32,
location: '부산'
};
return (
<div className="App">
<h1>나의 사용자 프로필</h1>
{/* UserProfile 컴포넌트에 props 전달 */}
<UserProfile name={user1.name} age={user1.age} location={user1.location} />
{/* 또는 객체 전체를 전달할 수도 있습니다. <UserProfile {...user1} /> */}
<UserProfile name="최프로퍼티" age={25} location="제주">
{/* 자식 요소로 직접 JSX를 전달 (props.children으로 접근) */}
<p>특기: 농구</p>
<p>취미: 독서</p>
</UserProfile>
<UserProfile {...user2}> {/* 스프레드 문법으로 객체 속성들을 props로 전달 */}
<p>안녕하세요! 저는 {user2.name}입니다.</p>
</UserProfile>
</div>
);
}
export default App;개발 서버의 페이지를 브라우저에서 열면 서로 다른 정보를 담은 UserProfile 카드 세 장이 보입니다. 첫 카드는 “이름: 김리액트”, “나이: 28세”, “지역: 서울”을 보여 주며, children이 없어 추가 정보 영역이 없습니다. 둘째 카드는 “최프로퍼티”, “25세”, “제주”와 함께 추가 정보 아래에 특기와 취미의 두 p 요소를 보여 주고, 셋째 카드는 user2의 “박컴포넌트”, “32세”, “부산”과 “안녕하세요! 저는 박컴포넌트입니다.”라는 인사 문장을 보여 줍니다.
props는 HTML 속성처럼 name="값"의 형태로 넘기고, 자바스크립트 변수나 표현식을 넘길 때는 name={변수명}처럼 중괄호를 씁니다. 셋째 카드의 스프레드 {...user2}는 객체를 통째로 하나의 숨은 속성에 넣는 것이 아니라, 객체의 각 속성을 입력으로 펼칩니다. 같은 속성을 다시 적으면 뒤에 적은 값이 앞의 값을 덮어씁니다.
개발자 도구(F12)의 Console 탭에서는 UserProfile이 console.log(props)로 출력한 props 객체도 볼 수 있습니다. 다만 결과를 확인할 때는 로그가 몇 번 찍혔는지보다 세 카드에 넘긴 값과 화면의 내용을 비교하세요. Vite 템플릿의 main.jsx는 App을 <StrictMode>로 감싸 두는데, Strict Mode는 개발 모드에서 렌더링 중의 실수를 일찍 찾으려고 컴포넌트 함수를 한 번 더 호출할 수 있습니다. 그래서 카드 하나에 로그가 정확히 한 번 찍히는지로 성공을 판단하지 않습니다. React가 값을 넘겼는지 보는 로그와 사용자에게 보이는 출력은 서로 다른 확인 방법입니다.
비구조화 할당으로 props 사용하기
props.name, props.age처럼 매번 props.를 붙이는 것은 번거로울 수 있습니다. 이럴 때 자바스크립트의 구조 분해 할당(destructuring assignment)을 쓰면 코드가 훨씬 간결해집니다. 제목과 코드 주석의 비구조화 할당은 같은 문법을 부르는 다른 이름입니다. 구조 분해와 스프레드 문법 자체가 낯설다면 웹 입문 교재의 구조 분해 할당과 스프레드 연산자를 먼저 읽어 보세요. UserProfile.jsx를 다음과 같이 고쳐 보세요.
// props 객체에서 name, age, location, children 속성을 바로 추출
function UserProfile({ name, age, location, children }) {
return (
<div style={{ border: '1px solid #ddd', padding: '15px', margin: '10px', borderRadius: '8px' }}>
<h3>이름: {name}</h3> {/* props.name 대신 바로 name 사용 */}
<p>나이: {age}세</p>
<p>지역: {location}</p>
{children && (
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px dashed #eee' }}>
<h4>추가 정보:</h4>
{children}
</div>
)}
</div>
);
}
export default UserProfile;함수의 인자 자리에 { name, age, location, children }처럼 쓰면, props 객체에서 그 이름의 속성을 꺼내 바로 변수로 쓸 수 있습니다. 코드가 훨씬 깔끔해졌으므로, 앞으로는 주로 이 방식을 씁니다.
props.children
앞의 예제에서 <UserProfile> ... </UserProfile>의 태그 사이에 다른 JSX를 넣었습니다. 이렇게 컴포넌트 태그 사이에 놓인 내용은 props.children이라는 특별한 props로 전달됩니다. children 덕분에 컴포넌트도 일반 HTML 태그처럼 안에 콘텐츠를 품을 수 있어, 훨씬 유연하게 컴포넌트를 설계할 수 있습니다.
UserProfile.jsx의 children && (...)는 이 예제처럼 JSX를 넘겼을 때 추가 정보 영역을 보여 줍니다. 하지만 이 조건은 렌더링할 수 있는 모든 값이 있는지 검사하는 조건은 아닙니다. 예를 들어 숫자 0을 children으로 넘기면 조건식의 결과가 0이 되어, 추가 정보 영역 없이 0만 화면에 나타납니다(React 문서의 조건부 렌더링).
props는 읽기 전용이다 (Read-Only)
앞에서 props의 특징으로 단방향 데이터 흐름과 읽기 전용을 들었습니다. 이는 자식 컴포넌트 안에서 넘겨받은 props의 값을 직접 바꿔서는 안 된다는 뜻입니다.
function MyComponent(props) {
// ❌ 이렇게 props 값을 직접 변경하려고 시도해서는 안 됩니다!
// props.name = '새로운 이름'; // 에러 발생 또는 예상치 못한 동작
return (
<div>
<p>이름: {props.name}</p>
</div>
);
}React는 props가 바뀌지 않는다고 가정하고 최적화를 합니다. 자식은 props 안의 객체나 배열도 직접 고치지 않아야 합니다. 자바스크립트 객체의 모든 중첩 값이 자동으로 얼려진다는 뜻이 아니라, 받은 렌더 입력을 읽기 전용으로 다루기로 한 약속입니다. 자식이 props를 바꾸면 단방향 데이터 흐름이 깨지고 애플리케이션의 상태를 예측하기 어려워져, 복잡하고 디버깅하기 힘든 버그가 생길 수 있습니다.
자식이 부모에게서 받은 데이터를 바꿔야 한다면 두 가지 방법을 씁니다. 하나는 부모 컴포넌트의 state를 쓰는 방법입니다. 부모가 관리하는 state를 props로 넘기고, 자식은 부모가 넘겨준 변경 함수를 호출해 부모에게 state의 변경을 요청합니다. 그림 아래쪽의 콜백 경로가 이 방법이며, state는 다음 글에서 자세히 다룹니다.
다른 하나는 자식 컴포넌트 안의 state를 쓰는 방법입니다. 수정 중인 초안처럼 자식이 따로 관리할 값이라면, props로 받은 데이터를 자식 state의 초기값으로 쓸 수 있습니다. 다만 같은 컴포넌트가 유지되는 동안 초기값은 첫 렌더에만 쓰이므로, 이후 부모가 새 props를 넘겨도 그 state가 저절로 따라 바뀌지는 않습니다. 언제나 부모의 최신 값을 보여 줘야 한다면 props를 그대로 읽는 설계를 먼저 고려합니다.
props는 그 렌더에서 부모가 넘긴 입력입니다. 다음 렌더에서는 다른 값이 올 수 있으므로 영원히 고정된 데이터로 생각하지 말고, 자식은 현재의 입력을 읽되 변경이 필요하면 부모에게 새 입력을 요청합니다(React 문서의 props 전달).
더 깊이: 개발 모드에서 props가 얼려지는 범위
개발 빌드의 React는 JSX로 element를 만들 때 그 element와 props 객체를 Object.freeze로 얼립니다(React 저장소의 ReactJSXElement.js). 함수 컴포넌트가 인자로 받는 props가 바로 이 객체이므로, 엄격 모드(strict mode)로 실행되는 ES 모듈에서 props.name = '새로운 이름'을 실행하면 TypeError가 납니다. 위 예제의 주석이 말하는 에러가 이 경우입니다.
다만 이 보호에는 두 가지 빈틈이 있습니다. 하나는 이 코드가 개발 빌드에만 들어 있다는 점으로, 프로덕션 빌드에서는 같은 대입이 오류 없이 지나가 버그가 조용히 남습니다. 다른 하나는 Object.freeze가 객체의 바로 아래 속성만 얼리는 얕은(shallow) 동결이라는 점으로, props.user.address.city = '부산'처럼 안쪽 객체를 고치는 실수는 개발 모드에서도 오류 없이 실행됩니다. 그래서 개발 모드에서 오류가 나지 않았다는 사실만으로 props를 바꾸지 않았다고 판단할 수는 없습니다.
기존 PropTypes 코드 읽기
큰 프로젝트를 진행하거나 여러 개발자가 협업할 때는 특정 props가 문자열, 숫자, 불리언 가운데 어떤 타입이어야 하는지 밝혀 두는 것이 중요합니다. 다음은 React 18 이하에서 쓰던 prop-types 예제입니다. React 19는 함수 컴포넌트의 propTypes 검사를 무시하므로, 최신 프로젝트에서 이 코드로 경고를 확인할 수는 없습니다(React 19 업그레이드 안내). 새 코드에서는 TypeScript 같은 타입 검사와 필요한 런타임 입력 검증을 구분해 씁니다. 여기서는 기존 프로젝트의 코드를 읽기 위해 구조를 살펴봅니다.
기존 프로젝트에서는 먼저 프로젝트 폴더에서 다음 명령으로 prop-types를 설치했습니다.
npm install prop-typesprop-types는 React와 별도의 패키지이므로, 이 검사를 쓰던 프로젝트는 package.json의 의존성에 이 이름이 들어 있습니다. 그다음 UserProfile.jsx에 PropTypes를 적용했습니다. 아래 코드에서 컴포넌트 함수 자체는 평범하며, 검사 규칙은 맨 아래에서 함수에 propTypes 속성을 붙이는 객체에 따로 모여 있습니다.
import PropTypes from 'prop-types'; // PropTypes 임포트
function UserProfile({ name, age = 20, location, children }) {
return (
<div style={{ border: '1px solid #ddd', padding: '15px', margin: '10px', borderRadius: '8px' }}>
<h3>이름: {name}</h3>
<p>나이: {age}세</p>
<p>지역: {location}</p>
{children && (
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px dashed #eee' }}>
<h4>추가 정보:</h4>
{children}
</div>
)}
</div>
);
}
// PropTypes 정의
UserProfile.propTypes = {
name: PropTypes.string.isRequired, // name은 문자열이며 필수값입니다.
age: PropTypes.number, // age는 숫자입니다. (필수 아님)
location: PropTypes.string.isRequired, // location은 문자열이며 필수값입니다.
children: PropTypes.node // children은 렌더링 가능한 모든 것 (문자열, 숫자, React 요소 등)
};
export default UserProfile;PropTypes.string.isRequired는 name과 location이 문자열이어야 하고 반드시 넘겨야 한다는 뜻이며, 그렇지 않으면 React 18 이하의 개발 모드에서 Console에 경고가 나타났습니다. PropTypes.number는 age가 숫자여야 한다는 뜻이고 isRequired가 없으므로 필수는 아닙니다. PropTypes.node는 children이 숫자, 문자열, React 요소처럼 렌더링할 수 있는 값이라는 뜻입니다. 이처럼 PropTypes는 UserProfile이 어떤 props를 기대하는지 코드 가까이에 남겨 두고 잘못된 값이 들어오면 개발 중에 경고로 알려 주는 안전망이었지만, React 19에서 이 코드를 실행해도 같은 경고를 기대해서는 안 됩니다.
이 코드에는 age = 20이라는 기본값도 들어 있습니다. 구조 분해의 기본값은 그 속성을 생략했거나 값이 undefined일 때만 쓰이며, null에는 적용되지 않습니다.
정리
- props는 부모가 자식에게 넘기는 읽기 전용 입력이며, 언제나 객체의 형태로 전달됩니다.
- 문자열은
name="값"으로, 변수나 표현식은name={값}으로 넘기고, 스프레드{...객체}는 각 속성을 펼쳐 넘기며 뒤에 적은 속성이 앞의 값을 덮어씁니다. - 컴포넌트 태그 사이의 내용은
children으로 전달되며,children && (...)는 숫자 0을 그대로 표시한다는 점에 주의합니다. - 자식은 props와 그 안의 객체·배열을 직접 고치지 않고, 바꿔야 하면 부모가 넘긴 함수를 호출해 요청합니다.
- props를 자식 state의 초기값으로 쓰면 이후 부모의 변경이 저절로 반영되지 않습니다.
- React 19는
propTypes검사를 무시하므로, 새 코드에서는 TypeScript 같은 타입 검사와 런타임 검증을 씁니다.
연습 문제
1. 속성의 순서와 기본값
user1이 앞의 예제와 같을 때, <UserProfile {...user1} age={30} />와 <UserProfile age={30} {...user1} />의 나이는 각각 몇 세로 보일까요? 또 age = 20 기본값이 있는 PropTypes 버전에서 age를 생략한 경우와 age={null}을 넘긴 경우는 어떻게 다를까요? 나머지 필수 입력은 원래 예제와 같다고 가정합니다.
해설 보기
첫 번째는 마지막에 적은 age가 이기므로 “나이: 30세”, 두 번째는 뒤에 펼친 user1.age가 28이므로 “나이: 28세”입니다.
기본값은 생략했거나 undefined일 때만 쓰이므로, age를 생략하면 “나이: 20세”가 됩니다. null에는 기본값이 적용되지 않고 null은 화면에 아무것도 그리지 않으므로, 숫자 자리가 비어 “나이: 세”가 보입니다.
2. children이 숫자 0일 때
원래의 UserProfile에 children={0}을 넘기면 추가 정보 영역과 숫자는 어떻게 보일까요?
해설 보기
조건식 children && (...)의 결과가 숫자 0이므로, 추가 정보의 제목과 감싸는 div 없이 0만 화면에 나타납니다. 이 조건식은 표시할 수 있는 children이 있는지를 검사하는 조건과 같지 않습니다.
3. ExtraInfo 컴포넌트
원래 UserProfile의 children && (...)는 숫자 0을 추가 정보 영역 밖에 표시합니다. 이를 연습하는 별도의 ExtraInfo 컴포넌트를 작성해, 숫자와 문자열, JSX 요소는 제목과 함께 영역 안에 보여 주고 null, undefined, 불리언은 영역 전체를 숨기세요. 배열은 넘기지 않으며, 빈 문자열은 제목만 있는 빈 영역으로 보여 준다고 정합니다. 0, false, null, 빈 문자열, <p>농구</p> 각각의 결과도 적어 보세요.
해설 보기
무엇을 보여 줄지에 대한 규칙을 불리언 조건으로 만들어, 그 결과로 영역을 넣을지 정합니다. null과 undefined를 함께 제외하는 children != null과, 불리언을 제외하는 타입 검사를 조합합니다.
export default function ExtraInfo({ children }) {
const show = children != null && typeof children !== 'boolean';
return show ? (
<section>
<h4>추가 정보:</h4>
{children}
</section>
) : null;
}<ExtraInfo>{0}</ExtraInfo>는 제목과 숫자 0을 section 안에 보여 줍니다. false와 null이면 section 자체가 없습니다. 빈 문자열은 제목만 보여 주고, p 요소를 넘기면 제목 아래에 “농구”가 나타납니다. 이 코드는 이 문제에서 정한 규칙을 구현한 것일 뿐, 모든 종류의 children에서 내용이 실제로 보이는지 검사하는 일반적인 함수는 아닙니다.
4. 중첩 객체의 복사
부모가 넘긴 user가 { name: '민수', address: { city: '서울' } }입니다. 자식에서 const next = { ...user }를 만든 뒤 next.address.city = '부산'을 실행하면 원본도 바뀔까요? 원본의 name과 address는 그대로 두고 부산을 가진 새 객체를 만드는 식을 작성해 보세요. 새 객체를 만드는 계산만 다루며, 부모 화면의 갱신은 다음 글에서 다룹니다.
해설 보기
바깥 객체를 스프레드하는 것만으로는 address의 참조가 공유되므로, 이 코드는 원본의 city까지 부산으로 바꿉니다. 바꿀 address도 새 객체로 만들어야 원본의 서울이 유지됩니다.
const user = { name: '민수', address: { city: '서울' } };
const next = {
...user,
address: { ...user.address, city: '부산' }
};
// user.address.city === '서울'
// next.address.city === '부산'
// next.address !== user.address이 계산은 새 값을 준비할 뿐 React에 렌더를 요청하지는 않습니다. 받은 객체를 고치는 것과 부모에게 새 입력을 요청하는 것을 구별하세요. 스프레드가 얕은 복사라는 점과 중첩 객체를 복사하는 방법은 React 문서의 객체 state 갱신에서 설명합니다.
5. 문자열 28과 숫자 28
나이를 자식에게 넘길 때 age="28"과 age={28}은 같은 타입일까요? 각각의 경우에 age + 1의 결과를 적어 보고, 화면에 “28세”가 보였다는 것만으로 숫자 타입이 확인되는지 설명해 보세요.
해설 보기
따옴표 안의 "28"은 문자열이라서 age + 1은 문자열 "281"이 되고, 중괄호 안의 28은 숫자라서 결과는 29입니다.
React는 두 값 모두 텍스트로 보여 줄 수 있으므로, 화면만 보고 입력의 타입을 확인할 수는 없습니다. 원래 PropTypes 예제의 경고도 React 18 이하의 개발 모드에서만 나타났으므로, React 19에서는 그 코드를 타입 검증 대신 쓸 수 없습니다.
다음 글 state와 useState 훅 기초에서는 props와 함께 컴포넌트의 동적인 상태를 관리하는 또 하나의 핵심 개념인 state를 알아봅니다.