minstudio

리액트 소개: 가상 DOM과 선언적 프로그래밍 이해하기

React의 기본 철학과 탄생 배경

리액트(React)는 사용자 인터페이스를 구축하기 위한 자바스크립트 라이브러리입니다. 기존의 제이쿼리(jQuery)나 바닐라 자바스크립트 방식은 DOM을 직접 찾아 조작하는 명령형(Imperative) 프로그래밍이었습니다.
반면, 리액트는 "화면에 무엇(What)이 보여야 하는가"를 선언하는 선언적(Declarative) 프로그래밍 방식을 채택했습니다. 상태(State)가 변경되면 리액트가 알아서 DOM을 업데이트합니다.
명령형 vs 선언적 프로그래밍 비교
❌ 명령형 (Vanilla JS)
"버튼을 찾고, 클릭 이벤트를 달고, 텍스트를 'Clicked!'로 바꾼다"
const button = document.getElementById('btn');
button.addEventListener('click', () => {
    button.innerText = 'Clicked!';
});
⭕ 선언적 (React)
"상태(clicked)에 따라 버튼 텍스트가 이렇게 보여야 한다"
import { useState } from 'react';

function App() {
  const [clicked, setClicked] = useState(false);
  
  return (
    
  );
}

가상 DOM (Virtual DOM) 이란?

리액트는 실제 DOM을 조작하기 전에, 메모리상에 가상의 DOM 트리를 만듭니다. 상태(State) 변경이 발생하면 새로운 가상 DOM을 생성하고, 이전 가상 DOM과 비교(Diffing)하여 실제 변경된 부분만 브라우저 DOM에 반영(Patch)합니다. 이를 통해 느린 실제 DOM 조작을 최소화하고 렌더링 성능을 극대화합니다.

Real DOM vs Virtual DOM 비교

Real DOM (비용이 큼) 하나만 바뀌어도 전체 화면 재연산 발생 VS Virtual DOM (빠르고 가벼움) 가상 메모리에서 변경점만 비교(Diffing) 후 반영


가상 DOM의 패치(Patch) 과정

Old Virtual DOM Diffing 비교 알고리즘 New Virtual DOM Patch 반영 Real Browser DOM 변경된 2개의 노드만 실제 화면에 렌더링 됨
새로운 가상 DOM에서 빨간색으로 표시된 변경점만 찾아 실제 브라우저 DOM에 한 번에 업데이트(Patch)합니다.
Vite 기반 프로젝트 시작하기: 개발 환경 세팅과 폴더 구조

왜 CRA 대신 Vite인가요?

과거에는 React 프로젝트를 시작할 때 Create React App (CRA)를 표준처럼 사용했습니다. 그러나 웹팩(Webpack) 기반의 CRA는 프로젝트 규모가 커질수록 빌드 및 로컬 서버 시작 속도가 느려지는 치명적인 단점이 있습니다.
현재의 표준은 Vite(바이트)입니다. 프랑스어로 '빠르다'를 뜻하는 Vite는 브라우저의 네이티브 ES Module을 활용하여 대규모 프로젝트에서도 즉각적인 HMR(Hot Module Replacement)과 초고속 서버 구동을 보장합니다.
bash - React Installation
$ npm create vite@latest my-react-app -- --template react

➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
명령어 상세 설명
  • npm create vite@latest : 가장 최신 버전의 Vite 툴을 실행하여 스캐폴딩(초기 세팅)을 시작합니다.
  • my-react-app : 생성될 프로젝트의 폴더 이름입니다. 원하는 이름으로 자유롭게 변경 가능합니다.
  • -- : npm에게 "이 뒤에 나오는 옵션들은 npm 자체가 아닌, 실행할 패키지(Vite)에게 직접 전달해라"라는 의미를 가지는 관례적인 구분자(Separator)입니다.
  • --template react : 여러 지원 환경 중 순수 React 템플릿을 사용하여 자동 구성하겠다는 옵션 명령어입니다. (TypeScript를 쓰고 싶다면 react-ts를 입력합니다)

Vite 프로젝트 폴더 구조

my-app
node_modules/
public/
favicon.svg
src/
assets/
App.css - App 컴포넌트 전용 스타일
App.jsx - 최상위 루트 컴포넌트
index.css - 전체 앱 공통 스타일
main.jsx - React 렌더링 엔트리 포인트
.gitignore
eslint.config.js - 코드 린터(Linter) 설정 파일
index.html - 메인 HTML 파일 (Vite의 진입점)
package-lock.json
package.json - 프로젝트 의존성 라이브러리 및 스크립트
README.md
vite.config.js - Vite 빌드 도구 설정 파일

💡 StrictMode란 무엇인가요?

main.jsx 파일을 보면 <StrictMode> 컴포넌트가 전체 앱을 감싸고 있는 것을 볼 수 있습니다.
Strict Mode(엄격 모드)는 애플리케이션 내의 잠재적인 문제를 찾아내기 위한 도구입니다. UI를 렌더링하지 않으며, 하위 컴포넌트들에 대해 부가적인 문법 검사와 경고를 활성화합니다. (예: 안전하지 않은 생명주기 메서드 사용 경고, 레거시 API 사용 경고 등)
핵심 특징: 개발 모드에서 2번 렌더링 React 18 이상에서 개발 모드(Development)로 실행할 경우, Strict Mode는 컴포넌트의 렌더링 과정(예: useEffect)을 의도적으로 두 번씩 실행합니다.
이는 불순물(Side Effect)이 있는 렌더링 로직이나 메모리 누수를 개발자가 빨리 발견하고 수정할 수 있도록 돕기 위함입니다. 실제 배포되는 운영(Production) 환경에서는 한 번만 렌더링되므로 안심하셔도 됩니다.
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)
리액트 핵심: 컴포넌트의 이해와 JSX 문법

리액트(React)에서 컴포넌트(Component)는 UI를 구성하는 가장 기본적이고 독립적인 빌딩 블록입니다. 쉽게 말해 레고 블록을 조립하여 하나의 멋진 완성품을 만들 듯, 여러 개의 컴포넌트를 결합하여 복잡한 사용자 인터페이스를 만듭니다.

컴포넌트의 장점

  • 재사용성 (Reusability): 한 번 만들어 둔 컴포넌트는 다른 화면이나 프로젝트에서 쉽게 다시 사용할 수 있습니다.
  • 유지보수성 (Maintainability): 코드가 독립적으로 분리되어 있어 특정 부분에 문제가 생겼을 때 해당 컴포넌트만 수정하면 됩니다.
  • 가독성 (Readability): 복잡한 HTML 구조를 의미 있는 이름(예: <Header />, <UserProfile />)으로 추상화할 수 있습니다.
React 패러다임 전환 클래스 컴포넌트 (Object-Oriented) this.state = { ... } Lifecycle Methods componentDidMount() componentDidUpdate() render() { return JSX } Hooks 도입 (v16.8+) 함수 컴포넌트 (Functional + Hooks) const [state, setState] = useState() useEffect(() => { ... }, []) 간결해진 로직 (this 바인딩 없음) return ( JSX )

리액트(React) 생태계는 시간이 지남에 따라 크게 진화해 왔으며, 그 중심에는 클래스(Class) 컴포넌트에서 함수(Function) 컴포넌트로의 패러다임 전환이 있습니다.

과거의 주류: 클래스 컴포넌트

초기 리액트에서는 상태(State)를 가지거나 생명주기(Lifecycle) 메서드(예: componentDidMount)를 사용하려면 반드시 ES6의 class 문법을 사용해야 했습니다.

  • 장점: 생명주기 메서드가 명확하게 구분되어 있어 직관적입니다.
  • 단점: this 바인딩 규칙이 복잡하고 코드가 길어집니다. 로직을 재사용하기 어렵습니다(HOC, Render Props 등의 패턴 강제).

모던 리액트의 표준: 함수 컴포넌트와 Hooks

리액트 16.8 버전에서 Hooks(useState, useEffect 등)가 도입되면서, 함수 컴포넌트에서도 상태 관리와 생명주기 제어가 가능해졌습니다.

  • 장점: 코드가 간결하고 읽기 쉽습니다. this를 신경 쓸 필요가 없으며, Custom Hook을 통해 로직 재사용이 매우 쉽습니다.
  • 현재 상태: 리액트 공식 문서와 최신 생태계는 함수 컴포넌트를 표준으로 채택하고 있으며, 새로운 코드는 모두 함수형으로 작성하는 것을 강력히 권장합니다.

요약: 왜 함수 컴포넌트를 쓰나요?

과거 유지보수 프로젝트가 아니라면, 무조건 함수 컴포넌트와 Hooks를 사용하는 것이 최선의 선택입니다. 클래스 컴포넌트는 레거시 시스템을 이해하기 위한 배경지식으로만 알아두셔도 충분합니다.

JSX 문법의 핵심 규칙

JSX는 JavaScript 내에서 HTML과 유사한 마크업을 작성할 수 있게 해주는 문법 확장입니다.

  • 반드시 하나의 부모 요소로 감싸야 합니다: 형제 노드들을 나열할 때는 <div><> ... </> (Fragment)로 묶어줍니다.
  • 자바스크립트 표현식은 중괄호 {}를 사용합니다: 변수나 함수 실행 결과를 화면에 출력할 때 사용합니다.
  • class 대신 className을 사용합니다: JavaScript의 예약어인 class와 겹치지 않기 위함입니다.
  • 태그는 반드시 닫아야 합니다: <img />, <br /> 처럼 빈 태그도 꼭 닫아주어야 합니다.
App.jsx
// 리액트 컴포넌트는 대문자로 시작하는 자바스크립트 함수입니다.
export default function App() {
  // JSX를 사용하여 화면에 보여줄 HTML을 반환(return)합니다.
  return (
    <div style={{ padding: '20px', textAlign: 'center' }}>
      <h1 style={{ color: '#61DAFB' }}>Hello, World!</h1>
      <p>나의 첫 번째 리액트 컴포넌트입니다.</p>
    </div>
  );
}
데이터 흐름과 상태 관리: Props와 useState 마스터
리액트의 핵심인 단방향 데이터 흐름(One-way Data Flow)을 이해하고, 부모-자식 컴포넌트 간에 데이터를 전달하는 방법(Props)과 컴포넌트 내부에서 변경되는 데이터(State)를 관리하는 방법을 마스터합니다. '상태 끌어올리기(Lifting State Up)' 패턴과 배열/객체 상태의 불변성(Immutability) 유지 방법도 함께 익힙니다.
<PropsExample /> (Parent Component) globalCount (State) setGlobalCount() Props (Data) Props (Callback) Event Triggers <UserCard /> (Child) Reads Props (Read-only) <CounterButton /> (Child) Calls onIncrease() One-way Data Flow & Lifting State Up

📌 1. 단방향 데이터 흐름 (One-way Data Flow)

리액트에서 데이터는 항상 위에서 아래로(부모 컴포넌트 -> 자식 컴포넌트)만 흐릅니다. 부모가 가진 데이터를 자식에게 물려주는 매개체를 Props라고 합니다.

  • Props는 읽기 전용(Read-only): 자식 컴포넌트는 부모로부터 전달받은 Props를 직접 수정할 수 없습니다. UI를 그리기 위한 '설명서'처럼 다루어야 합니다.
  • 상태 끌어올리기 (Lifting State Up): 자식 컴포넌트 내부에서 부모의 상태를 변경해야 하는 경우 어떻게 할까요? 위 다이어그램처럼 부모 컴포넌트가 '상태를 변경하는 함수(setState)'를 자식에게 Props로 넘겨주고, 자식이 버튼 클릭 등의 이벤트가 발생했을 때 이 함수를 대신 호출하는 방식을 사용합니다.

📌 2. 왜 불변성(Immutability)을 지켜야 할까요?

useState를 다룰 때, 특히 객체나 배열의 경우 push()obj.key = value와 같이 원본 자체를 수정하면 안 됩니다. 리액트는 성능 최적화를 위해 객체의 모든 속성을 하나하나 비교하지 않고, 메모리의 주소값(참조값)이 바뀌었는지만 확인하는 '얕은 비교(Shallow Compare)'를 수행하기 때문입니다.

💡 중요: push()를 사용해 배열에 요소를 추가하면, 배열의 내용물은 바뀌었지만 주소값은 그대로이기 때문에 리액트는 "상태가 바뀌지 않았다"고 판단하여 화면을 다시 그리지 않습니다.

따라서, ...(스프레드 연산자)나 filter(), map() 등을 활용해 완전히 새로운 객체/배열을 만들어 setState에 전달해야 합니다.

import React, { useState } from 'react';
import { UserCard } from './UserCard';
import { Container } from './Container';
import { CounterButton } from './CounterButton';

// 부모 컴포넌트 (상태 관리자)
export function PropsExample() {
  // 상태 끌어올리기 (Lifting State Up)
  // 자식 컴포넌트들이 공유해야 할 상태는 부모가 가지고 있어야 합니다.
  const [globalCount, setGlobalCount] = useState(0);

  return (
    <Container title={"팀원 목록 (총 " + globalCount + "명 조회)"}>
      <UserCard name="홍길동" job="프론트엔드 개발자" />
      <UserCard name="김철수" /> {/* job을 안 넘기면 기본값이 들어갑니다 */}
      
      {/* 함수를 Props로 전달하여 자식이 부모의 상태를 바꿀 수 있게 합니다 */}
      <CounterButton onIncrease={() => setGlobalCount(prev => prev + 1)} />
    </Container>
  );
}
사용자 상호작용: 이벤트 핸들링 완벽 가이드

리액트의 합성 이벤트 (Synthetic Event)

리액트는 브라우저 간의 이벤트 차이를 없애고 일관된 동작을 보장하기 위해 브라우저의 기본 이벤트를 감싼 합성 이벤트 객체(e)를 전달합니다.

합성 이벤트(Synthetic Event) 흐름도

브라우저 (Chrome 등) Native Event React Event System (Root 이벤트 위임) SyntheticEvent 크로스 브라우징 포장 내 컴포넌트 onClick={(e)}
리액트는 각각의 버튼마다 이벤트를 달지 않습니다. 대신 최상단(Root)에서 이벤트 위임(Event Delegation) 방식으로 모든 Native Event를 가로챈 뒤, 브라우저 호환성이 완벽하게 보장되는 SyntheticEvent(합성 이벤트) 객체로 예쁘게 포장하여 우리가 작성한 컴포넌트의 onClick 핸들러 등에 전달합니다.
주의: 함수 실행문을 바로 넣지 마세요!
이벤트 핸들러를 속성으로 전달할 때, 함수 실행문(onClick={handleClick()})을 넣으면 컴포넌트가 렌더링될 때 즉시 실행되어 무한 루프에 빠질 수 있습니다. 반드시 함수 그 자체(onClick={handleClick})를 전달하거나, 익명 화살표 함수(onClick={() => handleClick(id)}) 형태로 전달해야 합니다.

자주 쓰는 이벤트 종류

  • onClick: 클릭 발생 시
  • onChange: Input 태그의 값이 변경될 때마다 (실시간 타이핑 감지)
  • onSubmit: Form 내부의 버튼이 클릭되어 제출될 때
LoginForm.jsx
import React, { useState } from 'react';

export default function LoginForm() {
  const [userId, setUserId] = useState('');

  const handleInputChange = (e) => {
    setUserId(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    alert(`로그인 시도: ${userId}`);
  };

  const handleSpecialClick = (msg) => {
    console.log("전달받은 메시지:", msg);
  };

  return (
    <form onSubmit={handleSubmit} style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
      <label style={{ display: 'block', marginBottom: '10px' }}>
        아이디:
        <input 
          type="text" 
          value={userId} 
          onChange={handleInputChange} 
          placeholder="아이디를 입력하세요"
          style={{ marginLeft: '10px', padding: '5px' }}
        />
      </label>
      
      <button type="submit" style={{ padding: '8px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', marginRight: '10px' }}>로그인</button>

      <button type="button" onClick={() => handleSpecialClick('비밀번호 찾기')} style={{ padding: '8px 16px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
        비밀번호를 잊으셨나요?
      </button>
    </form>
  );
}
동적 UI 제어: 조건부 렌더링과 리스트 렌더링

1. 조건부 렌더링 (Conditional Rendering)

JSX 내부에서는 if문이나 for문을 직접 사용할 수 없습니다. 대신 자바스크립트의 논리 연산자를 활용하여 렌더링을 제어합니다.
  1. 삼항 연산자 (조건 ? 참 : 거짓): 조건에 따라 두 UI 중 하나를 명확히 선택할 때 사용합니다. (예: 로그인 버튼 / 로그아웃 버튼 스위칭)
  2. 논리 AND 연산자 (조건 && 참): 조건이 참일 때만 무언가를 렌더링하고, 거짓일 때는 아무것도 그리지 않을 때 사용합니다. (예: 에러 메시지 팝업, 알림창)
&& 사용 시 주의사항
0 && <div>...</div>의 경우 숫자 0이 화면에 그대로 렌더링됩니다. 배열 길이를 체크할 때는 items.length > 0 && ... 처럼 명확한 boolean 값으로 평가되도록 작성하세요!
===

2. 리스트 렌더링 (List Rendering)

배열 데이터를 반복하여 렌더링할 때는 자바스크립트 배열의 map() 함수를 사용합니다. 데이터가 많아질수록 일일이 하드코딩할 수 없기 때문에 실무에서 가장 많이 쓰이는 패턴 중 하나입니다.
이때 반복되는 요소의 최상위 태그에는 반드시 고유한 key 속성을 부여해야 합니다. 리액트는 이 key를 통해 어떤 아이템이 추가/수정/삭제되었는지 빠르고 효율적으로 추적합니다. (주의: 배열의 인덱스 index를 key로 사용하면 아이템 순서가 바뀔 때 버그가 발생할 수 있으므로, 고유한 ID값을 사용하는 것이 좋습니다.)
import React, { useState } from 'react';

export default function LoginGreeting() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
      <h2 style={{ marginTop: 0 }}>조건부 렌더링 예제</h2>
      
      {/* 삼항 연산자 (? :) 활용 */}
      <div style={{ marginBottom: '20px' }}>
        <p>상태: {isLoggedIn ? <strong style={{color: 'green'}}>로그인 됨</strong> : <strong style={{color: 'red'}}>로그아웃 상태</strong>}</p>
        <button 
          onClick={() => setIsLoggedIn(!isLoggedIn)}
          style={{ padding: '8px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
        >
          {isLoggedIn ? '로그아웃 하기' : '로그인 하기'}
        </button>
      </div>

      {/* 논리 AND (&&) 연산자 활용 */}
      {isLoggedIn && (
        <div style={{ padding: '15px', background: '#dcfce7', color: '#166534', borderRadius: '8px' }}>
          환영합니다! 회원 전용 콘텐츠입니다. 👋
        </div>
      )}
    </div>
  );
}
필수 내장 Hooks 마스터: useEffect와 useRef 완벽 가이드

useEffect로 부수 효과(Side Effect) 관리하기

리액트 컴포넌트는 오직 '입력(Props/State)을 받아 UI(JSX)를 반환하는' 순수한 역할에 집중해야 합니다. 하지만 실제 앱에서는 화면을 그리는 일 외에 서버에서 데이터 가져오기(Fetch), 브라우저 타이머 세팅, DOM 직접 조작 같은 부수 효과(Side Effect)가 반드시 필요합니다.
useEffect는 렌더링이 화면에 반영된 직후에 비동기적으로 이러한 작업들을 수행할 수 있게 해주는 훅입니다.
컴포넌트 생명주기(Lifecycle)와 useEffect
Mount (화면에 나타남) Update (상태/Props 변경) Unmount (화면에서 사라짐) 초기 렌더링 재렌더링 (Re-render) 컴포넌트 소멸 React가 DOM 업데이트 React가 DOM 업데이트 useEffect() Setup 함수 실행 이전 Cleanup 함수 먼저 실행됨 새로운 Setup 함수 실행됨 마지막 Cleanup 함수 실행 후 컴포넌트 파기
1. Mount (생성)
브라우저에 실제 HTML(DOM)이 그려진 직후useEffect가 딱 한 번 실행됩니다. 주로 백엔드 API 데이터를 불러오거나 이벤트를 등록할 때 쓰입니다.
2. Update (업데이트)
상태(State)가 바뀌어 화면이 다시 그려지면, 이전 상태의 찌꺼기를 지우기 위해 Cleanup 함수가 먼저 실행되고, 그다음 새로운 Setup 함수가 실행됩니다.
3. Unmount (소멸)
다른 페이지로 이동하거나 조건부 렌더링에 의해 컴포넌트가 사라질 때, 메모리 누수를 막기 위해 마지막으로 Cleanup 함수가 한 번 실행됩니다.

의존성 배열(Dependency Array) 완벽 이해

  • useEffect(() => { ... }) : 렌더링될 때마다 매번 실행됩니다. (거의 안 씀)
  • useEffect(() => { ... }, []) : 빈 배열. 컴포넌트가 처음 화면에 나타날 때(Mount) 단 한 번만 실행됩니다. API 초기 호출에 주로 씁니다.
  • useEffect(() => { ... }, [상태]) : 배열 안의 상태가 변경될 때마다 실행됩니다.
또한 컴포넌트가 사라질 때(Unmount) 리소스를 해제(Cleanup)하려면 return으로 정리 함수를 반환하면 됩니다.

useRef의 두 가지 핵심 용도

useRef는 리액트 컴포넌트 생애주기 동안 유지되는 변경 가능한 객체(.current)를 생성합니다. useState와 비슷해 보이지만 가장 큰 차이점은 값이 변경되어도 컴포넌트가 리렌더링되지 않는다는 점입니다.

  1. DOM 요소 직접 선택하기
    특정 input에 포커스를 주거나, 스크롤 위치를 계산하는 등 바닐라 JS의 getElementById처럼 실제 DOM 노드에 직접 접근해야 할 때 사용합니다.
  2. 리렌더링을 유발하지 않는 변수 저장
    타이머 ID, 이전 상태값 기억 등 값이 바뀌어도 굳이 화면을 다시 그릴 필요가 없는 내부 데이터를 보관할 때 사용합니다.
import React, { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // 1. Mount 시점에 1초마다 동작하는 타이머 세팅
    console.log("타이머 시작!");
    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // 2. Cleanup 함수 반환
    // 컴포넌트가 화면에서 사라질 때(Unmount) 반드시 타이머를 해제해야 메모리 누수를 막습니다.
    return () => {
      console.log("타이머 정리됨");
      clearInterval(intervalId);
    };
  }, []); // 빈 배열이므로 처음 한 번만 실행됨

  return <p style={{ fontSize: '1.2rem', fontWeight: 'bold', color: '#0f172a' }}>경과 시간: {seconds}초</p>;
}

export default function App() {
  const [show, setShow] = useState(true);
  return (
    <div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
      <h3 style={{ marginTop: 0 }}>타이머 마운트/언마운트 예제</h3>
      <button 
        onClick={() => setShow(!show)}
        style={{ padding: '8px 16px', background: show ? '#ef4444' : '#10b981', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', marginBottom: '15px' }}
      >
        {show ? '타이머 숨기기 (Unmount)' : '타이머 켜기 (Mount)'}
      </button>
      
      <div style={{ minHeight: '50px', padding: '15px', background: '#fff', borderRadius: '6px', border: '1px solid #e2e8f0' }}>
        {show ? <Timer /> : <p style={{ color: '#64748b' }}>타이머가 화면에서 사라졌습니다.</p>}
      </div>
    </div>
  );
}
Tailwind CSS: 리액트와 함께 쓰는 유틸리티 기반 스타일링

Tailwind CSS와 React의 환상적인 궁합

기존에는 CSS 파일을 별도로 작성하고 className을 지어주는 과정(BEM 방법론 등)이 매우 번거로웠습니다. Tailwind CSSflex, text-center, pt-4처럼 미리 정의된 유틸리티 클래스들을 조립하여 별도의 CSS 파일 없이 HTML(JSX) 안에서 직접 디자인을 완성하는 프레임워크입니다.

전통적 CSS vs Tailwind CSS

전통적 방식 (BEM 등) <button class="btn-primary"> .btn-primary { background: blue; padding: 1rem; Tailwind CSS 방식 <button className="bg-blue-500 p-4" > CSS 파일 열 필요 없음! ⚡️
React 컴포넌트와의 완벽한 시너지
Tailwind는 클래스명이 길어지는 단점이 있지만, 리액트에서는 UI를 <Button />과 같은 컴포넌트로 분리하기 때문에 지저분한 클래스명을 한 곳에 캡슐화(숨김)할 수 있습니다.

Tailwind 핵심 규칙

  • 여백: m-4 (margin: 1rem), px-2 (padding-x: 0.5rem) - 1단위는 보통 0.25rem(4px)
  • 색상: bg-blue-500, text-white, border-gray-200
  • 상태 및 반응형: hover:bg-blue-600, md:flex (화면이 md 크기 이상일 때 flex 적용)
TailwindCard.jsx
// Tailwind CSS가 설정된 환경이라고 가정합니다.

function TailwindCard() {
  return (
    // 부모 컨테이너: 최대 너비 설정, 중앙 정렬, 상하 여백
    <div className="max-w-sm mx-auto mt-10">
      
      {/* 카드 래퍼: 흰색 배경, 그림자, 둥근 모서리, 패딩, 테두리 */}
      <div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6 overflow-hidden">
        
        {/* 태그 뱃지 */}
        <span className="bg-indigo-100 text-indigo-800 text-xs font-semibold px-2.5 py-0.5 rounded-full">
          New Course
        </span>
        
        {/* 제목 텍스트 */}
        <h2 className="mt-3 text-xl font-bold text-gray-800">
          Tailwind CSS 완벽 가이드
        </h2>
        
        <p className="mt-2 text-gray-600 text-sm leading-relaxed">
          별도의 CSS 파일 없이 오직 클래스 조합만으로 아름다운 반응형 UI를 구축하는 비법을 배웁니다.
        </p>
        
        {/* 액션 버튼: flex 박스 정렬, 배경색, 호버 효과, 트랜지션 */}
        <div className="mt-5 flex justify-end">
          <button className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200">
            수강하기
          </button>
        </div>
        
      </div>
    </div>
  );
}
TypeScript 도입: 인터페이스와 타입을 활용한 안정성 확보

React와 TypeScript의 결합

순수 자바스크립트는 런타임(브라우저 실행 중)에 에러가 터지기 전까지 문제를 모르는 경우가 많습니다. React에 TypeScript를 결합하면 컴포넌트의 Props나 State가 어떤 형태의 데이터를 받아야 하는지 미리 엄격하게 정의(Type Definition)하여 버그를 사전에 차단할 수 있습니다.

React에서 자주 쓰이는 주요 타입

  • React.FC: 함수형 컴포넌트 타입 지정 (최근에는 잘 쓰지 않고 매개변수에 직접 지정하는 추세)
  • React.MouseEvent<HTMLButtonElement>: 버튼 클릭 이벤트 객체 타입
  • React.ReactNode: children prop으로 들어올 수 있는 모든 렌더링 가능한 노드 타입
📘 React + TypeScript (견고한 UI) ⚛️ React Component (Props & State) + 🛡️ Type Definition interface Props { name: string; } 런타임 에러가 터지기 전에, 컴파일 타임에서 어떤 형태의 데이터를 받아야 하는지 미리 엄격하게 정의
💡 이제 TypeScript는 선택이 아닌 필수
최근 실무 환경에서 거의 모든 모던 프론트엔드 프로젝트는 TypeScript로 구축됩니다. 앞서 배운 Zustand, TanStack Query, 그리고 커스텀 훅 설계 시 강력한 자동완성과 타입 추론을 제공하여 개발 생산성을 극대화하고 런타임 에러를 완벽히 예방합니다.
UserProfile.tsx
// TypeScript 환경 (.tsx 파일)
import React, { useState } from 'react';

// 1. Props의 형태를 interface로 명확히 정의합니다.
interface UserCardProps {
  name: string;
  age: number;
  // ?를 붙이면 선택적(Optional) 속성이 됩니다.
  email?: string;
  isOnline: boolean;
}

// 2. 컴포넌트 매개변수에 타입을 적용합니다.
function UserProfile({ name, age, email, isOnline }: UserCardProps) {
  return (
    <div style={{ border: '1px solid #ccc', padding: '1rem', borderRadius: '8px' }}>
      <h2>
        {name} ({age}세)
        <span style={{ color: isOnline ? 'green' : 'gray', marginLeft: '8px' }}>
          ●
        </span>
      </h2>
      {email && <p>이메일: {email}</p>}
    </div>
  );
}

function App() {
  // State에도 제네릭(<>) 문법으로 타입을 명시할 수 있습니다.
  const [users, setUsers] = useState<UserCardProps[]>([]);

  // 만약 props 타입에 맞지 않는 데이터를 넘기면 VScode 편집기에서 에디터가 빨간 줄로 경고해줍니다!
  return (
    <div>
      <UserProfile name="Minstudio" age={28} isOnline={true} />
    </div>
  );
}
폼 핸들링과 유효성 검사 (react-hook-form & Zod)

리액트에서 폼(Form)의 상태를 효율적으로 제어하는 방식과 실무 표준인 react-hook-formzod를 결합하여 복잡한 유효성 검사를 쉽고 빠르게 구현하는 방법을 배웁니다.

📋 폼 상태 관리 및 검증 아키텍처 React Hook Form 비제어 컴포넌트(Uncontrolled) 방식으로 렌더링 최소화 register(), handleSubmit() 검증 위임 Zod (스키마 검증) 강력한 타입스크립트 기반 유효성 검사 스키마 선언 z.string().min(4)
React Hook Form 핵심 장점
기존 리액트의 제어 컴포넌트(Controlled) 방식은 키보드 타이핑이 발생할 때마다 화면이 리렌더링되어 성능이 저하됩니다. 반면 React Hook Form은 비제어 컴포넌트 방식을 사용하여 렌더링을 획기적으로 최소화하고 폼 상태를 매우 가볍고 빠르게 관리합니다.
Zod를 통한 스키마 검증
Zod는 TypeScript 기반의 강력한 스키마 선언 라이브러리입니다. 이메일 정규식, 비밀번호 최소 길이, 비밀번호 확인 일치 여부 등 복잡한 유효성 검사 규칙을 직관적인 체이닝(z.string().email())으로 손쉽게 정의할 수 있습니다.
LoginForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// 1. Zod를 사용하여 폼의 스키마(규칙) 정의
const loginSchema = z.object({
  email: z.string().email("올바른 이메일 형식을 입력해주세요."),
  password: z.string().min(6, "비밀번호는 최소 6자 이상이어야 합니다."),
});

type LoginFormInputs = z.infer<typeof loginSchema>;

export default function LoginForm() {
  // 2. useForm에 zodResolver를 연결하여 검증 위임
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(loginSchema),
  });

  const onSubmit = (data) => {
    console.log("검증 성공! 전송된 데이터:", data);
    alert(JSON.stringify(data, null, 2));
  };

  return (
    <div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
      <form onSubmit={handleSubmit(onSubmit)} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
        <div>
          <input 
            {...register("email")} 
            placeholder="이메일" 
            style={{ width: '100%', padding: '10px', borderRadius: '4px', border: '1px solid #cbd5e1' }}
          />
          {/* 에러 메시지 출력 */}
          {errors.email && <p style={{ color: '#ef4444', fontSize: '0.875rem', marginTop: '5px' }}>{errors.email.message}</p>}
        </div>

        <div>
          <input 
            type="password" 
            {...register("password")} 
            placeholder="비밀번호" 
            style={{ width: '100%', padding: '10px', borderRadius: '4px', border: '1px solid #cbd5e1' }}
          />
          {errors.password && <p style={{ color: '#ef4444', fontSize: '0.875rem', marginTop: '5px' }}>{errors.password.message}</p>}
        </div>

        <button 
          type="submit" 
          style={{ padding: '10px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold' }}
        >
          로그인
        </button>
      </form>
    </div>
  );
}
React Router: SPA(싱글 페이지 애플리케이션) 라우팅 구현

SPA와 React Router DOM

리액트는 하나의 빈 HTML 페이지를 로드한 뒤 자바스크립트로 화면을 갈아끼우는 SPA(Single Page Application) 방식을 사용합니다. 브라우저가 실제로 새 페이지를 요청하지 않으므로 화면 깜빡임이 없고 매우 빠릅니다.

🌐 전통적 방식(MPA) vs React의 방식(SPA) 전통적 웹 (MPA) Page 1 새로고침 발생 (서버 통신) Page 2 상태(State) 완전 초기화 ❌ React 라우팅 (SPA) Component A 빠른 교체 (JS 가로채기) Component B 상태(State) 그대로 유지 ⚡️
🚨 <a> 태그의 치명적 단점
HTML의 기본 <a href="..."> 태그를 사용하면 브라우저가 페이지를 완전히 새로고침합니다. 이 경우 메모리에 저장되어 있던 리액트의 모든 State(상태값)가 날아가버리므로 절대 사용해서는 안 됩니다.
💡 <Link> vs useNavigate()
<Link>는 사용자가 명시적으로 클릭하는 메뉴바나 네비게이션에 사용합니다. 반면 useNavigate()는 '로그인 버튼 클릭 후 성공하면 메인으로 이동'과 같이 프로그래밍 방식(특정 로직 내부)에서 주소를 이동할 때 씁니다.

하지만 사용자는 주소(URL)에 따라 다른 화면을 보고 싶어 하고, 뒤로가기 버튼도 작동해야 합니다. 이를 구현하기 위해 브라우저의 History API를 가로채어 가짜 페이지 이동을 구현하는 표준 라이브러리가 바로 react-router-dom입니다.

핵심 컴포넌트

  • BrowserRouter: 최상위를 감싸 라우팅 환경을 활성화합니다.
  • Routes / Route: URL 경로(path)에 따라 보여줄 컴포넌트(element)를 매핑합니다.
  • Link: HTML의 <a> 태그를 대체합니다. 페이지 새로고침 없이 URL만 변경합니다.
bash
npm install react-router-dom
모던 전역 상태 관리 아키텍처: Context API의 한계와 Zustand 도입

🚇 Context API: Props Drilling 문제 해결하기

Props Drilling의 고통과 Context API

부모에서 아주 깊은 곳에 있는 자식 컴포넌트로 데이터를 전달하려면, 중간에 있는 컴포넌트들은 데이터를 쓰지도 않으면서 오직 전달만을 위해 Props를 받아 넘겨야 합니다. 이를 Props Drilling(프롭스 내리꽂기)이라고 합니다.
Context API는 이러한 문제를 해결하는 리액트 내장 전역 상태 관리 도구입니다. 최상위에서 Provider로 감싸고 값을 공급하면, 하위의 어떤 컴포넌트든 깊이와 상관없이 useContext 훅을 이용해 해당 값을 바로 꺼내 쓸 수 있습니다.
Props Drilling vs Context API
❌ Props Drilling (고통스러운 전달) <App /> (데이터) props 전달 <Header /> props 전달 <Nav /> props 전달 <User /> (소비) ✅ Context API (직통 터널) <Provider> <App /> <Header /> <Nav /> useContext <User /> (소비)
언제 Context API를 써야 할까?
Context API는 만능 전역 상태 관리 도구가 아닙니다. 'Props Drilling을 우회하여 값을 전달하는 통로'에 가깝습니다. 상태가 자주 변경되는 경우 하위 컴포넌트 전체가 리렌더링 될 수 있으므로, 테마(Dark/Light), 로그인 유저 정보, 다국어 설정 등 변경이 잦지 않은 전역 데이터에 사용하는 것이 적합합니다.
===

Redux의 시대는 가고, Zustand의 시대가 왔다

Context API는 전역 상태가 바뀔 때마다 하위 컴포넌트가 전부 리렌더링되는 성능 이슈가 있어, 단순한 테마 변경 정도에만 적합합니다. 그래서 과거에는 무겁고 복잡한 Redux를 사용했습니다.
독일어로 '상태'를 뜻하는 Zustand(쥬스탠드)는 Redux의 단점(방대한 보일러플레이트 코드, 복잡한 설정)을 모두 없앤 초경량 상태 관리 라이브러리입니다. Provider로 앱을 감쌀 필요도 없고, 훅을 생성하여 어디서든 직관적으로 상태와 함수를 꺼내 쓸 수 있습니다.
Zustand의 전역 상태 구조
React Component Tree ❌ <Provider> 감싸기 불필요! <App /> <Header /> <Main /> <Cart /> Zustand Store (트리 외부에 독립적으로 존재) State { cartItems: [] } Actions addItem() 상태 구독 액션 호출
Zustand 스토어는 리액트 컴포넌트 트리 바깥에 완전히 독립적으로 존재합니다. 따라서 <Provider>로 앱을 감쌀 필요가 없으며, 깊숙한 곳에 있는 컴포넌트라도 훅(Hook)을 통해 스토어에 직접 연결(Subscribe)되어 최상의 렌더링 성능을 보장합니다.
===
위 명령어로 Zustand를 설치한 뒤, 아래와 같이 스토어를 생성하고 컴포넌트에서 활용할 수 있습니다.
bash
import React, { useState } from 'react';
import { ThemeContext } from './ThemeContext';
import MiddleComponent from './MiddleComponent';

// 4. 최상위 부모 컴포넌트
export default function App() {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');

  return (
    // Provider의 value 속성으로 공유할 데이터와 함수를 전달합니다.
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
        <h3 style={{ marginTop: 0 }}>Context API 예제</h3>
        <MiddleComponent />
      </div>
    </ThemeContext.Provider>
  );
}
서버 데이터 통신과 캐싱: API 연동 실습부터 TanStack Query까지

📊 API 연동 실습: 외부 REST API를 활용한 데이터 대시보드 만들기

실전 API 데이터 연동 패턴

지금까지 배운 useState, useEffect, 비동기 fetch/axios를 총동원하여 실제 외부 API와 통신하고 데이터를 화면에 뿌려주는 과정을 실습합니다.

안정적인 데이터 페칭의 3요소

사용자 경험(UX)을 위해서는 데이터를 가져올 때 발생할 수 있는 3가지 상태를 반드시 모두 처리해야 합니다.

  1. 로딩 상태 (Loading): 스켈레톤이나 스피너를 보여주어 앱이 멈추지 않았음을 알림
  2. 성공 상태 (Success): 받아온 데이터를 바탕으로 UI 렌더링
  3. 실패 상태 (Error): 네트워크 오류나 서버 장애 시 에러 메시지와 재시도 버튼 제공
===

서버 상태(Server State) 관리의 종결자, TanStack Query

서버에서 데이터를 가져오기 위해 useStateuseEffect를 조합하면 앞서 배운 로딩 상태 관리, 에러 처리, 캐싱, 데이터 재요청 등 복잡한 코드를 직접 다 짜야 합니다. 클라이언트 상태(UI 토글 등)와 서버 상태(DB 데이터)를 분리하는 것이 모던 프론트엔드의 핵심 트렌드입니다.

TanStack Query (구 React Query)는 API 데이터를 훅 한 줄로 가져오고, 알아서 캐싱하며, 백그라운드에서 최신 데이터로 동기화(Stale-while-revalidate)해주는 강력한 라이브러리입니다.

Stale-While-Revalidate 캐싱 생명주기

React UI useQuery() Cache (Stale Data) Server Database 1. 즉시 반환 (빠름!) 4. UI 조용히 업데이트 2. 백그라운드 요청 3. 최신 데이터(Fresh)

Stale-while-revalidate 원리:

  • 사용자가 페이지를 열면, 오래된(Stale) 캐시 데이터라도 먼저 즉시 화면에 렌더링합니다. (로딩 스피너 없음!)
  • 동시에 백그라운드에서 서버로 새로운 데이터를 몰래 요청합니다.
  • 서버에서 최신 데이터(Fresh)가 도착하면, 캐시를 업데이트하고 UI를 부드럽게(조용히) 갱신합니다.
===
위 명령어로 React Query를 설치한 뒤, 아래와 같이 QueryClient를 세팅하여 사용할 수 있습니다.
bash
import CryptoDashboard from './CryptoDashboard';

export default function App() {
  return (
    <div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
      <CryptoDashboard />
    </div>
  );
}
고급 컴포넌트 패턴 (Portals & forwardRef)

UI의 제약을 넘어서는 고급 렌더링 패턴

부모 컴포넌트의 DOM 계층 구조를 벗어나 렌더링하는 createPortal을 활용한 모달(Modal) 구현법과, 부모 컴포넌트가 자식 커스텀 컴포넌트 내부의 DOM 요소에 직접 접근할 수 있게 해주는 forwardRef의 활용법을 다룹니다.

🚪 createPortal <div id="root"> (일반 컴포넌트) <div id="modal-root"> (포탈) z-index 충돌 방지를 위해 최상단에 렌더링 🎯 forwardRef <Parent /> (Ref 생성) <CustomInput /> (Ref 수신) 부모가 자식의 실제 DOM(input 등)을 직접 제어
🚪 createPortal: z-index 지옥 탈출
모달이나 툴팁을 렌더링할 때, 부모 컨테이너의 overflow: hidden이나 z-index에 막혀 화면에 제대로 표시되지 않는 경우가 많습니다. createPortal을 사용하면 논리적인 React 트리 구조는 유지하면서, 실제 DOM은 최상단 <body> 직속으로 빼내어 렌더링하므로 이러한 CSS 충돌을 완벽히 해결할 수 있습니다.
🎯 forwardRef: 컴포넌트를 관통하는 참조
부모 컴포넌트에서 커스텀 입력창 컴포넌트에 ref를 전달하고 싶을 때, 기본적으로 커스텀 컴포넌트는 ref를 받지 못합니다. forwardRef로 자식 컴포넌트를 감싸주면 부모가 넘긴 ref를 내부의 실제 <input> 등의 DOM에 바로 꽂아넣어 포커스 제어 등 직접적인 조작이 가능해집니다.
import React, { useRef } from 'react';
import CustomInput from './CustomInput';
import Modal from './Modal';

export default function App() {
  const inputRef = useRef(null);

  const focusInput = () => {
    // 부모 컴포넌트에서 자식 컴포넌트 내부의 DOM 함수(focus)를 직접 호출
    inputRef.current?.focus();
  };

  return (
    <div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
      <CustomInput ref={inputRef} label="사용자 이름" />
      <button 
        onClick={focusInput} 
        style={{ marginTop: '16px', background: '#3b82f6', color: 'white', padding: '8px 16px', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
        포커스 이동하기
      </button>
      
      <Modal isOpen={true}>
        <h2 style={{marginTop: 0}}>이것은 포탈로 열린 모달입니다!</h2>
      </Modal>
    </div>
  );
}
고급 패턴과 성능 최적화: Custom Hooks 설계와 메모이제이션

Custom Hooks: 나만의 레고 블록 만들기

여러 컴포넌트에서 useStateuseEffect를 조합한 비슷한 코드가 계속 반복된다면 어떻게 해야 할까요? 리액트에서는 이러한 상태 관리 로직만 따로 빼내어 재사용 가능한 함수로 만들 수 있습니다. 이를 커스텀 훅(Custom Hook)이라고 부릅니다.
🚨 커스텀 훅의 절대 규칙
함수 이름이 반드시 use로 시작해야 합니다. (예: useFetch, useInput). 그래야만 리액트가 이 함수 내부에서 다른 Hook의 사용을 허용하고, 라이프사이클을 정상적으로 추적할 수 있습니다.
===

메모이제이션(Memoization): 세상에서 가장 쉬운 이해

영수증으로 이해하는 메모이제이션

복잡한 수학 공식을 풀어야 한다고 상상해보세요. 어제 "745 × 382"의 정답을 힘들게 계산해서 영수증(메모장)에 적어두었습니다.

오늘 누군가 또 "745 × 382"를 물어본다면? 다시 계산할 필요 없이 영수증에 적힌 답을 그대로 읽어주면 됩니다. (이것이 useMemo 입니다!)

하지만 "745 × 383"을 물어본다면? 조건(의존성 배열)이 달라졌으므로 영수증을 버리고 새로 계산해야 합니다.

  • useMemo: 비싼 계산의 결과값(답안지)을 캐싱하여, 조건이 안 바뀌면 재사용합니다.
  • useCallback: 함수 자체(계산하는 방법론)를 캐싱하여, 컴포넌트가 렌더링될 때마다 함수가 새로 생성되는 것을 막습니다.
  • React.memo: 컴포넌트 자체를 통째로 씌워, 부모가 렌더링되어도 내가 받는 Props가 안 바뀌었으면 나는 렌더링 안 하겠다고 선언합니다.

useMemo / useCallback 동작 원리

부모 컴포넌트 리렌더링 발생! 의존성(deps) 배열 값(조건)이 바뀌었나? Yes 비싼 연산 새로 계산 🥵 No (그대로다) 영수증(캐시) 값 그대로 재사용 ⚡
import ResponsiveApp from './ResponsiveApp';

export default function App() {
  return (
    <div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
      <ResponsiveApp />
    </div>
  );
}
프론트엔드 테스팅 기초 (Vitest & React Testing Library)

버그 없는 UI를 향한 첫걸음, 프론트엔드 테스팅

안정적인 프론트엔드 애플리케이션을 보장하기 위한 단위 테스트(Unit Test) 작성법입니다. 가장 빠르고 현대적인 Vitest와 실제 사용자의 동작을 모방하는 React Testing Library(RTL)를 사용하여 견고한 컴포넌트를 설계합니다.

🧪 테스트 주도 프론트엔드 (RTL) 1. Render 컴포넌트를 가상 DOM에 그립니다. 2. Action 사용자처럼 버튼을 클릭합니다. 3. Expect 화면의 숫자가 변경되었는지 검증합니다!
⚡ Vitest: Vite 기반의 압도적인 속도
과거에는 Jest를 주로 사용했지만, 최근 Vite 환경의 대두와 함께 Vitest가 표준으로 자리잡고 있습니다. Vite의 빌드 파이프라인을 그대로 공유하므로 설정이 극도로 간단하며, 테스트 실행 속도가 기존 대비 압도적으로 빠릅니다.
👤 RTL: 사용자 중심의 테스트 철학
React Testing Library는 컴포넌트의 내부 상태(State)나 구현 방식을 검증하지 않습니다. 대신 철저하게 "실제 사용자가 화면에서 텍스트를 보고, 버튼을 클릭했을 때 올바른 결과가 나오는가?"라는 행동 중심으로 테스트를 작성하도록 강제합니다.
import React from 'react';
import Counter from './Counter';

export default function App() {
  return (
    <div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px', textAlign: 'center' }}>
      <h2 style={{ marginTop: 0 }}>Vitest 테스트 대상 컴포넌트</h2>
      <p style={{ color: '#64748b', marginBottom: '20px' }}>아래의 카운터 컴포넌트가 테스트 대상입니다.</p>
      <Counter />
    </div>
  );
}
리액트 소개: 가상 DOM과 선언적 프로그래밍 이해하기
Vite 기반 프로젝트 시작하기: 개발 환경 세팅과 폴더 구조
리액트 핵심: 컴포넌트의 이해와 JSX 문법
데이터 흐름과 상태 관리: Props와 useState 마스터
사용자 상호작용: 이벤트 핸들링 완벽 가이드
동적 UI 제어: 조건부 렌더링과 리스트 렌더링
필수 내장 Hooks 마스터: useEffect와 useRef 완벽 가이드
Tailwind CSS: 리액트와 함께 쓰는 유틸리티 기반 스타일링
TypeScript 도입: 인터페이스와 타입을 활용한 안정성 확보
폼 핸들링과 유효성 검사 (react-hook-form & Zod)
React Router: SPA(싱글 페이지 애플리케이션) 라우팅 구현
모던 전역 상태 관리 아키텍처: Context API의 한계와 Zustand 도입
서버 데이터 통신과 캐싱: API 연동 실습부터 TanStack Query까지
고급 컴포넌트 패턴 (Portals & forwardRef)
고급 패턴과 성능 최적화: Custom Hooks 설계와 메모이제이션
프론트엔드 테스팅 기초 (Vitest & React Testing Library)

목차