Passing Props to a Component

김동현·2026년 3월 15일

title: 컴포넌트에 Props 전달하기

리액트 컴포넌트들은 서로 통신하기 위해 props를 사용해요. 모든 부모 컴포넌트는 자식 컴포넌트에게 props를 주어서 어떤 정보든 전달할 수 있답니다. HTML 속성(attributes)이 떠오르실 수도 있겠지만, props를 통해서는 객체(objects), 배열(arrays), 함수(functions)를 포함한 어떠한 자바스크립트 값이라도 자유롭게 전달할 수 있어요.

🧑‍🏫 강사의 부연 설명:
프론트엔드 개발을 공부하시면서 일반적인 자바스크립트 '함수(function)'에 '인자(parameter)'를 넘겨주는 방식을 떠올려 보시면 이해하기 쉬워요. 컴포넌트도 결국 함수이기 때문에, 부모가 자식에게 데이터를 넘겨주는 아주 기본적이고 핵심적인 방법이 바로 이 props입니다. 리액트의 심장과도 같으니 꽉 잡고 가보자고요!

  • 컴포넌트에 props를 전달하는 방법
  • 컴포넌트에서 props를 읽어오는 방법
  • props의 기본값(default values)을 지정하는 방법
  • 컴포넌트에 JSX를 전달하는 방법
  • 시간이 지남에 따라 props가 어떻게 변하는지

친숙한 props {/familiar-props/}

Props는 여러분이 JSX 태그에 전달하는 정보예요. 예를 들어, className, src, alt, width, height 같은 것들이 여러분이 <img> 태그에 전달할 수 있는 props 중 일부죠.

function Avatar() {
  return (
    <img
      className="avatar"
      src="[https://i.imgur.com/1bX5QH6.jpg](https://i.imgur.com/1bX5QH6.jpg)"
      alt="Lin Lanying"
      width={100}
      height={100}
    />
  );
}

export default function Profile() {
  return (
    <Avatar />
  );
}
body { min-height: 120px; }
.avatar { margin: 20px; border-radius: 50%; }

<img> 태그에 전달할 수 있는 props는 이미 사전에 정의되어 있어요 (ReactDOM은 HTML 표준을 따르거든요). 하지만 여러분이 직접 만든 나만의 컴포넌트(예: <Avatar>)에는 어떤 props든 마음대로 전달해서 커스터마이징할 수 있답니다. 어떻게 하는지 한번 살펴볼까요?

컴포넌트에 props 전달하기 {/passing-props-to-a-component/}

아래 코드를 보면, Profile 컴포넌트는 자식 컴포넌트인 Avatar에게 아무런 props도 전달하지 않고 있어요.

export default function Profile() {
  return (
    <Avatar />
  );
}

딱 두 단계만 거치면 Avatar에게 props를 줄 수 있답니다.

1단계: 자식 컴포넌트에 props 전달하기 {/step-1-pass-props-to-the-child-component/}

먼저, Avatar에 몇 가지 props를 전달해 볼게요. 예를 들어, person (객체)과 size (숫자)라는 두 개의 props를 전달해 봅시다.

export default function Profile() {
  return (
    <Avatar
      person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
      size={100}
    />
  );
}

🧑‍🏫 강사의 부연 설명:
만약 person= 뒤에 있는 이중 중괄호 {{ }} 때문에 헷갈리신다면, 바깥쪽 {}는 JSX 안에서 자바스크립트를 쓰겠다는 신호이고, 안쪽 {}는 자바스크립트 객체(object)를 만드는 괄호라고 쪼개서 생각하시면 편해요. 즉, 이중 중괄호는 단순히 JSX 중괄호 안에 있는 하나의 객체(object)일 뿐이라는 걸 기억해 주세요.

이제 Avatar 컴포넌트 안에서 이 props를 읽어올 수 있어요.

2단계: 자식 컴포넌트 내부에서 props 읽기 {/step-2-read-props-inside-the-child-component/}

function Avatar 바로 뒤에 있는 ({}) 안에 쉼표로 구분해서 person, size 처럼 이름을 나열하면 이 props를 읽을 수 있어요. 이렇게 하면 일반적인 변수를 사용할 때처럼 Avatar 코드 내부에서 이것들을 자유롭게 사용할 수 있답니다.

function Avatar({ person, size }) {
  // 여기서 person과 size를 사용할 수 있어요
}

이제 렌더링을 위해 personsize props를 사용하는 로직을 Avatar에 추가하기만 하면 끝입니다.

이렇게 하면 다양한 props를 사용해서 Avatar가 여러 가지 방식으로 렌더링되도록 구성할 수 있어요. 아래 코드에서 값들을 이리저리 바꿔보면서 테스트해 보세요!

// src/App.js
import { getImageUrl } from './utils.js';

function Avatar({ person, size }) {
  return (
    <img
      className="avatar"
      src={getImageUrl(person)}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

export default function Profile() {
  return (
    <div>
      <Avatar
        size={100}
        person={{ 
          name: 'Katsuko Saruhashi', 
          imageId: 'YfeOqp2'
        }}
      />
      <Avatar
        size={80}
        person={{
          name: 'Aklilu Lemma', 
          imageId: 'OKS67lh'
        }}
      />
      <Avatar
        size={50}
        person={{ 
          name: 'Lin Lanying',
          imageId: '1bX5QH6'
        }}
      />
    </div>
  );
}
// src/utils.js
export function getImageUrl(person, size = 's') {
  return (
    '[https://i.imgur.com/](https://i.imgur.com/)' +
    person.imageId +
    size +
    '.jpg'
  );
}
body { min-height: 120px; }
.avatar { margin: 10px; border-radius: 50%; }

Props를 사용하면 부모 컴포넌트와 자식 컴포넌트를 완전히 독립적으로 생각할 수 있어요. 예를 들어, Avatar가 그 데이터들을 어떻게 내부에서 지지고 볶는지 신경 쓰지 않고도 Profile 안에서 person이나 size props를 자유롭게 변경할 수 있죠. 마찬가지로 Profile 코드를 쳐다보지 않고도 Avatar가 이 props를 화면에 어떻게 그려낼지 마음대로 변경할 수 있습니다.

Props를 여러분이 돌리면서 조절할 수 있는 "손잡이(다이얼)" 같은 거라고 생각해 보세요. 일반적인 함수에서 인자(arguments)가 하는 역할과 완벽하게 똑같습니다. 사실, props는 리액트 컴포넌트에 전달되는 유일한 인자랍니다! 리액트 컴포넌트 함수는 단 하나의 인자, 즉 props 객체 하나만을 받습니다.

function Avatar(props) {
  let person = props.person;
  let size = props.size;
  // ...
}

보통은 props 객체 전체가 한 번에 필요한 게 아니기 때문에, 위에서 배운 것처럼 개별 props로 구조 분해(destructure)해서 사용한답니다.

🧑‍🏫 강사의 부연 설명:
아래 내용은 실무에서도 주니어 개발자들이 은근히 자주 하는 실수이니 꼼꼼히 확인해 보세요!

props를 선언할 때 () 안에 있는 {} 중괄호 쌍을 빼먹지 않도록 주의하세요:

function Avatar({ person, size }) {
  // ...
}

이 문법은 "구조 분해 할당(destructuring)"이라고 부르며, 함수의 매개변수로 전달된 객체에서 속성들을 읽어오는 것과 완전히 똑같은 동작을 합니다.

function Avatar(props) {
  let person = props.person;
  let size = props.size;
  // ...
}

prop에 기본값(default value) 지정하기 {/specifying-a-default-value-for-a-prop/}

만약 아무런 값이 전달되지 않았을 때 대신 사용할 기본값을 prop에 주고 싶다면, 구조 분해 할당을 할 때 매개변수 바로 뒤에 = 기호와 기본값을 넣어서 설정할 수 있어요.

function Avatar({ person, size = 100 }) {
  // ...
}

이제 만약 <Avatar person={...} />size prop 없이 렌더링된다면, size는 자동으로 100으로 설정될 거예요.

이 기본값은 size prop이 아예 빠져있거나 명시적으로 size={undefined}로 전달될 때만 사용돼요. 하지만 size={null}이나 size={0}을 전달한다면, 기본값은 사용되지 않습니다. (리액트는 undefinednull을 엄격하게 구분한다는 점, 꼭 기억해 두세요!)

JSX 전개 구문(spread syntax)으로 props 전달하기 {/forwarding-props-with-the-jsx-spread-syntax/}

가끔은 props를 전달하는 코드가 굉장히 길고 반복적일 때가 있어요.

function Profile({ person, size, isSepia, thickBorder }) {
  return (
    <div className="card">
      <Avatar
        person={person}
        size={size}
        isSepia={isSepia}
        thickBorder={thickBorder}
      />
    </div>
  );
}

반복적인 코드가 잘못된 건 절대 아니에요. 오히려 어떤 데이터가 넘어가고 있는지 한눈에 보여서 가독성이 더 좋을 수도 있거든요. 하지만 때로는 간결함이 더 가치 있을 때가 있죠. 일부 컴포넌트들은 방금 본 ProfileAvatar에게 하는 것처럼, 자신이 받은 모든 props를 자식들에게 고스란히 전달하기도 해요. 이럴 때는 자신이 직접 props를 사용하지 않기 때문에, 더 간결한 "전개(spread)" 구문을 사용하는 것이 합리적일 수 있습니다.

🧑‍🏫 강사의 부연 설명:
늦은 밤 피곤한 상태로 타이핑하실 때 코드 한 줄, 타이핑 한 번이라도 줄이고 싶으시죠? 이 spread 문법이 아주 유용하게 쓰일 겁니다.

function Profile(props) {
  return (
    <div className="card">
      <Avatar {...props} />
    </div>
  );
}

이렇게 하면 이름들을 하나하나 나열하지 않고도 Profile의 모든 props를 Avatar에게 싹 다 전달할 수 있답니다.

하지만 전개 구문은 절제해서 사용하세요. 만약 여러분이 작성하는 거의 모든 컴포넌트에서 이 문법을 쓰고 있다면, 뭔가 아키텍처적으로 잘못되고 있는 거예요. 종종 이건 컴포넌트를 분리하고 자식들을 JSX로 전달해야 한다는 신호이기도 하거든요. 이에 대한 자세한 내용은 바로 다음에 나옵니다!

자식을 JSX로 전달하기 {/passing-jsx-as-children/}

내장된 브라우저 태그들을 중첩해서(nest) 사용하는 건 아주 흔한 일이죠.

<div>
  <img />
</div>

때로는 여러분이 직접 만든 컴포넌트들도 똑같은 방식으로 중첩하고 싶을 거예요.

<Card>
  <Avatar />
</Card>

JSX 태그 안에 콘텐츠를 중첩하면, 부모 컴포넌트는 그 안에 들어있는 콘텐츠를 children이라는 이름의 prop으로 받게 돼요. 예를 들어, 아래의 Card 컴포넌트는 <Avatar />로 설정된 children prop을 받아서 wrapper 역할을 하는 div 안에 렌더링할 거예요.

// src/App.js
import Avatar from './Avatar.js';

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

export default function Profile() {
  return (
    <Card>
      <Avatar
        size={100}
        person={{ 
          name: 'Katsuko Saruhashi',
          imageId: 'YfeOqp2'
        }}
      />
    </Card>
  );
}
// src/Avatar.js
import { getImageUrl } from './utils.js';

export default function Avatar({ person, size }) {
  return (
    <img
      className="avatar"
      src={getImageUrl(person)}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}
// src/utils.js
export function getImageUrl(person, size = 's') {
  return (
    '[https://i.imgur.com/](https://i.imgur.com/)' +
    person.imageId +
    size +
    '.jpg'
  );
}
.card {
  width: fit-content;
  margin: 5px;
  padding: 5px;
  font-size: 20px;
  text-align: center;
  border: 1px solid #aaa;
  border-radius: 20px;
  background: #fff;
}
.avatar {
  margin: 20px;
  border-radius: 50%;
}

<Card> 안에 있는 <Avatar>를 다른 단순한 텍스트로 바꿔보세요. Card 컴포넌트가 안에 중첩된 어떤 콘텐츠든 상관없이 어떻게 감싸주는지 확인할 수 있을 거예요. Card는 자기 안에 구체적으로 무엇이 렌더링되는지 "알" 필요가 전혀 없답니다. 이런 유연한 래퍼(wrapper) 패턴은 실무에서 정말 수없이 보게 되실 거예요.

children prop을 가진 컴포넌트는 부모 컴포넌트가 임의의 JSX로 맘대로 "채워 넣을 수 있는" 일종의 "구멍(hole)"을 가지고 있다고 생각하시면 돼요. 패널, 그리드, 모달(Modal) 창 등 시각적인 틀을 잡아주는 래퍼를 만들 때 children prop을 아주 자주 사용하시게 될 겁니다.

A puzzle-like Card tile with a slot for "children" pieces like text and Avatar

시간이 지남에 따라 props가 어떻게 변하는지 {/how-props-change-over-time/}

아래에 있는 Clock 컴포넌트는 부모 컴포넌트로부터 colortime이라는 두 개의 props를 받아요. (부모 컴포넌트의 코드는 생략되었는데, 아직 우리가 깊게 다루지 않은 상태(state)라는 개념을 사용하고 있기 때문이에요. 걱정 마세요, 곧 배우게 될 겁니다!)

아래의 셀렉트 박스에서 색상을 한 번 변경해 보세요:

// src/Clock.js
export default function Clock({ color, time }) {
  return (
    <h1 style={{ color: color }}>
      {time}
    </h1>
  );
}
// src/App.js
import { useState, useEffect } from 'react';
import Clock from './Clock.js';

function useTime() {
  const [time, setTime] = useState(() => new Date());
  useEffect(() => {
    const id = setInterval(() => {
      setTime(new Date());
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return time;
}

export default function App() {
  const time = useTime();
  const [color, setColor] = useState('lightcoral');
  return (
    <div>
      <p>
        Pick a color:{' '}
        <select value={color} onChange={e => setColor(e.target.value)}>
          <option value="lightcoral">lightcoral</option>
          <option value="midnightblue">midnightblue</option>
          <option value="rebeccapurple">rebeccapurple</option>
        </select>
      </p>
      <Clock color={color} time={time.toLocaleTimeString()} />
    </div>
  );
}

이 예제는 컴포넌트가 시간이 지남에 따라 다른 props를 받을 수도 있다는 것을 잘 보여줘요. Props는 처음 한 번 세팅되고 끝나는 정적인(static) 데이터가 아니랍니다! 여기서 time prop은 1초마다 실시간으로 변하고, color prop은 여러분이 드롭다운에서 다른 색상을 선택할 때마다 변하죠. Props는 단순히 시작할 때의 데이터가 아니라, 어느 시점이든 그 순간의 컴포넌트 상태 데이터를 거울처럼 반영해요.

하지만 매우 중요한 사실이 하나 있습니다. props는 불변(immutable)입니다. 컴퓨터 공학 용어로 "한 번 만들어지면 내부 값을 변경할 수 없다"는 뜻이죠. 컴포넌트가 자신의 화면을 업데이트하기 위해 props를 변경해야 할 때(예를 들어 사용자 상호작용이나 서버에서 새로운 데이터를 가져왔을 때), 컴포넌트는 부모 컴포넌트에게 완전히 새로운 다른 props(새로운 객체!)를 내려달라고 "요청"해야 해요. 그러면 예전 props는 미련 없이 버려지고, 결국 자바스크립트 엔진의 가비지 컬렉터가 그것들이 차지하던 메모리를 청소하게 됩니다.

🧑‍🏫 강사의 부연 설명:
절대로 "props를 직접 수정(mutate)"하려고 하지 마세요. props.color = 'red' 이런 식으로 코드를 짜면 리액트가 화면을 제대로 다시 그리지 못해서 엄청난 버그에 시달리게 됩니다. 선택된 색상을 바꾸는 것처럼 사용자 입력에 반응해야 할 때는 "state(상태)를 설정"해야 하는데, 이 내용은 나중에 State: 컴포넌트의 기억(State: A Component's Memory) 파트에서 자세히 다루게 될 테니 기대해 주세요!

  • 컴포넌트에 props를 전달하려면, HTML 속성을 작성하는 것과 똑같이 JSX 태그에 추가하면 됩니다.
  • props를 읽어오려면, function Avatar({ person, size }) 와 같은 구조 분해(destructuring) 문법을 사용하세요.
  • 값이 빠져있거나 undefined로 전달될 때를 대비해 size = 100 처럼 기본값을 지정할 수 있습니다.
  • <Avatar {...props} /> 와 같은 JSX 전개(spread) 구문을 사용하면 모든 props를 한 번에 넘길 수 있습니다. (하지만 너무 남용하지는 마세요!)
  • <Card><Avatar /></Card> 처럼 중첩된 JSX는 부모 Card 컴포넌트의 children prop으로 쏙 들어갑니다.
  • Props는 특정 시간의 스냅샷(snapshot)과도 같아서 오직 읽기 전용(read-only)입니다. 렌더링이 일어날 때마다 컴포넌트는 새로운 버전의 props를 받습니다.
  • props를 직접 변경할 수는 없습니다. 상호작용(interactivity)이 필요하다면 state(상태)를 설정해야 합니다.

컴포넌트 추출하기 {/extract-a-component/}

Gallery 컴포넌트는 두 개의 프로필에 대해 아주 유사한 마크업을 중복해서 포함하고 있어요. 이 중복을 줄이기 위해 Profile 컴포넌트를 분리해서 추출해 보세요. 이 새로운 컴포넌트가 어떤 props를 받아야 할지 고민해 보셔야 할 거예요.

// src/App.js
import { getImageUrl } from './utils.js';

export default function Gallery() {
  return (
    <div>
      <h1>Notable Scientists</h1>
      <section className="profile">
        <h2>Maria Skłodowska-Curie</h2>
        <img
          className="avatar"
          src={getImageUrl('szV5sdG')}
          alt="Maria Skłodowska-Curie"
          width={70}
          height={70}
        />
        <ul>
          <li>
            <b>Profession: </b> 
            physicist and chemist
          </li>
          <li>
            <b>Awards: 4 </b> 
            (Nobel Prize in Physics, Nobel Prize in Chemistry, Davy Medal, Matteucci Medal)
          </li>
          <li>
            <b>Discovered: </b>
            polonium (chemical element)
          </li>
        </ul>
      </section>
      <section className="profile">
        <h2>Katsuko Saruhashi</h2>
        <img
          className="avatar"
          src={getImageUrl('YfeOqp2')}
          alt="Katsuko Saruhashi"
          width={70}
          height={70}
        />
        <ul>
          <li>
            <b>Profession: </b> 
            geochemist
          </li>
          <li>
            <b>Awards: 2 </b> 
            (Miyake Prize for geochemistry, Tanaka Prize)
          </li>
          <li>
            <b>Discovered: </b>
            a method for measuring carbon dioxide in seawater
          </li>
        </ul>
      </section>
    </div>
  );
}
// src/utils.js
export function getImageUrl(imageId, size = 's') {
  return (
    '[https://i.imgur.com/](https://i.imgur.com/)' +
    imageId +
    size +
    '.jpg'
  );
}
.avatar { margin: 5px; border-radius: 50%; min-height: 70px; }
.profile {
  border: 1px solid #aaa;
  border-radius: 6px;
  margin-top: 20px;
  padding: 10px;
}
h1, h2 { margin: 5px; }
h1 { margin-bottom: 10px; }
ul { padding: 0px 10px 0px 20px; }
li { margin: 5px; }

먼저 과학자 한 명의 마크업을 통째로 추출하는 것부터 시작해 보세요. 그런 다음 두 번째 과학자의 데이터와 일치하지 않는 부분들을 쏙쏙 찾아내어 외부에서 props로 주입받을 수 있도록 구성해 보세요.

아래 해답에서 Profile 컴포넌트는 imageId(문자열), name(문자열), profession(문자열), awards(문자열 배열), discovery(문자열), 그리고 imageSize(숫자) 등 다양한 데이터를 props로 받도록 변경되었어요.

imageSize prop에는 기본값이 설정되어 있다는 점을 눈여겨보세요. 그렇기 때문에 부모 컴포넌트에서 이 prop을 따로 전달하지

profile
프론트에_가까운_풀스택_개발자

0개의 댓글