드디어 마지막 편이에요! 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->>사용자: 선택된 감정 하이라이트
  1. Editor가 onClick 함수를 EmotionItem에 전달
  2. 사용자가 EmotionItem 클릭
  3. EmotionItem이 받아둔 onClick(id) 실행
  4. 사실 이건 Editor의 함수였음!
  5. Editor에서 setState 실행
  6. 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. 1편: React 프로젝트 생성 및 기본 구조 이해
  2. 2편: React Router로 페이지 만들기
  3. 3편: useState로 데이터 관리하기
  4. 4편: 재사용 가능한 컴포넌트 만들기 ← 오늘

4편 핵심 요약:

  1. 🧩 큰 컴포넌트는 작은 부품으로 나누자
  2. 📦 Props로 데이터 전달
  3. ⬆️ 자식이 부모 State를 바꾸려면 함수를 Props로 받아야 함
  4. 🎨 조건부 스타일링으로 사용자 피드백

다음 단계는?

이제 기본 일기장 앱은 완성했어요! 여기서 더 발전시키고 싶다면:

  • 데이터 영속화: localStorage나 Firebase로 일기 저장
  • Context API: 전역 State 관리로 여러 페이지에서 일기 데이터 공유
  • 일기 목록: 저장된 일기들을 리스트로 보여주기
  • 수정/삭제: 기존 일기를 수정하거나 삭제하는 기능
  • 필터링: 감정별로 일기 필터링
  • 반응형 디자인: 모바일에서도 예쁘게!

이런 기능들을 추가하면 진짜 쓸 수 있는 일기장 앱이 돼요!

여러분 모두 수고하셨어요! 🎊

처음에는 React가 어려워 보였을 텐데, 4편까지 따라오시느라 정말 대단해요. 이제 React의 핵심 개념들은 다 이해하셨을 거예요.

계속 연습하면서 자신만의 프로젝트를 만들어보세요. 코딩의 진짜 재미는 직접 만들면서 느끼는 거거든요!


시리즈 네비게이션