포스트

[이제와서 시작하는 React 마스터하기 #4] Props - 컴포넌트끼리 대화하기

[이제와서 시작하는 React 마스터하기 #4] Props - 컴포넌트끼리 대화하기

이 포스트를 읽고 나면 ✅ Props가 무엇인지 이해할 수 있어요 ✅ 부모와 자식 컴포넌트가 데이터를 주고받을 수 있어요 ✅ 재사용 가능한 컴포넌트를 만들 수 있어요


📚 시작하기 전에

이전 포스트 Day 3: useState에서 화면을 움직이게 만드는 방법을 배웠어요.

하지만 지금까지는 하나의 컴포넌트 안에서만 작업했죠?

실제 앱은 여러 컴포넌트로 나누어져 있어요:

1
2
3
4
5
6
7
8
9
10
앱
├── 헤더
├── 사이드바
├── 메인 컨텐츠
│   ├── 포스트 목록
│   │   ├── 포스트 1
│   │   ├── 포스트 2
│   │   └── 포스트 3
│   └── 페이지네이션
└── 푸터

“이 컴포넌트들이 어떻게 서로 대화할까요?”

바로 Props를 사용해요!


🤔 Props가 뭐예요?

Props는 Properties(속성)의 줄임말이에요.

쉽게 말하면:

  • 부모 컴포넌트에서 자식 컴포넌트로 전달하는 데이터
  • 함수의 매개변수(파라미터)와 비슷해요!

일상 생활의 Props

1
2
3
4
5
6
7
8
엄마 → 딸에게 심부름
"마트에 가서 우유(props) 사와!"

선생님 → 학생에게 숙제
"이 문제집(props) 10페이지까지 풀어와!"

앱 → 버튼 컴포넌트
"이 텍스트(props)를 보여줘!"

🎯 Props 첫 만남

가장 간단한 예제

먼저 컴포넌트를 분리해볼게요.

Before: 한 컴포넌트에 모든 코드

1
2
3
4
5
6
7
8
function App() {
  return (
    <div>
      <h1>안녕하세요, 홍길동님!</h1>
      <p>환영합니다!</p>
    </div>
  );
}

After: 컴포넌트 분리 + Props 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 자식 컴포넌트
function Greeting(props) {
  return (
    <div>
      <h1>안녕하세요, {props.name}님!</h1>
      <p>환영합니다!</p>
    </div>
  );
}

// 부모 컴포넌트
function App() {
  return (
    <div>
      <Greeting name="홍길동" />
    </div>
  );
}

export default App;

실행해보기

src/App.jsx에 위 코드를 입력하고 저장하세요!

결과: “안녕하세요, 홍길동님!”이 보여요.

코드 이해하기

1. Props 전달하기 (부모 → 자식)

1
<Greeting name="홍길동" />
  • name="홍길동": name이라는 prop을 전달
  • HTML 속성처럼 써요!

2. Props 받기 (자식)

1
2
3
function Greeting(props) {
  return <h1>안녕하세요, {props.name}님!</h1>;
}
  • props: 전달받은 모든 데이터가 담긴 객체
  • props.name: name prop에 접근

💡 Props의 특징

1. 읽기 전용 (Read-only)

1
2
3
4
5
6
7
function Greeting(props) {
  // ❌ Props를 바꿀 수 없어요!
  props.name = "김철수";  // 에러!

  // ✅ Props는 읽기만 해요
  return <h1>{props.name}</h1>;
}

왜 그럴까요?

Props는 부모가 주는 데이터예요. 자식이 마음대로 바꾸면 안 되죠!

2. 여러 개 전달 가능

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function UserCard(props) {
  return (
    <div>
      <h2>{props.name}</h2>
      <p>나이: {props.age}</p>
      <p>직업: {props.job}</p>
    </div>
  );
}

function App() {
  return (
    <UserCard
      name="홍길동"
      age={25}
      job="개발자"
    />
  );
}

주의: 문자열이 아닌 값은 중괄호 {}로 감싸요!

  • 문자열: name="홍길동"
  • 숫자: age={25}
  • 불린: isActive={true}
  • 배열: tags={['react', 'javascript']}

3. 구조 분해 할당으로 간단하게

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Before: props.name, props.age...
function UserCard(props) {
  return (
    <div>
      <h2>{props.name}</h2>
      <p>{props.age}</p>
    </div>
  );
}

// After: 바로 name, age로 사용
function UserCard({ name, age, job }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>{age}</p>
      <p>{job}</p>
    </div>
  );
}

이제부터는 구조 분해 방식을 쓸게요! (더 깔끔하거든요 😊)


💪 실습 1: 버튼 컴포넌트 만들기

재사용 가능한 버튼을 만들어봐요!

목표

1
2
3
4
같은 버튼 컴포넌트를 사용하지만
- 텍스트가 다르고
- 색상이 다르고
- 클릭하면 다른 동작을 해요

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { useState } from 'react';

// 버튼 컴포넌트
function Button({ text, color, onClick }) {
  return (
    <button
      onClick={onClick}
      style={{
        backgroundColor: color,
        color: 'white',
        padding: '10px 20px',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        margin: '5px'
      }}
    >
      {text}
    </button>
  );
}

// 앱 컴포넌트
function App() {
  const [message, setMessage] = useState('');

  return (
    <div style={{ padding: '20px' }}>
      <h1>버튼 예제</h1>

      <Button
        text="성공"
        color="#28a745"
        onClick={() => setMessage('성공 버튼을 클릭했어요!')}
      />

      <Button
        text="경고"
        color="#ffc107"
        onClick={() => setMessage('경고 버튼을 클릭했어요!')}
      />

      <Button
        text="위험"
        color="#dc3545"
        onClick={() => setMessage('위험 버튼을 클릭했어요!')}
      />

      {message && (
        <p style={{ marginTop: '20px', fontSize: '18px' }}>
          {message}
        </p>
      )}
    </div>
  );
}

export default App;

결과 확인

버튼 3개가 다른 색으로 보이고, 각각 클릭하면 다른 메시지가 나와요!

이게 Props의 힘이에요! 🎉 하나의 컴포넌트를 여러 곳에서 다르게 사용할 수 있어요.


💪 실습 2: 프로필 카드 만들기

해보기

여러 사람의 프로필 카드를 만들어봐요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { useState } from 'react';

// 프로필 카드 컴포넌트
function ProfileCard({ name, job, age, email, image }) {
  return (
    <div
      style={{
        border: '1px solid #ddd',
        borderRadius: '10px',
        padding: '20px',
        margin: '10px',
        maxWidth: '300px'
      }}
    >
      <img
        src={image}
        alt={name}
        style={{
          width: '100px',
          height: '100px',
          borderRadius: '50%',
          objectFit: 'cover'
        }}
      />
      <h2>{name}</h2>
      <p style={{ color: '#666' }}>{job}</p>
      <p>나이: {age}</p>
      <p>이메일: {email}</p>
    </div>
  );
}

// 앱 컴포넌트
function App() {
  return (
    <div style={{ padding: '20px' }}>
      <h1>우리 팀 소개</h1>

      <div style={{ display: 'flex', flexWrap: 'wrap' }}>
        <ProfileCard
          name="홍길동"
          job="프론트엔드 개발자"
          age={25}
          email="hong@example.com"
          image="https://via.placeholder.com/100"
        />

        <ProfileCard
          name="김철수"
          job="백엔드 개발자"
          age={28}
          email="kim@example.com"
          image="https://via.placeholder.com/100"
        />

        <ProfileCard
          name="이영희"
          job="디자이너"
          age={26}
          email="lee@example.com"
          image="https://via.placeholder.com/100"
        />
      </div>
    </div>
  );
}

export default App;

직접 해보기

위 코드에서 네 번째 팀원을 추가해보세요!


💪 실습 3: 자식 → 부모로 데이터 전달하기

지금까지는 부모 → 자식으로만 전달했어요.

반대로는 어떻게 할까요?

핵심 아이디어

함수를 Props로 전달하면 돼요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { useState } from 'react';

// 자식 컴포넌트
function TodoInput({ onAdd }) {
  const [input, setInput] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (input.trim()) {
      onAdd(input);  // 부모에게 데이터 전달!
      setInput('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="할 일을 입력하세요"
        style={{ padding: '5px', marginRight: '5px' }}
      />
      <button type="submit">추가</button>
    </form>
  );
}

// 부모 컴포넌트
function App() {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = (todoText) => {
    console.log('자식으로부터 받은 데이터:', todoText);
    setTodos([...todos, { id: Date.now(), text: todoText }]);
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>할 일 목록</h1>

      {/* 함수를 Props로 전달 */}
      <TodoInput onAdd={handleAddTodo} />

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

흐름 이해하기

  1. 부모가 handleAddTodo 함수를 만듦
  2. 그 함수를 onAdd prop으로 자식에게 전달
  3. 자식이 onAdd(input)을 호출
  4. 부모의 handleAddTodo가 실행됨
  5. 부모의 state가 업데이트됨!

이게 React의 데이터 흐름이에요! 📊


💪 실습 4: children Props

특별한 prop이 하나 더 있어요: children

children이 뭐예요?

컴포넌트 태그 사이에 있는 내용이에요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 카드 컴포넌트
function Card({ title, children }) {
  return (
    <div
      style={{
        border: '2px solid #0066cc',
        borderRadius: '10px',
        padding: '20px',
        margin: '10px'
      }}
    >
      <h2 style={{ borderBottom: '1px solid #ddd', paddingBottom: '10px' }}>
        {title}
      </h2>
      <div style={{ marginTop: '10px' }}>
        {children}
      </div>
    </div>
  );
}

// 사용 예제
function App() {
  return (
    <div style={{ padding: '20px' }}>
      <Card title="공지사항">
        <p>내일은 휴무일입니다.</p>
        <p>즐거운 주말 보내세요!</p>
      </Card>

      <Card title="사용자 정보">
        <p>이름: 홍길동</p>
        <p>이메일: hong@example.com</p>
        <button>수정</button>
      </Card>

      <Card title="이미지">
        <img
          src="https://via.placeholder.com/300x200"
          alt="샘플"
          style={{ width: '100%' }}
        />
      </Card>
    </div>
  );
}

export default App;

children의 장점:

  • 재사용 가능한 레이아웃 컴포넌트를 만들 수 있어요
  • 모달, 카드, 박스 같은 걸 쉽게 만들어요

⚠️ 자주 하는 실수

실수 1: Props를 바꾸려고 하기

1
2
3
4
5
6
function Greeting({ name }) {
  // ❌ Props는 읽기 전용!
  name = "다른이름";  // 안 돼요!

  return <h1>{name}</h1>;
}

해결책: 값을 바꾸고 싶다면 state를 써요!

1
2
3
4
5
6
7
8
9
10
11
12
function Greeting({ initialName }) {
  const [name, setName] = useState(initialName);

  return (
    <div>
      <h1>{name}</h1>
      <button onClick={() => setName("새이름")}>
        이름 바꾸기
      </button>
    </div>
  );
}

실수 2: Props 이름 오타

1
2
3
4
5
6
7
// 부모
<UserCard userName="홍길동" />

// 자식 - 오타!
function UserCard({ username }) {  // userName이 아니라 username
  return <h1>{username}</h1>;  // undefined가 나와요!
}

해결책: 정확히 같은 이름을 써요!

실수 3: 중괄호 빠뜨리기

1
2
3
4
5
// ❌ 숫자인데 따옴표로 쓰면
<UserCard age="25" />  // 문자열 "25"가 전달돼요

// ✅ 중괄호를 써요
<UserCard age={25} />  // 숫자 25가 전달돼요

🎯 종합 실습: 댓글 시스템 만들기

배운 내용을 모두 활용해서 댓글 시스템을 만들어봐요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import { useState } from 'react';

// 댓글 컴포넌트
function Comment({ author, text, time, onDelete }) {
  return (
    <div
      style={{
        border: '1px solid #ddd',
        borderRadius: '5px',
        padding: '10px',
        marginBottom: '10px'
      }}
    >
      <div style={{ display: 'flex', justifyContent: 'space-between' }}>
        <strong>{author}</strong>
        <button onClick={onDelete} style={{ color: 'red', cursor: 'pointer' }}>
          삭제
        </button>
      </div>
      <p style={{ margin: '10px 0' }}>{text}</p>
      <small style={{ color: '#999' }}>{time}</small>
    </div>
  );
}

// 댓글 입력 컴포넌트
function CommentInput({ onSubmit }) {
  const [text, setText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      onSubmit(text);
      setText('');
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="댓글을 입력하세요..."
        style={{
          width: '100%',
          padding: '10px',
          borderRadius: '5px',
          border: '1px solid #ddd',
          marginBottom: '10px'
        }}
        rows="3"
      />
      <button type="submit" style={{ padding: '5px 15px' }}>
        댓글 달기
      </button>
    </form>
  );
}

// 메인 앱
function App() {
  const [comments, setComments] = useState([
    {
      id: 1,
      author: '홍길동',
      text: '정말 유익한 글이네요!',
      time: '5분 전'
    },
    {
      id: 2,
      author: '김철수',
      text: '저도 도움이 되었습니다.',
      time: '10분 전'
    }
  ]);

  const handleAddComment = (text) => {
    const newComment = {
      id: Date.now(),
      author: '',
      text: text,
      time: '방금 전'
    };
    setComments([newComment, ...comments]);
  };

  const handleDeleteComment = (id) => {
    setComments(comments.filter(comment => comment.id !== id));
  };

  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
      <h1>댓글 ({comments.length})</h1>

      <CommentInput onSubmit={handleAddComment} />

      <div>
        {comments.map(comment => (
          <Comment
            key={comment.id}
            author={comment.author}
            text={comment.text}
            time={comment.time}
            onDelete={() => handleDeleteComment(comment.id)}
          />
        ))}
      </div>

      {comments.length === 0 && (
        <p style={{ color: '#999', textAlign: 'center' }}>
          아직 댓글이 없습니다. 첫 댓글을 달아보세요!
        </p>
      )}
    </div>
  );
}

export default App;

📝 정리

자, Day 4를 마무리할 시간이에요!

오늘 배운 내용 체크리스트

  • Props가 무엇인지 이해했어요
  • 부모에서 자식으로 데이터를 전달할 수 있어요
  • 자식에서 부모로 함수를 통해 데이터를 전달할 수 있어요
  • children props를 사용할 수 있어요
  • 재사용 가능한 컴포넌트를 만들 수 있어요
  • 댓글 시스템을 만들었어요

핵심 요약 3줄

  1. Props: 부모가 자식에게 전달하는 데이터 (읽기 전용!)
  2. 전달 방법: <Child name="값" age={25} />
  3. 역방향 통신: 함수를 Props로 전달해서 자식이 호출

💪 숙제 (선택사항)

  1. 좋아요 버튼 컴포넌트
    • props로 초기 좋아요 수를 받아요
    • 버튼을 누르면 좋아요 수가 증가해요
    • 한 번 더 누르면 취소돼요
  2. 별점 컴포넌트
    • props로 별점(1-5)을 받아요
    • 별점만큼 ⭐을 표시해요
    • 클릭하면 별점을 바꿀 수 있어요
  3. 모달 컴포넌트
    • children을 사용해서 내용을 받아요
    • 닫기 버튼을 클릭하면 모달이 사라져요

🚀 다음 단계

축하합니다! Props를 마스터했어요! 🎉

이제 컴포넌트끼리 대화할 수 있어요. 하지만 아직 한 가지 더 배울 게 있어요.

“컴포넌트가 화면에 나타난 후에 뭔가를 하고 싶다면?” 예를 들어 데이터를 불러온다든지, 타이머를 시작한다든지…

다음 포스트에서는 useEffect를 배워서 컴포넌트의 생명주기를 다루는 방법을 알아볼 거예요!

➡️ Day 5: useEffect - 컴포넌트 생명주기에서 만나요!


💬 Props가 헷갈리나요?

처음엔 모두 그래요! “부모 → 자식은 알겠는데, 자식 → 부모는 왜 이렇게 복잡해?”

핵심을 기억하세요: React는 항상 위에서 아래로 데이터가 흘러요!

자식이 부모에게 뭔가를 알려주려면? → 부모가 준 함수를 호출하는 거예요.

몇 번 써보면 자연스러워져요. 댓글 시스템 예제를 직접 타이핑하면서 흐름을 이해해보세요! 💪


🎓 시리즈 목록

Phase 1: React 기초 (Day 1-8)

  1. React, 왜 배워야 할까요?
  2. 첫 React 앱 만들어보기
  3. useState - 화면을 움직이게 만들기
  4. Props - 컴포넌트끼리 대화하기 (현재 포스트)
  5. 조건부 렌더링과 리스트 렌더링
  6. 컴포넌트 생명주기 이해하기
  7. Context API로 전역 상태 관리하기
  8. Custom Hooks 만들기

Phase 2: React 활용 (Day 9-14)

  1. React Router로 SPA 만들기
  2. 폼과 유효성 검사
  3. 성능 최적화 기법
  4. 에러 처리와 경계
  5. Ref와 DOM 접근
  6. Portals와 고급 패턴

Phase 3: React 심화 (Day 15-20)

  1. TypeScript와 React
  2. 상태 관리 라이브러리
  3. 서버 상태 관리 (TanStack Query)
  4. 테스팅 전략
  5. SSR과 Next.js 기초
  6. 배포와 DevOps

🔗 유용한 리소스

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.