[Node.js] Express

Main·2024년 10월 3일
0

Node.js

목록 보기
13/20
post-thumbnail

Express는 Node.js에서 서버 사이드 웹 애플리케이션을 구축하기 위한 간단하면서도 유연한 웹 애플리케이션 프레임워크입니다. Express는 Node.js 환경에서 서버를 구축하고 HTTP 요청을 처리하는 과정을 쉽게 만들어줍니다.


Express 주요 객체 및 메서드

1 ) Express 컨테이너 객체 app(express())

app 객체는 Express 애플리케이션의 기본적인 컨테이너입니다. 이를 통해 서버의 주요 동작을 설정할 수 있습니다. 서버 설정, 미들웨어 추가, 라우팅 정의 등을 모두 이 객체를 통해 처리합니다.

  • app.get(path, callback) : 특정 경로에 대한 GET 요청을 처리합니다.
  • app.post(path, callback) : 특정 경로에 대한 POST 요청을 처리합니다.
  • app.put(path, callback) : 특정 경로에 대한 PUT 요청을 처리합니다.
  • app.patch(path, callback) : 특정 경로에 대한 PATCH 요청을 처리합니다.
  • app.delete(path, callback) : 특정 경로에 대한 DELETE 요청을 처리합니다.
  • app.use(path, callback) : 애플리케이션에 미들웨어를 추가합니다. 미들웨어는 요청과 응답 사이의 중간 처리 기능을 수행합니다.
  • app.set(key, value) : 웹 서버의 환경을 설정합니다.
  • app.listen(path, callback) : 서버를 특정 포트에서 실행합니다.

2 ) 요청 객체(req)

req 객체는 클라이언트로부터 전달된 HTTP 요청에 대한 정보를 담고 있습니다. 이 객체를 통해 요청 URL, HTTP 메서드, 헤더, 요청 본문 등의 정보를 얻을 수 있습니다.

  • req.params : URL 경로의 동적 파라미터를 접근할 수 있습니다.
  • req.query : URL의 쿼리 스트링 파라미터를 가져옵니다.
  • req.body : 요청 body의 데이터를 가져옵니다.
  • req.headers : 요청의 헤더 정보를 담고 있습니다. 특정 헤더 값을 가져올 때 사용합니다.
  • req.app : app 객체에 접근하여, req.app.get(’port’)와 같은 식으로 사용할 수 있습니다.
  • req.cookies : cookie-parser 미들웨어가 만드는 요청의 쿠키를 해석한 객체입니다.
  • req.ip : 요청의 ip 주소가 담겨 있습니다.
  • req,get(headerName) : 특정 헤더의 값을 가져올 때 사용합니다.

3 ) 응답 객체(res)

res 객체는 서버가 클라이언트로 HTTP 응답을 보낼 때 사용됩니다. 이 객체를 통해 상태 코드 설정, 본문 전송, 파일 전송 등 다양한 응답 작업을 할 수 있습니다.

  • res.send(body) : 클라이언트에게 응답을 보냅니다. 문자열, 객체, HTML 등을 보낼 수 있습니다.
  • res.json(JSON) : JSON 형식으로 데이터를 응답합니다.
  • res.status(code) : 응답의 상태 코드를 설정합니다. 일반적으로 다른 응답 메서드와 체이닝하여 사용합니다.
  • res.sendFile(path) : 지정된 경로의 파일을 클라이언트에 전송합니다.
  • res.render(view, data, callback) : 뷰 템플릿을 렌더링하고, 그 결과를 클라이언트에게 전달합니다.
    • view: 렌더링할 템플릿 파일의 이름입니다. Express는 기본적으로 views 디렉토리에서 이 파일을 찾습니다.
    • locals: 템플릿에 전달할 데이터입니다. 이 데이터는 템플릿 안에서 변수로 접근할 수 있습니다.
    • callback: 렌더링 후 실행되는 콜백 함수로, 선택 사항입니다. 에러나 결과를 처리할 때 사용됩니다.
  • res.end() : 데이터 없이 응답을 보냅니다.
  • res.setHeader(hedaer, value) : 응답의 헤더를 설정합니다.
  • res.redirect(url) : 특정 URL로 리다이렉트시킵니다.

4 ) 라우터 객체(Router)

Express의 Router 객체는 여러 개의 경로를 모듈화하고 독립적으로 관리하는 데 유용합니다. 애플리케이션의 특정 기능에 관련된 라우트를 한 곳에 모아 모듈화할 수 있어 큰 프로젝트의 유지보수가 수월해집니다.

  • router.get(path, callback) : app.get()과 비슷하게 특정 경로에 대한 GET 요청을 처리하지만, app 객체 대신 라우터에서 사용됩니다.
  • router.post(path, callback) : POST 요청을 처리하는 라우터 메서드입니다.
  • router.put(path, callback) : PUT요청을 처리하는 라우터 메서드입니다.
  • router.patch(path, callback) : PATCH요청을 처리하는 라우터 메서드입니다.
  • router.delete(path, callback) : DELETE요청을 처리하는 라우터 메서드입니다.
  • router.use(middleware) : 해당 라우터에 대한 미들웨어를 추가할 수 있습니다.

Express의 기능

1 ) 라우팅 처리

라우팅 처리는 Express의 가장 큰 장점 중 하나입니다. 다양한 HTTP 메소드(GET, POST, PUT, DELETE 등)에 대해 특정 경로를 간단하게 처리할 수 있습니다. 경로에 따라 분리된 라우터를 사용하면 더욱 구조적으로 코드를 관리할 수 있습니다.

// 필요한 모듈을 가져옵니다.
const express = require('express'); // Express 모듈 가져오기
const app = express(); // Express 애플리케이션 생성
const port = 3000; // 서버가 실행될 포트 번호

// 요청 본문을 JSON 형식으로 파싱하기 위한 미들웨어를 사용합니다.
app.use(express.json());

// 기본 경로 ('/')에 대한 GET 요청을 처리합니다.
app.get('/', (req, res) => {
  res.send('Hello, World! Express 서버에 오신 것을 환영합니다!');
});

// '/about' 경로에 대한 GET 요청을 처리합니다.
app.get('/about', (req, res) => {
  res.send('이 서버는 Express를 사용해 구축된 간단한 웹 서버입니다.');
});

// '/user' 경로에 대한 POST 요청을 처리합니다.
app.post('/user', (req, res) => {
  const userName = req.body.name;
  res.send(`사용자 ${userName}님을 추가했습니다!`);
});

// '/user/:id' 경로에 대한 GET 요청을 처리하여 특정 사용자의 정보를 가져옵니다.
// ':id'는 URL의 동적 파라미터로, 여러 사용자 ID를 처리할 수 있습니다.
app.get('/user/:id', (req, res) => {
  const userId = req.params.id; // URL에서 사용자 ID를 가져옴
  res.send(`ID가 ${userId}인 사용자의 정보를 반환합니다.`);
});

// '/user/:id' 경로에 대한 PUT 요청을 처리하여 특정 사용자의 정보를 업데이트합니다.
app.put('/user/:id', (req, res) => {
  const userId = req.params.id;
  const updatedName = req.body.name;
  res.send(`ID가 ${userId}인 사용자의 이름을 ${updatedName}으로 업데이트했습니다.`);
});

// '/user/:id' 경로에 대한 DELETE 요청을 처리하여 특정 사용자를 삭제합니다.
app.delete('/user/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`ID가 ${userId}인 사용자를 삭제했습니다.`);
});

// 서버를 지정된 포트에서 실행합니다.
app.listen(port, () => {
  console.log(`서버가 http://localhost:${port} 에서 실행 중입니다.`);
});

또한, Router 객체를 사용하면 각 경로(라우트)를 별도의 모듈로 분리하여 코드의 가독성을 높이고 유지보수를 용이하게 할 수 있습니다.

아래 코드는 위 예시 코드를 Router 객체를 이용하여 별도의 모듈로 분리한 예시 코드입니다.

// ./routes/index.js
const express = require('express');
const router = express().Router;

// '/' 경로에 대해 정의된 라우터를 사용합니다.
router.get('/',(req, res) => {
	res.send("Hello, World! Express 서버에 오신 것을 환영합니다!")
});

module.exports = router;
// ./routes/about.js

const express = require('express');
const router = express().Router;

// '/about' 경로에 대해 정의된 라우터를 사용합니다.
router.get('/',(req, res) => {
	res.send("이 서버는 Express를 사용해 구축된 간단한 웹 서버입니다.")
});

module.exports = router;
// ./routes/user.js

const express = require('express');
const router = express().Router;

// '/user' 경로에 대한 POST 요청을 처리합니다.
app.post('/', (req, res) => {
  const userName = req.body.name;
  res.send(`사용자 ${userName}님을 추가했습니다!`);
});

// '/user/:id' 경로에 대한 GET 요청을 처리하여 특정 사용자의 정보를 가져옵니다.
// ':id'는 URL의 동적 파라미터로, 여러 사용자 ID를 처리할 수 있습니다.
app.get('/:id', (req, res) => {
  const userId = req.params.id; // URL에서 사용자 ID를 가져옴
  res.send(`ID가 ${userId}인 사용자의 정보를 반환합니다.`);
});

// '/user/:id' 경로에 대한 PUT 요청을 처리하여 특정 사용자의 정보를 업데이트합니다.
app.put('/:id', (req, res) => {
  const userId = req.params.id;
  const updatedName = req.body.name;
  res.send(`ID가 ${userId}인 사용자의 이름을 ${updatedName}으로 업데이트했습니다.`);
});

// '/user/:id' 경로에 대한 DELETE 요청을 처리하여 특정 사용자를 삭제합니다.
app.delete('/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`ID가 ${userId}인 사용자를 삭제했습니다.`);
});

module.exports = router;
// app.js
const express = require('express');
const app = express();
const indexRouter = require('./routes');
const aboutRouter = require('./routes/about');
const userRouter = require('./routes/user')

// index router 연결
app.use('/', indexRouter);
// about router 연결
app.use('/about', aboutRouter);
// user router 연결
app.use('/user/', userRouter);

2 ) 미들웨어

Express의 미들웨어(Middleware)는 서버의 요청과 응답 사이에서 특정 작업을 수행하는 함수입니다. 미들웨어는 요청을 처리하거나 응답을 생성하기 전에 여러 단계에서 다양한 작업을 수행할 수 있도록 도와줍니다. 이러한 작업에는 로깅, 인증, 오류 처리, 요청 본문 파싱 등이 포함됩니다.

미들웨어는 Express의 매우 중요한 개념으로, 다음과 같은 역할을 합니다:

  • 요청 전처리: 인증, 데이터 파싱 등.
  • 요청 후처리: 로그 작성, 응답 데이터 수정 등.
  • 특정 경로에서 동작하는 기능: 특정한 URL에 대해서만 동작.

미들웨어의 구조

  • req : 클라이언트의 요청 객체.
  • res : 서버의 응답 객체.
  • next : 다음 미들웨어로 넘어가게 하는 함수. 미들웨어가 요청 처리를 끝내지 않고, 다음 미들웨어로 넘기고자 할 때 호출합니다. 만약 next()를 호출하지 않으면 요청은 해당 미들웨어에서 멈추게 됩니다. next에 인수로 값을 넣으면 에러 핸들러로 넘어갑니다.('route'인 경우 다음 라우터로 넘어갑니다.)
function middleware(req, res, next) {
  // 처리 로직
  next(); // 다음 미들웨어로 이동
}

미들웨어간 데이터 전달하기

req, res 객체 안에 값을 넣어 데이터를 전달할 수 있습니다.

app.set() 메소드와 차이점 : app.set() 메소드는 서버 내 유지 되지만 req, res는 요청 하나 동안만 유지됩니다.

req.body나 req.cookies 같은 미들웨어의 데이터와 겹치지 않게 주의해야합니다.

app.use((req, res, next) => {
  req.data = 'data';
  next();
}, (req, res, next) => {
  console.log(req.data);
  next();
})

미들웨어 확장

미들웨어에 새로운 미들웨어를 추가하여 확장할 수 있습니다.

morgan를 개발 또는 프로덕션 환경에 따라 다른 로그 형식을 적용합니다.

app.use((req, res, next) => {
	if(process.env.NODE_ENV === 'production') {
	 morgan('combined')(req, res, next);
	} else {
	 morgan('dev')(req, res, next);
	}
})

미들웨어의 종류

애플리케이션 레벨 미들웨어

  • 모든 라우트에 대해 동작하거나, 특정 경로에서만 동작하도록 설정할 수 있습니다.
const express = require('express');
const app = express();

// 모든 경로에 적용되는 미들웨어
app.use((req, res, next) => {
  console.log(`Request Method: ${req.method}, Request URL: ${req.url}`);
  next(); // 다음 미들웨어로 이동
});

// 특정 경로에서만 적용되는 미들웨어
app.use('/about', (req, res, next) => {
  console.log('About 페이지 요청이 들어왔습니다.');
  next();
});

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

라우터 레벨 미들웨어

  • 특정 라우터 객체에 미들웨어를 적용하는 방식입니다.
  • 이를 통해 특정 경로 그룹에만 미들웨어를 적용할 수 있습니다.
const router = express.Router();

// 특정 라우터에만 적용되는 미들웨어
router.use((req, res, next) => {
  console.log('라우터 레벨 미들웨어 실행 중');
  next();
});

router.get('/user/:id', (req, res) => {
  res.send(`User ID: ${req.params.id}`);
});

app.use('/user', router);

빌트인 미들웨어

  • Express는 미들웨어로 사용할 수 있는 여러 내장 기능을 제공합니다.
  • express.static(): 정적 파일을 제공하는 미들웨어로, HTML, CSS, 이미지 등과 같은 정적 리소스를 제공하는 데 사용됩니다.
app.use(express.static('public'));
  • express.json(): 요청 본문을 JSON 형식으로 파싱하는 미들웨어입니다. 이 미들웨어를 사용하면 POST 요청에서 전달된 JSON 데이터를 쉽게 req.body를 통해 접근할 수 있습니다.
app.use(express.json());

서드 파티 미들웨어

  • 외부 라이브러리에서 제공하는 미들웨어로, 특정 기능을 구현하는 데 도움을 줍니다.

주요 서드 파티 미들웨어

morgan : 로그 기록을 남기기 위한 미들웨어입니다.

const express = require('express');
const app = express();
const morgan = require('morgan');
app.use(morgan('dev')); // 요청 로그를 콘솔에 출력

cors : CORS (Cross-Origin Resource Sharing)를 설정하기 위한 미들웨어로, 다른 도메인에서의 요청을 허용하는 경우에 사용됩니다.

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

app.use(cors());

cookie-parser : 쿠키를 쉽게 처리할 수 있도록 도와주는 미들웨어입니다.

const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();

app.use(cookieParser());

// 쿠키 설정하기
app.get('/set-cookie', (req, res) => {
  res.cookie('username', 'gptonline', { maxAge: 900000, httpOnly: true });
  res.send('쿠키가 설정되었습니다.');
});

// 쿠키 읽기
app.get('/get-cookie', (req, res) => {
  let username = req.cookies.username;
  res.send(`저장된 쿠키: ${username}`);
});

express-session : 세션 관리를 위한 미들웨어입니다.

const session = require('express-session');
const express = require('express');
const app = express();

app.use(session({
  secret: 'your-secret-key', // 세션 암호화를 위한 비밀 키
  resave: false,             // 세션 데이터가 변경되지 않더라도 세션을 저장할지 여부
  saveUninitialized: true,   // 초기화되지 않은 세션을 저장할지 여부
  cookie: { secure: false }  // true면 HTTPS에서만 쿠키 전달 (개발 환경에서는 false)
}));

app.get('/', (req, res) => {
  // 세션 데이터 저장
  req.session.user = 'John Doe';
  res.send('세션이 설정되었습니다!');
});

app.get('/session', (req, res) => {
  // 세션 데이터 접근
  if (req.session.user) {
    res.send(`세션에 저장된 사용자: ${req.session.user}`);
  } else {
    res.send('세션이 없습니다.');
  }
});

app.listen(8080, () => {
  console.log('Server is running on port 8080');
});

에러 처리 미들웨어

  • 네 개의 인자를 받는 특별한 미들웨어로, Express에서 발생하는 오류를 처리하는 데 사용됩니다.
  • 오류가 발생하면 이 미들웨어가 호출되어 오류 메시지를 클라이언트에 전달합니다.
  • 여기서 네 개의 인자(err, req, res, next)를 사용하는 점이 특징입니다. 이는 일반적인 미들웨어와 다르게 오류 정보를 첫 번째 인자로 전달받아 처리할 수 있게 해줍니다.
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something wrong!');
});

3 ) 정적 파일 제공

Express는 정적 파일(HTML, CSS, JS 등)을 쉽게 제공할 수 있는 기능을 제공합니다. express.static() 미들웨어를 이용하여 정적 파일이 있는 디렉터리를 지정하면 서버에서 이 디렉터리에 있는 파일을 직접 제공할 수 있습니다.

app.use(express.static('public'));

위와 같은 코드를 추가하면 public 폴더의 모든 정적 파일을 제공하게 됩니다. 예를 들어, public/index.html/index.html로 접근할 수 있습니다.


4 ) 템플릿 엔진 통합

Express는 템플릿 엔진을 통합하여 서버 사이드에서 HTML을 동적으로 생성할 수 있도록 돕습니다. 대표적으로 Pug, EJS와 같은 템플릿 엔진을 사용할 수 있습니다.

app.set('view engine', 'pug');

app.get('/', (req, res) => {
    res.render('index', { title: 'Express', message: 'Hello there!' });
});

위 코드는 pug 템플릿 엔진을 사용하여 템플릿 파일에서 데이터를 바인딩하여 동적으로 HTML을 생성하고 클라이언트에게 전달할 수 있습니다.


express 서버 구축하기

express 프레임워크를 이용하여 서버를 구축하는 예시 코드입니다.

ejs 템플릿 엔진을 사용하였으며, Router 객체를 이용하여 라우터를 모듈로 분리하였습니다.

express.static()를 통해 정적 파일(js, css)을 제공하였습니다.

// ./src/views/home.ejs

<!DOCTYPE html>
<html lang="ko-KR">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Home</title>
  <link href="/css/home.css" rel="stylesheet" />
</head>
<body>
  <header>
    <h1>My Website</h1>
    <nav>
      <ul>
        <li><a href="/">HOME</a></li>
        <li><a href="/posts/create">CREATE</a></li>
        <li><a href="/posts">POSTS</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <section>
      <h2>환영합니다!</h2>
      <p>여기는 메인 페이지입니다. 이곳에서 다양한 게시물을 관리하고 새로운 게시물을 생성할 수 있습니다.</p>
      <button class="primary-btn" onclick="location.href='/posts'">게시물 목록 보기</button>
    </section>
  </main>
</body>
</html>
// ./src/views/posts.ejs

<!DOCTYPE html>
<html lang="ko-KR">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>게시글 목록</title>
    <link href="/css/posts.css" rel="stylesheet" />
    <script src="/js/posts.js" defer></script>
  </head>
  <body>
    <div class="container">
      <h1>게시글 목록</h1>
      <div class="posts">
        <% posts.forEach(post => { %>
          <div class="post" data-id="<%= post.id %>">
            <img src="<%= post.image %>" alt="post image" class="post-image" loading="lazy" width="300" height="300"/>
            <h2><%= post.title %></h2>
            <p><%= post.content %></p>
            <div class="btn-group">
              <button type="button" class="edit-btn">수정</button>
              <button type="button" class="delete-btn">삭제</button>
            </div>
          </div>
        <% }) %>
      </div>
    </div>
  </body>
</html>
// ./src/views/create.ejs

<!DOCTYPE html>
<html lang="ko-KR">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>게시물 생성</title>
  <script src="/js/posts.js" defer></script>
</head>
<body>
  <h1>게시물 생성</h1>
  <form id="post-form">
    <label for="title-input">제목:</label>
    <input id="title-input" type="text" placeholder="제목 입력" required/><br><br>
    
    <label for="content-input">내용:</label>
    <input id="content-input" type="text" placeholder="내용 입력" required/><br><br>
    
    <button id="submit-btn" type="submit">생성</button>
  </form>
</body>
</html>
// ./src/views/edit.ejs

<!DOCTYPE html>
<html lang="ko-KR">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>게시물 수정</title>
  <script src="/js/posts.js" defer></script>
</head>
<body>
  <h1>게시물 수정</h1>
  <form id="post-form">
    <label for="title-input">제목:</label>
    <input id="title-input" type="text" value="<%= post.title %>" placeholder="제목 입력" required/><br><br>
    
    <label for="content-input">내용:</label>
    <input id="content-input" type="text" value="<%= post.content %>" placeholder="내용 입력" required/><br><br>

    <label for="content-input">이미지 URL:</label>
    <input id="image-input" type="text" value="<%= post.image %>" placeholder="이미지 URL 입력" required/><br><br>
    
    <button id="submit-btn" type="submit">수정</button>
  </form>
</body>
</html>
// ./src/public/js

"use strict";
const pathname = location.pathname;
// 폼 제출 이벤트를 처리하는 함수
if (pathname.includes("create")) {
  document
    .getElementById("post-form")
    .addEventListener("submit", function (event) {
      event.preventDefault(); // 기본 폼 제출을 막음

      // 입력된 제목과 내용을 가져옴
      const id = new Date().getTime();
      const title = document.getElementById("title-input").value;
      const content = document.getElementById("content-input").value;

      // 서버로 보낼 데이터
      const postData = {
        id,
        title,
        content,
        image: "https://picsum.photos/300",
      };

      // Fetch API를 이용해 POST 요청 전송
      fetch("/posts", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ postData }), // JSON 형태로 변환 후 전송
      })
        .then((response) => {
          if (response.ok) {
            location.href = "/posts";
            alert("게시물이 성공적으로 생성되었습니다.");
          }
        })
        .catch((error) => {
          console.error("오류:", error);
          alert("게시물 생성에 실패했습니다.");
        });
    });
}

if (pathname.includes("posts")) {
  document.querySelectorAll(".delete-btn").forEach((button) => {
    button.addEventListener("click", async function () {
      const postElement = this.closest(".post"); // 해당 게시글 요소
      const postId = postElement.getAttribute("data-id"); // 게시글 id

      // 서버로 삭제 요청 보내기
      const response = await fetch("/posts", {
        method: "DELETE",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ postId }), // 객체 형태로 전달
      });

      if (response.ok) {
        // 삭제 성공 시, 해당 게시글을 DOM에서 제거
        postElement.remove();
        alert("게시글이 삭제되었습니다.");
      } else {
        alert("삭제 중 오류가 발생했습니다.");
      }
    });
  });

  document.querySelectorAll(".edit-btn").forEach((button) => {
    button.addEventListener("click", () => {
      const postElement = button.closest(".post"); // 해당 게시글 요소를 button 기준으로 찾음
      const postId = postElement.getAttribute("data-id"); // 게시글 id
      location.href = `/posts/edit?id=${postId}`;
    });
  });
}

if (pathname.includes("edit")) {
  document
    .getElementById("post-form")
    .addEventListener("submit", function (event) {
      event.preventDefault(); // 기본 폼 제출을 막음

      // 입력된 제목과 내용을 가져옴
      const searchParams = new URL(location.href).searchParams;
      const id = searchParams.get("id");
      const title = document.getElementById("title-input").value;
      const content = document.getElementById("content-input").value;
      const image = document.getElementById("image-input").value;

      // 서버로 보낼 데이터
      const postData = {
        id,
        title,
        content,
        image,
      };

      // Fetch API를 이용해 POST 요청 전송
      fetch("/posts", {
        method: "PATCH",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ postData }), // JSON 형태로 변환 후 전송
      })
        .then((response) => {
          if (response.ok) {
            location.href = "/posts";
            alert("게시물이 성공적으로 수정되었습니다.");
          }
        })
        .catch((error) => {
          console.error("오류:", error);
          alert("게시물 수정에 실패했습니다.");
        });
    });
}
/*./public/css/home.css */

/* 전체 스타일 */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Arial', sans-serif;
  background-color: #f4f4f4;
  color: #333;
  line-height: 1.6;
}

/* 헤더 및 네비게이션 */
header {
  background-color: #333;
  color: white;
  padding: 10px 20px;
  text-align: center;
}

header h1 {
  font-size: 2rem;
  margin-bottom: 10px;
}

nav ul {
  list-style: none;
  padding: 0;
  display: flex;
  justify-content: center;
}

nav ul li {
  margin: 0 15px;
}

nav ul li a {
  color: white;
  text-decoration: none;
  font-size: 1.1rem;
}

nav ul li a:hover {
  text-decoration: underline;
}

/* 메인 컨텐츠 */
main {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

section {
  margin-bottom: 40px;
}

section h2 {
  font-size: 1.8rem;
  margin-bottom: 20px;
}

p {
  font-size: 1.1rem;
}

/* 버튼 스타일 */
.primary-btn {
  background-color: #28a745;
  color: white;
  border: none;
  padding: 10px 20px;
  font-size: 1.2rem;
  cursor: pointer;
}

.primary-btn:hover {
  background-color: #218838;
}

/* 게시물 미리보기 */
.featured {
  display: flex;
  gap: 20px;
}

.post-preview {
  background-color: white;
  padding: 20px;
  border: 1px solid #ddd;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  width: 300px;
  text-align: center;
}

.post-preview img {
  max-width: 100%;
  height: auto;
  margin-bottom: 15px;
}

.post-preview h3 {
  font-size: 1.5rem;
  margin-bottom: 10px;
}

.post-preview p {
  font-size: 1rem;
  color: #555;
}

/* 푸터 */
footer {
  background-color: #333;
  color: white;
  text-align: center;
  padding: 20px 0;
  position: fixed;
  bottom: 0;
  width: 100%;
}
/* ./public/css/posts.css */

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: Arial, sans-serif;
  background-color: #f4f4f4;
}

.container {
  width: 80%;
  margin: 0 auto;
  padding: 20px;
}

h1 {
  text-align: center;
  margin-bottom: 20px;
}

.posts {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  justify-content: center;
}

.post {
  background-color: white;
  border: 1px solid #ddd;
  padding: 20px;
  width: 300px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.post-image {
  width: 100%;
  height: auto;
  margin-bottom: 15px;
}

h2 {
  font-size: 1.5rem;
  margin-bottom: 10px;
}

p {
  font-size: 1rem;
  color: #555;
}

.btn-group{
  margin-top: 2rem;
  display: flex;
  justify-content: end;
  gap: 8px;
}

.delete-btn,
.edit-btn {
  padding: 6px 12px;
  background-color: rgb(252, 168, 157);
  border-radius: 6px;
  border: none;
}

.edit-btn {
  background-color: rgb(157, 150, 253);
}
// ./src/routes/index.js

const express = require("express");
const router = express.Router();

router.get("/", (req, res) => {
  res.render("home");
});

module.exports = router;
// ./src/routes/posts.js

const express = require("express");
const router = express.Router();
const fs = require("fs").promises; // fs.promises 사용
const HomeController = require("../controllers/home.controller");

router.get("/", async (req, res) => {
  const postsData = await HomeController.getPosts();
  const posts = JSON.parse(postsData).posts;
  res.render("posts", { posts });
});

router.get("/create", (req, res) => {
  res.render("create");
});

router.get("/edit", async (req, res) => {
  const postId = req.query.id;
  const postsData = await HomeController.getPosts();
  const posts = JSON.parse(postsData).posts;
  const post = posts.find((post) => post.id === parseInt(postId, 10));
  res.render("edit", { post });
});

router.post("/", async (req, res) => {
  const postData = req.body.postData;
  const postsData = await HomeController.getPosts(); // 기존 게시물 데이터 불러오기
  const posts = JSON.parse(postsData).posts;

  // 새로운 게시물 추가
  posts.push(postData);

  // 업데이트된 데이터를 파일에 저장 (fs.promises.writeFile 사용)
  await fs.writeFile("./src/db/posts.json", JSON.stringify({ posts }, null, 2));

  return res.status(201).json({ message: "게시물 생성 성공", postData });
});

router.patch("/", async (req, res) => {
  const postData = req.body.postData;
  const postsData = await HomeController.getPosts(); // 기존 게시물 데이터 불러오기
  const posts = JSON.parse(postsData).posts;

  const updatedPosts = posts.map((post) => {
    if (post.id === parseInt(postData.id, 10)) {
      return { ...post, ...postData, id: parseInt(postData.id, 10) };
    } else {
      return post;
    }
  });

  // 업데이트된 데이터를 파일에 저장 (fs.promises.writeFile 사용)
  await fs.writeFile(
    "./src/db/posts.json",
    JSON.stringify({ posts: updatedPosts }, null, 2)
  );

  // 클라이언트에 응답
  return res.status(200).json({ message: "게시물 수정 성공", postData });
});

router.delete("/", async (req, res) => {
  const postId  = req.body.postId; // 객체에서 postId 가져오기
  const postsData = await HomeController.getPosts(); // 기존 게시물 데이터 불러오기
  const posts = JSON.parse(postsData).posts;
  const updatedPosts = posts.filter((post) => post.id !== parseInt(postId, 10));

  // 업데이트된 데이터를 파일에 저장 (fs.promises.writeFile 사용)
  await fs.writeFile(
    "./src/db/posts.json",
    JSON.stringify({ posts: updatedPosts }, null, 2)
  );

  return res.status(200).json({ message: "게시물 삭제 성공" });
});

module.exports = router;
// ./src/controllers/home.controller.js

const fs = require("fs").promises;
const path = require("path");

class HomeController {
  static async getPosts() {
    const filePath = path.join(__dirname, "../db/posts.json");
    const posts = await fs.readFile(filePath, "utf-8");
    return posts;
  }
}

module.exports = HomeController;
const express = require("express");
const app = express();
const path = require("path");
const homeRoutes = require("./src/routes/home"); // 홈 라우터
const postsRoutes = require("./src/routes/posts"); // 게시물 라우터

app.set("port", 8080); // 서버 포트 설정
app.set("view engine", "ejs"); // 템플릿 엔진 설정
app.set("views", "./src/views"); // 뷰 폴더 설정

app.use(express.static(path.join(__dirname, "public"))); // 정적 파일 제공

app.use(express.json()); // JSON 파싱 미들웨어
app.use(express.urlencoded({ extended: true })); // URL-encoded 파싱 미들웨어

app.use("/", homeRoutes); // 홈 라우터 사용
app.use("/posts", postsRoutes); // 게시물 라우터 사용

// 에러 처리 라우트
app.use((err, req, res, next) => {
  console.error(err);
  res.send(`에러: ${err}`);
});

// 서버 실행
app.listen(app.get("port"), () => {
  console.log("8080 포트에서 서버 실행 중");
  console.log("http://localhost:8080/");
});
profile
함께 개선하는 프론트엔드 개발자

0개의 댓글