6장. 익스프레스 웹서버 만들기

My_Code·2024년 1월 19일

Node.js 교과서

목록 보기
6/11
post-thumbnail

다음 내용은 인프런에서 공부한 내용을 복습하는 차원에서 기록한 것입니다.
출처 : https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-js-%EA%B5%90%EA%B3%BC%EC%84%9C


💻 6.1 익스프레스 프로젝트 시작하기

📌 Express 소개

✏️ http 모듈로 웹 서버를 만들 때 코드가 보기 좋지 않고, 확장성도 떨어짐

  • 프레임워크로 해결
  • 대표적인 것이 Express(익스프레스), Koa(코아), Hapi(하피)
  • 코드 관리도 용이하고 편의성이 많이 높아짐

📌 app.js 작성하기

✏️ 서버 구동의 핵심이 되는 파일

  • app.set(‘port’, 포트)로 서버가 실행될 포트 지정
  • app.get(‘주소’, 라우터)로 GET 요청이 올 때 어떤 동작을 할지 지정
  • app.listen(‘포트’, 콜백)으로 몇 번 포트에서 서버를 실행할지 지정
const express = require('express')

const app = express();

app.set('port', process.env.PORT || 3000);
app.get('/', (req, res) => {
    res.send('hello express123123');
});

app.post('/', (req, res) => {
    res.send('hello express');
});

app.get('/about', (req, res) => {
    res.send('hello express');
});

app.listen(app.get('port'), () => {
    console.log('익스프레스 서버 실행');
});

📌 익스프레스 서버 실행하기

✏️ npm start(package.json의 start 스크립트) 콘솔에서 실행


📌 HTML 서빙하기

✏️ res.sendFile로 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.sendFile(path.join(__dirname, 'index.html'));
});

app.post('/', (req, res) => {
    res.send('hello express');
});

app.get('/about', (req, res) => {
    res.send('hello express');
});

app.listen(app.get('port'), () => {
    console.log('익스프레스 서버 실행');
});
  • index.js
<html>
<head>
  <meta charset="UTF-8" />
  <title>익스프레스 서버</title>
</head>
<body>
  <h1>익스프레스</h1>
  <p>배워봅시다.</p>
</body>
</html>


💻 6.2 자주 사용하는 미들웨어

📌 미들웨어

✏️ 익스프레스는 미들웨어로 구성됨

  • 요청과 응답의 중간에 위치하여 미들웨어
  • app.use(미들웨어)로 장착
  • 위에서 아래로 순서대로 실행됨.
  • 미들웨어는 req, res, next가 매개변수인 함수
  • req: 요청, res: 응답 조작 가능
  • next()로 다음 미들웨어로 넘어감 (사용해야 다른 라우터로 넘어가서면 실행됨)

📌 에러처리 미들웨어

✏️ 에러가 발생하면 에러 처리 미들웨어로

  • err, req, res, next까지 매개변수가 4개
  • 첫 번째 err에는 에러가 관한 정보가 담김
  • res.status 메서드로 HTTP 상태 코드를 지정 가능(기본값 200)
  • 에러 처리 미들웨어를 안 연결해도 익스프레스가 에러를 알아서 처리해주긴 함.
  • 특별한 경우가 아니면 가장 아래에 위치하도록 함.
const express = require('express')
const path = require('path')

const app = express();

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

app.use((req, res, next) => {
    console.log("모든 요청에 실행!");
    next();
});

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

app.post('/', (req, res) => {
    res.send('hello express');
});

// 와일드카드 처리
// app.get('/:name', (req, res) => {
//     res.send(`hello ${req.params.name}`);
// });

app.get('/about', (req, res) => {
    res.send('hello express');
});

app.use((req, res, next) => {
    res.status(404).send("404 입니다!!");
})

// 에러처리 미들웨어
app.use((err, req, res, next) => {
    console.log(err);
    res.status(404).send("에러 발생!!!");
});

app.listen(app.get('port'), () => {
    console.log('익스프레스 서버 실행');
});

📌 next

✏️ next를 호출해야 다음 코드로 넘어감

  • next를 주석 처리하면 응답이 전송되지 않음
  • 다음 미들웨어(라우터 미들웨어)로 넘어가지 않기 때문
  • next에 인수로 값을 넣으면 에러 핸들러로 넘어감(‘route’인 경우 다음 라우터로)
app.use((req, res, next) => {
    console.log("모든 요청에 실행!");
    next();
});

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

// 에러처리 미들웨어
app.use((err, req, res, next) => {
    console.log(err);
    res.status(404).send("에러 발생!!!");
});

📌 자주쓰는 미들웨어

  • app.use로 장착
  • 내부에서 알아서 next를 호출해서 다음 미들웨어로 넘어감

📌 morgan

✏️ 서버로 들어온 요청과 응답을 기록해주는 미들웨어

  • 로그의 자세한 정도 선택 가능(dev, tiny, short, common, combined)
  • 예시) GET / 200 51.267 ms – 1539
  • 순서대로 HTTP요청 요청주소 상태코드 응답속도 – 응답바이트
  • 개발환경에서는 dev, 배포환경에서는 combined를 애용함.

✏️ 요청 헤더의 쿠키를 해석해주는 미들웨어

  • parseCookies 함수와 기능 비슷
  • req.cookies 안에 쿠키들이 들어있음
  • 비밀 키로 쿠키 뒤에 서명을 붙여 내 서버가 만든 쿠키임을 검증할 수 있음

✏️ 실제 쿠키 옵션들을 넣을 수 있음

  • expires, domain, httpOnly, maxAge, path, secure, sameSite 등
  • 지울 때는 clearCookie로(expires와 maxAge를 제외한 옵션들이 일치해야 함)
const cookieParser = require('cookie-parser');
app.use(cookieParser('password'));

app.get('/', (req, res) => {
    req.cookies;
  	req.signedCookies;
    res.cookie('name', encodeURIComponent("test"), {
        expires: new Date(),
        httpOnly: true,
        path: '/',
    });
    res.clearCookie('name', encodeURIComponent(name), {
        httpOnly: true,
        path: '/',
    });
    res.sendFile(path.join(__dirname, 'index.html'));
});

📌 body-parser

✏️ 요청의 본문을 해석해주는 미들웨어

  • 폼 데이터나 AJAX 요청의 데이터 처리
  • json 미들웨어는 요청 본문이 json인 경우 해석, urlencoded 미들웨어는 폼 요청 해석
  • put이나 patch, post 요청 시에 req.body에 프런트에서 온 데이터를 넣어줌
app.use(express.json());  // 클라이언트에서 JSON 데이터를 받을 때 파싱
app.use(express.urlencoded({extended: true}));  //form 으로 받을 때 파싱

📌 static

✏️ 정적인 파일들을 제공하는 미들웨어

  • 인수로 정적 파일의 경로를 제공
  • 파일이 있을 때 fs.readFile로 직접 읽을 필요 없음
  • 요청하는 파일이 없으면 알아서 next를 호출해 다음 미들웨어로 넘어감
  • 파일을 발견했다면 다음 미들웨어는 실행되지 않음
app.use('요청경로', express.static('실제 경로'));
app.use('/', express.static(__dirname, 'public'));

✏️ 컨텐츠 요청 주소와 실제 컨텐츠의 경로를 다르게 만들 수 있음

  • 요청 주소 localhost:3000/stylesheets/style.css
  • 실제 컨텐츠 경로 /public/stylesheets/style.css
  • 서버의 구조를 파악하기 어려워져서 보안에 도움이 됨
  • static 미들웨어는 보통 위쪽에서 실행
    - public에서 요청하는 파일을 찾으면 반환하고 다음으로 넘어가지 않음
    - 원하는 파일 없으면 다음으로 넘어감
    - public 폴더에서 찾기도 전에 cookie, session 등의 미들웨어를 동작시키는 것은 낭비이기 때문에

📌 express-session

✏️ 세션 관리용 미들웨어

  • 세션 쿠키에 대한 설정(secret: 쿠키 암호화, cookie: 세션 쿠키 옵션)
  • 세션 쿠키는 앞에 s%3A가 붙은 후 암호화되어 프런트에 전송됨
  • resave: 요청이 왔을 때 세션에 수정사항이 생기지 않아도 다시 저장할지 여부
  • saveUninitialized: 세션에 저장할 내역이 없더라도 세션을 저장할지
  • req.session.save로 수동 저장도 가능하지만 할 일 거의 없음
const session = require('express-session');
app.use(session({
    resave: false,
    saveUninitialized: false,
    secret: 'password',
    cookie: {
        httpOnly: true,
      	secure: false,
    },
}));

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

✏️ req나 res 객체 안에 값을 넣어 데이터 전달 가능

  • app.set과의 차이점: app.set은 서버 내내 유지, req, res는 요청 하는 동안만 유지
  • app.set은 그냥 모두에게 공개됨
  • 일반적으로 res.locals 객체를 데이터 전달용으로 많이 사용함 (또는 req.data)
  • 계속 유지시키고 싶은 데이터는 req.session.data에 넣어서 사용
app.use((req, res, next) => {
    req.data = 'password123'
});

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

📌 미들웨어 확장하기

✏️ 미들웨어 안에 미들웨어를 넣는 방법

  • 아래 두 코드는 동일한 역할
app.use(morgan('dev'));
// 또는
app.use((req, res, next) => {
  morgan('dev')(req, res, next);
});
  • 아래처럼 활용 가능
app.use((req, res, next) => {
    if (req.session.id) {
        express.static(__dirname, 'public')(req, res, next)
    } else {
        next();
    }
});

📌 멀티파트 데이터 형식

✏️ form 태그의 enctype이 multipart/form-data인 경우

  • body-parser로는 요청 본문을 해석할 수 없음
  • multer 패키지 필요

📌 multer 설정하기

✏️ multer 함수를 호출

  • storage는 저장할 공간에 대한 정보
  • diskStorage는 하드디스크에 업로드 파일을 저장한다는 것
  • destination은 저장할 경로를 done의 두 번째 인수로 넘기면 됨
  • filename은 저장할 파일명(파일명+날짜+확장자 형식)을 done으로 넘기면 됨
  • limits는 파일 개수나 파일 사이즈를 제한할 수 있음.
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 },
});
  • 실제 서버 운영 시에는 서버 디스크 대신에 S3같은 스토리지 서비스에 저장하는 게 좋음
    - storage 설정만 바꿔주면 됨

📌 multer 미들웨어들

✏️ single과 none, array, fields 미들웨어 존재

  • single은 하나의 파일을 업로드할 때, none은 파일을 아예 업로드하지 않을 때
  • req.file 안에 업로드 정보 저장
app.get('/upload', (req, res) => {
    res.sendFile(path.join(__dirname, 'multipart.html'));
});

app.post('/upload', upload.single('image'), (req, res) => {
  	// none, single, array, fields 활용해서 받는 이미지의 수 조절 가능
    console.log(req.file);
    res.send('ok');
});
  • array와 fields는 여러 개의 파일을 업로드 할 때 사용
  • array는 하나의 요청 body 이름 아래 여러 파일이 있는 경우
  • fields는 여러 개의 요청 body 이름 아래 파일이 하나씩 있는 경우
  • 두 경우 모두 업로드된 이미지 정보가 req.files 아래에 존재

코드 중간 정리

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


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

// 보안을 위한 정적인 파일 주소를 사용할 수 있게 하는 미들웨어
app.use((req, res, next) => {
    if (req.session) {
        express.static(__dirname, 'public')(req, res, next)
    } else {
        next();
    }
});

app.use(morgan('dev'));  // 전송 결과를 확인하는 미들웨어
app.use(cookieParser('password'));  // 헤더에 있는 쿠키 데이터를 파싱해주는 미들웨어
// 세션 쿠키에 대한 설정과 같은 세션 관리용 미들웨어
app.use(session({
    resave: false,
    saveUninitialized: false,
    secret: 'password',
    cookie: {
        httpOnly: true,
    },
}));
app.use(express.json());  // 클라이언트에서 JSON 데이터를 받을 때 파싱
app.use(express.urlencoded({extended: true}));  // form 으로 받을 때 파싱

// 이미지나 동영상과 같은 데이터를 처리할 수 있게 해주는 미들웨어
const multer = require('multer');
const fs = require('fs');

try {
    fs.readdirSync('uploads');
  	// 노드에서는 동기(Sync)를 사용하지 않는게 좋다고 했지만
  	// 서버 실행 전에 실행시켜야 할 내용들은 동기처리 하는 게 좋음
  } 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.single('image'), (req, res) => {
    // none, single, array, fields 활용
  	// upload가 호출시 위에서 설정한 upload(multer 미들웨어 객체)가 실행됨
    console.log(req.file);
    res.send('ok');
});

// 미들웨어간 데이터 전송하기
app.use((req, res, next) => {
    req.data = 'password123'  // res 나 req를 활용
    next()
});

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

app.post('/', (req, res) => {
    res.send('hello express');
});

//와일드카드 처리
app.get('/:name', (req, res) => {
    res.send(`hello ${req.params.name}`);
});

app.get('/about', (req, res) => {
    res.send('hello express');
});

app.use((req, res, next) => {
    res.status(404).send("404 입니다!!");
})

// 에러처리 미들웨어
app.use((err, req, res, next) => {
    console.log(err);
    res.status(404).send("에러 발생!!!");
});

app.listen(app.get('port'), () => {
    console.log('익스프레스 서버 실행');
});


📌 dotenv

✏️ .env 파일을 읽어서 process.env로 만듦

  • dot(점) + env
  • process.env.COOKIE_SECRET에 cookiesecret 값이 할당됨(키=값 형식)
  • 비밀 키들을 소스 코드에 그대로 적어두면 소스 코드가 유출되었을 때 비밀 키도 같이 유출됨
  • .env 파일에 비밀 키들을 모아두고 .env 파일만 잘 관리하면 됨
  • .env 파일을 권한에 따라 다르게 키를 부여하는 것이 좋음


💻 6.3 Router 객체로 라우터 분리하기

📌 express.Router

✏️ app.js가 길어지는 것을 막을 수 있음

  • userRouter의 get은 /user와 /가 합쳐져서 GET /user/가 됨
  • app.js
const indexRouter = require('./routes/index');
const userRouter = require('./routes/user');

....

app.use('/', indexRouter);
app.use('/user', userRouter);
  • 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');
});

module.exports = router;

📌 라우트 매개변수

✏️ :id를 넣으면 req.params.id로 받을 수 있음

  • 동적으로 변하는 부분을 라우트 매개변수로 만듦
  • 앞에서 와일드 카드라고 불렀지만 정확히는 라우트 매개변수임
  • 일반 라우터보다 뒤에 위치해야 함
router.get('/user/:id', function(req, rse) {
  console.log(req.params, req.query);
})
  • /user/123?limit=5&skip=10 주소 요청인 경우
// 출력 결과
// { id: '123' } { limit: '5', skip: '10' }


💻 6.4 req, res 객체 살펴보기

📌 req

✏️ req.app: req 객체를 통해 app 객체에 접근할 수 있습니다.

✏️ req.app.get('port')와 같은 식으로 사용할 수 있습니다.

✏️ req.body: body-parser 미들웨어가 만드는 요청의 본문을 해석한 객체입니다.

✏️ req.ip: 요청의 ip 주소가 담겨 있습니다.

✏️ req.params: 라우트 매개변수에 대한 정보가 담긴 객체입니다.

✏️ req.query: 쿼리스트링에 대한 정보가 담긴 객체입니다.

✏️ req.signedCookies: 서명된 쿠키들은 req.cookies 대신 여기에 담겨 있습니다.

✏️ req.get(헤더 이름): 헤더의 값을 가져오고 싶을 때 사용하는 메서드입니다


📌 res

✏️ res.app: req.app처럼 res 객체를 통해 app 객체에 접근할 수 있습니다.

✏️ res.cookie(키, 값, 옵션): 쿠키를 설정하는 메서드입니다.

✏️ res.clearCookie(키, 값, 옵션): 쿠키를 제거하는 메서드입니다.

✏️ res.end(): 데이터 없이 응답을 보냅니다.

✏️ res.json(JSON): JSON 형식의 응답을 보냅니다.

✏️ res.redirect(주소): 리다이렉트할 주소와 함께 응답을 보냅니다.

✏️ res.render(뷰, 데이터): 다음 절에서 다룰 템플릿 엔진을 렌더링해서 응답할 때 사용하는 메서드입니다.

  • ex) res.render('index', {title: 'Express'});
  • 퍼그에서 index.pug 파일에 title이라는 변수로 사용할 수 있게 전송

✏️ res.send(데이터): 데이터와 함께 응답을 보냅니다. 데이터는 문자열일 수도 있고H TML일 수도 있으며, 버퍼일 수도 있고 객체나 배열일 수도 있습니다.

✏️ res.sendFile(경로): 경로에 위치한 파일을 응답합니다.

✏️ res.setHeader(헤더, 값): 응답의 헤더를 설정합니다.

✏️ res.status(코드): 응답 시의 HTTP 상태 코드를 지정합니다.


📌 기타

✏️ 응답은 한 번만 보내야 함



💻 6.5 템플릿 엔진 사용하기

📌 템플릿 엔진

✏️ HTML의 정적인 단점을 개선

  • 반복문, 조건문, 변수 등을 사용할 수 있음
  • 동적인 페이지 작성 가능
  • PHP, JSP와 유사함

📌 Pug(구 Jade)

✏️ 문법이 Ruby와 비슷해 코드 양이 많이 줄어듦

  • HTML과 많이 달라 호불호가 갈림
  • 익스프레스에 app.set으로 퍼그 연결

📌 Pug - HTML 표현


📌 Pug - 변수

✏️ res.render에서 두 번째 인수 객체에 Pug 변수를 넣음

router.get('/', function(req, res) {
  res.render('index', {title: 'Express'});
});
  • res.locals 객체에 넣는 것도 가능(미들웨어간 공유됨)
router.get('/', function(req, res){
  res.local.title = 'Express'  //title이라는 변수로 선언 다른 미들웨어에서도 사용 가능
  res.render('index')
});
  • =이나 #{}으로 변수 렌더링 가능(= 뒤에는 자바스크립트 문법 사용 가능)

📌 Pug - 파일 내 변수

✏️ 퍼그 파일 안에서 변수 선언 가능

  • - 뒤에 자바스크립트 사용
  • 변수 값을 이스케이프 하지 않을 수도 있음(자동 이스케이프)

📌 Pug - 반복문

✏️ for in이나 each in으로 반복문 돌릴 수 있음

  • 값과 인덱스 가져올 수 있음

📌 Pug - 조건문

✏️ if else문, case when문 사용 가능


📌 Pug - include

✏️ 퍼그 파일에 다른 퍼그 파일을 넣을 수 있음

  • 헤더, 푸터, 내비게이션 등의 공통 부분을 따로 관리할 수 있어 편리
  • include로 파일 경로 지정

📌 Pug - extends와 block

✏️ 레이아웃을 정할 수 있음

공통되는 레이아웃을 따로 관리할 수 있어 좋음, include와도 같이 사용


📌 넌적스

✏️ Pug의 문법에 적응되지 않는다면 넌적스를 사용하면 좋음

  • Pug를 지우고 Nunjucks 설치
  • 확장자는 html 또는 njk(view engine을 njk로)

📌 넌적스 - 변수

✏️ {{변수}}

✏️ 내부 변수 선언 가능 {%set 자바스크립트 구문 }


📌 넌적스 - 반복문

✏️ {% %} 안에 for in 작성(인덱스는 loop 키워드)


📌 넌적스 - 조건문

✏️ {% if %} 안에 조건문 작성


📌 넌적스 - include

✏️ 파일이 다른 파일을 불러올 수 있음

  • include에 파일 경로 넣어줄 수 있음

📌 넌적스 - 레이아웃

✏️ 레이아웃을 정할 수 있음

  • 공통되는 레이아웃을 따로 관리할 수 있어 좋음, include와도 같이 사용

📌 에러처리 미들웨어

✏️ 에러 발생 시 템플릿 엔진과 상관없이 템플릿 엔진 변수를 설정하고 error 템플릿을 렌더링함

  • res.locals.변수명으로도 템플릿 엔진 변수 생성 가능
  • process.env.NODE_ENV는 개발환경인지 배포환경인지 구분해주는 속성
profile
조금씩 정리하자!!!

0개의 댓글