
먼저 문제 원본은 아래와 같다.
이번 실습은React컴포넌트 간 통신 방법을 단계별로 구현하는 문제다.
문제에서 실제로 구현해야 하는 조건만 다시 정리하면 이렇다.
LabComp9컴포넌트를 중심으로 둘리 친구들의 간식 파티 앱을 만든다.- 친구 카드 목록을 화면에 출력해야 한다.
- 친구를 선택할 수 있어야 한다.
- 간식을 선택할 수 있어야 한다.
- 선택한 간식이 다른 컴포넌트에도 반영되어야 한다.
- 파티 모드를 켜고 끌 수 있어야 한다.
- 파티 모드 상태가 여러 컴포넌트에 동시에 반영되어야 한다.
FriendCard,SnackPicker,SnackPreview,PartyButton,PartyMessage컴포넌트를 사용해야 한다.이번 실습에서 만드는 컴포넌트 구성은 아래와 같다.
LabComp9 ├─ FriendCard ├─ SnackPicker ├─ SnackPreview ├─ PartyButton └─ PartyMessage
LabComp9는 전체 상태를 관리하는 부모 컴포넌트다.
FriendCard는 친구 정보를 출력하는 컴포넌트다.
SnackPicker는 간식을 선택하는 컴포넌트다.
SnackPreview는 선택된 간식을 미리보기로 보여주는 컴포넌트다.
PartyButton은 파티 모드를 변경하는 컴포넌트다.
PartyMessage는 현재 파티 모드 상태 메시지를 출력하는 컴포넌트다.
이 문제는 단순히 친구 카드 몇 개를 화면에 출력하는 문제로 끝나지 않는다.
React에서 컴포넌트끼리 데이터를 어떻게 주고받는지 단계별로 확인하는 실습이다.
처음에는 부모 컴포넌트인LabComp9가 가지고 있는 친구 데이터를 자식 컴포넌트인FriendCard로 내려보낸다.
이 단계가props사용이다.
그다음에는FriendCard안에서 선택하기 버튼을 클릭했을 때 선택한 친구 정보를 다시 부모 컴포넌트인LabComp9로 올려보낸다.
이 단계가 콜백 함수 사용이다.
이후에는SnackPicker에서 선택한 간식을SnackPreview에서도 보여줘야 한다.
두 컴포넌트는 형제 관계이기 때문에 서로 직접 데이터를 주고받지 않는다.
그래서 공통 부모인LabComp9에 간식 상태를 만들고, 그 상태를 두 컴포넌트에 각각 전달한다.
이 흐름이 상태 끌어올리기다.
마지막에는 파티 모드를 여러 컴포넌트가 동시에 사용해야 한다.
FriendCard,PartyButton,PartyMessage가 모두 같은 파티 모드 값을 봐야 하기 때문에Context API를 사용한다.
즉 이 실습의 핵심 흐름은 아래와 같다.Props로 부모 데이터를 자식에게 전달 → 콜백 함수로 자식 이벤트를 부모 상태에 반영 → 상태 끌어올리기로 형제 컴포넌트 연결 → Context API로 여러 컴포넌트가 공통 상태 공유
이번 실습은 처음부터 전체 코드를 한 번에 완성하는 방식이 아니라, 컴포넌트 통신 방법을 단계별로 추가하면서 완성했다.
그래서 풀이 순서도 실제 구현 순서에 맞춰 정리한다.
1.LabComp9에서 친구 데이터 배열을 만든다.
2.FriendCard컴포넌트를 만들고 친구 정보를 출력한다.
3.friends.map()으로 친구 수만큼FriendCard를 반복 출력한다.
4.friend데이터를props로FriendCard에 전달한다.
5. 선택된 친구를 저장할selectedFriend상태를 만든다.
6. 자식 컴포넌트에서 부모 상태를 변경할 수 있도록 콜백 함수handleSelectFriend를 만든다.
7.FriendCard의 선택하기 버튼에onClick이벤트를 연결한다.
8. 선택한 친구 이름을 화면에 출력한다.
9. 선택된 간식을 저장할selectedSnack상태를 만든다.
10. 간식 선택 이벤트를 처리할handleSelectSnack함수를 만든다.
11.SnackPicker에서 간식 목록을select태그로 출력한다.
12.SnackPicker에서 선택한 간식 값을 부모 상태로 올려보낸다.
13.SnackPreview에서 선택된 간식을 출력한다.
14. 파티 모드를 저장할isPartyMode상태를 만든다.
15.PartyContext를 만들고Provider로 파티 모드 상태를 공유한다.
16.FriendCard,PartyButton,PartyMessage에서useContext로 파티 모드 상태를 사용한다.
17.PartyButton에서 파티 모드를 켜고 끄도록 만든다.
18. 파티 모드 상태에 따라 카드 문구, 카드 배경색, 버튼 문구, 메시지가 함께 바뀌는지 확인한다.
자세한 풀이는 각 단계에서 작성한 전체 코드를 먼저 확인한 뒤, 그 단계의 핵심 코드를 부분별로 나누어 설명한다.
여기서 코드 설명은 문법과 코드 역할을 이해하는 구간이고, 데이터 흐름은 실제 값이 어느 컴포넌트에서 어느 컴포넌트로 이동하는지 확인하는 구간이다.
1단계 Props 사용하기
1단계에서는 부모 컴포넌트가 가진 친구 데이터를 자식 컴포넌트로 전달했다.
여기서 부모는LabComp9이고, 자식은FriendCard다.
먼저1단계까지 작성한 전체 코드는 아래와 같다.export function LabComp9() { const cardContainerStyle = { display: 'flex', justifyContent: 'center', }; const friends = [ { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }, { id: 2, name: '또치', role: '타조', emoji: '🐥' }, { id: 3, name: '도우너', role: '외계인', emoji: '👽' }, ]; const snackEmoji = { 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟', }; return ( <> <h1 style={{ textAlign: 'center' }}>1단계 Props</h1> <div className="card-container" style={cardContainerStyle}> {friends.map(friend => ( <FriendCard key={friend.id} friend={friend} /> ))} </div> <h1 style={{ textAlign: 'center' }}>2단계 콜백함수</h1> <h1 style={{ textAlign: 'center' }}>3단계 상태 끌어올리기</h1> <h1 style={{ textAlign: 'center' }}>4단계 Context API</h1> </> ); } function FriendCard({ friend }) { const cardStyle = { display: 'flex', width: '150px', flexDirection: 'column', alignItems: 'center', border: '1px solid #ccc', borderRadius: '8px', padding: '16px', margin: '16px', textAlign: 'center', }; return ( <div className="friend-card" style={cardStyle}> <div> <span>{friend.emoji}</span> <h2>{friend.name}</h2> <p>{friend.role}</p> <button>선택하기</button> </div> </div> ); } function SnackPicker() { return ( <div className="snack-picker"> <h2>SnackPicker</h2> </div> ); } function SnackPreview() { return ( <div className="snack-preview"> <h2>SnackPreview</h2> </div> ); } function PartyButton() { return ( <div className="party-button"> <h2>PartyButton</h2> </div> ); } function PartyMessage() { return ( <div className="party-message"> <h2>PartyMessage</h2> </div> ); }코드 설명
1단계의 핵심은 부모 컴포넌트인LabComp9가 가진 친구 데이터를 자식 컴포넌트인FriendCard에게 전달하는 것이다.
이때 사용하는 문법이props다.
props는 부모가 자식에게 값을 전달할 때 사용하는 통로라고 보면 된다.
먼저 친구 데이터는 배열 안에 객체를 넣은 형태로 만들었다.const friends = [ { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }, { id: 2, name: '또치', role: '타조', emoji: '🐥' }, { id: 3, name: '도우너', role: '외계인', emoji: '👽' }, ];
이 배열을 인덱스로 직접 보면 아래와 같다.
friends[0]; // { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' } friends[1]; // { id: 2, name: '또치', role: '타조', emoji: '🐥' } friends[2]; // { id: 3, name: '도우너', role: '외계인', emoji: '👽' }
즉
friends는 친구 여러 명을 담고 있는 배열이고, 배열 안의 객체 하나가 친구 한 명의 정보다.
이제 이 배열을 화면에 출력하기 위해map()을 사용한다.{friends.map(friend => ( <FriendCard key={friend.id} friend={friend} /> ))}
map()은 배열의 값을 하나씩 꺼내서 새로운 결과를 만들 때 사용한다.
여기서는friends배열에서 친구 객체를 하나씩 꺼낸 뒤, 그 객체 하나를FriendCard컴포넌트 하나로 바꾼다.
여기서friend는 특별히 정해진 이름이 아니다.
내가 정한 임시 변수 이름이다.
아래처럼 이름을item으로 바꿔도 같은 코드가 된다.{friends.map(item => ( <FriendCard key={item.id} friend={item} /> ))}위 코드에서 중요한 점은
map()안의 이름은 아무 이름이나 가능하지만, 안쪽에서 같은 이름으로 사용해야 한다는 것이다.
예를 들어item으로 꺼냈으면item.id,friend={item}처럼 써야 한다.
다시 원래 코드 기준으로map()이 실제로 반복되는 모습을 펼치면 아래와 같다.// 1번째 반복 friend = { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }; <FriendCard key={1} friend={friend} /> // 실제 전달 형태 <FriendCard key={1} friend={{ id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }} /> // 2번째 반복 friend = { id: 2, name: '또치', role: '타조', emoji: '🐥' }; <FriendCard key={2} friend={friend} /> // 실제 전달 형태 <FriendCard key={2} friend={{ id: 2, name: '또치', role: '타조', emoji: '🐥' }} /> // 3번째 반복 friend = { id: 3, name: '도우너', role: '외계인', emoji: '👽' }; <FriendCard key={3} friend={friend} /> // 실제 전달 형태 <FriendCard key={3} friend={{ id: 3, name: '도우너', role: '외계인', emoji: '👽' }} />
이렇게 보면
map(friend => ...)의friend는 고정된 하나의 값이 아니라, 반복할 때마다 현재 꺼낸 친구 객체가 들어오는 임시 이름이라는 것을 알 수 있다.
다음으로friend={friend}를 보면 왼쪽과 오른쪽의 의미가 다르다.<FriendCard friend={friend} />
이 코드를 실제 의미로 풀어 쓰면 아래와 같다.
왼쪽 friend → 자식 컴포넌트에게 전달할 props 이름 오른쪽 friend → map()에서 꺼낸 현재 친구 객체
둘리 객체가 들어온 상황으로 보면 아래와 같다.
<FriendCard friend={{ id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }} />
즉
FriendCard에게friend라는 이름으로 둘리 객체를 전달한 것이다.
자식 컴포넌트는 전달받은 값을 매개변수 자리에서 받는다.function FriendCard({ friend }) {
원래
props는 객체 형태로 들어온다.
둘리 카드 기준으로 보면 실제props는 아래와 같다.props = { friend: { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' } };
그런데 코드에서는 구조 분해 할당을 사용했다.
function FriendCard({ friend }) {
이 코드는 아래 코드와 같은 의미다.
function FriendCard(props) { const friend = props.friend; }
즉
props.friend를 매번 길게 쓰지 않고,friend라는 변수로 바로 꺼내 쓰기 위해 구조 분해 할당을 사용한 것이다.
마지막으로 자식 컴포넌트 안에서는friend객체의 값을 화면에 출력한다.<span>{friend.emoji}</span> <h2>{friend.name}</h2> <p>{friend.role}</p>
둘리 객체가 들어온 상태로 보면 실제 출력 값은 아래와 같다.
<span>{'🦕'}</span> <h2>{'둘리'}</h2> <p>{'아기공룡'}</p>
JSX안에서{}를 사용하면 자바스크립트 값을 화면에 넣을 수 있다.
그래서{friend.name}은 친구 객체 안의name값을 꺼내 화면에 출력한다.
정리하면1단계의 코드 설명 핵심은 아래와 같다.
friends배열은 친구 데이터를 저장한다.map()은 친구 데이터 개수만큼FriendCard를 반복 생성한다.map(friend => ...)의friend는 내가 정한 임시 변수 이름이다.friend라는 이름은item,friendInfo처럼 바꿔도 되지만, 안쪽에서도 같은 이름을 사용해야 한다.friend={friend}에서 왼쪽friend는props이름이고, 오른쪽friend는 현재 친구 객체다.function FriendCard({ friend })는props.friend를 바로 꺼내 쓰는 구조 분해 할당이다.{friend.name}같은 문법은 객체 안의 값을 화면에 출력한다.
데이터 흐름
1단계의 데이터 흐름은 부모에서 자식으로만 이동한다.
아직 자식 컴포넌트에서 부모 상태를 바꾸는 기능은 없다.
먼저 부모 컴포넌트인LabComp9에 친구 배열이 있다.const friends = [ { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }, { id: 2, name: '또치', role: '타조', emoji: '🐥' }, { id: 3, name: '도우너', role: '외계인', emoji: '👽' }, ];
이 배열이
map()으로 들어간다.friends.map(friend => ( <FriendCard key={friend.id} friend={friend} /> ));
첫 번째 반복에서는
friends[0]값이friend라는 이름으로 들어간다.// 1. 부모 배열의 첫 번째 값 확인 friends[0]; // 결과: { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' } // 2. map() 안에서 현재 값에 friend라는 임시 이름이 붙음 const friend = friends[0]; // friend = { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }
이
friend값이FriendCard로 전달된다.<FriendCard key={friend.id} friend={friend} />
실제 값으로 바꾸면 아래와 같다.
<FriendCard key={1} friend={{ id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }} />
FriendCard는 이 값을props로 받는다.props = { friend: { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' } };
그리고 구조 분해 할당으로
props.friend를 바로 꺼낸다.function FriendCard({ friend }) {
이 코드는 실제로 아래와 같다.
const friend = props.friend; friend = { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' };
그 다음 화면 출력에 사용되는 값은 이렇게 연결된다.
friend.emoji; // '🦕' friend.name; // '둘리' friend.role; // '아기공룡'
그래서 첫 번째 카드는 아래처럼 출력된다.
🦕 둘리 아기공룡 선택하기
두 번째 반복에서는
friends[1]값이friend라는 이름으로 들어간다.// 1. 부모 배열의 두 번째 값 확인 friends[1]; // 결과: { id: 2, name: '또치', role: '타조', emoji: '🐥' } // 2. map() 안에서 현재 값에 friend라는 임시 이름이 붙음 const friend = friends[1]; // friend = { id: 2, name: '또치', role: '타조', emoji: '🐥' }
실제로 만들어지는 컴포넌트는 아래와 같다.
<FriendCard key={2} friend={{ id: 2, name: '또치', role: '타조', emoji: '🐥' }} />
FriendCard내부 출력 값은 아래와 같다.friend.emoji; // '🐥' friend.name; // '또치' friend.role; // '타조'
세 번째 반복에서는
friends[2]값이friend라는 이름으로 들어간다.// 1. 부모 배열의 세 번째 값 확인 friends[2]; // 결과: { id: 3, name: '도우너', role: '외계인', emoji: '👽' } // 2. map() 안에서 현재 값에 friend라는 임시 이름이 붙음 const friend = friends[2]; // friend = { id: 3, name: '도우너', role: '외계인', emoji: '👽' }
실제로 만들어지는 컴포넌트는 아래와 같다.
<FriendCard key={3} friend={{ id: 3, name: '도우너', role: '외계인', emoji: '👽' }} />
FriendCard내부 출력 값은 아래와 같다.friend.emoji; // '👽' friend.name; // '도우너' friend.role; // '외계인'
즉
1단계의 값 이동은 아래처럼 정리할 수 있다.LabComp9의 friends 배열 → friends[0], friends[1], friends[2]를 map()이 하나씩 꺼냄 → 꺼낸 객체가 friend라는 임시 이름에 들어감 → <FriendCard friend={friend} />로 전달 → FriendCard({ friend })에서 props.friend를 꺼냄 → friend.emoji, friend.name, friend.role 출력
1단계의 핵심은 부모 컴포넌트가 가진 데이터를 props로 자식 컴포넌트에 전달하는 것이다.
2단계 콜백 함수 사용하기
2단계에서는 자식 컴포넌트에서 발생한 클릭 이벤트를 부모 컴포넌트로 전달했다.
즉FriendCard에서 선택하기 버튼을 클릭하면, 선택한 친구 정보가LabComp9의 상태로 저장된다.
먼저2단계까지 작성한 전체 코드는 아래와 같다.import { useState } from 'react'; export function LabComp9() { const cardContainerStyle = { display: 'flex', justifyContent: 'center', }; const friends = [ { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }, { id: 2, name: '또치', role: '타조', emoji: '🐥' }, { id: 3, name: '도우너', role: '외계인', emoji: '👽' }, ]; const snackEmoji = { 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟', }; const [selectedFriend, setSelectedFriend] = useState(null); const handleSelectFriend = friend => { setSelectedFriend(friend); }; return ( <> <h1 style={{ textAlign: 'center' }}>1단계 Props</h1> <div className="card-container" style={cardContainerStyle}> {friends.map(friend => ( <FriendCard key={friend.id} friend={friend} onSelectFriend={handleSelectFriend} /> ))} </div> <h1 style={{ textAlign: 'center' }}>2단계 콜백함수</h1> <div style={{ textAlign: 'center' }}>선택된 친구: {selectedFriend?.name || '아직 없음'}</div> <h1 style={{ textAlign: 'center' }}>3단계 상태 끌어올리기</h1> <h1 style={{ textAlign: 'center' }}>4단계 Context API</h1> </> ); } function FriendCard({ friend, onSelectFriend }) { const cardStyle = { display: 'flex', width: '150px', flexDirection: 'column', alignItems: 'center', border: '1px solid #ccc', borderRadius: '8px', padding: '16px', margin: '16px', textAlign: 'center', }; return ( <div className="friend-card" style={cardStyle}> <div> <span>{friend.emoji}</span> <h2>{friend.name}</h2> <p>{friend.role}</p> <button onClick={() => onSelectFriend(friend)}>선택하기</button> </div> </div> ); } function SnackPicker() { return ( <div className="snack-picker"> <h2>SnackPicker</h2> </div> ); } function SnackPreview() { return ( <div className="snack-preview"> <h2>SnackPreview</h2> </div> ); } function PartyButton() { return ( <div className="party-button"> <h2>PartyButton</h2> </div> ); } function PartyMessage() { return ( <div className="party-message"> <h2>PartyMessage</h2> </div> ); }코드 설명
2단계의 핵심은 자식 컴포넌트에서 버튼을 클릭했을 때, 부모 컴포넌트의state를 변경하는 것이다.
자식 컴포넌트가 부모의state를 직접 바꾸는 것이 아니라, 부모가 만든 함수를 자식에게 넘겨주고 자식이 그 함수를 실행하는 구조다.
먼저useState를 사용하기 위해react에서 가져온다.import { useState } from 'react';
useState는 컴포넌트 안에서 값을 기억하게 만드는 기능이다.
그리고 그 값이 바뀌면 화면도 다시 렌더링된다.
선택된 친구를 저장할 상태를 만든다.const [selectedFriend, setSelectedFriend] = useState(null);
이 코드를 실제 역할로 나누면 아래와 같다.
selectedFriend → 현재 선택된 친구 정보를 저장하는 state setSelectedFriend → selectedFriend 값을 바꾸는 함수 useState(null) → 처음 selectedFriend 값은 null
처음 상태를 값으로 보면 아래와 같다.
selectedFriend = null;
즉 처음에는 아무 친구도 선택되지 않은 상태다.
그다음 부모 컴포넌트 안에서 선택된 친구를 상태에 저장하는 함수를 만든다.const handleSelectFriend = friend => { setSelectedFriend(friend); };
이 함수는
friend라는 값을 받아서setSelectedFriend(friend)로 상태에 저장한다.
예를 들어 둘리 객체가 들어오면 아래처럼 실행된다.handleSelectFriend({ id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' });
그리고 함수 내부에서는 아래 코드가 실행된다.
setSelectedFriend({ id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' });
즉
handleSelectFriend는 선택한 친구 객체를selectedFriend상태에 넣는 함수다.
이제 이 함수를FriendCard에게 넘긴다.<FriendCard key={friend.id} friend={friend} onSelectFriend={handleSelectFriend} />
여기서 중요한 부분은 아래 코드다.
onSelectFriend={handleSelectFriend}
이 코드는 부모의
handleSelectFriend함수를 자식에게onSelectFriend라는 이름으로 넘긴다는 뜻이다.
즉 자식 컴포넌트 안에서 사용하는onSelectFriend는 새로 만든 함수가 아니라, 부모가 내려준handleSelectFriend함수다.
둘리 카드 기준으로 펼쳐 보면 아래와 같다.<FriendCard key={1} friend={{ id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }} onSelectFriend={handleSelectFriend} />
이때
FriendCard로 들어가는props는 아래처럼 이해하면 된다.props = { friend: { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }, onSelectFriend: handleSelectFriend };
여기서 중요한 점은
FriendCard안에서 실제로 사용할 수 있는 함수 이름은onSelectFriend라는 것이다.
handleSelectFriend라는 이름은 부모 컴포넌트 쪽 이름이고, 자식 컴포넌트에서는 그 함수가onSelectFriend라는 이름으로 들어온다.// 이해용 코드 // 실제로 FriendCard 안에서 handleSelectFriend라는 이름을 직접 쓰는 것은 아니다. // 부모가 넘긴 handleSelectFriend 함수가 자식에서는 onSelectFriend라는 이름으로 들어온다. onSelectFriend = handleSelectFriend;
자식 컴포넌트는 이 값을 구조 분해 할당으로 받는다.
function FriendCard({ friend, onSelectFriend }) {
둘리 카드 기준으로 보면 자식 안에서 사용할 수 있는 값은 아래와 같다.
friend = { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }; // 이해용 코드 // 부모가 넘긴 handleSelectFriend 함수가 자식에서는 onSelectFriend라는 이름으로 들어온다. onSelectFriend = handleSelectFriend;
버튼에는
onClick이벤트를 연결한다.<button onClick={() => onSelectFriend(friend)}>선택하기</button>
여기서
onClick은 버튼을 클릭했을 때 실행할 코드를 지정하는 이벤트 속성이다.
버튼을 클릭하면onSelectFriend(friend)가 실행된다.
그런데onSelectFriend안에는 부모가 내려준handleSelectFriend함수가 들어 있기 때문에 아래 흐름으로 이어진다.// 자식에서 실행한 코드 onSelectFriend(friend); // 실제 의미 handleSelectFriend(friend); // 부모 함수 내부 실행 setSelectedFriend(friend);
즉 버튼 클릭 흐름을 실제 값 기준으로 펼치면 아래와 같다.
// 1. 현재 FriendCard가 가진 friend 값 friend = { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }; // 2. 버튼 클릭 <button onClick={() => onSelectFriend(friend)}>선택하기</button> // 3. 클릭하면 자식에서 실행되는 코드 onSelectFriend(friend); // 4. onSelectFriend 안에는 부모의 handleSelectFriend 함수가 들어 있음 handleSelectFriend(friend); // 5. 부모 함수 내부 실행 setSelectedFriend(friend); // 6. friend 값이 둘리 객체이므로 최종 실행은 아래와 같음 setSelectedFriend({ id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' });
이렇게 보면
onSelectFriend(friend)가 단순히 자식 안에서 끝나는 함수가 아니라, 부모의 상태 변경 함수까지 연결되는 흐름이라는 것을 알 수 있다.
선택된 친구 이름은 화면에 이렇게 출력한다.<div style={{ textAlign: 'center' }}>선택된 친구: {selectedFriend?.name || '아직 없음'}</div>
처음에는
selectedFriend가null이므로 아래처럼 계산된다.selectedFriend = null; selectedFriend?.name; // undefined undefined || '아직 없음'; // '아직 없음'
그래서 처음 화면에는 아래처럼 출력된다.
선택된 친구: 아직 없음
둘리를 선택한 뒤에는
selectedFriend에 둘리 객체가 들어가므로 아래처럼 계산된다.selectedFriend = { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }; selectedFriend?.name; // '둘리' '둘리' || '아직 없음'; // '둘리'
그래서 화면에는 아래처럼 출력된다.
선택된 친구: 둘리
정리하면
2단계의 핵심은 아래와 같다.
- 부모는
handleSelectFriend함수를 만든다.- 부모는 그 함수를
onSelectFriend라는 이름으로 자식에게 넘긴다.- 자식은 버튼 클릭 시
onSelectFriend(friend)를 실행한다.onSelectFriend안에는 부모가 넘겨준handleSelectFriend함수가 들어 있다.- 최종적으로
setSelectedFriend(friend)가 실행되어 부모의selectedFriend상태가 바뀐다.
데이터 흐름
2단계의 데이터 흐름은 자식에서 부모로 올라가는 흐름이다.
정확히는 자식이 부모의 상태를 직접 바꾸는 것이 아니라, 부모에게 받은 함수를 실행해서 부모 상태를 바꾸는 구조다.
초기 상태는 아래와 같다.selectedFriend = null;
부모는
FriendCard를 만들 때 친구 객체와 선택 처리 함수를 함께 내려준다.<FriendCard key={1} friend={{ id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }} onSelectFriend={handleSelectFriend} />
이때 자식이 받은 값은 아래와 같다.
friend = { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }; // 이해용 코드 // 부모가 넘긴 handleSelectFriend 함수가 자식에서는 onSelectFriend라는 이름으로 들어온다. onSelectFriend = handleSelectFriend;
사용자가 둘리 카드의 선택하기 버튼을 클릭하면 아래 흐름이 실행된다.
// 1. 버튼 클릭 전 현재 값 selectedFriend = null; friend = { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }; // 이해용 코드 // 부모가 넘긴 handleSelectFriend 함수가 자식에서는 onSelectFriend라는 이름으로 들어온다. onSelectFriend = handleSelectFriend; // 2. 버튼 클릭 onSelectFriend(friend); // 3. onSelectFriend 안에는 부모의 handleSelectFriend 함수가 들어 있음 handleSelectFriend(friend); // 4. 부모 함수 내부 const handleSelectFriend = friend => { setSelectedFriend(friend); }; // 5. 실제 friend 값이 들어간 최종 실행 setSelectedFriend({ id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' });
상태는 아래처럼 바뀐다.
변경 전 selectedFriend null
변경 후 selectedFriend { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }
상태가 바뀌면 화면도 다시 계산된다.
selectedFriend?.name || '아직 없음'
변경 전에는 아래처럼 계산된다.
null?.name || '아직 없음'; // 결과: '아직 없음'
변경 후에는 아래처럼 계산된다.
({ id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' })?.name || '아직 없음'; // 결과: '둘리'
그래서 화면 출력은 아래처럼 바뀐다.
선택된 친구: 아직 없음 → 선택된 친구: 둘리
즉
2단계의 값 이동은 아래처럼 정리할 수 있다.FriendCard 버튼 클릭 → onSelectFriend(friend) 실행 → onSelectFriend 안에는 handleSelectFriend 함수가 들어 있음 → handleSelectFriend(friend) 실행 → setSelectedFriend(friend) 실행 → selectedFriend가 null에서 친구 객체로 변경 → selectedFriend?.name으로 친구 이름 출력
2단계의 핵심은 부모가 내려준 콜백 함수를 자식이 실행해서 부모 상태를 변경하는 것이다.
3단계 상태 끌어올리기
3단계에서는 간식 선택 기능을 만들었다.
여기서 중요한 점은SnackPicker와SnackPreview가 형제 컴포넌트라는 것이다.
SnackPicker에서 간식을 선택하고,SnackPreview에서 선택한 간식을 보여줘야 한다.
하지만 형제 컴포넌트끼리는 직접 데이터를 주고받지 않는다.
그래서 공통 부모인LabComp9에selectedSnack상태를 만들었다.
먼저3단계까지 작성한 전체 코드는 아래와 같다.import { useState } from 'react'; export function LabComp9() { const cardContainerStyle = { display: 'flex', justifyContent: 'center', }; const friends = [ { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }, { id: 2, name: '또치', role: '타조', emoji: '🐥' }, { id: 3, name: '도우너', role: '외계인', emoji: '👽' }, ]; const snackEmoji = { 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟', }; const [selectedFriend, setSelectedFriend] = useState(null); const [selectedSnack, setSelectedSnack] = useState(null); const handleSelectFriend = friend => { setSelectedFriend(friend); }; const handleSelectSnack = snack => { setSelectedSnack(snack); }; return ( <> <div style={{ textAlign: 'center' }}> <h1>1단계 Props</h1> <div className="card-container" style={cardContainerStyle}> {friends.map(friend => ( <FriendCard key={friend.id} friend={friend} onSelectFriend={handleSelectFriend} /> ))} </div> </div> <div style={{ textAlign: 'center' }}> <h1>2단계 콜백함수</h1> <div>선택된 친구: {selectedFriend?.name || '아직 없음'}</div> </div> <div style={{ textAlign: 'center' }}> <h1>3단계 상태 끌어올리기</h1> <h3>간식 선택</h3> <SnackPicker snackEmoji={snackEmoji} onSelectSnack={handleSelectSnack} /> <h3>간식 미리보기</h3> <SnackPreview selectedSnack={selectedSnack} snackEmoji={snackEmoji} /> </div> <div style={{ textAlign: 'center' }}> <h1>4단계 Context API</h1> </div> </> ); } function FriendCard({ friend, onSelectFriend }) { const cardStyle = { display: 'flex', width: '150px', flexDirection: 'column', alignItems: 'center', border: '1px solid #ccc', borderRadius: '8px', padding: '16px', margin: '16px', textAlign: 'center', }; return ( <div className="friend-card" style={cardStyle}> <div> <span>{friend.emoji}</span> <h2>{friend.name}</h2> <p>{friend.role}</p> <button onClick={() => onSelectFriend(friend)}>선택하기</button> </div> </div> ); } function SnackPicker({ snackEmoji, onSelectSnack }) { return ( <div className="snack-picker"> <select onChange={e => onSelectSnack(e.target.value)}> <option value="">간식을 선택</option> {Object.keys(snackEmoji).map(snack => ( <option key={snack} value={snack}> {snack} {snackEmoji[snack]} </option> ))} </select> </div> ); } function SnackPreview({ selectedSnack, snackEmoji }) { return ( <div className="snack-preview"> {selectedSnack && ( <p> {selectedSnack} {snackEmoji[selectedSnack]} </p> )} </div> ); } function PartyButton() { return ( <div className="party-button"> <h2>PartyButton</h2> </div> ); } function PartyMessage() { return ( <div className="party-message"> <h2>PartyMessage</h2> </div> ); }코드 설명
3단계의 핵심은SnackPicker에서 선택한 값을 부모 상태에 저장하고, 그 값을 다시SnackPreview로 내려보내는 것이다.
이 구조가 상태 끌어올리기다.
먼저 간식 이름과 이모지를 객체로 준비한다.const snackEmoji = { 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟', };
이 객체는 간식 이름으로 이모지를 찾기 위한 데이터다.
실제 값 접근은 아래처럼 된다.snackEmoji['초코과자']; // '🍪' snackEmoji['딸기우유']; // '🥛' snackEmoji['라면']; // '🍜' snackEmoji['붕어빵']; // '🐟'
선택된 간식을 저장할 상태를 만든다.
const [selectedSnack, setSelectedSnack] = useState(null);
이 코드를 역할별로 보면 아래와 같다.
selectedSnack → 현재 선택된 간식 이름을 저장하는 state setSelectedSnack → selectedSnack 값을 변경하는 함수 useState(null) → 처음 selectedSnack 값은 null
처음 상태는 아래와 같다.
selectedSnack = null;
부모 컴포넌트 안에서 간식 선택 결과를 상태에 저장하는 함수를 만든다.
const handleSelectSnack = snack => { setSelectedSnack(snack); };
이 함수에
라면이 들어오면 실제 실행은 아래처럼 된다.handleSelectSnack('라면');
그리고 함수 내부에서는 아래 코드가 실행된다.
setSelectedSnack('라면');
즉
handleSelectSnack은 선택한 간식 이름을selectedSnack상태에 넣는 함수다.
이제 이 함수를SnackPicker에게 넘긴다.<SnackPicker snackEmoji={snackEmoji} onSelectSnack={handleSelectSnack} />
여기서 중요한 부분은 아래 코드다.
onSelectSnack={handleSelectSnack}
이 코드는 부모의
handleSelectSnack함수를 자식에게onSelectSnack이라는 이름으로 넘긴다는 뜻이다.
즉SnackPicker안에서 사용하는onSelectSnack은 실제로 부모의handleSelectSnack함수다.
실제로 들어가는 값을 펼쳐 보면 아래와 같다.<SnackPicker snackEmoji={{ 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟' }} onSelectSnack={handleSelectSnack} />
SnackPicker가 받은props는 아래처럼 볼 수 있다.props = { snackEmoji: { 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟' }, onSelectSnack: handleSelectSnack };
여기서도 실제
SnackPicker안에서 사용할 수 있는 함수 이름은onSelectSnack이다.
handleSelectSnack은 부모 쪽 이름이고, 자식에서는 그 함수가onSelectSnack이라는 이름으로 들어온다.// 이해용 코드 // 실제로 SnackPicker 안에서 handleSelectSnack이라는 이름을 직접 쓰는 것은 아니다. // 부모가 넘긴 handleSelectSnack 함수가 자식에서는 onSelectSnack이라는 이름으로 들어온다. onSelectSnack = handleSelectSnack;
SnackPicker에서는select태그를 사용한다.<select onChange={e => onSelectSnack(e.target.value)}>
onChange는 선택값이 바뀔 때 실행된다.
e.target.value에는 사용자가 선택한option의value값이 들어온다.
간식 목록은Object.keys()와map()으로 만든다.{Object.keys(snackEmoji).map(snack => ( <option key={snack} value={snack}> {snack} {snackEmoji[snack]} </option> ))}
먼저
Object.keys(snackEmoji)의 결과는 아래와 같다.Object.keys(snackEmoji); // ['초코과자', '딸기우유', '라면', '붕어빵']
그다음
map()이 각 간식 이름을option으로 만든다.
여기서snack도friend처럼 내가 정한 임시 이름이다.
snack대신food라고 써도 되지만, 안쪽에서도 같은 이름을 사용해야 한다.// snack이라는 이름을 사용한 코드 Object.keys(snackEmoji).map(snack => ( <option key={snack} value={snack}> {snack} {snackEmoji[snack]} </option> )); // food라는 이름으로 바꿔도 같은 흐름 Object.keys(snackEmoji).map(food => ( <option key={food} value={food}> {food} {snackEmoji[food]} </option> ));
실제로 반복되는 모습은 아래와 같다.
// 1번째 반복 snack = '초코과자'; <option key="초코과자" value="초코과자"> 초코과자 🍪 </option> // 2번째 반복 snack = '딸기우유'; <option key="딸기우유" value="딸기우유"> 딸기우유 🥛 </option> // 3번째 반복 snack = '라면'; <option key="라면" value="라면"> 라면 🍜 </option> // 4번째 반복 snack = '붕어빵'; <option key="붕어빵" value="붕어빵"> 붕어빵 🐟 </option>
사용자가
라면 🍜을 선택하면 화면에는라면 🍜이 보이지만, 실제value값은라면이다.e.target.value = '라면';
그러면 아래 코드가 실행된다.
onSelectSnack(e.target.value);
실제 값 기준으로 펼치면 아래와 같다.
onSelectSnack('라면');
onSelectSnack안에는 부모가 내려준handleSelectSnack함수가 들어 있기 때문에 아래 흐름으로 이어진다.// 자식에서 실행한 코드 onSelectSnack('라면'); // 실제 의미 handleSelectSnack('라면'); // 부모 함수 내부 실행 setSelectedSnack('라면');
이제 부모는 변경된
selectedSnack값을SnackPreview로 내려준다.<SnackPreview selectedSnack={selectedSnack} snackEmoji={snackEmoji} />
처음에는 아래처럼 전달된다.
<SnackPreview selectedSnack={null} snackEmoji={{ 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟' }} />
라면을 선택한 뒤에는 아래처럼 전달된다.
<SnackPreview selectedSnack="라면" snackEmoji={{ 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟' }} />
즉
SnackPreview는 직접 선택 이벤트를 처리하지 않고, 부모에게서 변경된 값을 받아서 출력한다.
SnackPreview에서는 선택된 간식이 있을 때만 출력한다.{selectedSnack && ( <p> {selectedSnack} {snackEmoji[selectedSnack]} </p> )}
처음에는
selectedSnack이null이므로 아래처럼 된다.selectedSnack = null; selectedSnack && (...); // 결과: 아무것도 출력하지 않음
라면을 선택한 뒤에는 아래처럼 계산된다.
selectedSnack = '라면'; snackEmoji[selectedSnack]; // snackEmoji['라면'] snackEmoji['라면']; // '🍜'
그래서 최종 출력은 아래처럼 된다.
<p>라면 🍜</p>
정리하면
3단계의 핵심은 아래와 같다.
- 부모는
handleSelectSnack함수를 만든다.- 부모는 그 함수를
onSelectSnack이라는 이름으로SnackPicker에 넘긴다.SnackPicker는 간식을 선택하면onSelectSnack(e.target.value)를 실행한다.onSelectSnack안에는 부모가 넘겨준handleSelectSnack함수가 들어 있다.- 최종적으로
setSelectedSnack('라면')이 실행되어 부모의selectedSnack상태가 바뀐다.- 바뀐
selectedSnack값은 다시SnackPreview로 내려간다.
데이터 흐름
3단계의 데이터 흐름은SnackPicker → LabComp9 → SnackPreview로 이어진다.
형제 컴포넌트끼리 직접 값을 주고받지 않고, 공통 부모인LabComp9를 거쳐서 값이 이동한다.
초기 상태는 아래와 같다.selectedSnack = null;
부모는
SnackPicker에게 간식 데이터와 상태 변경 함수를 내려준다.<SnackPicker snackEmoji={{ 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟' }} onSelectSnack={handleSelectSnack} />
SnackPicker가 받은 값은 아래와 같다.snackEmoji = { 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟' }; // 이해용 코드 // 부모가 넘긴 handleSelectSnack 함수가 자식에서는 onSelectSnack이라는 이름으로 들어온다. onSelectSnack = handleSelectSnack;
Object.keys(snackEmoji)는 객체에서 키만 뽑아 배열을 만든다.Object.keys(snackEmoji); // ['초코과자', '딸기우유', '라면', '붕어빵']
map()은 이 배열에서 간식 이름을 하나씩 꺼낸다.// 1번째 반복 snack = '초코과자'; // 2번째 반복 snack = '딸기우유'; // 3번째 반복 snack = '라면'; // 4번째 반복 snack = '붕어빵';
사용자가
라면을 선택하면 아래 흐름이 실행된다.// 1. 라면 option 선택 <option key="라면" value="라면"> 라면 🍜 </option> // 2. select의 변경 이벤트 발생 const event = { target: { value: '라면' } }; // 3. 그래서 e.target.value는 라면 const e = event; e.target.value; // '라면' // 4. 자식에서 실행되는 코드 onSelectSnack(e.target.value); // 5. 실제 값이 들어간 코드 onSelectSnack('라면'); // 6. onSelectSnack 안에는 부모의 handleSelectSnack 함수가 들어 있음 handleSelectSnack('라면'); // 7. 부모 함수 내부 실행 const handleSelectSnack = snack => { setSelectedSnack(snack); }; // 8. 실제 snack 값이 라면이므로 최종 실행 setSelectedSnack('라면');
상태는 아래처럼 바뀐다.
변경 전 selectedSnack null
변경 후 selectedSnack 라면
상태가 바뀌면
LabComp9가 다시 렌더링되고, 변경된 값이SnackPreview로 내려간다.<SnackPreview selectedSnack="라면" snackEmoji={{ 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟' }} />
SnackPreview안에서는 아래처럼 값이 계산된다.selectedSnack = '라면'; snackEmoji[selectedSnack] = snackEmoji['라면']; snackEmoji['라면'] = '🍜';
그래서 최종 출력은 아래처럼 된다.
<p>라면 🍜</p>
즉
3단계의 값 이동은 아래처럼 정리할 수 있다.SnackPicker에서 라면 선택 → e.target.value = '라면' → onSelectSnack('라면') 실행 → onSelectSnack 안에는 handleSelectSnack 함수가 들어 있음 → handleSelectSnack('라면') 실행 → setSelectedSnack('라면') 실행 → selectedSnack이 null에서 '라면'으로 변경 → LabComp9가 다시 렌더링됨 → SnackPreview에 selectedSnack="라면" 전달 → snackEmoji['라면']으로 🍜 찾음 → 라면 🍜 출력
3단계의 핵심은 형제 컴포넌트끼리 직접 연결하지 않고, 공통 부모의 state를 통해 값을 공유하는 것이다.
4단계 Context API 사용하기
4단계에서는 파티 모드 상태를 여러 컴포넌트가 함께 사용하도록 만들었다.
파티 모드는FriendCard,PartyButton,PartyMessage에서 모두 필요하다.
이 값을 계속props로 하나씩 내려주면 코드가 복잡해질 수 있다.
그래서Context API를 사용해서 공통으로 사용할 값을 공유했다.
먼저4단계까지 작성한 전체 코드는 아래와 같다.import { createContext, useContext, useState } from 'react'; const PartyContext = createContext(); export function LabComp9() { const cardContainerStyle = { display: 'flex', justifyContent: 'center', }; const friends = [ { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }, { id: 2, name: '또치', role: '타조', emoji: '🐥' }, { id: 3, name: '도우너', role: '외계인', emoji: '👽' }, ]; const snackEmoji = { 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟', }; const [selectedFriend, setSelectedFriend] = useState(null); const [selectedSnack, setSelectedSnack] = useState(null); const [isPartyMode, setIsPartyMode] = useState(false); const togglePartyMode = () => { setIsPartyMode(prev => !prev); }; const handleSelectFriend = friend => { setSelectedFriend(friend); }; const handleSelectSnack = snack => { setSelectedSnack(snack); }; return ( <PartyContext.Provider value={{ isPartyMode, togglePartyMode }}> <div style={{ textAlign: 'center' }}> <h1>1단계 Props</h1> <div className="card-container" style={cardContainerStyle}> {friends.map(friend => ( <FriendCard key={friend.id} friend={friend} onSelectFriend={handleSelectFriend} /> ))} </div> </div> <div style={{ textAlign: 'center' }}> <h1>2단계 콜백함수</h1> <div>선택된 친구: {selectedFriend?.name || '아직 없음'}</div> </div> <div style={{ textAlign: 'center' }}> <h1>3단계 상태 끌어올리기</h1> <h3>간식 선택</h3> <SnackPicker snackEmoji={snackEmoji} onSelectSnack={handleSelectSnack} /> <h3>간식 미리보기</h3> <SnackPreview selectedSnack={selectedSnack} snackEmoji={snackEmoji} /> </div> <div style={{ textAlign: 'center' }}> <h1>4단계 Context API</h1> <PartyButton /> <PartyMessage /> </div> </PartyContext.Provider> ); } function FriendCard({ friend, onSelectFriend }) { const { isPartyMode } = useContext(PartyContext); const cardStyle = { display: 'flex', width: '150px', flexDirection: 'column', alignItems: 'center', border: '1px solid #ccc', borderRadius: '8px', padding: '16px', margin: '16px', textAlign: 'center', backgroundColor: isPartyMode ? 'yellow' : 'white', }; return ( <div className="friend-card" style={cardStyle}> <div> <span>{friend.emoji}</span> <h2>{friend.name}</h2> <p>{friend.role}</p> <p>{isPartyMode ? '댄스 준비 완료' : '대기중 ...'}</p> <button onClick={() => onSelectFriend(friend)}>선택하기</button> </div> </div> ); } function SnackPicker({ snackEmoji, onSelectSnack }) { return ( <div className="snack-picker"> <select onChange={e => onSelectSnack(e.target.value)}> <option value="">간식을 선택</option> {Object.keys(snackEmoji).map(snack => ( <option key={snack} value={snack}> {snack} {snackEmoji[snack]} </option> ))} </select> </div> ); } function SnackPreview({ selectedSnack, snackEmoji }) { return ( <div className="snack-preview"> {selectedSnack && ( <p> {selectedSnack} {snackEmoji[selectedSnack]} </p> )} </div> ); } function PartyButton() { const { isPartyMode, togglePartyMode } = useContext(PartyContext); return ( <div className="party-button"> <button onClick={togglePartyMode}>{isPartyMode ? '파티 모드 끄기' : '파티 모드 켜기'}</button> </div> ); } function PartyMessage() { const { isPartyMode } = useContext(PartyContext); return ( <div className="party-message"> <h3>{isPartyMode ? '🎉 모두 신나는 파티 중입니다!' : '아직 조용한 대기 모드입니다.'}</h3> </div> ); }코드 설명
4단계의 핵심은 여러 컴포넌트가 같은 값을 함께 사용하도록 공통 저장 공간을 만드는 것이다.
이때 사용하는 문법이Context API다.
먼저 필요한 기능을 가져온다.import { createContext, useContext, useState } from 'react';
각 기능의 역할은 아래와 같다.
createContext → 공유 공간을 만들 때 사용 useContext → 공유 공간의 값을 컴포넌트 안에서 꺼낼 때 사용 useState → 파티 모드 상태를 만들 때 사용
공유 공간을 만든다.
const PartyContext = createContext();
이 코드는 파티 모드 관련 값을 담을 공유 공간을 만드는 코드다.
이 공유 공간에 값을 넣어두면, 하위 컴포넌트들이props를 여러 번 전달받지 않아도 값을 꺼내 쓸 수 있다.
파티 모드 상태를 만든다.const [isPartyMode, setIsPartyMode] = useState(false);
이 코드를 역할별로 나누면 아래와 같다.
isPartyMode → 현재 파티 모드 상태를 저장하는 state setIsPartyMode → isPartyMode 값을 바꾸는 함수 useState(false) → 처음 isPartyMode 값은 false
처음 상태는 아래와 같다.
isPartyMode = false;
즉 처음에는 파티 모드가 꺼진 상태다.
파티 모드를 켜고 끄는 함수를 만든다.const togglePartyMode = () => { setIsPartyMode(prev => !prev); };
이 함수는 이전 상태를 받아서 반대 값으로 바꾼다.
초기값이false일 때 실행되면 아래처럼 동작한다.prev = false; !prev; // true setIsPartyMode(true);
파티 모드가 켜진 상태에서 다시 실행되면 아래처럼 동작한다.
prev = true; !prev; // false setIsPartyMode(false);
즉
togglePartyMode는false → true,true → false로 값을 뒤집는 함수다.
이제Provider로 값을 공유한다.<PartyContext.Provider value={{ isPartyMode, togglePartyMode }}>
Provider는Context값을 하위 컴포넌트들에게 제공하는 역할을 한다.
value안에 넣은 값이 실제로 공유되는 값이다.
초기 상태를 기준으로value를 펼쳐 보면 아래와 같다.value={{ isPartyMode: false, togglePartyMode: togglePartyMode }}
파티 모드를 켠 뒤에는 아래처럼 바뀐다.
value={{ isPartyMode: true, togglePartyMode: togglePartyMode }}
정확히 말하면
value객체 자체를 직접 수정하는 것이 아니다.
setIsPartyMode(true)가 실행되면LabComp9가 다시 렌더링되고, 다시 렌더링된 결과로isPartyMode: true가 들어간 새value가 하위 컴포넌트에 전달된다.
FriendCard에서는 파티 모드 값을 꺼내 쓴다.const { isPartyMode } = useContext(PartyContext);
초기 상태에서는 실제로 아래 값이 들어온다.
isPartyMode = false;
파티 모드를 켠 뒤에는 아래 값이 들어온다.
isPartyMode = true;
그래서 카드 배경색은 아래처럼 바뀐다.
backgroundColor: isPartyMode ? 'yellow' : 'white',
초기 상태에서는 아래처럼 계산된다.
isPartyMode = false; false ? 'yellow' : 'white'; // 결과: 'white'
파티 모드를 켠 뒤에는 아래처럼 계산된다.
isPartyMode = true; true ? 'yellow' : 'white'; // 결과: 'yellow'
카드 문구도 같은 방식으로 바뀐다.
<p>{isPartyMode ? '댄스 준비 완료' : '대기중 ...'}</p>
초기 상태에서는 아래처럼 계산된다.
isPartyMode = false; false ? '댄스 준비 완료' : '대기중 ...'; // 결과: '대기중 ...'
파티 모드를 켠 뒤에는 아래처럼 계산된다.
isPartyMode = true; true ? '댄스 준비 완료' : '대기중 ...'; // 결과: '댄스 준비 완료'
PartyButton에서는 파티 모드 값과 변경 함수를 함께 꺼낸다.const { isPartyMode, togglePartyMode } = useContext(PartyContext);
초기 상태에서 받은 값은 아래와 같다.
isPartyMode = false; togglePartyMode = () => { setIsPartyMode(prev => !prev); };
버튼 코드는 아래와 같다.
<button onClick={togglePartyMode}>{isPartyMode ? '파티 모드 끄기' : '파티 모드 켜기'}</button>
초기 상태에서는 버튼 문구가 아래처럼 계산된다.
isPartyMode = false; false ? '파티 모드 끄기' : '파티 모드 켜기'; // 결과: '파티 모드 켜기'
버튼을 클릭하면 아래 함수가 실행된다.
togglePartyMode();
이 함수는 실제로 아래 코드와 연결된다.
const togglePartyMode = () => { setIsPartyMode(prev => !prev); };
초기 상태에서 클릭하면 아래처럼 실행된다.
prev = false; !prev; // true setIsPartyMode(true);
즉 버튼 클릭 한 번으로
isPartyMode가false에서true로 바뀐다.
PartyMessage에서는 파티 모드 값만 꺼낸다.const { isPartyMode } = useContext(PartyContext);
메시지 코드는 아래와 같다.
<h3>{isPartyMode ? '🎉 모두 신나는 파티 중입니다!' : '아직 조용한 대기 모드입니다.'}</h3>
초기 상태에서는 아래처럼 계산된다.
isPartyMode = false; false ? '🎉 모두 신나는 파티 중입니다!' : '아직 조용한 대기 모드입니다.'; // 결과: '아직 조용한 대기 모드입니다.'
파티 모드를 켠 뒤에는 아래처럼 계산된다.
isPartyMode = true; true ? '🎉 모두 신나는 파티 중입니다!' : '아직 조용한 대기 모드입니다.'; // 결과: '🎉 모두 신나는 파티 중입니다!'
정리하면
4단계의 핵심은 아래와 같다.
PartyContext는 파티 모드 값을 공유할 공간이다.Provider는isPartyMode와togglePartyMode를 하위 컴포넌트에 공유한다.FriendCard는isPartyMode를 꺼내 카드 배경색과 문구를 바꾼다.PartyButton은isPartyMode와togglePartyMode를 꺼내 버튼 문구와 클릭 기능을 처리한다.PartyMessage는isPartyMode를 꺼내 현재 파티 메시지를 출력한다.togglePartyMode()는 실제로setIsPartyMode(prev => !prev)를 실행한다.setIsPartyMode()로 상태가 바뀌면LabComp9가 다시 렌더링되고,Provider가 새로운value를 전달한다.
데이터 흐름
4단계의 데이터 흐름은PartyContext.Provider가 공유한 값을 여러 컴포넌트가 동시에 사용하는 구조다.
이 단계에서는props를 여러 번 내려보내지 않고,Context를 통해 값을 꺼내 쓴다.
처음 상태는 아래와 같다.const [isPartyMode, setIsPartyMode] = useState(false); isPartyMode = false;
부모 컴포넌트에서 파티 모드를 바꾸는 함수도 만든다.
const togglePartyMode = () => { setIsPartyMode(prev => !prev); };
초기 상태에서
Provider가 공유하는 값은 아래와 같다.<PartyContext.Provider value={{ isPartyMode: false, togglePartyMode: togglePartyMode }}
실제로 이 값을 꺼내 쓰는 컴포넌트는 아래와 같다.
PartyContext 사용 컴포넌트 FriendCard PartyButton PartyMessage
초기 상태에서 각 컴포넌트가 꺼내는 값은 아래와 같다.
// FriendCard const { isPartyMode } = useContext(PartyContext); isPartyMode = false; // PartyButton const { isPartyMode, togglePartyMode } = useContext(PartyContext); isPartyMode = false; togglePartyMode = () => { setIsPartyMode(prev => !prev); }; // PartyMessage const { isPartyMode } = useContext(PartyContext); isPartyMode = false;
초기 화면은 아래처럼 계산된다.
// FriendCard 배경색 isPartyMode = false; false ? 'yellow' : 'white'; // 결과: 'white' // FriendCard 문구 false ? '댄스 준비 완료' : '대기중 ...'; // 결과: '대기중 ...' // PartyButton 문구 false ? '파티 모드 끄기' : '파티 모드 켜기'; // 결과: '파티 모드 켜기' // PartyMessage 문구 false ? '🎉 모두 신나는 파티 중입니다!' : '아직 조용한 대기 모드입니다.'; // 결과: '아직 조용한 대기 모드입니다.'
사용자가
PartyButton의 버튼을 클릭하면 아래 흐름이 실행된다.// 1. 현재 버튼 <button onClick={togglePartyMode}> 파티 모드 켜기 </button> // 2. 버튼 클릭 togglePartyMode(); // 3. togglePartyMode 실제 함수 const togglePartyMode = () => { setIsPartyMode(prev => !prev); }; // 4. 현재 isPartyMode 값이 false이므로 prev는 false prev = false; // 5. 반대 값 계산 !prev; // true // 6. 최종 state 변경 setIsPartyMode(true);
상태는 아래처럼 바뀐다.
변경 전 isPartyMode false
변경 후 isPartyMode true
상태가 바뀌면
LabComp9가 다시 렌더링된다.
그리고 다시 렌더링된 결과로Provider가 새로운value를 하위 컴포넌트에게 전달한다.<PartyContext.Provider value={{ isPartyMode: true, togglePartyMode: togglePartyMode }}
같은
Context값을 보고 있던 컴포넌트들은 다시 변경된 값을 받는다.// FriendCard isPartyMode = true; // PartyButton isPartyMode = true; togglePartyMode = () => { setIsPartyMode(prev => !prev); }; // PartyMessage isPartyMode = true;
이제 화면은 아래처럼 다시 계산된다.
// FriendCard 배경색 isPartyMode = true; true ? 'yellow' : 'white'; // 결과: 'yellow' // FriendCard 문구 true ? '댄스 준비 완료' : '대기중 ...'; // 결과: '댄스 준비 완료' // PartyButton 문구 true ? '파티 모드 끄기' : '파티 모드 켜기'; // 결과: '파티 모드 끄기' // PartyMessage 문구 true ? '🎉 모두 신나는 파티 중입니다!' : '아직 조용한 대기 모드입니다.'; // 결과: '🎉 모두 신나는 파티 중입니다!'
다시 버튼을 클릭하면 반대 흐름이 실행된다.
// 1. 현재 isPartyMode 값 isPartyMode = true; // 2. 버튼 클릭 togglePartyMode(); // 3. 이전 값 prev = true; // 4. 반대 값 계산 !prev; // false // 5. 최종 state 변경 setIsPartyMode(false);
그러면 상태와 화면은 다시 아래처럼 돌아간다.
isPartyMode = false FriendCard 배경색: white FriendCard 문구: 대기중 ... PartyButton 문구: 파티 모드 켜기 PartyMessage 문구: 아직 조용한 대기 모드입니다.
즉
4단계의 값 이동은 아래처럼 정리할 수 있다.PartyButton 클릭 → togglePartyMode() 실행 → setIsPartyMode(prev => !prev) 실행 → isPartyMode가 false에서 true로 변경 → LabComp9가 다시 렌더링됨 → Provider가 isPartyMode: true가 들어간 새 value를 다시 전달 → FriendCard, PartyButton, PartyMessage가 true 값을 다시 받음 → 카드 배경색, 카드 문구, 버튼 문구, 메시지가 동시에 변경
4단계의 핵심은 여러 컴포넌트가 함께 사용하는 상태를 Context로 공유하고, 하나의 상태 변화가 여러 컴포넌트에 동시에 반영되는 것이다.
아래 코드는 이번 실습의 최종 코드다.
앞에서 단계별로 나누어 작성한props, 콜백 함수, 상태 끌어올리기,Context API흐름이 모두 포함되어 있다.import { createContext, useContext, useState } from 'react'; const PartyContext = createContext(); export function LabComp9() { const cardContainerStyle = { display: 'flex', justifyContent: 'center', }; const friends = [ { id: 1, name: '둘리', role: '아기공룡', emoji: '🦕' }, { id: 2, name: '또치', role: '타조', emoji: '🐥' }, { id: 3, name: '도우너', role: '외계인', emoji: '👽' }, ]; const snackEmoji = { 초코과자: '🍪', 딸기우유: '🥛', 라면: '🍜', 붕어빵: '🐟', }; const [selectedFriend, setSelectedFriend] = useState(null); const [selectedSnack, setSelectedSnack] = useState(null); const [isPartyMode, setIsPartyMode] = useState(false); const togglePartyMode = () => { setIsPartyMode(prev => !prev); }; const handleSelectFriend = friend => { setSelectedFriend(friend); }; const handleSelectSnack = snack => { setSelectedSnack(snack); }; return ( <PartyContext.Provider value={{ isPartyMode, togglePartyMode }}> <div style={{ textAlign: 'center' }}> <h1>1단계 Props</h1> <div className="card-container" style={cardContainerStyle}> {friends.map(friend => ( <FriendCard key={friend.id} friend={friend} onSelectFriend={handleSelectFriend} /> ))} </div> </div> <div style={{ textAlign: 'center' }}> <h1>2단계 콜백함수</h1> <div>선택된 친구: {selectedFriend?.name || '아직 없음'}</div> </div> <div style={{ textAlign: 'center' }}> <h1>3단계 상태 끌어올리기</h1> <h3>간식 선택</h3> <SnackPicker snackEmoji={snackEmoji} onSelectSnack={handleSelectSnack} /> <h3>간식 미리보기</h3> <SnackPreview selectedSnack={selectedSnack} snackEmoji={snackEmoji} /> </div> <div style={{ textAlign: 'center' }}> <h1>4단계 Context API</h1> <PartyButton /> <PartyMessage /> </div> </PartyContext.Provider> ); } function FriendCard({ friend, onSelectFriend }) { const { isPartyMode } = useContext(PartyContext); const cardStyle = { display: 'flex', width: '150px', flexDirection: 'column', alignItems: 'center', border: '1px solid #ccc', borderRadius: '8px', padding: '16px', margin: '16px', textAlign: 'center', backgroundColor: isPartyMode ? 'yellow' : 'white', }; return ( <div className="friend-card" style={cardStyle}> <div> <span>{friend.emoji}</span> <h2>{friend.name}</h2> <p>{friend.role}</p> <p>{isPartyMode ? '댄스 준비 완료' : '대기중 ...'}</p> <button onClick={() => onSelectFriend(friend)}>선택하기</button> </div> </div> ); } function SnackPicker({ snackEmoji, onSelectSnack }) { return ( <div className="snack-picker"> <select onChange={e => onSelectSnack(e.target.value)}> <option value="">간식을 선택</option> {Object.keys(snackEmoji).map(snack => ( <option key={snack} value={snack}> {snack} {snackEmoji[snack]} </option> ))} </select> </div> ); } function SnackPreview({ selectedSnack, snackEmoji }) { return ( <div className="snack-preview"> {selectedSnack && ( <p> {selectedSnack} {snackEmoji[selectedSnack]} </p> )} </div> ); } function PartyButton() { const { isPartyMode, togglePartyMode } = useContext(PartyContext); return ( <div className="party-button"> <button onClick={togglePartyMode}>{isPartyMode ? '파티 모드 끄기' : '파티 모드 켜기'}</button> </div> ); } function PartyMessage() { const { isPartyMode } = useContext(PartyContext); return ( <div className="party-message"> <h3>{isPartyMode ? '🎉 모두 신나는 파티 중입니다!' : '아직 조용한 대기 모드입니다.'}</h3> </div> ); }
이번 실습에서는
React컴포넌트 사이에서 데이터가 이동하는 방향을 단계별로 확인했다.
처음에는 부모에서 자식으로 데이터를 내려보냈고, 이후에는 자식에서 발생한 이벤트를 부모 상태에 반영했다.
마지막에는 여러 컴포넌트가 같은 상태를 함께 사용하도록Context API를 적용했다.
props는 부모 컴포넌트가 자식 컴포넌트에 데이터를 전달할 때 사용한다.- 자식 컴포넌트는 부모의
state를 직접 수정할 수 없다.- 자식에서 부모 상태를 바꾸고 싶으면 부모가 만든 함수를
props로 내려주고, 자식이 그 함수를 호출해야 한다.selectedFriend는 친구 카드에서 버튼을 클릭했을 때 선택한 친구 객체로 변경된다.selectedSnack은SnackPicker에서 간식을 선택했을 때 선택한 간식 문자열로 변경된다.- 형제 컴포넌트끼리는 직접 데이터를 주고받지 않고, 공통 부모에 상태를 두고 공유한다.
Context API를 사용하면 여러 컴포넌트가 같은 값을props없이 꺼내 쓸 수 있다.isPartyMode가 바뀌면FriendCard,PartyButton,PartyMessage가 같은 값 기준으로 동시에 다시 렌더링된다.정리하면 이번 실습은
props, 콜백 함수, 상태 끌어올리기,Context API를 한 화면 안에서 순서대로 확인한 실습이다.
각 단계는 따로 떨어진 개념이 아니라, 컴포넌트가 많아질수록 데이터를 더 깔끔하게 전달하기 위한 흐름으로 이어진다.