// server.js
const express = require('express');
const path = require('path');
const app = express();
app.listen(8080, function () {
console.log('listening on 8080')
});
# 프로젝트 초기화
npm init -y
# Express 설치
npm install express
# 서버 실행
nodemon server.js # 또는 node server.js
| 구분 | 일반 HTML | React |
|---|---|---|
| 페이지 전환 | 새로고침 발생 | 새로고침 없음 |
| 개발 복잡도 | 단순 | 복잡 |
| 사용자 경험 | 기본 | 고급 |
| 코드 관리 | 어려움 | 컴포넌트화 |
# React 프로젝트 생성
npx create-react-app react-project
# 개발 서버 실행
npm run start
# 빌드 (배포용)
npm run build
project/
├── server.js # Node.js 서버
├── react-project/ # React 프로젝트
│ ├── src/ # 소스 코드
│ ├── public/ # 정적 파일
│ └── build/ # 빌드 결과물
└── package.json
// server.js
const express = require('express');
const path = require('path');
const app = express();
// React 빌드 파일 서빙
app.use(express.static(path.join(__dirname, 'react-project/build')));
// 메인 페이지
app.get('/', function (요청, 응답) {
응답.sendFile(path.join(__dirname, '/react-project/build/index.html'));
});
app.listen(8080, function () {
console.log('listening on 8080')
});
// server.js 하단에 추가
app.get('*', function (요청, 응답) {
응답.sendFile(path.join(__dirname, '/react-project/build/index.html'));
});
주의: 이 코드는 항상 가장 하단에 위치해야 함
// server.js 상단에 추가
app.use(express.json());
const cors = require('cors');
app.use(cors());
# CORS 설치
npm install cors
// React 컴포넌트에서
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => setProducts(data))
}, [])
// react-project/package.json
{
"name": "react-project",
"version": "0.1.0",
"proxy": "http://localhost:8080"
}
// 서버에서 HTML 생성
app.get('/products', async (요청, 응답) => {
let products = await db.collection('product').find().toArray()
응답.render('products.ejs', { products })
})
특징:
// 서버는 API만 제공
app.get('/api/products', async (요청, 응답) => {
let products = await db.collection('product').find().toArray()
응답.json(products)
})
// React에서 데이터 가져오기
const [products, setProducts] = useState([])
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => setProducts(data))
}, [])
특징:
// server.js
app.get('/api/products', async (요청, 응답) => {
try {
let products = await db.collection('product').find().toArray()
응답.json(products)
} catch (error) {
응답.status(500).json({ error: '서버 에러' })
}
})
// ProductList.js
import { useState, useEffect } from 'react'
function ProductList() {
const [products, setProducts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data)
setLoading(false)
})
.catch(err => {
console.error(err)
setLoading(false)
})
}, [])
if (loading) return <div>로딩중...</div>
return (
<div>
<h2>제품 목록</h2>
{products.map(product => (
<div key={product._id}>
<h3>{product.name}</h3>
<p>{product.price}원</p>
</div>
))}
</div>
)
}
export default ProductList
// server.js
app.use('/', express.static(path.join(__dirname, 'public')))
app.use('/react', express.static(path.join(__dirname, 'react-project/build')))
app.get('/', function(요청, 응답) {
응답.sendFile(path.join(__dirname, 'public/main.html'))
})
app.get('/react', function(요청, 응답) {
응답.sendFile(path.join(__dirname, 'react-project/build/index.html'))
})
// react-project/package.json
{
"homepage": "/react",
"version": "0.1.0",
...
}
app.post('/add', upload.single('img1'), async (요청, 응답) => {
await db.collection('post').insertOne({
title: 요청.body.title,
content: 요청.body.content,
user: 요청.user._id, // 현재 로그인된 유저 ID
username: 요청.user.username, // 유저명
img: 요청.file?.location
})
응답.redirect('/list')
})
app.delete('/delete', async (요청, 응답) => {
await db.collection('post').deleteOne({
_id: new ObjectId(요청.query.docid),
user: 요청.user._id // 본인 글만 삭제 가능
})
응답.send('삭제완료')
})
// 글 테이블
{ _id: 1, title: '제목', content: '내용', userId: 123 }
// 유저 테이블
{ _id: 123, username: 'john', email: 'john@email.com' }
장점: 데이터 정확성
단점: 조회시 JOIN 필요 (속도 저하)
// 글 document
{
_id: 1,
title: '제목',
content: '내용',
user: 123,
username: 'john' // 중복 저장
}
장점: 빠른 조회 속도
단점: 데이터 불일치 가능성
{
_id: 1,
title: '제목',
content: '내용',
comments: [
{ content: '댓글1', writer: 'user1' },
{ content: '댓글2', writer: 'user2' }
]
}
문제점:
// comment collection
{
_id: ObjectId,
content: '댓글 내용',
writerId: ObjectId,
writer: 'username',
parentId: ObjectId, // 부모 글 ID
createdAt: new Date()
}
<!-- detail.ejs -->
<div class="detail-bg">
<h4><%= result.title %></h4>
<p><%= result.content %></p>
<hr style="margin-top: 60px">
<!-- 댓글 목록 -->
<% for (let i = 0; i < result2.length; i++) { %>
<p><strong><%= result2[i].writer %></strong> <%= result2[i].content %></p>
<% } %>
<!-- 댓글 작성 폼 -->
<form action="/comment" method="POST">
<input name="content" placeholder="댓글을 입력하세요">
<input name="parentId" value="<%= result._id %>" style="display: none">
<button type="submit">댓글작성</button>
</form>
</div>
app.post('/comment', async (요청, 응답) => {
await db.collection('comment').insertOne({
content: 요청.body.content,
writerId: new ObjectId(요청.user._id),
writer: 요청.user.username,
parentId: new ObjectId(요청.body.parentId),
createdAt: new Date()
})
// 이전 페이지로 돌아가기
응답.redirect('back') // Express 4.x
// 응답.redirect(요청.get('Referrer')) // Express 5.x
})
app.get('/detail/:id', async (요청, 응답) => {
// 글 정보 가져오기
let result = await db.collection('post')
.findOne({ _id: new ObjectId(요청.params.id) })
// 해당 글의 댓글들 가져오기
let result2 = await db.collection('comment')
.find({ parentId: new ObjectId(요청.params.id) })
.toArray()
응답.render('detail.ejs', {
result: result,
result2: result2
})
})
npm run start (포트 3000)nodemon server.js (포트 8080)npm run buildReact.memo() 사용useMemo(), useCallback() 활용