React + Node.js 풀스택 앱 배포하기 – 완성까지 한번에!

프로젝트 소개 및 목표

React + Node.js 풀스택 앱 배포하기는 프론트엔드부터 백엔드, 그리고 실제 운영 환경까지 전체 개발 사이클을 경험할 수 있는 실전 프로젝트입니다. 이 가이드를 통해 React로 사용자 인터페이스를 구축하고, Node.js와 Express로 REST API 서버를 만들며, MongoDB를 연결한 후 Heroku와 Vercel 같은 클라우드 플랫폼에 배포하는 전 과정을 단계별로 익힐 수 있습니다. 단순한 튜토리얼을 넘어 실무에서 바로 활용 가능한 아키텍처 설계와 배포 전략을 배우며, 포트폴리오로도 활용할 수 있는 완성도 높은 풀스택 애플리케이션을 만들어봅니다.

필요한 기술 스택

이 프로젝트를 완성하기 위해서는 다음의 기술 스택이 필요합니다:

  • 프론트엔드: React 18+, React Router, Axios, Tailwind CSS
  • 백엔드: Node.js 18+, Express.js, MongoDB, Mongoose
  • 배포: Vercel (프론트엔드), Render 또는 Railway (백엔드), MongoDB Atlas
  • 개발 도구: Git, npm/yarn, Postman (API 테스트), VS Code

기본적인 JavaScript 지식과 React 기초, Node.js 기본 문법을 이해하고 있다면 충분히 따라올 수 있습니다.

프로젝트 셋업

먼저 프로젝트의 기본 구조를 설정합니다. 이 프로젝트는 모노레포 형태로 구성하여 프론트엔드와 백엔드를 하나의 저장소에서 관리합니다.

# 프로젝트 루트 디렉토리 생성
mkdir fullstack-app && cd fullstack-app

# 백엔드 설정
mkdir server && cd server
npm init -y
npm install express mongoose dotenv cors
npm install --save-dev nodemon

# 프론트엔드 설정
cd ..
npx create-react-app client
cd client
npm install axios react-router-dom
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

프로젝트 구조는 다음과 같이 구성됩니다:

fullstack-app/
├── server/          # Node.js 백엔드
│   ├── models/
│   ├── routes/
│   ├── controllers/
│   └── server.js
└── client/          # React 프론트엔드
    ├── src/
    ├── public/
    └── package.json

단계별 구현 과정

1단계: MongoDB 데이터베이스 설정

먼저 MongoDB Atlas에서 무료 클러스터를 생성합니다. MongoDB Atlas 웹사이트에 접속하여 계정을 만들고, 새 클러스터를 생성한 후 연결 문자열을 복사합니다.

서버 디렉토리에 .env 파일을 생성합니다:

MONGODB_URI=mongodb+srv://username:[email protected]/myapp?retryWrites=true&w=majority
PORT=5000
NODE_ENV=development

2단계: Express 서버 구축

server/server.js 파일을 생성하여 기본 서버를 설정합니다:

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();

const app = express();
const PORT = process.env.PORT || 5000;

// 미들웨어
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// MongoDB 연결
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
})
.then(() => console.log('MongoDB 연결 성공'))
.catch(err => console.error('MongoDB 연결 실패:', err));

// 기본 라우트
app.get('/api', (req, res) => {
  res.json({ message: 'API 서버가 정상적으로 작동 중입니다!' });
});

app.listen(PORT, () => {
  console.log(`서버가 포트 ${PORT}에서 실행 중입니다.`);
});

3단계: 데이터 모델 및 API 엔드포인트 생성

간단한 할 일 관리 앱을 만들어봅니다. server/models/Task.js를 생성합니다:

const mongoose = require('mongoose');

const taskSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true,
    trim: true
  },
  description: {
    type: String,
    trim: true
  },
  completed: {
    type: Boolean,
    default: false
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

module.exports = mongoose.model('Task', taskSchema);

server/routes/tasks.js에 CRUD API를 구현합니다:

const express = require('express');
const router = express.Router();
const Task = require('../models/Task');

// 모든 할 일 조회
router.get('/', async (req, res) => {
  try {
    const tasks = await Task.find().sort({ createdAt: -1 });
    res.json(tasks);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

// 새 할 일 생성
router.post('/', async (req, res) => {
  const task = new Task({
    title: req.body.title,
    description: req.body.description
  });
  
  try {
    const newTask = await task.save();
    res.status(201).json(newTask);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
});

// 할 일 업데이트
router.patch('/:id', async (req, res) => {
  try {
    const task = await Task.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true }
    );
    res.json(task);
  } catch (error) {
    res.status(400).json({ message: error.message });
  }
});

// 할 일 삭제
router.delete('/:id', async (req, res) => {
  try {
    await Task.findByIdAndDelete(req.params.id);
    res.json({ message: '할 일이 삭제되었습니다' });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});

module.exports = router;

server.js에 라우트를 추가합니다:

const taskRoutes = require('./routes/tasks');
app.use('/api/tasks', taskRoutes);

4단계: React 프론트엔드 구현

client/src/services/api.js에 API 통신 로직을 작성합니다:

import axios from 'axios';

const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';

const api = axios.create({
  baseURL: API_URL,
  headers: {
    'Content-Type': 'application/json'
  }
});

export const getTasks = () => api.get('/tasks');
export const createTask = (task) => api.post('/tasks', task);
export const updateTask = (id, task) => api.patch(`/tasks/${id}`, task);
export const deleteTask = (id) => api.delete(`/tasks/${id}`);

export default api;

client/src/components/TaskList.jsx를 생성합니다:

import React, { useState, useEffect } from 'react';
import { getTasks, createTask, updateTask, deleteTask } from '../services/api';

function TaskList() {
  const [tasks, setTasks] = useState([]);
  const [newTask, setNewTask] = useState({ title: '', description: '' });
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchTasks();
  }, []);

  const fetchTasks = async () => {
    try {
      const response = await getTasks();
      setTasks(response.data);
      setLoading(false);
    } catch (error) {
      console.error('할 일 조회 실패:', error);
      setLoading(false);
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await createTask(newTask);
      setNewTask({ title: '', description: '' });
      fetchTasks();
    } catch (error) {
      console.error('할 일 생성 실패:', error);
    }
  };

  const toggleComplete = async (task) => {
    try {
      await updateTask(task._id, { completed: !task.completed });
      fetchTasks();
    } catch (error) {
      console.error('할 일 업데이트 실패:', error);
    }
  };

  const handleDelete = async (id) => {
    try {
      await deleteTask(id);
      fetchTasks();
    } catch (error) {
      console.error('할 일 삭제 실패:', error);
    }
  };

  if (loading) return 
로딩 중...
; return (

할 일 관리

setNewTask({...newTask, title: e.target.value})} className="w-full p-3 border rounded-lg" required />