🎯 학습 목표
- jQuery/Vanilla JS DOM 조작 방식의 한계를 이해하고, React가 왜 필요한지 설명할 수 있다
- Virtual DOM의 동작 원리(diffing, reconciliation)를 이해한다
- Vite로 React 프로젝트를 생성하고 각 파일의 역할을 설명할 수 있다
- React StrictMode가 무엇인지, 왜 개발 환경에서 두 번 실행되는지 이해한다
- 컴포넌트 기반 개발의 철학을 이해한다
📖 핵심 개념 1 — 왜 React인가?
웹 개발 초기에는 jQuery나 Vanilla JS로 DOM을 직접 조작하는 방식이 표준이었습니다. 버튼을 클릭하면 document.getElementById로 요소를 찾아 텍스트를 바꾸거나, AJAX로 데이터를 받아와 HTML 문자열을 조립해 innerHTML에 집어넣었죠.
소규모 페이지에서는 이 방식이 잘 동작합니다. 하지만 애플리케이션 규모가 커지면 심각한 문제가 발생합니다.
Vanilla JS DOM 조작의 문제점
// ❌ jQuery 시절 방식 — 상태와 UI가 뒤섞임
let cartCount = 0;
let userLoggedIn = false;
let items = [];
function addToCart(item) {
cartCount++;
items.push(item);
// DOM을 직접 찾아서 수동으로 업데이트해야 함
$('#cart-count').text(cartCount);
$('#cart-badge').show();
// 로그인 상태에 따라 버튼도 바꿔야 함
if (userLoggedIn) {
$('#checkout-btn').prop('disabled', false);
}
// 장바구니 목록도 다시 그려야 함
let html = '';
items.forEach(function(i) {
html += '' + i.name + ' - ' + i.price + '원 ';
});
$('#cart-list').html(html);
// 총합도 계산해서 업데이트
let total = items.reduce((sum, i) => sum + i.price, 0);
$('#cart-total').text(total + '원');
}
이 코드의 문제점은 명확합니다. 상태(데이터)와 뷰(UI)가 분리되지 않았고, 상태가 바뀔 때마다 어떤 DOM 요소를 어떻게 업데이트해야 하는지를 개발자가 일일이 추적해야 합니다. 기능이 추가될수록 이 관계가 스파게티처럼 얽힙니다. 패널에서 “도메인 로직과 DOM 조작이 같은 함수 안에 공존하는 것이 최대 문제”라고 지적했습니다.
React의 해결 방식
// ✅ React 방식 — 상태만 변경하면 UI는 자동으로 따라옴
import { useState } from 'react';
function Cart() {
const [items, setItems] = useState([]);
function addToCart(item) {
// 상태만 변경. DOM은 React가 알아서 업데이트함
setItems(prev => [...prev, item]);
}
const total = items.reduce((sum, i) => sum + i.price, 0);
return (
<div>
<span>장바구니 ({items.length}개)</span>
<ul>
{items.map(item => (
<li key={item.id}>{item.name} - {item.price}원</li>
))}
</ul>
<strong>총합: {total}원</strong>
</div>
);
}
React는 “UI는 상태의 함수다 (UI = f(state))”라는 철학을 따릅니다. 개발자는 상태만 관리하면 되고, React가 상태 변화에 맞춰 UI를 효율적으로 업데이트합니다.
📖 핵심 개념 2 — Virtual DOM
React가 “효율적으로” UI를 업데이트할 수 있는 비밀이 바로 Virtual DOM입니다. 실제 DOM 조작은 느립니다. 브라우저는 DOM이 변경될 때마다 레이아웃 계산, 페인트 등의 작업을 수행하기 때문입니다.
React는 이를 최소화하기 위해 메모리 안에 가상의 DOM 트리를 유지합니다. 상태가 변경되면 다음 순서로 동작합니다.
- 렌더(Render Phase): React가 새로운 Virtual DOM 트리를 생성합니다.
- Diffing: 이전 Virtual DOM과 새 Virtual DOM을 비교해 변경된 부분만 찾아냅니다.
- Reconciliation(재조정): 찾아낸 변경 사항만 실제 DOM에 반영합니다.
// 예: 카운터 상태 변화 시 Virtual DOM diffing
// 이전 Virtual DOM
<div>
<h1>카운트: 0</h1> // 이 부분만 변경됨
<button>+1</button> // 변경 없음 → 실제 DOM 건드리지 않음
</div>
// 새 Virtual DOM (setState 후)
<div>
<h1>카운트: 1</h1> // ← React는 이 텍스트 노드만 실제 DOM에 반영
<button>+1</button>
</div>
이 과정 덕분에 개발자가 매번 어떤 DOM을 업데이트할지 추적하지 않아도, React가 최소한의 DOM 변경으로 화면을 갱신합니다. 패널에서는 “Virtual DOM의 진짜 가치는 속도보다 개발자 경험(DX) 향상에 있다”고 강조했습니다. 상태만 신경 쓰면 된다는 점에서 코드 복잡도가 획기적으로 줄어듭니다.
📖 핵심 개념 3 — Vite로 프로젝트 생성
과거에는 Create React App(CRA)이 표준이었지만, 현재는 Vite가 사실상 표준으로 자리잡았습니다. Vite는 개발 서버 시작이 매우 빠르고, 핫 모듈 교체(HMR)가 즉각적입니다.
// 터미널에서 실행
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
생성된 프로젝트 구조를 살펴보겠습니다.
my-react-app/
├── public/ // 정적 파일 (빌드 시 그대로 복사됨)
│ └── vite.svg
├── src/ // 소스 코드
│ ├── assets/ // 이미지, 폰트 등 에셋
│ ├── App.css // App 컴포넌트 스타일
│ ├── App.jsx // 루트 컴포넌트
│ ├── index.css // 전역 스타일
│ └── main.jsx // 앱 진입점
├── index.html // Vite의 진입 HTML (CRA와 다르게 루트에 위치)
├── package.json
└── vite.config.js // Vite 설정 파일
각 파일의 역할
main.jsx — 앱의 진입점. React를 실제 DOM에 마운트합니다.
// src/main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
// index.html의 <div id="root"></div>에 React 앱을 마운트
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
App.jsx — 루트 컴포넌트. 앱의 최상위 UI 구조를 정의합니다.
// src/App.jsx
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<div>
<h1>Hello React + Vite</h1>
<button onClick={() => setCount(count + 1)}>
클릭 수: {count}
</button>
</div>
)
}
export default App
vite.config.js — Vite 빌드 도구 설정.
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
// 개발 서버 포트 변경 예시
// server: { port: 3000 }
})
📖 핵심 개념 4 — React StrictMode
<StrictMode>는 개발 중 잠재적인 문제를 조기에 발견하도록 도와주는 React의 도구입니다. 프로덕션 빌드에는 영향 없고, 개발 모드에서만 활성화됩니다.
StrictMode가 하는 일:
- 컴포넌트를 두 번 렌더링합니다 (순수하지 않은 렌더링을 감지하기 위해)
- Effect를 두 번 실행합니다 (cleanup이 제대로 구현되었는지 확인)
- deprecated API 사용 시 경고를 표시합니다
// StrictMode 때문에 콘솔에 로그가 두 번 찍히는 현상 — 정상입니다
function MyComponent() {
console.log('렌더링됨'); // 개발 모드에서 두 번 출력됨 (정상)
return <div>안녕</div>;
}
// StrictMode를 제거하면 한 번만 실행됨 — 하지만 권장하지 않음
// main.jsx에서 <StrictMode> 태그를 제거하면 비활성화됨
패널에서는 “StrictMode 때문에 두 번 실행된다며 제거하는 개발자를 자주 본다. 하지만 이 두 번 실행이 side effect 버그를 조기에 잡아주는 핵심 도구이므로 절대 제거하지 말라”고 강하게 권고했습니다.
💻 코드 예제 — 첫 번째 React 앱
// src/App.jsx — 간단한 카운터 앱
import { useState } from 'react';
function Counter() {
// state 선언: count는 현재 값, setCount는 값을 바꾸는 함수
const [count, setCount] = useState(0);
return (
<div style={{ textAlign: 'center', padding: '20px' }}>
<h2>카운터</h2>
<p style={{ fontSize: '48px' }}>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(0)}>초기화</button>
</div>
);
}
export default Counter;
이 예제는 React의 핵심 철학을 보여줍니다. setCount를 호출하면 React가 자동으로 컴포넌트를 다시 렌더링하고 화면을 업데이트합니다. 개발자는 DOM을 직접 건드리지 않아도 됩니다.
⚠️ 흔한 실수 (よくあるミス)
- 컴포넌트 이름을 소문자로 작성:
<myComponent />는 HTML 태그로 인식됩니다. 반드시 대문자로 시작해야 합니다:<MyComponent /> - StrictMode 제거: “두 번 실행되니 제거하자”는 접근은 버그를 숨기는 행위입니다.
- public과 src/assets 혼동:
public/의 파일은 URL로 직접 참조(/logo.png),src/assets/의 파일은 import로 참조합니다. 에셋은 가급적src/assets/에 두어 번들 최적화 혜택을 받으세요. - node_modules를 git에 커밋:
.gitignore에node_modules가 반드시 포함되어야 합니다. Vite 템플릿은 자동으로 추가해줍니다. - index.html을 src/로 이동: Vite에서는
index.html이 프로젝트 루트에 있어야 합니다. CRA와 다른 점입니다.
💡 실무 팁
- VS Code 확장 필수 설치: ES7+ React/Redux/React-Native snippets, Prettier, ESLint.
rafce단축어로 함수형 컴포넌트를 즉시 생성할 수 있습니다. - React DevTools 설치: Chrome/Firefox 브라우저 확장으로 컴포넌트 트리와 state를 실시간으로 확인할 수 있습니다. 개발 시 필수 도구입니다.
- 파일 확장자 .jsx vs .js: JSX 문법이 포함된 파일은
.jsx, 순수 JS는.js로 구분하면 팀 협업 시 파일 역할을 즉시 파악할 수 있습니다. - Vite 개발 서버 포트 변경:
vite.config.js에server: { port: 3000 }을 추가하면 포트를 변경할 수 있습니다. - 프로덕션 빌드:
npm run build로dist/폴더에 최적화된 파일이 생성됩니다.npm run preview로 로컬에서 빌드 결과를 미리 볼 수 있습니다.