익스프레스 웹 서버 만들기

gdhi·2024년 1월 10일

Node.js

목록 보기
5/7

익스프레스클라이언트서버 사이의 요청과 응답에서 필터 같은 역할을 해주는 미들웨이 같은 것

📖익스프레스 프로젝트 시작하기

👉 생성


📌package.json

{
    "name": "learn-express",
    "version": "0.0.1",
    "description": "익스프레스를 배우자",
    "main": "01_app.js",
    "scripts": {
        "start": "nodemon app"
    },
    "author": "gdhi",
    "license": "MIT"
}



📍콘솔



📍app.js

const express = require('express');

// express 모듈을 실행해 app 변수에 할당
// 익스프레스 내부에 http 모듈이 내장되어 있으므로 서버 역할 수행가능
const app = express();

// 서버가 실행될 포트를 설정
// process.env 객체에 PORT 속성이 있다면 그 값을 사용 후 저장 👉 나중에 가져올 수 있음
app.set('port', process.env.PORT || 3000);

// / 주소에 대한 GET 요청이 올 때 어떤 동작?
// req는 요청에 관한 정보 객체, res는 응답에 관한 정보 객체
// / 요청시 응답으로 Hello, Express 전송
app.get('/', (req, res) => {
    res.send('Hello, Express');
});

// http 웹 서버와 동일
app.listen(app.get('port'), () => {
    console.log(app.get('port'), '번 포트에서 대기 중');
});



📍index.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>익스프레스 서버</title>
</head>
<body>
    <h1>익스프레스</h1>
    <p>배워봅시다.</p>
</body>
</html>



📍app.js 수정

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

const app = express();
app.set('port', process.env.PORT || 3000);
app.get('/', (req, res) => {
  // res.send('Hello, Express');
  res.sendFile(path.join(__dirname, '/02_index.html'));
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기 중');
});

👉 수정하면 알아서 재실행 된다. 신기









📖자주 사용하는 미들웨어

미들웨어는 익스프레스의 핵심이다. 요청과 응답의 중간(middle)에 위치하기 때문에 미들웨어(middleware)라고 부르는 것이다. 뒤에 나오는 라우터에러 핸들러 또한 미들웨어의 일종이므로 미들웨어가 익스프레스의 전부라고 해도 과언이 아니다. 미들웨어는 요청과 응답을 조작해 기능을 추가하기도 하고, 나쁜 요청을 걸러내기도 한다.

미들웨어는 app.use와 함께 사용된다. app.use(미들웨어) 꼴이다.

📌app.js 수정

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

const app = express();
app.set('port', process.env.PORT || 3000);

// 미들웨어는 위에서부터 아래로 순서대로 실행
// 요청과 응답 사이에 특별한 기능을 추가할 수 있다.
// 주소를 첫 번째 인수로 넣어주지 않았을 때 실행 됨 👉 모든 주소에서 실행
// next는 다음 미들웨어로 넘어가는 함수
// next를 실행하지 않으면 다음 미들웨어가 실행되지 않는다
app.use((req,res,next) => {
    console.log('모든 요청에 다 실행 됩니다.');
    next();
});

// 주소 /를 첫 번째 인수로 넣었을 때
app.get('/', (req,res,next) => {
    console.log('Get / 요청에서만 실행됩니다.');
    next();
}, (req, res) => {
    throw new Error('에러는 에러 처리 미들웨어로 갑니다.')
});

// 에러처리 미들웨어. 매개변수가 4개이며 사용하지 않더라도 반드시 4개여야 함
// 자체 에러 처리가 있긴하지만 직접 에러 처리 미들웨어를 연결해주는 것이 좋다
// err : 에러에 관한 정보
// res.status : HTTP 상태 코드 지정 (기본값 200(성공))
app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).send(err.message);
});

/*
app.get('/', (req, res) => {
  // res.send('Hello, Express');
  res.sendFile(path.join(__dirname, '/02_index.html'));
});
*/

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기 중');
});
미들웨어실행 조건
app.use(미들웨어)모든 요청에서 미들웨어 실행
app.use('/abc', 미들웨어)/abc로 시작하는 요청에서 미들웨어 실행
app.post('/abc', 미들웨어)/abc로 시작하는 POST 요청에서 미들웨어 실행



📌다양한 기능 추가하기

package.json 복사해서 expressTest2 폴더 생성 후 붙여넣기



📍app.js 생성

const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');
const path = require('path');

dotenv.config();
const app = express();
app.set('port', process.env.PORT || 3000);

app.use(morgan('dev'));
app.use('/', express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
    resave: false,
    saveUninitialized: false,
    secret: process.env.COOKIE_SECRET,
    cookie: {
        httpOnly: true,
        secure: false,
    },
    name: 'session-cookie',
}));

app.use((req, res, next) => {
    console.log('모든 요청에 다 실행됩니다.');
    next();
});

app.get('/', (req,res,next) => {
    console.log('Get / 요청에서만 실행됩니다.');
    next();
}, (req, res) => {
    throw new Error('에러는 에러 처리 미들웨어로 갑니다.')
});

app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).send(err.message);
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기 중');
});



📍.env 생성

COOKIE_SECRET=cookiesecret



📍결과

📖Router 객체로 라우팅 분리하기

라우터를 만들 때, 요청 메서드와 주소별로 분기 처리를 하다 보니 코드가 꽤 복잡해졌다. if문으로 분기 처리하는 것은 코드의 가독성을 떨어뜨리며, 확장하기도 번거로웠다. 그래서 익스프레스를 사용하게 된 이유 중 하나는 라우팅을 깔끔하게 관리할 수 있다는 점이다.

app.js 파일에서 사용되는 app.get과 같은 메서드는 라우터 부분을 나타낸다. 익스프레스는 라우터를 쉽게 분리할 수 있는 방법을 제공하여, 많은 라우터를 연결해도 app.js 코드를 간결하게 유지할 수 있다.



📌index.js

const express = require('express');

const router = express.Router();

// GET / 라우터
router.get('/', (req, res) => {
    res.send('Hello, Express');
});

module.exports = router;



📌user.js

const express = require('express');

const router = express.Router();

// GET /user 라우터
router.get('/', (req, res) => {
    res.send('Hello, User');
});

router.get('/a', (req, res) => {
    res.send('Hello, user A');
});

module.exports = router;



📌app.js

const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');
const path = require('path');

dotenv.config();

const indexRouter = require('./routes');
const userRouter = require('./routes/user');

const app = express();
app.set('port', process.env.PORT || 3000);

// 응답에 대한 정보를 콘솔에 기록
app.use(morgan('dev'));
// 정적 메모리를 올린다
app.use('/', express.static(path.join(__dirname, 'public')));
// Json 형식의 데이터 전달 방식
// URL-encoded는 주소 형식으로 데이터를 보내는 방식
// extended: false 👉 querystring 모듈을 사용해 쿼리스트링으로 해석
// extended: true 👉 qs 모듈을 사용하여 쿼리스트링으로 해석
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
    resave: false,
    saveUninitialized: false,
    secret: process.env.COOKIE_SECRET,
    cookie: {
        httpOnly: true,
        secure: false,
    },
    name: 'session-cookie',
}));

app.use('/', indexRouter);
app.use('/user', userRouter);

// 파일 업로드
const multer = require('multer');
const fs = require('fs');

try {
    fs.readdirSync('uploads');
} catch (error) {
    console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
    fs.mkdirSync('uploads');
}
// 업로드 버튼 누르면 실행
const upload = multer({
    storage: multer.diskStorage({
        destination(req, file, done) {
            done(null, 'uploads/');
        },
        filename(req, file, done) {
            const ext = path.extname(file.originalname);
            done(null, path.basename(file.originalname, ext) + Date.now() + ext);
        },
    }),
    limits: { fileSize: 5 * 1024 * 1024 },
});

app.get('/upload', (req, res) => {
    res.sendFile(path.join(__dirname, 'multipart.html'));
});

app.post('/upload', upload.any('image1', 'image2'), (req, res) => {
    console.log(req.file);
    res.send('ok');
});

app.use((req, res, next) => {
    console.log('모든 요청에 다 실행됩니다.');
    next();
});

app.get('/', (req, res, next) => {
    console.log('Get / 요청에서만 실행됩니다.');
    next();
}, (req, res) => {
    throw new Error('에러는 에러 처리 미들웨어로 갑니다.')
});

app.use((req, res, next) => {
    res.status(404).send('Not Found');
})

app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).send(err.message);
});

app.listen(app.get('port'), () => {
    console.log(app.get('port'), '번 포트에서 대기 중');
});



📌결과

👉 .env 필요, 전에 한 것 복붙



📖req, res 객체 살펴보기

익스프레스의 req, res 객체는 http 모듈의 req, res 객체를 확장한 것이다. 따라서 기존 http 모듈의 메서드를 그대로 사용할 수 있을 뿐만 아니라, 익스프레스가 제공하는 메서드와 속성도 활용할 수 있다. 예를 들면, res.writeHead, res.write, res.end과 같은 기존 http 모듈의 메서드를 사용할 수 있으면서, res.send나 res.sendFile과 같은 편리한 익스프레스 메서드도 활용할 수 있다. 그러나 익스프레스의 메서드가 편리하고 간결하기 때문에, 일반적으로 기존 http 모듈의 메서드는 그렇게 자주 사용되지 않는다.

익스프레스는 많은 속성과 메서드를 추가했지만, 여기서는 주로 사용되는 것들에 중점을 두어 알아본다

req

속성 또는 메서드설명
req.appreq 객체를 통해 앱 객체에 접근 가능. req.app.get('port')와 같이 사용 가능.
req.bodybody-parser 미들웨어로 해석된 요청 본문 객체.
req.cookiescookie-parser 미들웨어로 해석된 요청 쿠키 객체.
req.ip요청의 IP 주소.
req.params라우트 매개변수에 대한 정보를 담은 객체.
req.query쿼리스트링 정보를 담은 객체.
req.signedCookies서명된 쿠키들이 여기에 담겨 있음.
req.get(헤더 이름)특정 헤더의 값을 가져오는 메서드.

res

속성 또는 메서드설명
res.appres 객체를 통해 앱 객체에 접근 가능.
res.cookie(키, 값, 옵션)쿠키를 설정하는 메서드.
res.clearCookie(키, 옵션)쿠키를 제거하는 메서드.
res.end()데이터 없이 응답을 보냄.
res.json(JSON)JSON 형식의 응답을 보냄.
res.locals하나의 요청 안에서 미들웨어 간에 데이터를 전달하는 객체.
res.redirect(주소)리다이렉트할 주소와 함께 응답을 보냄.
res.render(뷰, 데이터)템플릿 엔진을 렌더링하여 응답할 때 사용하는 메서드.
res.send(데이터)데이터와 함께 응답을 보냄. 데이터는 문자열, HTML, 버퍼, 객체, 배열 등이 될 수 있음.
res.sendFile(경로)경로에 위치한 파일을 응답함.
res.set(헤더, 값)응답의 헤더를 설정함.
res.status(코드)응답 시의 HTTP 상태 코드를 지정함.

req나 res 객체의 메서드는 다음과 같이 메서드 체이닝(method chaining)을 지원하는 경우가 많다. 메서드 체이닝을 활용하면 코드양을 줄일 수 있다.

res
  .status(201)
  .cookie('test', 'test')
  .redirect('/admin');









📖템플릿 엔진 사용하기

a

📌넌적스

📍app.js

const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');
const path = require('path');
const nunjucks = require('nunjucks');

// 넌적스 셋팅
dotenv.config();
const indexRouter = require('./routes');
const userRouter = require('./routes/user');

const app = express();
app.set('port', process.env.PORT || 3000);
app.set('view engine', 'html');
nunjucks.configure('views', {
    express: app,
    watch: true,
});

app.use(morgan<('dev'));
app.use('/', express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
    resave: false,
    saveUninitialized: false,
    secret: process.env.COOKIE_SECRET,
    cookie: {
        httpOnly: true,
        secure: false,
    },
    name: 'session-cookie',
}));

app.use('/', indexRouter);
app.use('/user', userRouter);

app.use((req, res, next) => {
    res.status(404).send('Not Found');
});

app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).send(err.message);
});

app.listen(app.get('port'), () => {
    console.log(app.get('port'), '번 포트에서 대기 중');
});



📍layout.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{Document}}</title>
    <link rel="stylesheet" href="/style.css">
</head>
<body>
    {% block content %}
    {% endblock %}
</body>
</html>



📍index.html, error.html

index.html

{% extends 'layout.html' %}

{% block content %}
<h1>{{title}}</h1>
<p>Welcome to {{title}}</p>
{% endblock %}

error.html

{% extends 'layout.html' %}

<h1><%= message %></h1>
<h2><%= error.status %></h2>
<pre><%= error.stack %></pre>



📍index.js, user.js

index.js

const express = require('express');

const router = express.Router(); // 라우터 객체 생성

// GET / 라우터
router.get('/', function(req, res, next) {
    res.locals.title = '홍길동';
    res.render('index');
});

module.exports = router;

user.js

const express = require('express');

const router = express.Router();

// GET /user 라우터
router.get('/', (req, res) => {
  res.send('Hello, User');
});

module.exports = router;



📍결과

0개의 댓글