[Node.js] React 연동, 댓글 기능

Comely·2025년 6월 9일

Node.js

목록 보기
13/14

서버 기본 설정

Node.js 서버 생성

const express = require('express');
const path = require('path');
const app = express();

app.listen(8080, function () {
  console.log('listening on 8080')
});

설치 과정
1. Node.js 설치
2. 작업폴더 생성 후 에디터로 오픈
3. server.js 파일 생성
4. 터미널에서 npm init -y 입력
5. npm install express 입력
6. nodemon server.js 또는 node server.js로 서버 실행


React의 역할과 특징

React가 필요한 이유

  • SPA(Single Page Application): 새로고침 없이 페이지 전환이 가능한 앱처럼 부드러운 웹사이트 제작
  • 기존 방식: 페이지 이동마다 전체 페이지를 새로 불러옴
  • React 방식: JavaScript로 페이지 내용만 교체하여 부드러운 사용자 경험 제공

React 프로젝트 생성

npx create-react-app 프로젝트명
cd 프로젝트명
npm run start  # 개발 서버 실행
npm run build  # 배포용 파일 생성

React와 Node.js 서버 연동

폴더 구조 예시

프로젝트 폴더/
├── server.js
└── react-project/
    └── build/
        ├── index.html
        ├── static/
        └── ...

서버에서 React 파일 제공

// server.js
app.use(express.static(path.join(__dirname, 'react-project/build')));

app.get('/', function (요청, 응답) {
  응답.sendFile(path.join(__dirname, '/react-project/build/index.html'));
});

정적 파일 제공 설정

  • express.static(): CSS, JS, 이미지 등 정적 파일들을 자동으로 제공
  • React 빌드 폴더의 모든 파일에 접근 가능하게 만드는 설정

라우팅 처리

React 라우터 사용 시 주의사항

  • 브라우저 URL 직접 입력 시 서버로 요청이 전달됨
  • React 라우터가 처리하지 못하는 상황 발생

React 라우팅 전권 위임

// 모든 경로를 React가 처리하도록 설정 (가장 하단에 배치)
app.get('*', function (요청, 응답) {
  응답.sendFile(path.join(__dirname, '/react-project/build/index.html'));
});

데이터 통신 방식

Server-side Rendering vs Client-side Rendering

Server-side Rendering
1. 서버에서 DB 데이터 조회
2. HTML에 데이터 삽입
3. 완성된 HTML을 클라이언트에 전송

Client-side Rendering (React 방식)
1. React에서 서버에 AJAX 요청으로 데이터 요청
2. 서버가 JSON 형태로 데이터만 전송
3. React가 받은 데이터로 동적으로 HTML 생성

React-서버 통신 설정

// server.js 상단에 추가
app.use(express.json());
var cors = require('cors');
app.use(cors());

설치 필요

npm install cors
  • express.json(): 클라이언트가 보낸 JSON 데이터 파싱
  • cors: 다른 도메인 간의 AJAX 요청 허용

개발 환경과 배포

개발 시 권장 방식

  • React: npm run start로 별도 서버 실행
  • Node.js: nodemon server.js로 별도 서버 실행
  • 각각 다른 포트에서 동시 실행

React에서 서버 통신

// 전체 URL 명시
fetch('http://localhost:8080/api/data')

// 또는 package.json에 proxy 설정
"proxy": "http://localhost:8080"

배포 시

  • npm run build로 최종 빌드 파일 생성
  • 빌드된 파일을 서버에서 정적 파일로 제공

서브디렉토리 배포

여러 앱을 다른 경로에서 제공

// 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 프로젝트 설정

// 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   // 현재 로그인 유저명
  })
})

비정규화 vs 정규화

정규화 방식 (관계형 DB)

  • 사용자 정보는 별도 테이블에 저장
  • 게시글에는 사용자 ID만 저장
  • 필요시 JOIN으로 데이터 결합
  • 장점: 데이터 정확성, 단점: 조회 속도 느림

비정규화 방식 (MongoDB 권장)

  • 게시글에 사용자명도 함께 저장
  • 데이터 중복 허용
  • 장점: 조회 속도 빠름, 단점: 데이터 불일치 가능성

권한 기반 기능 구현

본인 글만 삭제 가능

app.delete('/delete', async (요청, 응답) => {
  await db.collection('post').deleteOne({
    _id: new ObjectId(요청.query.docid),
    user: 요청.user._id  // 현재 로그인 유저와 작성자가 일치하는 경우만
  })
  응답.send('삭제완료')
})

추가 구현 아이디어
1. 본인 글만 수정 가능: 삭제와 동일한 방식으로 조건 추가
2. 본인 글에만 삭제 버튼 표시: EJS에서 조건문으로 버튼 노출 제어
3. 삭제 성공 시에만 UI 업데이트: AJAX 응답 처리로 성공 여부 확인


댓글 시스템 설계

데이터 저장 방식 비교

방식 1: 게시글 내 배열로 저장

{
  title: '게시글 제목',
  content: '게시글 내용',
  comments: ['댓글1', '댓글2', '댓글3']
}
  • 단점: 댓글 개별 수정/삭제 어려움, 16MB 용량 제한, 부분 조회 불가

방식 2: 별도 컬렉션으로 관리

// comment 컬렉션
{
  content: '댓글 내용',
  writerId: ObjectId('작성자ID'),
  writer: '작성자명',
  parentId: ObjectId('부모게시글ID')
}
  • 장점: 개별 수정/삭제 용이, 용량 제한 없음, 필요한 댓글만 조회 가능

댓글 기능 구현

1. 댓글 UI 생성

<!-- 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>

2. 댓글 저장 API

app.post('/comment', async (요청, 응답) => {
  let result = await db.collection('comment').insertOne({
    content: 요청.body.content,
    writerId: new ObjectId(요청.user._id),
    writer: 요청.user.username,
    parentId: new ObjectId(요청.body.parentId)
  })
  
  응답.redirect('back')  // 이전 페이지로 이동
})

3. 댓글과 함께 상세페이지 표시

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})
})

고급 기능 추가

  • AJAX 댓글: 새로고침 없이 댓글 추가
  • 실시간 업데이트: WebSocket을 활용한 실시간 댓글 반영
  • 댓글 페이지네이션: 댓글이 많을 때 페이지 단위로 로딩
profile
App, Web Developer

0개의 댓글