React props로 부모와 자식 사이에 데이터와 변경 요청을 어떻게 전달하나요?
props는 부모 컴포넌트가 자식에게 전달하는 입력값입니다. 자식은 props를 읽기만 하고, 값을 바꾸려면 부모가 전달한 함수를 호출합니다.
적용 조건·범위: 이 부분 코드는 부모에 task 객체가 있다고 가정합니다. 클릭하면 onToggle(task.id)이 실행되어 Console에 id를 출력합니다. 이 함수에는 상태를 바꾸는 코드가 없으므로 완료 여부는 바뀌지 않습니다.
본문 근거: React — Passing Props to a Component · React — Responding to Events
앞 예제는 App.jsx(JavaScript), 버튼 실습은 App.tsx(React + TypeScript)를 사용합니다. 내 PC에서 실행하기 전 React Vite 환경 준비를 확인하세요.
props 사용 예제
function Greeting({ name }) {
return <p>안녕하세요, {name}님</p>;
}
export default function App() {
return <Greeting name="해비" />;
}
화면 결과: 안녕하세요, 해비님
부모 App이 name="해비"를 전달하고, 자식 Greeting이 { name }으로 꺼내 표시합니다. 위 JavaScript 예제를 React 프로젝트의 src/App.jsx에서 실행할 수 있습니다.
props를 직접 수정하지 않습니다
부모가 새 값을 전달하면 자식의 화면도 갱신됩니다. 자식에서 받은 객체를 직접 바꾸는 아래 코드는 피하세요.
function TaskItem({ task }) {
task.completed = true; // 잘못된 예: 받은 객체를 직접 수정
return <li>{task.title}</li>;
}
객체에 대입하는 문법 자체는 실행될 수 있지만, props를 수정해도 React에 상태 변경을 알리지 못합니다. 변경 요청은 다음처럼 부모의 함수에 전달하세요.
클릭 요청은 함수로 전달합니다
function TaskItem({ task, onToggle }) {
return (
<button onClick={() => onToggle(task.id)}>
{task.title}
</button>
);
}
// 부모의 JSX에서 사용
<TaskItem task={task} onToggle={(id) => console.log(id)} />
이 부분 코드는 부모에 task 객체가 있다고 가정합니다. 클릭하면 onToggle(task.id)이 실행되어 Console에 id를 출력합니다. 이 함수에는 상태를 바꾸는 코드가 없으므로 완료 여부는 바뀌지 않습니다.
작업과 처리 함수 전달
작업 표시
전달받은 함수 호출
TypeScript에서는 props 타입을 선언합니다
type GreetingProps = { name: string };
function Greeting({ name }: GreetingProps) {
return <p>안녕하세요, {name}님</p>;
}
{ name }은 props 객체의 값을 꺼내는 구조 분해 문법입니다. props.name으로 읽어도 같습니다. JSX의 {name}은 그 값을 화면에 표시합니다.
객체와 콜백 함수의 타입 보기
type Task = {
id: string;
title: string;
completed: boolean;
};
type TaskItemProps = {
task: Task;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
};
task는 표시할 객체이고, onToggle·onDelete는 작업 id를 받아 처리할 함수입니다. 타입 선언이 객체를 자동 동결하는 것은 아니므로 자식에서 직접 수정하지 않는 규칙도 지켜야 합니다.

자주 하는 실수
클릭할 함수를 즉시 실행하지 마세요
// 잘못된 예: 화면을 그릴 때 실행
<button onClick={onDelete(task.id)}>삭제</button>
// 수정: 클릭할 때 실행
<button onClick={() => onDelete(task.id)}>삭제</button>
인자가 필요하면 함수로 감싸고, 인자가 없는 함수는 onClick={handleClick}처럼 전달합니다.
부모와 자식의 prop 이름을 맞추세요
// 자식이 onDelete를 받는데 onRemove를 전달하면 연결되지 않음
<TaskItem task={task} onToggle={handleToggle} onRemove={handleDelete} />
// 수정
<TaskItem task={task} onToggle={handleToggle} onDelete={handleDelete} />
부모에서 정의한 함수의 이름은 handleDelete여도 괜찮습니다. 자식이 받는 prop 이름인 onDelete를 일치시켜야 합니다.
버튼으로 props 확인하기
props 버튼 예제
책 읽기는 부모가 전달한 제목입니다. 클릭하면 id만 출력되고, 완료 상태는 바뀌지 않습니다.
예제 크게 보기 (새 탭)직접 확인
독립 실습 코드에서 부모의 title을 ‘산책’으로 바꾸면 자식 버튼은 어떻게 바뀔까요? 버튼을 클릭하기만 해도 제목이 바뀔까요?
해설 확인
부모가 새 title을 전달하면 자식 버튼의 글자도 ‘산책’으로 바뀝니다. 이 예제의 클릭 함수는 Console에 one을 출력할 뿐, 부모의 값을 바꾸지 않으므로 클릭만으로 제목이나 완료 상태는 바뀌지 않습니다.
예제 코드와 내 PC에서 실행하는 방법
React Vite 환경 준비의 React + TypeScript 프로젝트에서 src/App.tsx를 아래 코드로 교체합니다. src/main.tsx는 유지하고 npm run dev로 실행합니다.
type Task = { id: string; title: string; completed: boolean };
type TaskItemProps = { task: Task; onToggle: (id: string) => void };
function TaskItem({ task, onToggle }: TaskItemProps) {
return (
<button onClick={() => onToggle(task.id)}>
{task.title}
</button>
);
}
export default function App() {
const task: Task = { id: 'one', title: '책 읽기', completed: false };
return (
<TaskItem task={task} onToggle={(id) => console.log(id)} />
);
}
결과: 책 읽기 버튼이 보이고, 클릭하면 개발자 도구 Console에 one이 출력됩니다. 부모의 title을 산책으로 바꾸면 버튼 글자도 바뀝니다.
더 해보기: 서로 다른 id와 제목을 전달하는 TaskItem 두 개를 만들고 각 버튼의 표시와 클릭 로그를 비교해 보세요.
값을 전달하는 경로와 콜백이 실행되는 시점을 설명할 수 있으면 다음 학습으로 넘어가세요.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 React로 화면과 상태를 이어서 만듭니다.
이번 회차는 함수 전달 경로를 확인하는 단계입니다. 버튼을 눌러도 완료 표시나 목록은 바뀌지 않습니다.
연재 실습: 할 일 앱에 적용하기
TaskFlow 프로젝트 적용
TaskFlow 5단계 실행
시작 상태와 완성 결과를 바꾸어 본문에서 적용한 화면과 동작을 비교하세요.
예제 크게 보기 (새 탭)TaskFlow는 이전 JSX 회차부터 만드는 할 일 앱입니다. 처음 읽는 분은 위 예제만 확인해도 됩니다. 연재를 따라오고 있다면 step-04-jsx에서 이어서 App → TaskList → TaskItem으로 작업과 함수를 전달합니다.
시작 체크포인트: 이전: React JSX 문법 사용법 조건부 렌더링과 리스트 처리의 완료 상태입니다. 이전 회차 완료 ZIP을 새 폴더에 풀어 시작할 수도 있습니다.
시작 코드는 배열을 map으로 렌더링하고 작업 객체를 TaskItem에 전달합니다. 여기에 이벤트 요청 경로를 다음 순서로 연결합니다.
src/App.tsx에handleToggle·handleDelete를 정의합니다. 현재 함수는toggle또는delete와 작업 id를 Console에 출력합니다.src/components/TaskList.tsx에서 두 함수를 props로 받아 각TaskItem에 전달합니다.src/components/TaskItem.tsx에서 완료·삭제 버튼의onClick에 해당 콜백을 연결합니다.
독자 질문과 직접 확인
질문: 자식 TaskItem의 제목을 바꾸려면 어느 컴포넌트의 데이터를 수정해야 할까요?
App의 작업 배열에서 제목 하나를 수정해 저장하고, 해당 항목의 표시가 따라 바뀌는지 확인하세요. 제목이 그대로라면 App → TaskList → TaskItem 경로에서 실제로 같은 task를 전달하는지 확인합니다.
실행 확인
step-05-props에서 npm install 후 npm run dev로 실행합니다. 다운로드 프로젝트는 package.json에 Node.js 22.12 이상을 지정하고 있습니다. npm run build로 타입과 빌드 오류를 검사할 수 있습니다.
| 조작 | Console의 예상 출력 |
|---|---|
| 첫 작업의 완료 버튼 클릭 | toggle task-1 |
| 두 번째 작업의 삭제 버튼 클릭 | delete task-2 |
이번 회차는 함수 전달 경로를 확인하는 단계입니다. 버튼을 눌러도 완료 표시나 목록은 바뀌지 않습니다. Console 출력이 없다면 prop 이름과 onClick 연결을 확인하세요. 실제 상태 변경은 07회 useState에서 구현합니다.
완료 코드 ZIP 다운로드 — step-05-props
전체 파일·코드 보기
TaskFlow step-05-props 전체 코드 보기
실습 프로젝트 전체 코드
완료 ZIP의 파일입니다. 독립 실습에는 필요하지 않습니다.
index.html
<!doctype html>
<html lang="ko"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>TaskFlow</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
package.json
{
"name": "taskflow-step-05-props",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "tsc --noEmit && vite build",
"preview": "vite preview --host 127.0.0.1"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"typescript": "7.0.2",
"vite": "8.3.0",
"@vitejs/plugin-react": "6.1.1",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"@types/node": "26.6.2"
}
}
src/App.tsx
import Header from './components/Header';
import TaskList from './components/TaskList';
import type { Task } from './types/task';
const tasks: Task[] = [
{ id: 'task-1', title: 'props 구조 익히기', completed: true },
{ id: 'task-2', title: '이벤트 함수 전달하기', completed: false },
];
function App() {
const handleToggle = (id: string) => {
console.log('toggle', id);
};
const handleDelete = (id: string) => {
console.log('delete', id);
};
return (
<main className="app">
<Header />
<TaskList
tasks={tasks}
onToggle={handleToggle}
onDelete={handleDelete}
/>
</main>
);
}
export default App;
src/components/Header.tsx
export default function Header() {
return (
<header>
<p className="eyebrow">React Learning Project</p>
<h1>TaskFlow</h1>
<p>오늘 할 일을 하나씩 정리해 보세요.</p>
</header>
);
}
src/components/TaskItem.tsx
import type { Task } from '../types/task';
type TaskItemProps = {
task: Task;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
};
export default function TaskItem({
task,
onToggle,
onDelete,
}: TaskItemProps) {
return (
<li>
<button type="button" onClick={() => onToggle(task.id)}>
{task.completed ? '완료 취소' : '완료'}
</button>
<span>{task.title}</span>
<button type="button" onClick={() => onDelete(task.id)}>
삭제
</button>
</li>
);
}
src/components/TaskList.tsx
import TaskItem from './TaskItem';
import type { Task } from '../types/task';
type TaskListProps = {
tasks: Task[];
onToggle: (id: string) => void;
onDelete: (id: string) => void;
};
export default function TaskList({
tasks,
onToggle,
onDelete,
}: TaskListProps) {
if (tasks.length === 0) {
return <p>등록된 작업이 없습니다.</p>;
}
return (
<section>
<h2>작업 목록</h2>
<ul>
{tasks.map((task) => (
<TaskItem
key={task.id}
task={task}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
</section>
);
}
src/index.css
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, sans-serif;
color: #111;
background: #f5f5f5;
}
.app {
width: min(720px, calc(100% - 32px));
margin: 48px auto;
}
.eyebrow {
color: #666;
font-size: 14px;
}
ul {
padding-left: 20px;
}
button, input, select { min-height: 44px; font: inherit; }
button, input, select { border: 1px solid #aaa; padding: 8px 12px; }
button { cursor: pointer; }
button:disabled { cursor: wait; opacity: .6; }
ul { list-style: none; padding: 0; display: grid; gap: 12px; }
.task-card { border: 1px solid #ccc; padding: 16px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
.task-card__content, .task-card__actions { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
form, section { margin-block: 20px; }
input:not([type="checkbox"]), select { max-width: 100%; }
details { margin-block: 24px; } summary { cursor: pointer; min-height: 44px; }
body { overflow-wrap: anywhere; }
[role="dialog"], [data-radix-popper-content-wrapper] { --radius: 0; }
src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
src/types/task.ts
export type Task = {
id: string;
title: string;
completed: boolean;
};
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"types": [
"vite/client"
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"src",
"vite.config.ts"
]
}
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({ plugins: [react()] });
관련 학습과 문서
다음 글에서는 children으로 UI를 전달하는 방법을 다룹니다.
공식 문서
- React — Passing Props to a Component: 값 전달, 구조 분해 할당, props의 읽기 전용 규칙
- React — Responding to Events: 이벤트 함수 전달과 즉시 호출의 차이
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 33개 · 필수 27개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.