React UI를 컴포넌트로 나누고 재사용할 기준은 무엇인가요?
React 컴포넌트는 역할이 분명한 화면 조각을 함수로 나누어 조합하는 방법입니다. 파일을 많이 만드는 것보다 제목과 목록처럼 책임이 갈리는 지점을 먼저 찾습니다.
적용 조건·범위: 반대로 JSX가 세 줄뿐이라는 이유만으로 무조건 파일을 만드는 것은 필요하지 않습니다.
본문 근거: React Quick Start
독립 실습: 화면을 두 컴포넌트로 나누기
PageHeader와 TaskList가 합쳐진 한 화면을 확인하세요.
예제 크게 보기 (새 탭)TaskFlow 체크포인트가 없어도 됩니다. React + TypeScript Vite 프로젝트가 없다면 아래 명령으로 별도 연습 폴더를 만듭니다.
npm create vite@latest component-practice -- --template react-ts
cd component-practice
npm install
npm run dev
src/App.tsx 전체를 다음 코드로 바꿉니다.
function PageHeader() {
return (
<header>
<h1>오늘의 작업</h1>
</header>
);
}
function TaskList() {
return (
<section aria-labelledby="tasks-heading">
<h2 id="tasks-heading">목록</h2>
<ul>
<li>React 컴포넌트 살펴보기</li>
<li>화면 나누기</li>
</ul>
</section>
);
}
export default function App() {
return (
<main>
<PageHeader />
<TaskList />
</main>
);
}
개발 서버가 알려 준 주소에서 ‘오늘의 작업’ 제목과 목록 두 항목이 보이면 성공입니다. App은 두 컴포넌트를 조합하고, PageHeader는 제목, TaskList는 목록을 담당합니다. 한 파일 안에서 역할부터 나눠 보았으며, 파일 분리는 필요할 때 진행하면 됩니다.
스스로 확인: ‘목록’ 아래 항목을 하나 추가하려면 어느 함수를 수정할까요? TaskList입니다. PageHeader는 제목만 담당하므로 수정할 필요가 없습니다. 화면이 비어 있으면 App의 export default, 대문자로 시작하는 컴포넌트 이름, 터미널 오류를 확인하세요.
컴포넌트란
컴포넌트는 UI의 일부를 표현하는 React 함수입니다. 작은 버튼일 수도 있고 한 페이지 전체일 수도 있습니다.
function WelcomeMessage() {
return <p>반갑습니다.</p>;
}
일반 함수와 가장 큰 차이는 반환값으로 JSX를 사용해 화면 구조를 표현한다는 점입니다.
컴포넌트 이름은 대문자로 시작합니다
React는 소문자로 시작하는 JSX 태그를 HTML 같은 내장 요소로 해석합니다.
function taskItem() {
return <li>작업</li>;
}
이 함수 이름으로 다음처럼 쓰면 기대한 컴포넌트로 동작하지 않습니다.
<taskItem />
컴포넌트는 대문자로 시작합니다.
function TaskItem() {
return <li>작업</li>;
}
컴포넌트를 조합합니다
컴포넌트 안에서 다른 컴포넌트를 사용할 수 있습니다.
function Header() {
return <h1>TaskFlow</h1>;
}
function App() {
return (
<main>
<Header />
</main>
);
}
이 방식으로 큰 화면을 작은 책임으로 나눕니다.
파일을 분리합니다
컴포넌트가 커지거나 재사용되는 위치가 생기면 파일로 분리합니다.
export default function Header() {
return (
<header>
<h1>TaskFlow</h1>
</header>
);
}
사용하는 파일에서는 import합니다.
import Header from './components/Header';
언제 컴포넌트로 나눌까
다음 조건 중 하나에 해당하면 분리를 검토할 수 있습니다.
- 같은 UI를 반복해서 사용합니다.
- 화면에서 역할이 명확하게 구분됩니다.
- 한 컴포넌트가 너무 많은 책임을 가집니다.
- 특정 영역만 따로 테스트하거나 수정하고 싶습니다.
반대로 JSX가 세 줄뿐이라는 이유만으로 무조건 파일을 만드는 것은 필요하지 않습니다.
자주 발생하는 오류와 해결
컴포넌트가 화면에 나오지 않습니다
export와 import 형태가 일치하는지 확인합니다.
export default function Header() {}
라면 다음처럼 가져옵니다.
import Header from './components/Header';
컴포넌트 이름을 소문자로 작성했습니다
React 컴포넌트 이름은 대문자로 시작합니다.
return을 빠뜨렸습니다
중괄호를 사용한 함수에서는 JSX를 명시적으로 return해야 합니다.
function Header() {
return <header>TaskFlow</header>;
}
학습 완료 체크리스트
- 함수 컴포넌트를 만들 수 있습니다.
- 컴포넌트를 다른 컴포넌트 안에서 사용할 수 있습니다.
- import/export 오류를 확인할 수 있습니다.
- UI를 컴포넌트로 나눌 기준을 설명할 수 있습니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 React로 화면과 상태를 이어서 만듭니다.
화면을 세 가지 역할로 나눕니다. Header: 프로젝트 제목 TaskList: 작업 목록 영역 TaskItem: 작업 한 건 지금은 props를 깊게 다루지 않습니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
TaskFlow 3단계 실행
시작 상태와 완성 결과를 바꾸어 본문에서 적용한 화면과 동작을 비교하세요.
예제 크게 보기 (새 탭)시작 체크포인트: 이전: React Vite 사용법 Vite 8 프로젝트 생성 실행 빌드의 완료 상태에서 이어갑니다.
이전 회차 완료 ZIP으로 시작하기. 새 폴더에 풀어 현재 회차 완료본과 나란히 비교합니다.
이전 완료본과 달라진 파일: package.json, src/App.tsx, src/components/Header.tsx, src/components/TaskItem.tsx, src/components/TaskList.tsx, src/index.css. 아래 변경 설명을 따라 수정하고 전체 코드의 같은 파일과 대조합니다.
완료 코드 ZIP 다운로드 — step-03-components
이전 체크포인트
step-02-vite-base
현재 상태
App.tsx 한 파일에 제목과 작업 목록 영역이 함께 들어 있습니다.
이번 목표
화면을 세 가지 역할로 나눕니다.
- Header: 프로젝트 제목
- TaskList: 작업 목록 영역
- TaskItem: 작업 한 건
지금은 props를 깊게 다루지 않습니다. TaskItem에는 고정된 예시 데이터를 사용하고, 다음 JSX·props 글에서 실제 데이터 흐름으로 바꿉니다.
실행 확인
npm run dev
npm run build
화면 모양은 이전 단계와 크게 달라지지 않아도 됩니다. 이번 단계의 목적은 UI 책임이 파일과 컴포넌트로 분리되는 것입니다.
완료 체크포인트:
step-03-components
전체 코드 뷰어의 모든 파일은 다운로드 ZIP과 같습니다. ZIP은 독립 실행할 수 있는 완료 시점의 코드입니다. Node.js 22.12 이상에서 npm install 후 npm run dev로 실행하고, npm run build로 검사합니다.
전체 파일·코드 보기
실습 프로젝트 전체 코드
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-03-components",
"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';
function App() {
return (
<main className="app">
<Header />
<TaskList />
</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
export default function TaskItem() {
return (
<li>
<span>React 컴포넌트 분리하기</span>
</li>
);
}
src/components/TaskList.tsx
import TaskItem from './TaskItem';
export default function TaskList() {
return (
<section>
<h2>작업 목록</h2>
<ul>
<TaskItem />
</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>,
);
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()] });
글 소개·학습 준비·전체 목차

React · 3/32 · TaskFlow 누적 실습
React 컴포넌트 개념 정리 UI 재사용 구조 잡기
React · 3 / 32 · 필수 학습 · 컴포넌트
React 애플리케이션은 하나의 거대한 함수로 만드는 것이 아니라 역할이 분명한 컴포넌트를 조합해 구성합니다. 그렇다고 파일을 작게 나누는 것 자체가 목표는 아닙니다. UI에서 책임이 나뉘는 지점을 찾는 것이 먼저입니다.
상단에서는 컴포넌트의 기본 원리를 작은 예제로 설명하고, 하단에서는 TaskFlow의 단일 화면을 Header·TaskList·TaskItem으로 나눕니다.
이번 글에서 배우는 내용
- React 컴포넌트가 무엇인지
- 함수 컴포넌트 작성 방법
- 컴포넌트 이름을 대문자로 시작하는 이유
- import/export 기본 흐름
- 어느 기준으로 컴포넌트를 나눌지
- 너무 일찍 컴포넌트를 잘게 쪼개면 생기는 문제
목차
다음 학습
다음 글에서는 배열 데이터를 JSX로 렌더링하고, 조건에 따라 완료 상태와 빈 목록을 다르게 표시합니다.
공식 문서
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 33개 · 필수 27개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.