본문으로 건너뛰기
Previous
Next
React & Next.js 입문 가이드

React & Next.js 입문 가이드

React & Next.js 입문 가이드

이 글의 핵심

React 19·Next.js 15 흐름에서 컴포넌트·Hooks·상태·App Router·Server Components까지 한 번에 잡는 입문 정리. 실전 예제와 코드로 개념부터 활용까지 정리합니다. React·Next.js·Frontend 중심으로 설명합니다.

이 글의 핵심

클라이언트 컴포넌트와 Hooks로 UI를 만들고, Next.js 쪽에서는 라우팅·서버 컴포넌트 개념까지 이어지게 정리했다. 버전 숫자보다 “어디 코드가 도는지”를 먼저 맞추는 쪽에 맞췄다.

사전 지식 (초보자를 위한 기초)

1. HTML, CSS, JavaScript 기초

HTML·CSS·JavaScript 기초가 있으면 이 글을 따라가기 쉽다. HTML (구조)

<!DOCTYPE html>
<html>
<head>
  <title>My Page</title>
</head>
<body>
  <h1>Hello World</h1>
  <button id="btn">Click me</button>
</body>
</html>

CSS (스타일)

h1 {
  color: blue;
  font-size: 24px;
}
button {
  background-color: #007bff;
  color: white;
  padding: 10px 20px;
}

JavaScript (동작)

const button = document.getElementById('btn');
button.addEventListener('click', () => {
  alert('Button clicked!');
});

2. ES6+ 문법

React 코드에서 자주 마주치는 ES6+ 문법이다. 화살표 함수

// 기존 함수
function add(a, b) {
  return a + b;
}
// 화살표 함수
const add = (a, b) => a + b;

구조 분해 할당

// 객체
const user = { name: 'Alice', age: 25 };
const { name, age } = user;
// 배열
const numbers = [1, 2, 3];
const [first, second] = numbers;

스프레드 연산자

// 변수 선언 및 초기화
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];  // [1, 2, 3, 4, 5]
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 };  // { a: 1, b: 2, c: 3 }

템플릿 리터럴

const name = 'Alice';
const message = `Hello, ${name}!`;  // "Hello, Alice!"

3. npm과 패키지 관리

npm은 Node 생태계에서 패키지를 설치·관리하는 기본 도구다.

# Node.js 설치 확인
node --version
npm --version
# 프로젝트 초기화
npm init -y
# 패키지 설치
npm install react react-dom
# 개발 의존성 설치
npm install --save-dev webpack

1. React란 무엇인가?

React의 핵심 개념

React는 Meta가 만든 UI 라이브러리다. 핵심 특징:

1. 컴포넌트 기반
   - UI를 재사용 가능한 조각으로 분리
   - 레고 블록처럼 조립
2. 선언적 (Declarative)
   - "무엇을" 보여줄지 선언
   - "어떻게" 구현할지는 React가 처리
3. Virtual DOM
   - 변경사항을 효율적으로 업데이트
   - 빠른 렌더링

React vs Vanilla JavaScript

Vanilla JavaScript (명령형)

// 카운터 구현
let count = 0;
const button = document.getElementById('btn');
const display = document.getElementById('count');
button.addEventListener('click', () => {
  count++;
  display.textContent = count;  // DOM 직접 조작
});

React (선언형)

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
// 상태가 변하면 자동으로 UI 업데이트!

2. React 시작하기

Vite로 프로젝트 생성

# Vite로 React 프로젝트 생성 (빠름!)
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
# 브라우저에서 http://localhost:5173 접속

프로젝트 구조

my-react-app/
├── public/          # 정적 파일
├── src/
│   ├── App.jsx      # 메인 컴포넌트
│   ├── main.jsx     # 진입점
│   └── index.css    # 스타일
├── index.html
├── package.json
└── vite.config.js

첫 번째 컴포넌트

src/App.jsx

function App() {
  return (
    <div>
      <h1>Hello React!</h1>
      <p>My first React app</p>
    </div>
  );
}
export default App;

3. 컴포넌트와 JSX

컴포넌트란?

컴포넌트는 UI를 나누는 재사용 단위다.

// Button 컴포넌트
function Button({ text, onClick }) {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  );
}
// 여러 곳에서 재사용
function App() {
  return (
    <div>
      <Button text="저장" onClick={() => console.log('저장')} />
      <Button text="취소" onClick={() => console.log('취소')} />
      <Button text="삭제" onClick={() => console.log('삭제')} />
    </div>
  );
}

JSX 문법

JSX는 JavaScript 안에서 마크업을 쓰기 위한 문법이다.

// JSX
const element = <h1>Hello, {name}!</h1>;
// 컴파일 후 JavaScript
const element = React.createElement('h1', null, `Hello, ${name}!`);

JSX 규칙:

// 1. 하나의 부모 요소로 감싸기
// ❌ 에러
function App() {
  return (
    <h1>Title</h1>
    <p>Content</p>
  );
}
// ✅ Fragment 사용
function App() {
  return (
    <>
      <h1>Title</h1>
      <p>Content</p>
    </>
  );
}
// 2. JavaScript 표현식은 {} 안에
function App() {
  const name = 'Alice';
  const age = 25;
  
  return <p>Name: {name}, Age: {age + 1}</p>;
}
// 3. className 사용 (class는 예약어)
<div className="container">Content</div>
// 4. 자체 닫기 태그
<img src="image.jpg" />
<input type="text" />

4. Hooks 정리

useState (상태 관리)

useState는 컴포넌트에 상태를 붙인다.

import { useState } from 'react';
function Counter() {
  // [현재값, 업데이트 함수] = useState(초기값)
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

여러 상태 관리:

function Form() {
  const [name, setName] = useState(');
  const [email, setEmail] = useState(');
  const [age, setAge] = useState(0);
  
  return (
    <form>
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
        placeholder="Name"
      />
      <input 
        value={email} 
        onChange={(e) => setEmail(e.target.value)} 
        placeholder="Email"
      />
      <input 
        type="number"
        value={age} 
        onChange={(e) => setAge(Number(e.target.value))} 
        placeholder="Age"
      />
    </form>
  );
}

useEffect (부수 효과)

useEffect는 렌더 이후에 돌릴 부수 효과(fetch, 구독 등)를 둔다.

import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // 컴포넌트 마운트 시 실행
    async function fetchUser() {
      setLoading(true);
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUser(data);
      setLoading(false);
    }
    
    fetchUser();
  }, [userId]);  // userId 변경 시 재실행
  
  if (loading) return <p>Loading...</p>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

의존성 배열:

// 1. 빈 배열: 마운트 시 1번만 실행
// 실행 예제
useEffect(() => {
  console.log('Component mounted');
}, []);
// 2. 의존성 있음: 의존성 변경 시 실행
useEffect(() => {
  console.log('Count changed:', count);
}, [count]);
// 3. 의존성 없음: 매 렌더링마다 실행 (비추천)
useEffect(() => {
  console.log('Every render');
});

정리 함수 (Cleanup)

useEffect(() => {
  // 타이머 시작
  const timer = setInterval(() => {
    console.log('Tick');
  }, 1000);
  
  // 정리 함수 (컴포넌트 언마운트 시 실행)
  return () => {
    clearInterval(timer);
  };
}, []);

useContext (전역 상태)

Context는 props를 깊게 파지 않고 전역에 가깝게 값을 공유할 때 쓴다.

import { createContext, useContext, useState } from 'react';
// Context 생성
const ThemeContext = createContext();
// Provider 컴포넌트
function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <Content />
    </ThemeContext.Provider>
  );
}
// Context 사용
function Header() {
  const { theme, setTheme } = useContext(ThemeContext);
  
  return (
    <header style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
      <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
        Toggle Theme
      </button>
    </header>
  );
}
function Content() {
  const { theme } = useContext(ThemeContext);
  
  return (
    <div style={{ color: theme === 'dark' ? '#fff' : '#000' }}>
      <p>Current theme: {theme}</p>
    </div>
  );
}

useMemo와 useCallback (성능 최적화)

useMemo: 계산 결과 캐싱

import { useMemo } from 'react';
function ExpensiveComponent({ numbers }) {
  // numbers가 변하지 않으면 재계산 안함
  const sum = useMemo(() => {
    console.log('Calculating sum...');
    return numbers.reduce((a, b) => a + b, 0);
  }, [numbers]);
  
  return <p>Sum: {sum}</p>;
}

useCallback: 함수 캐싱

import { useCallback } from 'react';
function Parent() {
  const [count, setCount] = useState(0);
  
  // count가 변하지 않으면 같은 함수 재사용
  const handleClick = useCallback(() => {
    console.log('Clicked:', count);
  }, [count]);
  
  return <Child onClick={handleClick} />;
}

5. 상태 관리

Props vs State

Props (속성)

  • 부모 → 자식으로 전달
  • 읽기 전용 (변경 불가)
function Parent() {
  return <Child name="Alice" age={25} />;
}
function Child({ name, age }) {
  // props는 변경 불가
  // name = "Bob";  // ❌ 에러
  
  return <p>{name} is {age} years old</p>;
}

State (상태)

  • 컴포넌트 내부 데이터
  • 변경 가능 (setState)
function Counter() {
  const [count, setCount] = useState(0);
  
  // state는 변경 가능
  const increment = () => setCount(count + 1);
  
  return <button onClick={increment}>{count}</button>;
}

상태 끌어올리기 (Lifting State Up)

// ❌ 각 컴포넌트가 독립적인 상태
function App() {
  return (
    <>
      <Counter />  {/* count: 0 */}
      <Counter />  {/* count: 0 */}
    </>
  );
}
// ✅ 부모에서 상태 관리
function App() {
  const [count, setCount] = useState(0);
  
  return (
    <>
      <Display count={count} />
      <Button setCount={setCount} />
    </>
  );
}
function Display({ count }) {
  return <p>Count: {count}</p>;
}
function Button({ setCount }) {
  return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}

복잡한 상태 관리: useReducer

import { useReducer } from 'react';
// Reducer 함수
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      return state;
  }
}
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

6. Next.js 소개

Next.js란?

Next.js는 React 위에 라우팅·빌드·SSR 등을 얹은 풀스택 프레임워크다. React vs Next.js:

React (라이브러리):
- UI만 담당
- 라우팅, SSR 등은 직접 구현
Next.js (프레임워크):
- React + 라우팅 + SSR + 빌드 최적화
- 즉시 프로덕션 준비 완료

Next.js 장점:

✅ 파일 기반 라우팅
✅ SSR (Server-Side Rendering)
✅ SSG (Static Site Generation)
✅ API Routes (백엔드 기능)
✅ 이미지 최적화
✅ 자동 코드 분할

Next.js 시작하기

# Next.js 프로젝트 생성
npx create-next-app@latest my-next-app
# 옵션 선택:
# ✓ TypeScript? Yes
# ✓ ESLint? Yes
# ✓ Tailwind CSS? Yes
# ✓ App Router? Yes
cd my-next-app
npm run dev
# http://localhost:3000 접속

7. App Router와 Server Components

App Router (Next.js 13+)

파일 기반 라우팅:

app/
├── page.js          → /
├── about/
│   └── page.js      → /about
├── blog/
│   ├── page.js      → /blog
│   └── [slug]/
│       └── page.js  → /blog/hello-world
└── api/
    └── users/
        └── route.js → /api/users

예제: 블로그 페이지 app/blog/page.js

export default function BlogPage() {
  return (
    <div>
      <h1>Blog</h1>
      <ul>
        <li><a href="/blog/first-post">First Post</a></li>
        <li><a href="/blog/second-post">Second Post</a></li>
      </ul>
    </div>
  );
}

app/blog/[slug]/page.js

export default function BlogPost({ params }) {
  const { slug } = params;
  
  return (
    <div>
      <h1>Post: {slug}</h1>
      <p>Content goes here...</p>
    </div>
  );
}

Server Components

Server Components서버에서만 실행·렌더링되는 컴포넌트다.

// app/posts/page.js (Server Component)
async function getPosts() {
  const res = await fetch('https://api.example.com/posts');
  return res.json();
}
export default async function PostsPage() {
  // 서버에서 데이터 fetch (클라이언트에 전송 안됨)
  const posts = await getPosts();
  
  return (
    <div>
      <h1>Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  );
}

Client Components (상호작용 필요)

// 실행 예제
'use client';  // 클라이언트 컴포넌트 선언
import { useState } from 'react';
export default function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

언제 무엇을 사용할까?

Server Component 사용:
✅ 데이터 fetch
✅ 백엔드 리소스 접근
✅ 민감한 정보 (API 키)
✅ 큰 의존성 (서버에만 로드)
Client Component 사용:
✅ 상호작용 (onClick, onChange)
✅ useState, useEffect 등 Hooks
✅ 브라우저 API (localStorage, window)

8. 데이터 Fetching

Server Component에서 데이터 가져오기

// app/users/page.js
async function getUsers() {
  const res = await fetch('https://api.example.com/users', {
    cache: 'no-store'  // 항상 최신 데이터
  });
  return res.json();
}
export default async function UsersPage() {
  const users = await getUsers();
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

캐싱 전략

// 1. 캐싱 안함 (항상 최신)
fetch(url, { cache: 'no-store' });
// 2. 캐싱 (기본값)
fetch(url, { cache: 'force-cache' });
// 3. 재검증 (10초마다)
fetch(url, { next: { revalidate: 10 } });

Loading과 Error 처리

app/posts/loading.js

export default function Loading() {
  return <p>Loading posts...</p>;
}

app/posts/error.js

'use client';
export default function Error({ error, reset }) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

9. 실전 프로젝트: Todo 앱

프로젝트 구조

app/
├── page.js          # 홈 (Todo 목록)
├── layout.js        # 레이아웃
└── api/
    └── todos/
        └── route.js # API 엔드포인트

구현

app/page.js

// 실행 예제
'use client';
import { useState, useEffect } from 'react';
export default function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState(');
  
  // 초기 데이터 로드
  useEffect(() => {
    fetch('/api/todos')
      .then(res => res.json())
      .then(data => setTodos(data));
  }, []);
  
  // Todo 추가
  const addTodo = async () => {
    if (!input.trim()) return;
    
    const response = await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: input })
    });
    
    const newTodo = await response.json();
    setTodos([...todos, newTodo]);
    setInput(');
  };
  
  // Todo 완료 토글
  const toggleTodo = async (id) => {
    const todo = todos.find(t => t.id === id);
    
    await fetch(`/api/todos/${id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ completed: !todo.completed })
    });
    
    setTodos(todos.map(t => 
      t.id === id ? { ...t, completed: !t.completed } : t
    ));
  };
  
  // Todo 삭제
  const deleteTodo = async (id) => {
    await fetch(`/api/todos/${id}`, { method: 'DELETE' });
    setTodos(todos.filter(t => t.id !== id));
  };
  
  return (
    <div style={{ maxWidth: '600px', margin: '50px auto' }}>
      <h1>Todo App</h1>
      
      {/* 입력 폼 */}
      <div style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="What needs to be done?"
          style={{ flex: 1, padding: '10px' }}
        />
        <button onClick={addTodo} style={{ padding: '10px 20px' }}>
          Add
        </button>
      </div>
      
      {/* Todo 목록 */}
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map(todo => (
          <li 
            key={todo.id}
            style={{
              display: 'flex',
              alignItems: 'center',
              gap: '10px',
              padding: '10px',
              borderBottom: '1px solid #eee'
            }}
          >
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span style={{
              flex: 1,
              textDecoration: todo.completed ? 'line-through' : 'none',
              color: todo.completed ? '#999' : '#000'
            }}>
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

app/api/todos/route.js

let todos = [
  { id: 1, text: 'Learn React', completed: false },
  { id: 2, text: 'Build a project', completed: false }
];
let nextId = 3;
// GET /api/todos
export async function GET() {
  return Response.json(todos);
}
// POST /api/todos
export async function POST(request) {
  const { text } = await request.json();
  const newTodo = { id: nextId++, text, completed: false };
  todos.push(newTodo);
  return Response.json(newTodo);
}
// PATCH /api/todos/:id
export async function PATCH(request) {
  const { id, completed } = await request.json();
  const todo = todos.find(t => t.id === id);
  if (todo) {
    todo.completed = completed;
  }
  return Response.json(todo);
}
// DELETE /api/todos/:id
export async function DELETE(request) {
  const url = new URL(request.url);
  const id = Number(url.pathname.split('/').pop());
  todos = todos.filter(t => t.id !== id);
  return Response.json({ success: true });
}

10. 스타일링

CSS Modules

// Button.module.css
.button {
  background-color: #007bff;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
}
.button:hover {
  background-color: #0056b3;
}
// Button.jsx
import styles from './Button.module.css';
export default function Button({ children, onClick }) {
  return (
    <button className={styles.button} onClick={onClick}>
      {children}
    </button>
  );
}

Tailwind CSS

// tailwind.config.js 설정 후
export default function Button({ children, onClick }) {
  return (
    <button 
      className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
      onClick={onClick}
    >
      {children}
    </button>
  );
}

11. 폼 처리

Controlled Components

function LoginForm() {
  const [email, setEmail] = useState(');
  const [password, setPassword] = useState(');
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Login:', { email, password });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
}

React Hook Form (추천)

npm install react-hook-form
import { useForm } from 'react-hook-form';
function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();
  
  const onSubmit = (data) => {
    console.log(data);
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('email', { 
          required: '이메일을 입력하세요',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: '유효한 이메일을 입력하세요'
          }
        })}
        placeholder="Email"
      />
      {errors.email && <p>{errors.email.message}</p>}
      
      <input
        type="password"
        {...register('password', { 
          required: '비밀번호를 입력하세요',
          minLength: {
            value: 8,
            message: '8자 이상 입력하세요'
          }
        })}
        placeholder="Password"
      />
      {errors.password && <p>{errors.password.message}</p>}
      
      <button type="submit">Login</button>
    </form>
  );
}

12. 상태 관리 라이브러리

Zustand (추천)

npm install zustand
// store.js
import { create } from 'zustand';
export const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 })
}));
// Counter.jsx
import { useStore } from './store';
export default function Counter() {
  const { count, increment, decrement, reset } = useStore();
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

13. 성능 최적화

React.memo (불필요한 리렌더링 방지)

import { memo } from 'react';
// ❌ 부모가 리렌더링되면 자식도 리렌더링
function Child({ name }) {
  console.log('Child rendered');
  return <p>{name}</p>;
}
// ✅ props가 변하지 않으면 리렌더링 안함
const Child = memo(function Child({ name }) {
  console.log('Child rendered');
  return <p>{name}</p>;
});

코드 분할 (Lazy Loading)

import { lazy, Suspense } from 'react';
// 동적 import (필요할 때만 로드)
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <HeavyComponent />
    </Suspense>
  );
}

14. 배포

Vercel 배포 (추천)

# Vercel CLI 설치
npm install -g vercel
# 배포
vercel
# 프로덕션 배포
vercel --prod

환경 변수

# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://...
// 클라이언트에서 접근 (NEXT_PUBLIC_ 접두사 필요)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// 서버에서만 접근
const dbUrl = process.env.DATABASE_URL;

FAQ

Q1. React vs Vue vs Angular?

  • React: 가장 인기, 생태계 방대, 자유도 높음
  • Vue: 배우기 쉬움, 한국 커뮤니티 활발
  • Angular: 대규모 엔터프라이즈, 학습 곡선 높음 Q2. Next.js를 꼭 써야 하나? 아니다. 다만 SEO·SSR·파일 기반 라우팅까지 한 번에 가져가고 싶으면 Next.js가 선택지가 된다. Vite+React만으로도 충분한 프로젝트가 많다. Q3. TypeScript를 써야 하나? 팀·코드베이스 규모가 커질수록 타입이 도움이 된다. 자동 완성·리팩터링 안전성이 체감된다. 작은 실험만 할 때는 JavaScript로 시작해도 된다. Q4. 상태 관리 라이브러리가 필요한가요? 프로젝트 규모에 따라:
  • 소규모: useState + Context API
  • 중규모: Zustand
  • 대규모: Redux Toolkit

요약

핵심 정리

React:

  • 컴포넌트 기반 UI 라이브러리
  • Hooks (useState, useEffect)
  • Virtual DOM Next.js:
  • React 풀스택 프레임워크
  • App Router, Server Components
  • SSR, SSG 지원 학습 로드맵:
  1. JavaScript ES6+ 문법
  2. React 기초 (컴포넌트, Props, State)
  3. React Hooks
  4. Next.js 기초
  5. 실전 프로젝트

다음 글 추천


키워드: React, Next.js, Frontend, JavaScript, TypeScript, Hooks, useState, useEffect, SSR, App Router, Server Components

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「React & Next.js 입문 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「React & Next.js 입문 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

React, Next.js, Frontend, JavaScript, TypeScript, Hooks, SSR, App Router, Server Components, 프론트엔드 등으로 검색하시면 이 글이 도움이 됩니다.