Guider/Frontend/FrontendDevGuide0006
Frontend#06

FrontendDevGuide0006

React 입문

⚛️ React 입문 — 컴포넌트로 웹 앱 만들기 완전 정복

📋 6단계 학습 개요 | 컴포넌트로 화면 조각 만들기 | 예상 학습 기간: 4~6주

React는 Facebook(Meta)이 만든 UI 라이브러리입니다.
웹페이지를 레고 블록(컴포넌트)처럼 잘게 나눠서 만들고, 조립하는 방식으로 개발합니다.
현재 전 세계에서 가장 인기 있는 프론트엔드 기술이며, 국내 채용 공고의 약 80%가 React를 요구합니다.

🆚 1. React vs 바닐라 JS — 왜 React를 쓰는가?

React vs 바닐라 JavaScript 비교표

📌 비유: 바닐라 JS로 만드는 것은 레고 블록 없이 모래성 쌓기.
React는 표준화된 레고 블록으로 집 짓기 — 같은 블록을 여러 곳에 재사용하고, 고장나면 그 블록만 교체!

🔄 2. Virtual DOM — React의 핵심 원리

Virtual DOM 동작 원리


🚀 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

컴포넌트 트리와 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와 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]);

React 컴포넌트 생명주기


🪝 7. 모든 Hooks 완전 정리

React 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 프로젝트 구조 (현업 표준)

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 — 실전 프로젝트로 계속 도전하세요! 🚀
반응형