⚛️ React 입문 — 컴포넌트로 웹 앱 만들기 완전 정복
📋 6단계 학습 개요 | 컴포넌트로 화면 조각 만들기 | 예상 학습 기간: 4~6주
React는 Facebook(Meta)이 만든 UI 라이브러리입니다.
웹페이지를 레고 블록(컴포넌트)처럼 잘게 나눠서 만들고, 조립하는 방식으로 개발합니다.
현재 전 세계에서 가장 인기 있는 프론트엔드 기술이며, 국내 채용 공고의 약 80%가 React를 요구합니다.
🆚 1. React vs 바닐라 JS — 왜 React를 쓰는가?
📌 비유: 바닐라 JS로 만드는 것은 레고 블록 없이 모래성 쌓기.
React는 표준화된 레고 블록으로 집 짓기 — 같은 블록을 여러 곳에 재사용하고, 고장나면 그 블록만 교체!
🔄 2. Virtual DOM — React의 핵심 원리
🚀 3. React 프로젝트 시작하기
# Vite로 React 프로젝트 생성 (권장 — Create React App보다 훨씬 빠름)
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev
# TypeScript 버전 (현업 표준)
npm create vite@latest my-app -- --template react-ts
🔑 Vite vs Create React App
• Vite — 2024년 이후 현업 표준. 빌드 속도 10~100배 빠름, HMR(핫 리로드) 즉각 반응
• Create React App — 느리고 구식, 2023년부터 공식 지원 축소
→ 새 프로젝트는 무조건 Vite + React 조합 사용!
📝 4. JSX 문법 완전 정복
JSX는 JavaScript 안에서 HTML처럼 쓸 수 있는 React의 특수 문법입니다.
브라우저는 JSX를 직접 이해하지 못하지만, Babel/Vite가 일반 JS로 변환해줍니다.
// JSX 기본 규칙
function MyComponent() {
const name = '홍길동';
const isLoggedIn = true;
const items = ['사과', '바나나', '딸기'];
return (
// ① 반드시 하나의 루트 요소로 감싸기
<div className="container">
{/* ② 주석은 이렇게 씁니다 */}
{/* ③ JS 표현식은 중괄호 {}로 */}
<h1>안녕하세요, {name}님!</h1>
<p>현재 시각: {new Date().toLocaleTimeString('ko-KR')}</p>
{/* ④ class → className, for → htmlFor */}
<label htmlFor="email" className="label">이메일</label>
<input id="email" type="email" />
{/* ⑤ 조건부 렌더링 */}
{isLoggedIn ? <p>로그인됨 ✅</p> : <p>로그아웃 상태</p>}
{isLoggedIn && <button>로그아웃</button>}
{/* ⑥ 리스트 렌더링 - 반드시 key 속성! */}
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
{/* ⑦ Fragment 사용 (불필요한 div 없애기) */}
</div>
);
}
⚠️ JSX key prop 중요성:
리스트 렌더링 시 key는 React가 어떤 항목이 변경됐는지 식별하는 데 사용됩니다.
배열 인덱스(index) 대신 고유한 id값을 key로 사용하는 것이 현업 표준입니다!
예:key={item.id}— 순서 변경, 삭제 시 버그 방지
🧩 5. 컴포넌트 & Props
// ① 함수형 컴포넌트 기본 (현업 표준)
function Button({ text, onClick, variant = 'primary', disabled = false }) {
return (
<button
className={'btn btn-' + variant}
onClick={onClick}
disabled={disabled}
>
{text}
</button>
);
}
// 사용 예시
function App() {
return (
<div>
<Button text="저장" onClick={() => alert('저장!')} />
<Button text="삭제" onClick={handleDelete} variant="danger" />
<Button text="로딩 중..." disabled={true} />
</div>
);
}
// ② 자식 → 부모 데이터 전달 (콜백 패턴)
function Child({ onReport }) {
return <button onClick={() => onReport('오류 발생!')}>신고하기</button>;
}
function Parent() {
const handleReport = (message) => {
console.log('자식에서 받은 메시지:', message);
};
return <Child onReport={handleReport} />;
}
// ③ children prop — 컴포넌트 사이 내용 전달
function Card({ title, children }) {
return (
<div className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</div>
);
}
// 사용: <Card title="제목"><p>카드 내용</p></Card>
🪝 6. useState & useEffect — 핵심 훅 마스터
// ===== useState 실전 패턴 =====
// ① 객체 state 업데이트 (스프레드 연산자 필수!)
const [user, setUser] = useState({ name: '', email: '', age: 0 });
const updateName = (name) => setUser(prev => ({ ...prev, name }));
// ② 배열 state 조작
const [items, setItems] = useState([]);
const addItem = (item) => setItems(prev => [...prev, item]);
const removeItem = (id) => setItems(prev => prev.filter(i => i.id !== id));
const updateItem = (id, data) => setItems(prev =>
prev.map(i => i.id === id ? { ...i, ...data } : i)
);
// ③ 로딩/에러 상태 패턴 (현업 필수 패턴)
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// ===== useEffect 실전 패턴 =====
// ④ 데이터 페칭 (마운트 시)
useEffect(() => {
let isMounted = true; // 언마운트 후 setState 방지
const fetchUsers = async () => {
setLoading(true);
try {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('서버 오류');
const json = await res.json();
if (isMounted) setData(json);
} catch (err) {
if (isMounted) setError(err.message);
} finally {
if (isMounted) setLoading(false);
}
};
fetchUsers();
return () => { isMounted = false; }; // cleanup
}, []);
// ⑤ 검색어 변경 시 재요청
const [query, setQuery] = useState('');
useEffect(() => {
if (!query) return;
const timer = setTimeout(() => {
fetch('/api/search?q=' + query)
.then(r => r.json())
.then(setData);
}, 300); // debounce 300ms
return () => clearTimeout(timer); // cleanup
}, [query]);
🪝 7. 모든 Hooks 완전 정리
// ===== useRef =====
import { useRef, useEffect } from 'react';
function SearchInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // 마운트 시 자동 포커스
}, []);
// 리렌더링 없이 값 추적 (이전 값 기억)
const prevCountRef = useRef(0);
useEffect(() => {
prevCountRef.current = count;
});
return <input ref={inputRef} type="text" />;
}
// ===== useContext — 전역 상태 공유 =====
import { createContext, useContext, useState } from 'react';
// 1. Context 생성
const ThemeContext = createContext('light');
// 2. Provider로 감싸기
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
</ThemeContext.Provider>
);
}
// 3. 어디서든 사용
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<header className={'header-' + theme}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
테마 전환
</button>
</header>
);
}
// ===== useMemo / useCallback — 성능 최적화 =====
import { useMemo, useCallback } from 'react';
// useMemo: 계산 결과 캐싱
const expensiveResult = useMemo(() => {
return items.filter(i => i.price > 10000).reduce((sum, i) => sum + i.price, 0);
}, [items]); // items 바뀔 때만 재계산
// useCallback: 함수 캐싱 (자식 컴포넌트에 넘길 때)
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(i => i.id !== id));
}, []); // 빈 배열: 최초 1회만 생성
// ===== 커스텀 훅 만들기 =====
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// 사용: 재사용 가능한 데이터 패칭
const { data: users, loading } = useFetch('/api/users');
const { data: posts } = useFetch('/api/posts');
🗺️ 8. React Router — 페이지 이동 (SPA 라우팅)
npm install react-router-dom
import { BrowserRouter, Routes, Route, Link, NavLink, useNavigate, useParams } from 'react-router-dom';
// App.jsx — 라우터 설정
function App() {
return (
<BrowserRouter>
<nav>
{/* NavLink: 현재 페이지 active 클래스 자동 추가 */}
<NavLink to="/" end>홈</NavLink>
<NavLink to="/about">소개</NavLink>
<NavLink to="/posts">게시판</NavLink>
</nav>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/posts" element={<PostsPage />} />
<Route path="/posts/:id" element={<PostDetailPage />} />
<Route path="*" element={<NotFoundPage />} /> {/* 404 */}
</Routes>
</BrowserRouter>
);
}
// 동적 파라미터 사용
function PostDetailPage() {
const { id } = useParams(); // URL의 :id 값 가져오기
const navigate = useNavigate(); // 프로그래밍 방식 이동
const { data: post, loading } = useFetch('/api/posts/' + id);
if (loading) return <div>로딩 중...</div>;
return (
<div>
<h1>{post?.title}</h1>
<button onClick={() => navigate(-1)}>뒤로 가기</button>
<button onClick={() => navigate('/posts')}>목록으로</button>
</div>
);
}
📁 9. React 프로젝트 구조 (현업 표준)
🎯 10. 실전 프로젝트 — Todo 앱 완성판
useState + useEffect + localStorage + 필터링 + 수정/삭제 — 면접에서도 자주 나오는 완성형 Todo 앱
import { useState, useEffect, useCallback } from 'react';
// TodoItem 컴포넌트
function TodoItem({ todo, onToggle, onDelete, onEdit }) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const handleSave = () => {
if (editText.trim()) {
onEdit(todo.id, editText.trim());
setIsEditing(false);
}
};
return (
<li className={'todo-item' + (todo.done ? ' done' : '')}>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
{isEditing ? (
<>
<input value={editText} onChange={e => setEditText(e.target.value)} />
<button onClick={handleSave}>저장</button>
<button onClick={() => setIsEditing(false)}>취소</button>
</>
) : (
<>
<span>{todo.text}</span>
<button onClick={() => setIsEditing(true)}>수정</button>
<button onClick={() => onDelete(todo.id)}>삭제</button>
</>
)}
</li>
);
}
// 메인 앱
function TodoApp() {
const [todos, setTodos] = useState(() => {
// localStorage에서 초기값 로드 (lazy initialization)
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});
const [input, setInput] = useState('');
const [filter, setFilter] = useState('all'); // all | active | done
// localStorage 동기화
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = () => {
if (!input.trim()) return;
setTodos(prev => [...prev, {
id: Date.now(),
text: input.trim(),
done: false,
createdAt: new Date().toISOString(),
}]);
setInput('');
};
const toggleTodo = useCallback((id) =>
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t))
, []);
const deleteTodo = useCallback((id) =>
setTodos(prev => prev.filter(t => t.id !== id))
, []);
const editTodo = useCallback((id, text) =>
setTodos(prev => prev.map(t => t.id === id ? { ...t, text } : t))
, []);
const filteredTodos = todos.filter(t => {
if (filter === 'active') return !t.done;
if (filter === 'done') return t.done;
return true;
});
const doneCount = todos.filter(t => t.done).length;
return (
<div className="todo-app">
<h1>📝 Todo App</h1>
<p>완료: {doneCount} / 전체: {todos.length}</p>
{/* 입력 */}
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addTodo()}
placeholder="할 일을 입력하세요..."
/>
<button onClick={addTodo}>추가</button>
</div>
{/* 필터 */}
<div>
{['all', 'active', 'done'].map(f => (
<button key={f} onClick={() => setFilter(f)}
className={filter === f ? 'active' : ''}>
{f === 'all' ? '전체' : f === 'active' ? '진행중' : '완료'}
</button>
))}
</div>
{/* 목록 */}
<ul>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>
))}
</ul>
{todos.some(t => t.done) && (
<button onClick={() => setTodos(prev => prev.filter(t => !t.done))}>
완료 항목 모두 삭제
</button>
)}
</div>
);
}
export default TodoApp;
🔥 11. 현업 필수 개념들
① CSS Modules — 스타일 충돌 방지
// Button.module.css
.button { padding: 8px 16px; border-radius: 4px; }
.primary { background: #3498db; color: white; }
// Button.jsx
import styles from './Button.module.css';
function Button({ variant = 'primary', children }) {
return <button className={styles.button + ' ' + styles[variant]}>{children}</button>;
}
② 환경변수 설정 (.env)
# .env.local (git에 올리지 않음!)
VITE_API_URL=https://api.example.com
VITE_GOOGLE_MAPS_KEY=AIzaSy...
# 사용 방법
const apiUrl = import.meta.env.VITE_API_URL;
③ React.memo — 불필요한 리렌더링 방지
import { memo } from 'react';
// memo로 감싸면 props가 변경될 때만 리렌더링
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
console.log('렌더링됨');
return <div>{data.name}</div>;
});
④ 에러 바운더리 (Error Boundary)
// 에러 바운더리는 클래스 컴포넌트로 만들어야 합니다
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>앗, 오류가 발생했습니다. 새로고침해 주세요.</div>;
}
return this.props.children;
}
}
// 사용
<ErrorBoundary>
<MyRiskyComponent />
</ErrorBoundary>
⑤ React Query (TanStack Query) — 서버 상태 관리
npm install @tanstack/react-query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// 데이터 조회 — 캐싱, 자동 리패칭, 로딩/에러 처리 자동화
function UserList() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
staleTime: 1000 * 60 * 5, // 5분간 캐시 유지
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// 데이터 변경 (POST/PUT/DELETE)
function AddUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newUser) => fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] }); // 목록 자동 갱신
},
});
return <button onClick={() => mutation.mutate({ name: '새 유저' })}>추가</button>;
}
📦 12. 상태 관리 라이브러리 선택 가이드
언제 어떤 걸 쓸까?
• 로컬 UI 상태 (모달 열림, 입력값) → useState
• 서버 데이터 캐싱 (API 응답) → React Query (TanStack Query) ★현업 1순위
• 전역 UI 상태 (테마, 언어, 인증) → Context API 또는 Zustand
• 복잡한 앱 전체 상태 → Zustand 또는 Redux Toolkit
2025년 현업 트렌드: useState + React Query + Zustand 조합이 가장 대중적
// Zustand — 간단하고 강력한 전역 상태 관리
npm install zustand
import { create } from 'zustand';
const useCartStore = create((set, get) => ({
items: [],
addItem: (item) => set(state => ({ items: [...state.items, item] })),
removeItem: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) })),
total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));
// 어디서든 바로 사용!
function CartButton() {
const { items, addItem } = useCartStore();
return <button>장바구니 ({items.length})</button>;
}
✅ 6단계 학습 체크리스트
✅ 기초 (반드시 마스터)
☐ Vite로 React 프로젝트 생성
☐ JSX 문법 (조건부 렌더링, 리스트 렌더링, key prop)
☐ 함수형 컴포넌트 + props 전달
☐ useState로 상태 관리
☐ useEffect로 API 데이터 불러오기
🚀 심화 (현업 수준)
☐ useRef, useContext, useMemo, useCallback
☐ 커스텀 훅 만들기 (useFetch, useForm)
☐ React Router v6 페이지 라우팅
☐ CSS Modules 스타일링
☐ React Query로 서버 상태 관리
☐ Zustand 전역 상태 관리
🏆 실전 미션
☐ Todo 앱 (localStorage + 필터링 + 수정/삭제)
☐ 영화 검색 앱 (TMDB API + React Query + 라우팅)
☐ 로그인/회원가입 폼 (React Hook Form + 유효성 검사)
🎯 다음 단계는?
React의 기초를 다졌다면, 이제 실제 프로젝트를 만들 차례입니다!
포트폴리오에 들어갈 실전 프로젝트를 처음부터 끝까지 완성해봅시다.
→ FrontendDevGuide0007 — 실전 프로젝트로 계속 도전하세요! 🚀
반응형