props를 이용한 데이터 전달
부모가 자식 컴포넌트에 props와 children을 전달하는 흐름을 익히고 읽기 전용 데이터라는 제약을 이해합니다.
이제 컴포넌트가 독립적인 UI 조각이라는 것을 이해했으니, 이 컴포넌트들이 서로 협력하여 하나의 완전한 애플리케이션을 만드는 방법을 배워야 합니다.
그 핵심적인 방법 중 하나가 바로 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 이하 자료입니다.
props란 무엇인가?
props는 properties(속성)의 줄임말입니다.
리액트에서 props는 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달할 때 사용하는 통로입니다.
마치 HTML 태그에 속성을 부여하듯이, 리액트 컴포넌트에도 속성을 부여하여 데이터를 전달할 수 있습니다.
props의 주요 특징
- 단방향 데이터 흐름:
props는 항상 부모에서 자식으로, 한 방향으로만 전달됩니다. 자식 컴포넌트는 전달받은props를 직접 변경할 수 없습니다. (읽기 전용) - 객체 형태:
props는 항상 자바스크립트 객체 형태로 전달됩니다. - 컴포넌트의 재사용성 증대: 동일한 컴포넌트라도 어떤
props를 전달하느냐에 따라 다양하게 변형하여 사용할 수 있어 컴포넌트의 재사용성을 극대화합니다.
레고 블록 비유를 다시 떠올려 보겠습니다.
부모 컴포넌트가 큰 레고판이라면, 자식 컴포넌트는 그 위에 얹을 작은 레고 블록입니다.
props는 이 작은 레고 블록의 색깔, 크기, 모양 등을 지정해주는 설명서와 같습니다.
설명서에 따라 블록을 조립하지만, 블록 자체가 설명서를 바꿀 수는 없는 것과 유사합니다.
props 사용하기: 부모에서 자식으로 데이터 전달
가장 기본적인 props 사용 예제를 통해 부모 컴포넌트가 자식 컴포넌트에 어떻게 데이터를 전달하는지 알아보겠습니다.
UserProfile 컴포넌트 만들기
UserProfile 컴포넌트 생성:
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 속성을 담는 자바스크립트 객체를 의미합니다. (인라인 스타일)
App.jsx에서 UserProfile 컴포넌트 사용:
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 카드가 렌더링된 것을 볼 수 있을 것입니다.
개발자 도구(F12)를 열어 콘솔 탭을 확인하면 UserProfile 컴포넌트에서 console.log(props)로 출력한 props 객체의 내용도 확인할 수 있습니다.
결과를 확인할 때는 콘솔 출력 횟수보다 세 카드에 전달된 값과 화면 내용을 비교합니다. 개발 모드의 StrictMode에서는 컴포넌트 함수를 추가로 호출할 수 있으므로, console.log(props)가 한 카드당 정확히 한 번 찍힌다는 기준으로 성공을 판단하지 않습니다. React가 값을 전달했는지 확인하는 로그와 사용자에게 보이는 출력은 서로 다른 관찰입니다.
위 예제에서 보듯이 props는 HTML 속성처럼 name="값" 형태로 전달하며, 자바스크립트 변수나 표현식을 전달할 때는 name={변수명}과 같이 중괄호 {}를 사용합니다.
비구조화 할당으로 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> ... </UserProfile> 태그 사이에 다른 JSX 내용을 넣은 것을 보셨을 것입니다.
이렇게 컴포넌트 태그 사이에 위치하는 내용은 특별한 props인 props.children으로 전달됩니다.
UserProfile.jsx의 children && (...)는 이 예제처럼 JSX를 전달했을 때 추가 정보 영역을 보여줍니다. 모든 렌더 가능한 값의 존재를 검사하는 조건은 아닙니다. 숫자 0을 children으로 전달하면 조건식의 결과가 0이 되어 추가 정보 영역 없이 0 자체가 표시됩니다.
이 props.children은 컴포넌트가 마치 일반 HTML 태그처럼 내부에 콘텐츠를 포함할 수 있도록 해주어 매우 유연한 컴포넌트 설계가 가능하게 합니다.
props는 읽기 전용이다 (Read-Only)
앞서 props의 특징으로 단방향 데이터 흐름과 읽기 전용임을 언급했습니다.
이는 자식 컴포넌트 내부에서 전달받은 props의 값을 직접 변경해서는 안 된다는 의미입니다.
function MyComponent(props) {
// ❌ 이렇게 props 값을 직접 변경하려고 시도해서는 안 됩니다!
// props.name = '새로운 이름'; // 에러 발생 또는 예상치 못한 동작
return (
<div>
<p>이름: {props.name}</p>
</div>
);
}리액트는 props가 변경될 수 없다고 가정하고 최적화를 수행합니다.
자식은 props 안의 객체·배열도 직접 수정하지 않아야 합니다. JavaScript 객체의 모든 중첩 값이 자동으로 얼려져 있다는 뜻이 아니라, 받은 렌더 입력을 읽기 전용으로 다루는 계약입니다. 만약 자식 컴포넌트에서 props를 변경하면, 리액트의 단방향 데이터 흐름 원칙이 깨지고 애플리케이션의 상태를 예측하기 어려워져 복잡하고 디버깅하기 힘든 버그가 발생할 수 있습니다.
만약 자식 컴포넌트에서 부모로부터 받은 데이터를 변경해야 할 필요가 있다면, 다음과 같은 방법을 사용합니다.
부모 컴포넌트의 상태(State) 사용: 부모 컴포넌트가 관리하는 상태를 자식에게 props로 전달하고, 자식은 부모가 전달해준 상태 변경 함수를 호출하여 부모의 상태를 변경하도록 요청합니다. (다음 절에서 state에 대해 자세히 다룰 것입니다.)
자식 컴포넌트 내부의 상태(State) 사용: 수정 초안처럼 독립적으로 관리할 값이라면 props로 받은 데이터를 자식 컴포넌트의 초기 상태로 사용할 수 있습니다. 같은 컴포넌트가 유지되는 동안 초기화는 첫 렌더에만 적용됩니다. 이후 부모가 새 props를 전달해도 그 state가 자동으로 따라 바뀌지는 않습니다. 항상 부모의 최신 값을 보여야 한다면 먼저 props를 그대로 읽는 설계를 사용합니다.
위 그림의 콜백 경로는 다음 절의 state 학습과 연결됩니다. 이 절의 UserProfile 코드는 전달된 값과 children을 표시합니다.
자식 컴포넌트는 받은 값을 직접 고치지 않고, 필요한 경우 부모가 내려준 함수를 호출해 다음 렌더링에 반영되도록 요청합니다.
props는 그 렌더에서 부모가 전달한 입력입니다. 다음 렌더에서는 다른 값이 올 수 있으므로 영원히 고정된 데이터로 생각하지 않습니다. 자식은 현재 입력을 읽고, 변경이 필요하면 부모에게 새 입력을 요청합니다. 공식 props 설명도 시간에 따른 새 입력과 읽기 전용 계약을 구분합니다.
기존 PropTypes 코드 읽기
대규모 프로젝트를 진행하거나 여러 개발자가 협업할 때, 특정 props가 어떤 타입(문자열, 숫자, 불리언 등)이어야 하는지 명시하는 것이 중요합니다.
다음은 React 18 이하에서 쓰던 prop-types 예제입니다. React 19는 컴포넌트의 propTypes 검사를 무시하므로 최신 프로젝트의 경고 검증 절차로 사용하지 않습니다. 새 코드에서는 TypeScript 등 타입 검사와 필요한 런타임 입력 검증을 구분합니다.
이는 개발 과정에서 잠재적인 버그를 미리 발견하는 데 도움을 줍니다.
prop-types 라이브러리 설치:
프로젝트 폴더에서 다음 명령어를 실행하여 prop-types를 설치합니다.
npm install prop-typesUserProfile.jsx에 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은 문자열 타입이어야 하며, 반드시 전달되어야 합니다. 그렇지 않으면 콘솔에 경고 메시지가 나타납니다.PropTypes.number:age는 숫자 타입이어야 합니다.isRequired가 없으므로 필수는 아닙니다.PropTypes.node:children은 숫자, 문자열, React 요소 등 렌더링 가능한 모든 것을 의미합니다.
PropTypes는 UserProfile이 어떤 props를 기대하는지 코드 가까이에 남겨 두고, 잘못된 값이 들어올 때 개발 중 경고로 확인하게 해주는 안전망입니다.
React 18 이하 개발 모드에서는 name 누락이나 타입 불일치에 경고가 나타납니다. React 19에서 이 원문 코드를 실행해도 같은 경고를 기대하면 안 됩니다.
2장 3절 props를 이용한 데이터 전달은 여기까지입니다.
props의 개념, 단방향 데이터 흐름, 비구조화 할당 사용법, children props, 그리고 props가 읽기 전용이라는 중요한 규칙까지 다루었습니다.
위 PropTypes 코드는 기존 프로젝트를 읽기 위한 자료이며, React 19의 타입 검사 기능을 구현한 예시는 아닙니다.
같은 카드의 입력을 바꿔 읽기
원 예제의 첫 카드는 김리액트 / 28세 / 서울이며 children이 없어 추가 정보 영역이 없습니다. 둘째 카드는 최프로퍼티 / 25세 / 제주와 두 p 요소를 표시하고, 셋째 카드는 user2의 박컴포넌트 / 32세 / 부산과 인사 문장을 표시합니다. 스프레드는 객체를 통째로 하나의 숨은 속성에 넣는 것이 아니라, 각 속성을 입력으로 펼칩니다. 같은 속성을 다시 적으면 뒤에 적은 값이 우선합니다.
혼자 확인하기
다음 세 사용을 각각 예측합니다. ① <UserProfile {...user1} age={30} />의 나이 ② <UserProfile age={30} {...user1} />의 나이 ③ 원 UserProfile에 children={0}을 전달했을 때 추가 정보 영역과 숫자 표시. 마지막으로 위의 기본값 age = 20이 있는 버전에서 age를 생략한 경우와 age={null}인 경우를 구별합니다. 나머지 필수 입력은 원 예제와 같다고 둡니다.
속성 순서·children·기본값 해설
① 마지막 age가 우선하므로 30세입니다. ② 뒤의 user1.age가 28이므로 28세입니다. ③ 원 조건식 children && (...)의 결과가 숫자 0이므로 추가 정보의 제목·wrapper 없이 0이 표시됩니다. 이 조건식은 모든 표시 가능한 children의 존재 검사와 같지 않습니다. 기본값은 생략 또는 undefined일 때 20이며, null에는 적용되지 않아 나이 숫자 자리가 비어 나이: 세가 됩니다.
입력 계약을 직접 고쳐 완성하기
- 원 UserProfile의
children && (...)는 숫자 0을 추가 정보 영역 밖에 표시합니다. 별도ExtraInfo컴포넌트를 작성하여 숫자·문자열·JSX 요소는 제목과 함께 영역 안에 표시하고, null·undefined·불리언은 영역 전체를 숨기세요. 이 문제에서는 배열을 전달하지 않으며, 빈 문자열은 제목만 있는 빈 영역으로 표시하는 계약입니다. 원 UserProfile을 변경한 코드가 아니라 이 계약을 연습하는 독립 컴포넌트로 작성합니다. 0, false, null, 빈 문자열,<p>농구</p>각각의 결과를 적습니다. - 부모가 전달한 user는
{ name: '민수', address: { city: '서울' } }입니다. 자식에서const next = { ...user }를 만든 뒤next.address.city = '부산'을 실행하면 원본도 바뀌는지 설명하세요. 원본의 name과 address를 유지하면서 부산을 가진 새 객체를 만드는 식을 작성합니다. 새 객체를 만드는 계산만 다루며, 부모 화면 갱신은 다음 절의 state 학습에서 다룹니다. - 프로필의 나이를 자식에게 전달할 때
age="28"과age={28}은 같은 타입인지 판단하세요. 문자열"28"이 들어온 상태에서age + 1을 계산한 결과와, 숫자 28의 결과를 각각 적습니다. 화면에 28세가 보였다는 이유만으로 숫자 타입이 검증되었는지도 설명하세요.
표시 조건·중첩 객체·입력 타입 해설
- 표시할 내용의 계약을 불리언 조건으로 만들어 영역 포함 여부를 결정합니다. 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에서 내용이 실제로 보이는지 검사하는 일반 함수는 아닙니다. React의 조건부 렌더링 설명도 숫자 0을 조건의 왼쪽에 두는 경우를 따로 다룹니다.
- 바깥 객체의 스프레드만으로는 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의 렌더를 요청하지 않습니다. 받은 객체를 고치는 것과 새 입력을 부모에게 요청하는 것을 구별합니다. 객체 상태 갱신의 공식 설명이 스프레드의 얕은 복사와 중첩 객체 복사를 설명합니다.
- 따옴표 안의
"28"은 문자열이며 더하기 결과는 문자열"281"입니다. 중괄호 안의 숫자 28은 숫자이고 결과는 29입니다. React는 두 값 모두 텍스트로 표시할 수 있으므로 화면만으로 입력 타입을 확인할 수 없습니다. 원 PropTypes 예제의 경고 조건은 React 18 이하 개발 모드이며, React 19에서는 그 코드를 타입 검증 대신으로 사용할 수 없습니다. React 19 전환 안내와 함께 확인합니다.
다음 절에서는 리액트 컴포넌트의 또 다른 핵심 개념이자 props와 함께 컴포넌트의 동적인 상태를 관리하는 state에 대해 알아보겠습니다.