2026년 3월 10일
이 글을 읽고 나면, React가 왜 만들어졌는지 이해하고, 첫 번째 React 프로젝트를 생성하고, 컴포넌트와 JSX를 직접 작성할 수 있습니다. 이 시리즈는 총 6편으로 구성되어 있으며, 마지막 6편에서는 배운 모든 것을 활용하여 Custom TodoList를 만들게 됩니다.
| 1편 (현재) | React 시작하기 — 등장 배경, 컴포넌트, JSX | | 2편 | React 핵심 — props, state, 이벤트 처리 | | 3편 | React 심화 — useEffect, 조건부 렌더링, 리스트 | | 4편 | TypeScript 기초 — 타입 시스템과 React에서의 TS | | 5편 | TailwindCSS — 유틸리티 기반 스타일링 | | 6편 | 커스텀 투두리스트 만들기 |
React를 이해하려면, React가 없던 시절의 문제를 먼저 알아야 합니다.
HTML, CSS, JavaScript만으로 웹 페이지를 만들 수 있습니다. 간단한 페이지라면 이것으로 충분합니다. 하지만 웹 애플리케이션이 점점 복잡해지면서 문제가 생기기 시작했습니다.
아래는 JavaScript만으로 "좋아요" 버튼을 구현한 코드입니다:
html
<!-- 순수 JavaScript로 좋아요 기능 구현 -->
<div id="like-section">
<span id="like-count">0</span>
<button id="like-button">좋아요</button>
</div>
<script>
let count = 0;
const countElement = document.getElementById("like-count");
const buttonElement = document.getElementById("like-button");
buttonElement.addEventListener("click", function () {
count += 1;
countElement.textContent = count; // 직접 DOM을 찾아서 수정
});
</script>이 코드는 잘 동작합니다. 하지만 여기서 기능이 추가된다고 생각해 보겠습니다.
각 기능마다 document.getElementById로 요소를 찾고, textContent나 classList를 직접 수정해야 합니다. 데이터가 바뀔 때마다 화면의 어떤 부분을 어떻게 업데이트할지 개발자가 하나하나 지시해야 합니다.
데이터가 바뀌면...
[순수 JavaScript의 방식]
1. 어떤 DOM 요소를 찾을지 결정
2. 해당 요소의 어떤 속성을 바꿀지 결정
3. 직접 수정하는 코드 작성
4. 관련된 다른 요소들도 찾아서 수정
5. 혹시 빠뜨린 곳은 없는지 확인...
기능이 10개, 20개로 늘어나면?
→ 수정할 곳이 기하급수적으로 늘어남
→ 하나를 수정하면 다른 곳이 깨짐
→ "데이터는 바뀌었는데 화면이 안 바뀌는" 버그 발생이것이 2010년대 초반 Facebook 개발팀이 직면한 문제였습니다. Facebook의 채팅, 알림, 뉴스피드 등은 실시간으로 데이터가 바뀌는 복잡한 UI였고, 데이터와 화면을 동기화하는 것이 점점 어려워졌습니다.
2013년, Facebook은 이 문제를 해결하기 위해 React를 공개했습니다. React의 핵심 아이디어는 단순합니다:
"데이터가 바뀌면, 화면 전체를 다시 그리자."
개발자가 "어떤 DOM을 어떻게 수정할지" 고민하는 대신, **"데이터가 이러할 때 화면은 이렇게 보여야 한다"**만 선언하면 됩니다. 데이터가 바뀌면 React가 알아서 화면을 업데이트합니다.
[React의 방식]
1. 데이터(상태)를 정의한다
2. 이 데이터가 이럴 때 화면은 이렇다고 선언한다
3. 데이터가 바뀌면, React가 알아서 화면을 업데이트한다
개발자는 "화면이 어떻게 보여야 하는지"만 신경 쓰면 됩니다.
React가 "어떻게 업데이트할지"는 알아서 처리합니다.같은 좋아요 기능을 React로 작성하면 이렇습니다:
function LikeButton() {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>좋아요</button>
</div>
);
}count라는 데이터가 바뀌면, React가 화면을 자동으로 다시 그립니다. 개발자가 DOM을 직접 조작하는 코드는 한 줄도 없습니다.
이 코드의 문법은 아직 이해하지 않아도 됩니다. 지금 중요한 것은 접근 방식의 차이입니다.
| 순수 JavaScript | React | |
|---|---|---|
| 방식 | 명령형 — "이 요소를 찾아서 이렇게 바꿔라" | 선언형 — "데이터가 이러면 화면은 이렇다" |
| DOM 조작 | 개발자가 직접 | React가 자동으로 |
| 데이터-화면 동기화 | 개발자가 관리 | React가 보장 |
| 복잡도 증가 시 | 관리가 기하급수적으로 어려워짐 | 비교적 일정하게 유지됨 |
React를 관통하는 핵심 철학은 세 가지입니다. 이 철학을 이해하면, React의 모든 기능이 왜 그렇게 설계되었는지 자연스럽게 이해됩니다.
"어떻게(How)" 대신 **"무엇(What)"**을 기술합니다.
비유: 식당 주문
명령형: "밀가루를 반죽하고, 토마토 소스를 만들고, 치즈를 올리고, 오븐에 넣어주세요"
선언형: "마르게리타 피자 하나 주세요"
주방에서 어떻게 만드는지는 식당(React)이 알아서 합니다.화면을 **독립적인 조각(컴포넌트)**으로 나누어 만듭니다.
블로그 페이지를 컴포넌트로 분리하면:
┌─────────────────────────────┐
│ <Header /> │
├─────────────────────────────┤
│ <Sidebar /> │ <Post /> │
│ │ ┌───────┐ │
│ │ │<Like/>│ │
│ │ └───────┘ │
│ │ <Comments/> │
├─────────────────────────────┤
│ <Footer /> │
└─────────────────────────────┘
각 컴포넌트는:
- 자기만의 화면(UI)을 담당합니다
- 자기만의 데이터(상태)를 관리합니다
- 다른 곳에서 재사용할 수 있습니다데이터는 항상 위에서 아래로 흐릅니다. 부모 컴포넌트에서 자식 컴포넌트로만 데이터를 전달합니다. 이렇게 하면 데이터가 어디서 왔는지 추적하기 쉽고, 버그를 찾기도 쉽습니다.
데이터 흐름:
<App /> ← 데이터의 원천
│
├── <Header /> ← App에서 데이터를 받음
│
└── <Post /> ← App에서 데이터를 받음
│
└── <Like /> ← Post에서 데이터를 받음
데이터가 위에서 아래로만 흐르기 때문에,
"이 데이터가 어디서 온 건지" 항상 추적할 수 있습니다.이 세 가지 철학은 앞으로의 시리즈에서 계속 등장합니다. 지금은 "이런 원칙이 있다" 정도만 기억해 두면 됩니다.
React 프로젝트를 만드는 도구는 여러 가지가 있지만, 이 시리즈에서는 **Vite(비트)**를 사용합니다. Vite는 프로젝트를 빠르게 생성하고 실행할 수 있게 도와주는 빌드 도구입니다.
터미널을 열고 아래 명령어를 실행합니다:
npm create vite@latest my-react-app실행하면 몇 가지 질문이 나타납니다. 아래와 같이 선택합니다:
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC💡 SWC가 무엇인가요?
SWC는 코드를 변환해 주는 도구입니다. TypeScript를 JavaScript로 바꾸는 등의 작업을 매우 빠르게 처리합니다. 자세한 내용은 몰라도 괜찮습니다. "더 빠른 옵션"이라고만 이해하면 됩니다.
프로젝트가 생성되었으면, 해당 폴더로 이동하고 필요한 패키지를 설치합니다:
cd my-react-app
npm install설치가 완료되면, 개발 서버를 실행합니다:
npm run dev터미널에 아래와 비슷한 메시지가 나타납니다:
VITE v6.x.x ready in xxx ms
➜ Local: http://localhost:5173/브라우저에서 http://localhost:5173에 접속하면 Vite + React 로고가 돌아가는 기본 화면이 나타납니다. 이 화면이 보이면 프로젝트 생성에 성공한 것입니다.
⚠️ 터미널에서
npm run dev를 실행한 상태를 유지해야 합니다. 터미널을 닫거나Ctrl + C를 누르면 개발 서버가 종료되어 브라우저에서 페이지가 표시되지 않습니다.
VSCode(또는 Cursor)에서 프로젝트 폴더를 열어봅니다:
cursor .아래와 같은 폴더 구조가 보입니다:
my-react-app/
├── node_modules/ ← 설치된 패키지들 (건드릴 일 없음)
├── public/ ← 정적 파일 (이미지 등)
├── src/ ← 우리가 코드를 작성하는 곳
│ ├── App.css
│ ├── App.tsx ← 메인 컴포넌트
│ ├── index.css
│ ├── main.tsx ← 앱의 시작점
│ └── vite-env.d.ts
├── index.html ← HTML 껍데기
├── package.json ← 프로젝트 정보 & 패키지 목록
├── tsconfig.json ← TypeScript 설정
└── vite.config.ts ← Vite 설정지금 중요한 파일은 딱 두 개입니다:
src/main.tsx — 앱의 시작점입니다. React 앱이 HTML 페이지에 연결되는 곳입니다.
📁 src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)이 코드는 "HTML의 root라는 id를 가진 요소에 <App /> 컴포넌트를 그려라"라는 뜻입니다. 이 파일은 거의 수정할 일이 없으므로, 이런 역할을 하는 파일이 있다는 것만 알면 됩니다.
src/App.tsx — 메인 컴포넌트입니다. 화면에 보이는 내용이 여기에 작성되어 있습니다.
src/App.tsx 파일의 기존 내용을 전부 지우고, 아래 코드로 교체합니다:
📁 src/App.tsx
function App() {
return (
<div>
<h1>안녕하세요!</h1>
<p>나의 첫 번째 React 앱입니다.</p>
</div>
);
}
export default App;파일을 저장하면(Ctrl + S), 브라우저가 자동으로 새로고침되면서 "안녕하세요!"가 화면에 나타납니다. 코드를 수정하고 저장할 때마다 브라우저에 즉시 반영되는 이 기능을 **HMR(Hot Module Replacement)**이라고 합니다.
레고 블록을 생각하면 이해하기 쉽습니다. 레고로 집을 만들 때, 벽 블록, 지붕 블록, 창문 블록을 따로 만들어서 조합합니다. 각 블록은 독립적으로 존재할 수 있고, 다른 구조물에서도 재사용할 수 있습니다. React의 컴포넌트도 마찬가지입니다.
React에서 컴포넌트는 함수입니다. HTML처럼 생긴 코드를 반환하는 함수를 작성하면, 그것이 컴포넌트가 됩니다.
// 이것이 컴포넌트입니다
function Greeting() {
return <h1>안녕하세요!</h1>;
}이 컴포넌트의 특징을 살펴보겠습니다:
Greeting처럼 첫 글자가 대문자입니다. 이것은 React의 규칙입니다. 소문자로 시작하면 HTML 태그(div, span 등)로 인식하고, 대문자로 시작하면 컴포넌트로 인식합니다.컴포넌트를 직접 만들어 보겠습니다. src 폴더 안에 Greeting.tsx라는 새 파일을 생성합니다.
📁 src/Greeting.tsx
function Greeting() {
return (
<div>
<h2>환영합니다!</h2>
<p>React 학습을 시작합니다.</p>
</div>
);
}
export default Greeting;마지막 줄의 export default Greeting은 "이 파일의 Greeting 컴포넌트를 다른 파일에서 사용할 수 있게 내보낸다"는 뜻입니다.
이제 App.tsx에서 이 컴포넌트를 사용합니다:
📁 src/App.tsx
import Greeting from "./Greeting"; // 👈 Greeting 컴포넌트를 가져옵니다
function App() {
return (
<div>
<h1>나의 React 앱</h1>
<Greeting /> {/* 👈 컴포넌트를 HTML 태그처럼 사용합니다 */}
</div>
);
}
export default App;저장하면 브라우저에 "나의 React 앱"과 "환영합니다!"가 함께 나타납니다.
핵심을 정리하면 이렇습니다:
export한다.import하고 <컴포넌트명 />으로 배치한다.컴포넌트의 진짜 힘은 재사용에 있습니다. 한 번 만들어두면 몇 번이든 사용할 수 있습니다.
📁 src/App.tsx
import Greeting from "./Greeting";
function App() {
return (
<div>
<h1>나의 React 앱</h1>
<Greeting />
<Greeting />
<Greeting />
</div>
);
}
export default App;저장하면 "환영합니다!" 영역이 3번 반복되어 나타납니다. 같은 코드를 3번 복사하지 않아도, 컴포넌트를 3번 배치하는 것만으로 충분합니다.
지금은 세 개가 완전히 동일한 내용을 보여주고 있습니다. 2편에서 배울 props를 사용하면, 같은 컴포넌트에 다른 데이터를 전달하여 각각 다른 내용을 표시할 수 있습니다.
"어디서부터 어디까지를 하나의 컴포넌트로 만들어야 하나?"는 처음에 많이 고민되는 부분입니다.
정답은 없지만, 실무에서 통용되는 기준은 아래와 같습니다:
| 기준 | 예시 |
|---|---|
| 반복되는 UI | 게시글 카드, 댓글 항목, 메뉴 아이템 |
| 독립적인 기능 단위 | 검색바, 로그인 폼, 네비게이션 |
| 페이지 영역 | Header, Footer, Sidebar |
처음에는 크게 나누고, 나중에 필요하면 더 작게 쪼개면 됩니다. 처음부터 완벽하게 나누려고 하지 않아도 괜찮습니다.
컴포넌트 안에서 작성한 HTML처럼 생긴 코드가 있었습니다. 이것은 실제 HTML이 아니라 **JSX(JavaScript XML)**라는 문법입니다.
// 이것은 HTML이 아니라 JSX입니다
function App() {
return <h1>안녕하세요!</h1>;
}JSX는 JavaScript 안에서 HTML과 유사한 구조를 작성할 수 있게 해주는 확장 문법입니다. 브라우저가 직접 이해할 수 있는 문법은 아니며, 빌드 과정에서 일반 JavaScript 코드로 변환됩니다.
JSX:
<h1>안녕하세요!</h1>
변환 후 JavaScript:
React.createElement('h1', null, '안녕하세요!')React 초기에는 React.createElement를 직접 작성해야 했지만, JSX 덕분에 훨씬 직관적으로 UI를 작성할 수 있게 되었습니다. JSX를 사용하는 이유는 읽기 쉽기 때문입니다.
JSX는 HTML과 비슷하지만, 몇 가지 중요한 차이점이 있습니다. 하나씩 살펴보겠습니다.
// ❌ 에러 — 두 개의 요소가 나란히 있음
function App() {
return (
<h1>제목</h1>
<p>내용</p>
);
}
// ✅ 올바른 방법 1 — div로 감싸기
function App() {
return (
<div>
<h1>제목</h1>
<p>내용</p>
</div>
);
}
// ✅ 올바른 방법 2 — Fragment 사용 (불필요한 div를 추가하고 싶지 않을 때)
function App() {
return (
<>
<h1>제목</h1>
<p>내용</p>
</>
);
}<>...</>는 Fragment라고 합니다. 실제 HTML에는 아무 태그도 추가하지 않으면서, JSX의 "하나의 부모" 규칙을 만족시키는 빈 껍데기입니다.
{}로 감쌉니다JSX 안에서 JavaScript 값을 사용하려면 중괄호 {}로 감싸면 됩니다.
function App() {
const name = "멋쟁이사자";
const currentYear = 2025;
return (
<div>
<h1>{name}처럼에 오신 것을 환영합니다!</h1>
<p>현재 연도: {currentYear}</p>
<p>간단한 계산: {10 + 20}</p>
</div>
);
}중괄호 안에는 값을 반환하는 모든 JavaScript 표현식을 넣을 수 있습니다. 변수, 계산, 함수 호출 등이 가능합니다.
// 중괄호 안에 넣을 수 있는 것들
{name} // 변수
{10 + 20} // 계산
{name.toUpperCase()} // 함수 호출
{`안녕, ${name}`} // 템플릿 리터럴
// 중괄호 안에 넣을 수 없는 것들
{if (true) {...}} // if문은 표현식이 아님
{for (let i...)} // for문은 표현식이 아님HTML에서 사용하던 일부 속성명이 JSX에서는 다릅니다. 이것은 JSX가 JavaScript이기 때문에, JavaScript의 예약어와 충돌하는 이름을 피하기 위한 것입니다.
| HTML | JSX | 이유 |
|---|---|---|
class | className | class는 JavaScript의 예약어 |
for | htmlFor | for는 JavaScript의 예약어 |
onclick | onClick | JSX는 카멜케이스(camelCase) 사용 |
tabindex | tabIndex | JSX는 카멜케이스 사용 |
// ❌ HTML 방식
<div class="container">
<label for="email">이메일</label>
</div>
// ✅ JSX 방식
<div className="container">
<label htmlFor="email">이메일</label>
</div>HTML에서는 <br>, <img>, <input> 같은 태그를 닫지 않아도 되었지만, JSX에서는 모든 태그를 반드시 닫아야 합니다.
// ❌ JSX에서 에러
<br>
<img src="photo.jpg">
<input type="text">
// ✅ 자기 스스로 닫는(self-closing) 태그 사용
<br />
<img src="photo.jpg" />
<input type="text" />HTML에서는 스타일을 문자열로 작성했지만, JSX에서는 JavaScript 객체로 전달합니다.
// HTML 방식
<div style="background-color: blue; font-size: 16px;">
// JSX 방식
<div style={{ backgroundColor: "blue", fontSize: "16px" }}>중괄호가 두 겹인 이유는, 바깥 {}는 "JSX 안에서 JavaScript를 쓴다"는 의미이고, 안쪽 {}는 JavaScript 객체를 나타내기 때문입니다. 그리고 CSS 속성명이 background-color 대신 backgroundColor처럼 카멜케이스로 바뀌는 점도 주의합니다.
| 규칙 | 핵심 |
|---|---|
| 하나의 부모 요소 | <div> 또는 <> (Fragment)로 감싸기 |
| JavaScript 표현식 | 중괄호 {} 사용 |
| 속성명 | className, htmlFor, 카멜케이스 |
| 태그 닫기 | 모든 태그 반드시 닫기 (<br />) |
| 인라인 스타일 | 객체로 전달, 속성명 카멜케이스 |
배운 내용을 활용하여 간단한 자기소개 카드를 만들어 보겠습니다.
📁 src/ProfileCard.tsx
function ProfileCard() {
const name = "홍길동";
const role = "프론트엔드 개발 지망생";
const skills = "HTML, CSS, JavaScript, React";
return (
<div
style={{
border: "1px solid #ddd",
borderRadius: "12px",
padding: "24px",
maxWidth: "300px",
textAlign: "center",
}}
>
<div
style={{
width: "80px",
height: "80px",
borderRadius: "50%",
backgroundColor: "#4F46E5",
color: "white",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "32px",
margin: "0 auto 16px",
}}
>
{name[0]}
</div>
<h2 style={{ margin: "0 0 8px" }}>{name}</h2>
<p style={{ color: "#666", margin: "0 0 12px" }}>{role}</p>
<p style={{ fontSize: "14px", color: "#888" }}>기술 스택: {skills}</p>
</div>
);
}
export default ProfileCard;{name[0]}은 이름의 첫 번째 글자를 가져오는 표현식입니다. "홍길동"의 [0]은 "홍"입니다.
📁 src/App.tsx
import ProfileCard from "./ProfileCard";
function App() {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
backgroundColor: "#f5f5f5",
}}
>
<ProfileCard />
</div>
);
}
export default App;저장하면, 화면 중앙에 자기소개 카드가 나타납니다. 이름의 첫 글자가 보라색 원 안에 표시되고, 이름과 역할, 기술 스택이 나열됩니다.
아래 항목들을 직접 변경해 보면서 JSX와 컴포넌트에 익숙해지는 것을 권장합니다:
name, role, skills 변수를 자신의 정보로 바꿔보기backgroundColor를 다른 색상으로 바꿔보기<ProfileCard />를 여러 개 배치해서 여러 명의 카드를 표시해 보기이번 1편에서 다룬 내용을 정리합니다.
📌 핵심 정리
- React는 "데이터가 바뀌면 화면을 다시 그린다"는 아이디어로 만들어진 UI 라이브러리입니다.
- React의 핵심 철학은 선언적 UI, 컴포넌트 기반, 단방향 데이터 흐름입니다.
- 컴포넌트는 UI의 독립적인 조각이며, 대문자로 시작하는 함수로 만듭니다.
- JSX는 JavaScript 안에서 HTML과 유사한 구조를 작성할 수 있게 해주는 문법입니다.
- 중괄호
{}를 사용하면 JSX 안에서 JavaScript 값을 사용할 수 있습니다.
다음 2편에서는 props와 state를 배웁니다. props를 사용하면 컴포넌트에 다른 데이터를 전달할 수 있고, state를 사용하면 사용자의 동작에 반응하는 인터랙티브한 UI를 만들 수 있습니다. 1편에서 만든 자기소개 카드를 props를 활용하여 여러 명의 카드로 확장하는 것부터 시작합니다.
댓글 0