☁️ goormTIL | Web #42

매루·2025년 11월 7일

goormTIL

목록 보기
40/67
post-thumbnail

📅 2025-11-07

➡️ json-server에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리


🔎 학습 리마인드

📌 json-server

🔗 https://www.npmjs.com/package/json-server

  • 단일 json 파일을 간이 데이터베이스처럼 취급하여 그 위에 REST 스타일의 API를 자동으로 띄워주는 개발용 서버
  • Backend와 실제 DB와 API Server가 구축될 때까지 Frontend 개발에 임시적으로 사용할 mock data를 생성하기 위해 사용
    • mock data: 실세 서버나 데이터베이스가 준비되지 않았을 때 임시로 사용하는 가짜 데이터

💡 사용해보기

  1. 설치

    yarn add json-server
    yarn add json-server -D # 개발 환경인 경우, -D 옵션을 함께 입력
  1. db.json 파일 생성

    • 프로젝트 루트 경로에 db.json 파일 생성

    • json-server는 db.json에 입력한 내용을 바탕으로 데이터베이스를 제공

      {
        "todos": []
      }
  1. 서버 실행
    • 리액트가 실행 중인 터미널이 아닌 다른 터미널을 새로 열어 json-server 실행
      yarn json-server db.json --port 4000
      • db.json을 db로 삼고, 4000 포트에 서버를 시작해라

💡 실습: json-server로 Todo 앱 구현

  • db.json
    {
      "users": [],
      "todos": []
    }
  • Todo 컴포넌트 구현
    import { useEffect, useState } from 'react';
    
    const BASE_URL = 'http://localhost:4000';
    
    const Todo = () => {
        const [form, setForm] = useState({
            email: '',
            password: '',
        });
    
        const [user, setUser] = useState(null);
        const [todoTitle, setTodoTitle] = useState('');
        const [todos, setTodos] = useState([]);
        const [filter, setFilter] = useState('all');
    
        const resetForm = () => {
            setForm({ email: '', password: '' });
        };
    
        useEffect(() => {
            if (user) fetchTodos(filter);
        }, [user, filter]);
    
        // Todo 목록 조회 + 필터링
        const fetchTodos = async (filterType = 'all') => {
            const filterQuery = {
                all: '',
                active: '&completed=false',
                completed: '&completed=true',
            };
    
            try {
                const res = await fetch(`${BASE_URL}/todos?userId=${user.id}${filterQuery[filterType]}`);
                const data = await res.json();
    
                setTodos(data);
            } catch (err) {
                console.error('Todo 목록 조회 실패', err);
            }
        };
    
        // 회원가입
        const handleSignup = async () => {
            const res = await fetch(`${BASE_URL}/users`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(form),
            });
    
            if (res.ok) {
                alert('회원가입 완료!');
                resetForm(); // 성공 시 인풋 초기화
            } else {
                alert('회원가입 실패!');
            }
        };
    
        // 로그인
        const handleLogin = async () => {
            const res = await fetch(`${BASE_URL}/users?email=${form.email}`);
            const data = await res.json();
    
            if (data.length && data[0].password === form.password) {
                setUser(data[0]);
                alert('로그인 성공!');
    
                resetForm();
            } else {
                alert('로그인 실패!');
            }
        };
    
        // Todo 추가
        const handleAddTodo = async () => {
            if (!todoTitle.trim()) return alert('할 일을 입력해주세요!');
    
            const res = await fetch(`${BASE_URL}/todos`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    userId: user.id,
                    title: todoTitle,
                    completed: false,
                }),
            });
            const newTodo = await res.json();
    
            setTodos((prev) => [...prev, newTodo]);
            setTodoTitle('');
        };
    
        // Todo 완료 토글
        const toggleTodo = async (todo) => {
            try {
                const res = await fetch(`${BASE_URL}/todos/${todo.id}`, {
                    method: 'PATCH',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ completed: !todo.completed }),
                });
                const updatedData = await res.json();
    
                setTodos((prev) => prev.map((t) => (t.id === todo.id ? updatedData : t)));
            } catch (err) {
                console.error('완료 상태 변경 실패', err);
            }
        };
    
        // Todo 삭제
        const handleDelete = async (id) => {
            try {
                await fetch(`${BASE_URL}/todos/${id}`, { method: 'DELETE' });
    
                setTodos((prev) => prev.filter((t) => t.id !== id));
            } catch (err) {
                console.error('Todo 삭제 실패', err);
            }
        };
    
        // 로그아웃
        const handleLogout = () => {
            setUser(null);
            setTodos([]);
            setTodoTitle('');
            resetForm();
        };
    
        if (!user)
            return (
                <div>
                    <h2>로그인 / 회원가입</h2>
                    <input
                        placeholder="email"
                        value={form.email}
                        onChange={(e) => setForm({ ...form, email: e.target.value })}
                    />
                    <input
                        type="password"
                        placeholder="password"
                        value={form.password}
                        onChange={(e) => setForm({ ...form, password: e.target.value })}
                    />
    
                    <div>
                        <button onClick={handleLogin}>로그인</button>
                        <button onClick={handleSignup}>회원가입</button>
                    </div>
                </div>
            );
    
        return (
            <div>
                <h2>{user.email}의 Todo List</h2>
    
                <div>
                    <input
                        type="text"
                        placeholder="할 일 입력"
                        value={todoTitle}
                        onChange={(e) => setTodoTitle(e.target.value)}
                    />
                    <button onClick={handleAddTodo}>추가</button>
                </div>
    
                <div>
                    <select value={filter} onChange={(e) => setFilter(e.target.value)}>
                        <option value="all">전체</option>
                        <option value="active">진행중</option>
                        <option value="completed">완료</option>
                    </select>
                </div>
    
                <ul>
                    {todos.map((todo) => (
                        <li key={todo.id}>
                            <span style={{ textDecoration: todo.completed ? 'line-through' : '' }}>
                                <span onClick={() => toggleTodo(todo)}>{todo.title}</span>
                            </span>
                            <button onClick={() => handleDelete(todo.id)}>삭제</button>
                        </li>
                    ))}
                </ul>
    
                <button onClick={handleLogout}>로그아웃</button>
            </div>
        );
    };
    
    export default Todo;
    기능HTTP 메서드엔드포인트
    회원가입POST/users
    로그인GET/users?email=
    할 일 조회GET/todos?userId=
    할 일 추가POST/todos
    완료 토글PATCH/todos/:id
    삭제DELETE/todos/:id

0개의 댓글