본문 바로가기
대외활동/코드잇부스트_백엔드 1기

codeIt! DEMO day! 개발 2일차) express 시작하기& 라우터 사용

by 피스타0204 2024. 8. 17.

 

우리는 node.js, MongoDB, express 라이브러리를 사용해 API를 개발할 것입니다.

  • node js : 웹브라우저 바깥에서 JS를 실행하는 환경
    • API를 제공하는 프로그램은 웹 브라우저 바깥에서 실행됨
  • Express : JS 백엔드에서 가장 유명한 라이브러리
    • 리퀘스트와 리스폰스를 쉽게 다룰 수 있음
  • mongo DB : 데이터를 테이블이 아닌 문서 형태로 저장하는 DB
  • 엔드포인트 = HTTP 메소드 + URL
  • API : 엔드포인트들을 모은 것

목차

1. express 시작하기

2. 미들웨어

3. 라우터 사용해보기

4. 라우터 레벨 미들웨어

 

 

 

 

1. express로 시작하기

express로 라우트, 특정 엔드포인트를 담당하는 코드를 작성할 것입니다.

 

1) package 설치

//express 설치
npm install express

//nodemon 설치
npm install --save-dev nodemon

 

node.js가 설치되어 있다면 npm install로 express 모듈을 설치할 수 있습니다.

nodemon을 설치하면 터미널에 npm start라는 간단한 명령어로 서버를 킬 수 있습니다.


 2) "type": "module"

위에서 npm install로 생성된 package.json에 "type": "module"을 추가합니다. "type": "module"이 있어야 import, export같은 ECMAscript 문법을 사용할 수 있기 때문에 반드시 작성해야 합니다.

 

#ECMAscript에 대해 더 알고 싶다면 아래 접은 글 확인

더보기

1) 모던 자바스크립트란? 

ECMAScript란 쉽게 말해 자바스크립트 언어의 표준을 말합니다. 
ECMAinternationl에서 ES6이후부터 1년마다 새로 출시해왔습니다. 하지만 모든 브라우저들이 ECMAScript가 최신화될 때마다 지원하지는 않습니다. 비용문제도 있고 이전에 사용하던 코드들이 문제가 생길 수 있기 때문입니다.  ES6+ 라고 이야기 할 만큼 ES6(ES2015)에서 크게 변화한 ES6이후 버전이면 보통 모던 자바스크립트를 지원하고 있는 브라우저들이라고 합니다.

 

그래서 ES의 최신버전을 바로 적용하지 않고 보편적으로 사용하는 브라우저들이 지원하는 범위(현 시점에 사용하기 적합한 범위 내에서) 최신 버전의 표준을 준수하는 자바스크립트를 Modern javaScript라고 부름.

즉, 모던 자바스크립트는 2015년에 나온 ECMAScript 2015(6판)와 그 이후의 판을 구현한 자바스크립트입니다.

 

 

 

2) JavaScript와 ECMAScript의 차이

  1. js는 프로그래밍 언어과 ECMAScript는 그 프로그래밍 언어의 표준임.
  2. js는 ECMAScript를 기반으로 하지만 DOM과 같이 WebIDL에서 표준화된 기능 등 부가적인 기능을 더 가지고 있음.

3) ECMAScript와 지원 브라우저에 대해 더 알아보고 싶어!

 ECMA-International 공식 ECMA-262문서
 한눈에 확인하는 호환성 테이블
 문법 검색으로 확인하는 호환성 테이블


3) "scripts" : {}

scripts를 package.json에 적습니다.

{
  "type": "module",
  "dependencies": {
    "express": "^4.19.2"
  },
  "devDependencies": {
    "nodemon": "^3.1.4"
  },"scripts": {
    "dev": "nodemon app.js",
    "start": "node app.js"
  }
}

 

nodemon에서 자동으로 실행하게 해줄 명령어를 설정하는 부분입니다.

이 과정으로 완료하고 npm start를 터미널에 입력하면 서버가 실행됩니다.


4) app.js, request.http 파일 만들기

//app.js

import express from 'express';
const app = express();
app.get('/hello', (req,res)=>{
    res.send('Hello Express!');
});

app.listen(3000, ()=> console.log('Server Started'));

 

//request.http

GET http://localhost:3000/hello

 

 

파일을 모두 만들고 npm run dev를 터미널에 입력하여 실행해보자!

npm start로 시작할 수도 있지만 npm start는 nodemon이 아니라 node로 실행되기 때문에 코드 수정시 서버를 매번 재시작해주어야 하므로 npm run dev를 사용하자.

2. 미들웨어

express에서는 request, response 사이에서 어떤 작업을 수행하는 함수를 미들웨어라고 부른다. express를 미들웨어의 집합이라고도 한다.

이름 있는 함수로 만들어 여러 경로에서 재사용할 수 도 있다.

3. 라우터 사용해보기

1) 실습 설명

다음 코드의 라우터를 분리하는 실습을 같이 해보며 라우터를 사용해보자

import express from 'express';

const app = express();
app.use(express.json()); // JSON 파싱 미들웨어 추가

// 임시 데이터 저장소
let posts = [];
let postIdCounter = 1;

// 게시글 등록 API
app.post('/posts', (req, res) => {
    const {
        nickname,
        title,
        content,
        postPassword,
        groupPassword,
        imageUrl,
        tags,
        location,
        moment,
        isPublic
    } = req.body;

    const { groupId } = req.query;

    // 필수 필드가 누락된 경우 400 에러 반환
    if (!nickname || !title || !content || !postPassword || !groupPassword || !groupId) {
        return res.status(400).json({ message: "잘못된 요청입니다" });
    }

    // 게시글을 저장하는 로직
    const newPost = {
        id: postIdCounter++,
        groupId: Number(groupId),
        nickname,
        title,
        content,
        imageUrl,
        tags,
        location,
        moment,
        isPublic,
        likeCount: 0,
        commentCount: 0,
        createdAt: new Date().toISOString(),
    };

    posts.push(newPost);

    // 성공적으로 생성되었음을 나타내는 200 응답
    return res.status(200).json(newPost);
});

// 게시글 목록 조회 API
app.get('/api/groups/:groupId/posts', (req, res) => {
    const { groupId } = req.params;
    const { page = 1, pageSize = 10, sortBy = 'latest', keyword = '', isPublic = 'true' } = req.query;

    const pageNumber = Number(page);
    const pageSizeNumber = Number(pageSize);
    const groupIdNumber = Number(groupId);
    const isPublicBoolean = isPublic === 'true';

    // 요청 파라미터 유효성 검사
    if (isNaN(pageNumber) || isNaN(pageSizeNumber) || isNaN(groupIdNumber)) {
        return res.status(400).json({ message: "잘못된 요청입니다" });
    }

    // 필터링
    let filteredPosts = posts.filter(post => 
        post.groupId === groupIdNumber &&
        post.title.includes(keyword) &&
        post.isPublic === isPublicBoolean
    );

    // 정렬
    if (sortBy === 'latest') {
        filteredPosts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
    } else if (sortBy === 'mostCommented') {
        filteredPosts.sort((a, b) => b.commentCount - a.commentCount);
    } else if (sortBy === 'mostLiked') {
        filteredPosts.sort((a, b) => b.likeCount - a.likeCount);
    }

    // 페이징
    const totalItemCount = filteredPosts.length;
    const totalPages = Math.ceil(totalItemCount / pageSizeNumber);
    const pagedPosts = filteredPosts.slice((pageNumber - 1) * pageSizeNumber, pageNumber * pageSizeNumber);

    return res.status(200).json({
        currentPage: pageNumber,
        totalPages: totalPages,
        totalItemCount: totalItemCount,
        data: pagedPosts
    });
});

app.listen(3000, () => console.log('Server Started on port 3000'));

 


2) 라우터와 app.js 분리

import express from 'express';

const app = express();

// 새로운 라우터 객체 생성
const productRouter = express.Router()

// 라우터 객체에 각 라우트 연결
/* app.route */
productRouter.route('/')
	.get((req, res) => {
	  res.json({ message: 'Product 목록 보기' });
	})
	.post((req, res) => {
	  res.json({ message: 'Product 추가하기' });
	});
	
productRouter.route('/:id')
	.patch((req, res) => {
	  res.json({ message: 'Product 수정하기' });
	})
	.delete((req, res) => {
	  res.json({ message: 'Product 삭제하기' });
	});

// app 객체에 라우터 연결
app.use('/products', productRouter)


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

app.js

// app.js
import express from 'express';
import postsRouter from './routes/posts.js';

const app = express();
app.use(express.json()); // JSON 파싱 미들웨어 추가

// '/api' 경로에 postsRouter 연결
app.use('/api', postsRouter);

app.listen(3000, () => console.log('Server Started on port 3000'));

 

라우터 파일을 분리할 때, app.use를 통해 라우터를 등록한 후에, 해당 라우터의 경로에 대한 모든 요청을 처리할 수 있습니다. app.use는 모든 HTTP 메서드(GET, POST, PUT, DELETE 등)를 처리할 수 있는 라우터를 연결하는 방법이기 때문에, get, post 등의 메서드를 별도로 추가할 필요는 없습니다.

 


posts.js

// routes/posts.js
import express from 'express';

const router = express.Router();

// 임시 데이터 저장소
let posts = [];
let postIdCounter = 1;

// 게시글 등록 API
router.post('/', (req, res) => {
    // (생략된 코드)
});

// 게시글 목록 조회 API
router.get('/groups/:groupId/posts', (req, res) => {
    // (생략된 코드)
});

export default router;

 

실제코드

더보기
// routes/posts.js
import express from 'express';

const router = express.Router();

// 임시 데이터 저장소
let posts = [];
let postIdCounter = 1;

// 게시글 등록 API
router.post('/groups/:groupId/posts', (req, res) => {
    const {
        nickname,
        title,
        content,
        postPassword,
        groupPassword,
        imageUrl,
        tags,
        location,
        moment,
        isPublic
    } = req.body;

    const { groupId } = req.params; // groupId는 URL 파라미터로 가져옵니다.

    // 필수 필드가 누락된 경우 400 에러 반환
    if (!nickname || !title || !content || !postPassword || !groupPassword || !groupId) {
        return res.status(400).json({ message: "잘못된 요청입니다" });
    }

    // 게시글을 저장하는 로직
    const newPost = {
        id: postIdCounter++,
        groupId: Number(groupId),
        nickname,
        title,
        content,
        imageUrl,
        tags,
        location,
        moment,
        isPublic,
        likeCount: 0,
        commentCount: 0,
        createdAt: new Date().toISOString(),
    };

    posts.push(newPost);

    // 성공적으로 생성되었음을 나타내는 200 응답
    return res.status(200).json(newPost);
});

// 게시글 목록 조회 API
router.get('/groups/:groupId/posts', (req, res) => {
    const { groupId } = req.params;
    const { page = 1, pageSize = 10, sortBy = 'latest', keyword = '', isPublic = 'true' } = req.query;

    const pageNumber = Number(page);
    const pageSizeNumber = Number(pageSize);
    const groupIdNumber = Number(groupId);
    const isPublicBoolean = isPublic === 'true';

    // 요청 파라미터 유효성 검사
    if (isNaN(pageNumber) || isNaN(pageSizeNumber) || isNaN(groupIdNumber)) {
        return res.status(400).json({ message: "잘못된 요청입니다" });
    }

    // 필터링
    let filteredPosts = posts.filter(post => 
        post.groupId === groupIdNumber &&
        post.title.includes(keyword) &&
        post.isPublic === isPublicBoolean
    );

    // 정렬
    if (sortBy === 'latest') {
        filteredPosts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
    } else if (sortBy === 'mostCommented') {
        filteredPosts.sort((a, b) => b.commentCount - a.commentCount);
    } else if (sortBy === 'mostLiked') {
        filteredPosts.sort((a, b) => b.likeCount - a.likeCount);
    }

    // 페이징
    const totalItemCount = filteredPosts.length;
    const totalPages = Math.ceil(totalItemCount / pageSizeNumber);
    const pagedPosts = filteredPosts.slice((pageNumber - 1) * pageSizeNumber, pageNumber * pageSizeNumber);

    return res.status(200).json({
        currentPage: pageNumber,
        totalPages: totalPages,
        totalItemCount: totalItemCount,
        data: pagedPosts
    });
});

export default router;

3) 라우터 중복 경로 합치기

route() 함수를 사용해 중복된 url 경로를 하나로 합칠 수 있습니다. 

 

<예제>

import express from 'express';

const app = express();

// 새로운 라우터 객체 생성
const productRouter = express.Router()

// 라우터 객체에 각 라우트 연결
/* app.route */
productRouter.route('/')
	.get((req, res) => {
	  res.json({ message: 'Product 목록 보기' });
	})
	.post((req, res) => {
	  res.json({ message: 'Product 추가하기' });
	});
	
productRouter.route('/:id')
	.patch((req, res) => {
	  res.json({ message: 'Product 수정하기' });
	})
	.delete((req, res) => {
	  res.json({ message: 'Product 삭제하기' });
	});

// app 객체에 라우터 연결
app.use('/products', productRouter)


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

 

실제코드

더보기
// routes/posts.js
import express from 'express';

const postRouter = express.Router();

// 임시 데이터 저장소
let posts = [];
let postIdCounter = 1;

postRouter.route('/groups/:groupId/posts')
    .post((req,res)=>{ // 게시글 등록 API
        const {
            nickname,
            title,
            content,
            postPassword,
            groupPassword,
            imageUrl,
            tags,
            location,
            moment,
            isPublic
        } = req.body;
    
        const { groupId } = req.params; // groupId는 URL 파라미터로 가져옵니다.
    
        // 필수 필드가 누락된 경우 400 에러 반환
        if (!nickname || !title || !content || !postPassword || !groupPassword || !groupId) {
            return res.status(400).json({ message: "잘못된 요청입니다" });
        }
    
        // 게시글을 저장하는 로직
        const newPost = {
            id: postIdCounter++,
            groupId: Number(groupId),
            nickname,
            title,
            content,
            imageUrl,
            tags,
            location,
            moment,
            isPublic,
            likeCount: 0,
            commentCount: 0,
            createdAt: new Date().toISOString(),
        };
    
        posts.push(newPost);
    
        // 성공적으로 생성되었음을 나타내는 200 응답
        return res.status(200).json(newPost);
    })
    .get((req, res) => {
        const { groupId } = req.params;
        const { page = 1, pageSize = 10, sortBy = 'latest', keyword = '', isPublic = 'true' } = req.query;
    
        const pageNumber = Number(page);
        const pageSizeNumber = Number(pageSize);
        const groupIdNumber = Number(groupId);
        const isPublicBoolean = isPublic === 'true';
    
        // 요청 파라미터 유효성 검사
        if (isNaN(pageNumber) || isNaN(pageSizeNumber) || isNaN(groupIdNumber)) {
            return res.status(400).json({ message: "잘못된 요청입니다" });
        }
    
        // 필터링
        let filteredPosts = posts.filter(post => 
            post.groupId === groupIdNumber &&
            post.title.includes(keyword) &&
            post.isPublic === isPublicBoolean
        );
    
        // 정렬
        if (sortBy === 'latest') {
            filteredPosts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
        } else if (sortBy === 'mostCommented') {
            filteredPosts.sort((a, b) => b.commentCount - a.commentCount);
        } else if (sortBy === 'mostLiked') {
            filteredPosts.sort((a, b) => b.likeCount - a.likeCount);
        }
    
        // 페이징
        const totalItemCount = filteredPosts.length;
        const totalPages = Math.ceil(totalItemCount / pageSizeNumber);
        const pagedPosts = filteredPosts.slice((pageNumber - 1) * pageSizeNumber, pageNumber * pageSizeNumber);
    
        return res.status(200).json({
            currentPage: pageNumber,
            totalPages: totalPages,
            totalItemCount: totalItemCount,
            data: pagedPosts
        });
    });


export default postRouter;

 

 

4. 라우터 레벨 미들웨어

 

const 라우터이름 = express.Router()로 라우터별로 미들웨어를 나눌 수 있는데 이것을 라우터 레벨 미들웨어라고 합니다. 이렇게 사용하면 라우터마다 다른 권한이나 설정을 적용해야 할 때 각 라우터 묶음 별로 미들웨어를 적용할 수 있습니다.

 

우리는 app.js에서 routes/posts 나 routes/group 을 나누어 이미 라우터레벨로 미들웨어를 분리하여 사용하고 있었습니다.

import express from 'express';

const app = express();

// 상품 라우터 레벨 미들웨어
const productRouter = express.Router()

productRouter.use((req, res, next) => {
	console.log('Product Router에서 항상 실행')
	next()
})

productRouter.route('/')
	.get((req, res) => {
	  res.json({ message: 'Product 목록 보기' });
	})
	.post((req, res) => {
	  res.json({ message: 'Product 추가하기' });
	});
	
productRouter.route('/:id')
	.patch((req, res) => {
	  res.json({ message: 'Product 수정하기' });
	})
	.delete((req, res) => {
	  res.json({ message: 'Product 삭제하기' });
	});


// 유저 라우터 레벨 미들웨어
const userRouter = express.Router()

userRouter.get('/', (req, res, next) => {
	  res.json({ message: 'User 목록 보기' });
});

app.use('/products', productRouter)

app.use('/users', userRouter)


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