React 감정 일기장 만들기 4편 - 재사용 가능한 컴포넌트 만들기
드디어 마지막 편이에요! 1-3편에서 프로젝트 설정부터 State 관리까지 배웠는데요, 3편에서 만든 Editor 컴포넌트를 보면 코드가 좀 길고 복잡하죠?
이번에는 큰 컴포넌트를 작은 부품들로 나눠서 재사용 가능하고 관리하기 쉽게 만드는 방법을 배워볼게요.
레고 블록처럼 작은 부품들을 조합해서 큰 걸 만드는 거예요!
이번 편에서 배울 내용
- ✅ 컴포넌트를 왜 나누는지 이해하기
- ✅ EmotionItem 컴포넌트 만들기 (개별 감정 선택 버튼)
- ✅ Button 컴포넌트 만들기 (재사용 가능한 버튼)
- ✅ Header 컴포넌트 만들기 (페이지 상단)
- ✅ Props로 데이터 전달하기
- ✅ 자식이 부모의 State를 바꾸는 패턴 이해하기
컴포넌트를 왜 나눌까요?
3편에서 만든 Editor를 보면 감정 버튼이 5개나 있었죠? 코드가 반복되고 길어지니까 읽기 힘들었어요.
컴포넌트를 나누면:
- 📦 재사용성: 같은 부품을 여러 곳에서 사용 가능
- 🔧 유지보수: 수정할 곳을 쉽게 찾음
- 📖 가독성: 코드가 짧고 명확함
- 🧪 테스트: 각 부품을 독립적으로 테스트 가능
컴포넌트 계층 구조
오늘 만들 구조를 다이어그램으로 먼저 볼게요:
graph TD
A["Editor<br/>(메인 컴포넌트)"] --> B["Header<br/>(제목 영역)"]
A --> C["EmotionItem × 5<br/>(감정 선택 버튼)"]
A --> D["Button × 2<br/>(취소/작성완료)"]
style A fill:#fff4e1
style B fill:#e1f5ff
style C fill:#ffe1e1
style D fill:#e1ffe1
Editor가 부모고, Header/EmotionItem/Button이 자식이에요. 부모가 자식들에게 데이터(Props)를 전달하는 구조예요.
Button 컴포넌트 만들기
가장 간단한 Button부터 시작해볼게요!
Button.js 생성
src/components/Button.js:
const Button = ({ text, type, onClick }) => {
// type이 positive나 negative가 아니면 default
const btnType = ['positive', 'negative'].includes(type) ? type : 'default';
return (
<button
className={`Button Button_${btnType}`}
onClick={onClick}
style={{
padding: '15px 30px',
fontSize: '16px',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginRight: '10px'
}}
>
{text}
</button>
);
};
export default Button;
Button.css 생성
src/components/Button.css:
.Button {
font-family: inherit;
}
/* 긍정 버튼 (작성완료) */
.Button_positive {
background-color: #64c964;
color: white;
}
.Button_positive:hover {
background-color: #4ea24e;
}
/* 부정 버튼 (취소, 삭제) */
.Button_negative {
background-color: #fd565f;
color: white;
}
.Button_negative:hover {
background-color: #d84850;
}
/* 기본 버튼 */
.Button_default {
background-color: #ececec;
color: #333;
}
.Button_default:hover {
background-color: #d4d4d4;
}
Props 설명
const Button = ({ text, type, onClick }) => {
text: 버튼에 표시할 글자type: 버튼 종류 (positive/negative/default)onClick: 클릭했을 때 실행할 함수
사용 예시
import Button from './components/Button';
// 긍정 버튼
<Button text="작성 완료" type="positive" onClick={handleSubmit} />
// 부정 버튼
<Button text="취소" type="negative" onClick={handleCancel} />
// 기본 버튼
<Button text="그냥 버튼" onClick={handleClick} />
같은 Button 컴포넌트인데 Props만 다르게 주면 다양하게 쓸 수 있어요!
Header 컴포넌트 만들기
페이지 상단에 쓸 Header를 만들어볼게요.
Header.js 생성
src/components/Header.js:
const Header = ({ title, leftChild, rightChild }) => {
return (
<div className="Header" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '20px 0',
borderBottom: '1px solid #e2e2e2',
marginBottom: '20px'
}}>
<div className="header_left">{leftChild}</div>
<div className="header_title" style={{
fontSize: '24px',
fontWeight: 'bold'
}}>{title}</div>
<div className="header_right">{rightChild}</div>
</div>
);
};
export default Header;
Props 설명
title: 가운데 제목leftChild: 왼쪽에 들어갈 컴포넌트rightChild: 오른쪽에 들어갈 컴포넌트
사용 예시
import Header from './components/Header';
import Button from './components/Button';
<Header
title="새 일기 쓰기"
leftChild={<Button text="< 뒤로가기" onClick={goBack} />}
rightChild={<div>오늘</div>}
/>
왼쪽, 가운데, 오른쪽을 자유롭게 채울 수 있어서 유연해요!
EmotionItem 컴포넌트 만들기
이제 핵심인 감정 선택 버튼을 만들어볼게요.
EmotionItem.js 생성
src/components/EmotionItem.js:
const EmotionItem = ({ id, name, onClick, isSelected }) => {
const handleOnClick = () => {
onClick(id); // 부모에게 "나 클릭됐어!" 알림
};
return (
<div
className={`EmotionItem ${isSelected ? `EmotionItem_on_${id}` : 'EmotionItem_off'}`}
onClick={handleOnClick}
style={{
display: 'inline-block',
margin: '5px',
padding: '20px',
border: '2px solid #ececec',
borderRadius: '10px',
cursor: 'pointer',
textAlign: 'center',
minWidth: '100px',
transition: 'all 0.2s'
}}
>
<div style={{ fontSize: '48px', marginBottom: '10px' }}>
{/* 감정 아이콘 (이모지) */}
{id === 1 && '😊'}
{id === 2 && '😄'}
{id === 3 && '😐'}
{id === 4 && '😞'}
{id === 5 && '😢'}
</div>
<span>{name}</span>
</div>
);
};
export default EmotionItem;
EmotionItem.css 생성
src/components/EmotionItem.css:
/* 기본 스타일 (선택 안 됨) */
.EmotionItem_off {
background-color: #ececec;
color: #333;
}
.EmotionItem_off:hover {
background-color: #d4d4d4;
}
/* 선택됨 - 각 감정별 색상 */
.EmotionItem_on_1 {
background-color: #64c964;
color: white;
border-color: #64c964 !important;
}
.EmotionItem_on_2 {
background-color: #9dd772;
color: white;
border-color: #9dd772 !important;
}
.EmotionItem_on_3 {
background-color: #fdce17;
color: white;
border-color: #fdce17 !important;
}
.EmotionItem_on_4 {
background-color: #fd8446;
color: white;
border-color: #fd8446 !important;
}
.EmotionItem_on_5 {
background-color: #fd565f;
color: white;
border-color: #fd565f !important;
}
자식이 부모를 바꾸는 패턴
이게 정말 중요해요! EmotionItem이 클릭되면 Editor의 State를 바꿔야 하는데, 자식은 부모의 State를 직접 못 바꿔요.
그래서 이렇게 해요:
sequenceDiagram
participant 사용자
participant EmotionItem
participant Editor
participant State
사용자->>EmotionItem: 클릭!
EmotionItem->>Editor: onClick(id) 호출
Note over EmotionItem,Editor: "부모님, 제가 클릭됐어요!"
Editor->>State: setState로 emotionId 변경
State->>Editor: 변경 완료, 다시 그려!
Editor->>EmotionItem: isSelected 값 변경됨
EmotionItem->>사용자: 선택된 감정 하이라이트
- Editor가
onClick함수를 EmotionItem에 전달 - 사용자가 EmotionItem 클릭
- EmotionItem이 받아둔
onClick(id)실행 - 사실 이건 Editor의 함수였음!
- Editor에서
setState실행 - State가 바뀌고 화면 다시 그려짐
Editor에 컴포넌트 적용하기
이제 3편에서 만든 Editor를 개선해볼게요!
Editor.js 전체 코드
src/components/Editor.js:
import { useState } from 'react';
import Button from './Button';
import EmotionItem from './EmotionItem';
import Header from './Header';
import './Editor.css';
import './Button.css';
import './EmotionItem.css';
// 감정 목록
const emotionList = [
{ id: 1, name: '완전 좋음' },
{ id: 2, name: '좋음' },
{ id: 3, name: '그럭저럭' },
{ id: 4, name: '나쁨' },
{ id: 5, name: '끔찍함' }
];
const Editor = ({ onSubmit }) => {
const [state, setState] = useState({
date: new Date().toISOString().split('T')[0],
emotionId: 3,
content: ''
});
const handleChangeDate = (e) => {
setState({
...state,
date: e.target.value
});
};
const handleChangeEmotion = (emotionId) => {
setState({
...state,
emotionId
});
};
const handleChangeContent = (e) => {
setState({
...state,
content: e.target.value
});
};
const handleSubmit = () => {
if (onSubmit) {
onSubmit(state);
} else {
alert('일기 저장:\n' + JSON.stringify(state, null, 2));
}
};
const handleCancel = () => {
const result = window.confirm('정말 취소하시겠습니까?');
if (result) {
setState({
date: new Date().toISOString().split('T')[0],
emotionId: 3,
content: ''
});
}
};
return (
<div className="Editor">
<Header
title="새 일기 쓰기"
leftChild={<Button text="< 뒤로가기" onClick={handleCancel} />}
/>
{/* 날짜 선택 */}
<div style={{ marginBottom: '30px' }}>
<h4>오늘의 날짜</h4>
<input
type="date"
value={state.date}
onChange={handleChangeDate}
style={{
padding: '10px',
fontSize: '16px',
border: '1px solid #ccc',
borderRadius: '5px',
width: '200px'
}}
/>
</div>
{/* 감정 선택 */}
<div style={{ marginBottom: '30px' }}>
<h4>오늘의 감정</h4>
<div>
{emotionList.map((emotion) => (
<EmotionItem
key={emotion.id}
{...emotion}
onClick={handleChangeEmotion}
isSelected={state.emotionId === emotion.id}
/>
))}
</div>
</div>
{/* 일기 내용 */}
<div style={{ marginBottom: '30px' }}>
<h4>오늘의 일기</h4>
<textarea
value={state.content}
onChange={handleChangeContent}
placeholder="오늘은 어땠나요?"
style={{
width: '100%',
minHeight: '200px',
padding: '15px',
fontSize: '16px',
borderRadius: '5px',
border: '1px solid #ccc',
resize: 'vertical'
}}
/>
</div>
{/* 버튼들 */}
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button text="취소하기" type="negative" onClick={handleCancel} />
<Button text="작성 완료" type="positive" onClick={handleSubmit} />
</div>
</div>
);
};
export default Editor;
코드 개선 포인트
Before (3편):
// 감정 버튼이 인라인으로 5개...
<button onClick={() => handleChangeEmotion(1)}>완전 좋음</button>
<button onClick={() => handleChangeEmotion(2)}>좋음</button>
...
After (4편):
// 깔끔하게 map으로 처리
{emotionList.map((emotion) => (
<EmotionItem
key={emotion.id}
{...emotion}
onClick={handleChangeEmotion}
isSelected={state.emotionId === emotion.id}
/>
))}
훨씬 짧고 읽기 쉬워졌죠?
Spread Operator와 Props
<EmotionItem
{...emotion} // id, name을 한 번에 전달
onClick={handleChangeEmotion}
isSelected={state.emotionId === emotion.id}
/>
{...emotion}은 이렇게 풀려요:
// emotion = { id: 1, name: '완전 좋음' }
<EmotionItem
id={1}
name="완전 좋음"
onClick={handleChangeEmotion}
isSelected={state.emotionId === 1}
/>
최종 확인하기
모든 파일을 만들고 저장했다면, 브라우저에서 확인해보세요!
✅ 파일 생성 확인
src/components/Button.js✓src/components/Button.css✓src/components/Header.js✓src/components/EmotionItem.js✓src/components/EmotionItem.css✓src/components/Editor.js(업데이트) ✓
✅ 컴포넌트 동작 확인
- EmotionItem 5개가 정상적으로 렌더링
- 감정 클릭 시 색상 변경 (초록→노랑→빨강)
- Button의 type별 색상 차이 (positive=초록, negative=빨강)
- Header 왼쪽에 뒤로가기 버튼 표시
✅ 최종 플로우 테스트
- 날짜 선택 → 감정 선택 → 내용 입력 → 작성 완료
- “취소하기” 버튼 클릭 시 confirm 창 표시
- “작성 완료” 버튼 클릭 시 데이터 출력
자주 묻는 질문
Q1. Props는 변경할 수 없나요?
네, Props는 읽기 전용이에요. 자식 컴포넌트에서 Props를 직접 수정하면 안 돼요.
// ❌ 이렇게 하면 안 돼요
const EmotionItem = ({ name }) => {
name = "바뀐 이름"; // 에러!
...
Props를 바꾸고 싶으면 부모의 State를 바꿔야 해요.
Q2. 컴포넌트는 얼마나 작게 나눠야 하나요?
정답은 없지만 이런 기준으로 생각해보세요:
- 한 가지 일만 하는가? (단일 책임 원칙)
- 여러 곳에서 재사용할 수 있는가?
- 독립적으로 테스트 가능한가?
너무 잘게 쪼개면 오히려 복잡해질 수 있어요. 균형이 중요해요!
Q3. CSS 파일을 컴포넌트마다 나누는 게 좋나요?
네! 컴포넌트와 CSS를 같이 관리하면:
- 어떤 스타일이 어디 쓰이는지 명확함
- 컴포넌트 삭제 시 CSS도 함께 제거 가능
- 네이밍 충돌 방지 (컴포넌트명_스타일명)
Q4. key는 왜 필요한가요?
map()으로 컴포넌트를 여러 개 만들 때 React가 각각을 구별하려고 써요.
{emotionList.map((emotion) => (
<EmotionItem
key={emotion.id} // 이게 없으면 경고!
...
/>
))}
key가 없으면 React가 어떤 걸 업데이트할지 헷갈려서 성능이 나빠져요.
정리하며
드디어 4편 완성! 감정 일기장 시리즈를 모두 마쳤어요! 🎉
전체 시리즈 요약:
- 1편: React 프로젝트 생성 및 기본 구조 이해
- 2편: React Router로 페이지 만들기
- 3편: useState로 데이터 관리하기
- 4편: 재사용 가능한 컴포넌트 만들기 ← 오늘
4편 핵심 요약:
- 🧩 큰 컴포넌트는 작은 부품으로 나누자
- 📦 Props로 데이터 전달
- ⬆️ 자식이 부모 State를 바꾸려면 함수를 Props로 받아야 함
- 🎨 조건부 스타일링으로 사용자 피드백
다음 단계는?
이제 기본 일기장 앱은 완성했어요! 여기서 더 발전시키고 싶다면:
- 데이터 영속화: localStorage나 Firebase로 일기 저장
- Context API: 전역 State 관리로 여러 페이지에서 일기 데이터 공유
- 일기 목록: 저장된 일기들을 리스트로 보여주기
- 수정/삭제: 기존 일기를 수정하거나 삭제하는 기능
- 필터링: 감정별로 일기 필터링
- 반응형 디자인: 모바일에서도 예쁘게!
이런 기능들을 추가하면 진짜 쓸 수 있는 일기장 앱이 돼요!
여러분 모두 수고하셨어요! 🎊
처음에는 React가 어려워 보였을 텐데, 4편까지 따라오시느라 정말 대단해요. 이제 React의 핵심 개념들은 다 이해하셨을 거예요.
계속 연습하면서 자신만의 프로젝트를 만들어보세요. 코딩의 진짜 재미는 직접 만들면서 느끼는 거거든요!
시리즈 네비게이션
- 1편: React 프로젝트 시작하기 - 기본 구조 이해
- 2편: React Router로 페이지 만들기
- 3편: State로 데이터 관리하기
- 4편: 재사용 가능한 컴포넌트 만들기 ← 현재 글
- 5편: 전체 앱 연결 및 데이터 관리 (다음 편)