본문 바로가기
Node.js

24/09/04 - Node.js 입문(1 - 9): Request와 Response, 경로 변수

by Jini_Lamp 2024. 9. 4.

요청(Request)클라이언트가 서버에세 전달하려는 정보나 메시지를 담는 객체이다. 주로 URL, HTTP Method, Header, Query parameter, body data 등이 포함된다. 실제 API를 만들 때 어떤 경로를 사용하고 어떤 메서드를 사용할건지, 어떤 헤더를 사용할건지 등서버가 연산하고 처리하는 것들이 달라질 수 있다.

 

응답(Response)서버에서 클라이언트로 응답 메시지를 전송시켜주는 객체이다. 세부 사항에는 상태 코드(Status code), 응답 데이터(Response data), 응답 헤더(Response header) 등이 포함되어 있다. 여기서 헤더는 나중에 배우게 될 쿠키나 세션 같은 것들을 반환하기 위해서 사용한다.

알아두면 좋은 것!

Node.js의 서버 모듈에는 대표적으로 HTTP 모듈과 Express.js가 존재한다.
이중 HTTP 모듈은 Node.js에서 제공하는 기본 모듈이고, Express.js는 HTTP 모듈을 확장하여 제공하는 것이다.
따라서 Express.js는 기존 HTTP 모듈의 메서드도 사용할 수 있다.(최근에는 Express.js를 많이 사용하고 있다.)

 

 

 

Express.js 통신 흐름

  1. 클라이언트특정 URL과 데이터를 담은 요청을 서버에 전송한다.
  2. 서버받은 데이터에 따라 필요한 비즈니스 로직을 수행한다.
  3. 서버는 처리된 결과를 클라이언트에게 응답으로 보내준다.

이 흐름을 바탕으로 API를 생성하고, 실제 API를 실행했을 때 어떤 결과가 나오는지는 다음과 같다.

 

 

 

API 구현

기능 Method URL 요청(Request) 응답(Response)
뉴스 목록 조회 GET /api/news/ {} "뉴스 목록 조회 API 입니다."
뉴스 세부 조회 GET /api/news/:newsId {} {”data”: “뉴스 세부 조회 API 입니다.”}

 

여기서 뉴수 세부 조회의 URL을 보면 /:newsId가 있는데, 이는 패스파라미터라고 해서 경로 변수(Path Params)를 받는다.

또한 응답 부분을 보면 data라는 것처럼 하나의 객체 형태로 전달된다는 뜻으로, 확인하면 JSON 형태로 반환된다.

 

  • 전체 코드
더보기

app.js

import express from 'express';
import goodsRouter from './routes/goods.js';
import newsRouter from './routes/news.js';

const app = express();
const PORT = 3000;

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

// localhost:3000/api -> goodsRouter
// use: /api 주소로 들어오면 라우터 안에 있는 함수들을 조회하라.
// 2. 라우터를 등록(연결)한다.
app.use('/api', [goodsRouter, newsRouter]);

// 1. Express.js의 서버를 연다.
app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});

 

routes/news.js

import express from 'express';

const router = express.Router();

/** 뉴스 목록 조회 API **/
// 3. HTTP Method와 URL을 지정한 API를 정의합니다.
// 만약, localhost:3000/api/news 라는 URL로 GET 요청이 들어온다면 해당 코드를 실행합니다.
router.get('/news', (req, res) => {
  // 4. 사용자의 요청에 맞는 데이터를 반환합니다.
  return res // Express.js의 res 객체를 반환합니다.
    .status(200) // API의 상태 코드를 200번으로 전달합니다.
    .send('뉴스 목록 조회 API 입니다.'); // API의 결과값을 '뉴스 목록 조회 API 입니다.'로 전달합니다.
});

/** 뉴스 세부 조회 API **/
// 3. HTTP Method와 URL을 지정한 API를 정의합니다.
// 만약, localhost:3000/api/news/:newsId 라는 URL로 GET 요청이 들어온다면 해당 코드를 실행합니다.
router.get('/news/:newsId', (req, res) => {
  // 클라이언트가 전달한 Path Params 데이터를 받아옵니다.
  const params = req.params;

  // Path Params 데이터 중 newsId를 추출합니다.
  const newsId = params.newsId;

  // 서버 콘솔에 클라이언트가 전달한 newsId를 출력합니다.
  console.log('클라이언트로 부터 전달받은 뉴스 ID:', newsId);

  // 4. 사용자의 요청에 맞는 데이터를 json 형태로 반환합니다.
  return res.status(200).json({
    data: '뉴스 세부 조회 API 입니다.',
  });
});

// Express 라우터를 외부로 전달합니다.
export default router;

 

 

 

Express.js 흐름 분석

app.js는 서버의 설정과 기본적인 동작을 정의하는 곳이다

여기서 라우터를 이용해 특정 HTTP Method와 URL로 들어오는 요청을 어떤 함수에서 처리할지 연결해주는 역할을 담당하게 된다.

  • 서버 시작: Express.js는 지정된 포트 번호(Port)를 사용하여 서버를 시작
// 1. Express.js의 서버를 엽니다.
app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');	// 서버가 실행된다면 해당 내용이 출력
});

 

  • 라우터 등록: 연결하려는 모든 라우터들을 /api 주소에 연결
    즉, app.use()라는 미들웨어를 통해 api라고 들어오는 주소에는 오른쪽에 있는 각각의 라우터들을 연결한다. 따라서 API라는 경로가 들어오면 두 라우터에 연결된 API들을 조회하고, 맞는 API가 있다면 그 API를 실행한다.
    좀 더 쉽게 설명하면 /api가 기본 주소가 되는 셈이다.
import goodsRouter from './routes/goods.js';
import newsRouter from './routes/news.js';

// 2. 라우터를 등록 합니다.
app.use('/api', [goodsRouter, newsRouter]);

 

  • API 정의: 등록된 각 라우터를 순서대로 검토하여, HTTP MethodURL이 일치하는 API를 찾아 해당 API를 수행
// routes/news.js

/** 뉴스 목록 조회 API **/
// 3. HTTP Method와 URL을 지정한 API를 정의합니다.
// 만약, localhost:3000/api/news 라는 URL로 GET 요청이 들어온다면 해당 코드를 실행합니다.
router.get('/news', (req, res) => {
   ...
});

 

  • 결과 반환: API에서 모든 비즈니스 로직 처리가 완료된 후, 클라이언트(ex: 웹 브라우저)에게 결과를 전달
    즉, API 안에서는 응답(Response)가 반환되어야 한다. 따라서 Express.js에서도 res 객체를 통해 선택 코드를 지정하고, 다음에 반환할 데이터를 작성한다.
router.get('/news', (req, res) => {
  // 4. 사용자의 요청에 맞는 데이터를 반환합니다.
  return res // Express.js의 res 객체를 반환합니다.
    .status(200) // API의 상태 코드를 200번으로 전달합니다.
    .send('뉴스 목록 조회 API 입니다.'); // API의 결과값을 전달.
});

 

따라서 목록 조회 API는 클라이언트가 GET이라는 메서드를 통해 api/news/에 요청을 보낼 때 실행된다.

그래서 이 API는 실제로 비즈니스 로직이 그렇게 많지 않다. 단순하게 비즈니스 로직보다 클라이언트에게 응답을 보낼 때, 어떻게 상태 코드를 작성하고, 어떻게 response를 지정하는지 확인하게 된다.

하여, 위 코드에서는 res 객체에다가 상태 코드를 200번으로 설정하고, 반환할 문자열을 send에 작성하는 방식으로 구현하였다.

이를 API Client로 확인하면 다음과 같다.

 

세부 조회 API는 클라이언트가 GET 메서드를 통해 api/news/:newsID 에 요청을 보낼 때 실행된다.

그리고 위에서 말했듯이, /:newsId는 패스파라미터라고 해서 경로 변수(Path Params)이다. 이는 요청(request) 객체에 params(즉, req.params)를 통해 사용할 수 있고, 해당하는 데이터를 params 변수에 할당하여 그중 newsID만을 따로 추출하여 사용할 수 있다.

/** 뉴스 세부 조회 API **/
// 3. HTTP Method와 URL을 지정한 API를 정의합니다.
// 만약, localhost:3000/api/news/:newsId 라는 URL로 GET 요청이 들어온다면 해당 코드를 실행합니다.
router.get('/news/:newsId', (req, res) => {
  // 클라이언트가 전달한 경로 변수(Path Params) 데이터를 받아옵니다.
  const params = req.params;

  // Path Params 데이터 중 newsId를 추출합니다.
  const newsId = params.newsId;

  // 서버 콘솔에 클라이언트가 전달한 newsId를 출력합니다.
  console.log('클라이언트로 부터 전달받은 뉴스 ID:', newsId);

  // 4. 사용자의 요청에 맞는 데이터를 json 형태로 반환합니다.
  return res.status(200).json({
    data: '뉴스 세부 조회 API 입니다.',
  });
});

 

따라서 경로 변수에 해당하는 값이 실제로 params에 할당되고, 그 다음 params라는 객체에 전달받는 데이터가 newsID라는 변수명이기 때문에 뒤에도 newsID가 여기서 .newID로 사용된다는 걸 확인할 수 있다.

즉, 변수 newsID에 해당하는 데이터가 할당된다고 볼 수 있다.

 

마지막으로 클라이언트에 응답을 보낼 때 상태 코드를 200번으로 보내고, JSON 객체 형태로 데이터라는 key에 값을 할당한 것을 볼 수 있다. 실행하면 다음과 같다.

API Client 결과
VS CODE 터미널 결과

 

 

 

req 객체

실제로 클라이언트들이 전달한 데이터를 바탕으로 사용하는 값들이 존재한다.
요청한 클라이언트들이 어떠한 방식으로 서버에 요청했는가를 담고 있기 때문에, 그것과 관련된 데이터들이 주로 메서드들로 구현되어 있다.
  • req.body
    Request를 호출할 때 body로 전달된 정보가 담긴 객체이다.
    예를 들어, 회원가입 API가 있다고 가정했을 때, 해당 API에는 사용자의 ID/PW, 닉네임 등 여러가지가 포함되어 있을 것이다. 이런 것들을 사용하기 위해선 클라이언트가 전달한 데이터를 조회해야하는데, 조회할 때 데이터가 대부분 body 데이터에 담긴다. 만약 해당하는 사용자의 정보를 조회할 때는 body 데이터를 사용할 수 없지만, 생성/수정/삭제를 할 때는 body 데이터를 사용한다. 또한 이 데이터를 바탕으로 비즈니스 로직을 수행하기 때문에, Request 객체에서 가장 중요하다.
    • express.json()라는 미들웨어를 이용해야만 사용할 수 있다.
    • key - value의 데이터 형식을 가지고 있으며, 대표적으로 JSON 형태를 띄고 있다.
    • Query String, Path Variable(params)과 다르게, URL만을 가지고 어떤 데이터를 전달하였는지 확인할 수 없는 특징을 가지고 있다.
      즉, 클라이언트가 서버에 데이터를 보내게 됐을 때, 외부에서는 URL만 가지고는 확인을 할 수 없는 특징을 가지고 있다.
  • req.params
    라우터 매개변수에 대한 정보가 담긴 객체이다.
    클라이언트가 요청(Request)을 보냈을 때, URL에 원하는 데이터를 삽입하여 전달한다.

    URL 특정 경로를 매개 변수로써 사용하며, 특정 게시글을 선택하거나 명확한 리소스를 지정해야할 때 사용한다.

    만약 특정 데이터를 조회한다고 했을 때, 해당하는 데이터의 정보에 대해서 간단하게 요약해서 사용할 때 사용된다.
    예를 들어, books를 조회할 때, 모든 책을 조회한다면 books만 사용하면 되겠지만, 특정 책 하나를 지정하기 위해서는 해당 책이 어디에 있는지 정확히 서버에게 전달해야 한다. 그럴 때 우리가 조회할 책의 ID에 대해 전달받고 싶다면 라우터 매개변수에 전달받게 된다. 즉, 조회를 할 때 많이 사용된다.
  • req.query
    Request를 호출할 때 쿼리 스트링으로 전달된 정보가 담긴 객체이다.
    즉, 클라이언트가 Request를 호출했을 때, URL에 원하는 key - value를 삽입하여 데이터를 전달한다.
    URL 마지막에 ? 기호를 이용해 Query String을 사용할 수 있다.
    ex) https://sparta.com?name=이용우&age=29

    특정 콘텐츠의 위치를 표시하거나 웹 페이지에 특정한 옵션을 설정할 때 사용하며, 오름차순이나 내림차순으로 정렬할 때, 특정 페이지부터 조회할 때 쓰인다.
    GET과 같은 HTTP Method에서 사용된다.
클라이언트에서 서버로 보내는 메시지를 요청(Reuqest)이라고 한다.
body, params, query는 클라이언트가 서버에 요청을 보낼 때 데이터를 어떤 방식으로 전송하는지에 대한 여러가지 방법들을 나열한 것이다.

 

 

 

res 객체

서버가 클라이언트에게 데이터를 전달하기 위해서 사용한다.
  • res.status(코드)
    Response에 HTTP 상태 코드를 지정한다.(https://developer.mozilla.org/ko/docs/Web/HTTP/Status)
    200은 요청이 성공적, 404는 요청한 리소스가 서버에 존재하지 않음을 뜻한다.
    Express에서 상태 코드를 명시하지 않으면, 상태 코드는 200으로 자동 전달한다.

  • res.send(데이터)
    특정한 데이터를 포함하여 Response를 전달한다.
    Content-Type을 데이터 유형에 따라 다르게 설정할 수 있으며, Content-Type은 서버가 클라이언트에게 전달하는 데이터의 타입을 지정할 때 사용된다.
  • res.json(JSON)
    send와 비슷하지만, JSON 형식으로 Response를 전달한다.

  • res.end()
    데이터 없이 Response를 전달한다.
    일반적으로 해당하는 서버가 모든 비즈니스 로직을 처리하고, 클라이언트에게 데이터를 반환하진 않지만, Response를 끝내겠다는 걸 알리기 위해 사용한다.
    예를 들어 쿠키를 클라이언트에게 할당한다거나, 특정 헤더를 할당한다거나, 다른 방식으로 데이터 전달을 완료했다고 한다면 아무런 데이터 없이 클라이언트에게 Response을 전달한다.(그러나 요즘은 많이 사용하지 않는다.)
서버에서 클라이언트에게 보내는 메시지를 응답(Response)이라고 한다.
status는 서버가 클라이언트에게 응답을 보낼 때 HTTP 상태 코드를 전송하는 것을 나타내며, send, json은 서버가 클라이언트에게 응답 데이터를 전송하는 방법을 나열한 것이다.

또한 비즈니스 로직을 수행하다보면 조건 처리가 잘못되서 응답이 한 번이 아니라 두 번 처리되는 문제가 발생할 수 있다. 하지만 두 번 처리가 되면 에러가 발생한다. 왜냐하면 이미 첫번째 응답 때 클라이언트와의 연결이 끝이 났는데, 한 번 더 응답을 보내게 되면 이 응답이 정상적으로 클라이언트에게 전달되지 않고 유실되는 문제가 발생하기 때문이다.

 

 

 

상품 상세 조회 API

경로 변수를 이용해 특정 정보만 조회하는 방법은 다음과 같다.

경로 변수, 또는 경로 매개변수라고 불리며 라우트 주소에서 사용되는 특수한 패턴 중 하나이다.
이는 URL의 일부분으로, 언제든지 변경될 수 있는 값을 타나내기 위해 사용된다.

예를 들어, 상품 목록 조회 API의 주소가 /goods였다면 그뒤에 /:goodsIds라는 값을 추가해서 요청시 goodsIds 값을 받아올 수 있다.
이 값은 API 내에서 req.params 객체 안에서 해당하는 경로 매개변수의 이름을 (goodsIds)으로 접근하여 얻을 수 있다. 즉, req.prams.goodsIds를 통해 goodsIds 파라미터 값을 얻을 수 있다.
  • app.js
더보기
import express from 'express';
import goodsRouter from './routes/goods.js';
import newsRouter from './routes/news.js';

const app = express();
const PORT = 3000;

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

app.use('/api', [goodsRouter, newsRouter]);

app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});

 

  • goods.js
더보기
import express from 'express';
const router = express.Router();

// 상품 데이터
const goods = [
    {
      goodsId: 1,
      name: '상품 1',
      thumbnailUrl:
        'https://cdn.pixabay.com/photo/2016/09/07/19/54/wines-1652455_1280.jpg',
      category: 'drink',
      price: 6.2,
    },
    {
      goodsId: 2,
      name: '상품 2',
      thumbnailUrl:
        'https://cdn.pixabay.com/photo/2014/08/26/19/19/wine-428316_1280.jpg',
      category: 'drink',
      price: 0.11,
    },
    {
      goodsId: 3,
      name: '상품 3',
      thumbnailUrl:
        'https://cdn.pixabay.com/photo/2016/09/07/02/12/frogs-1650658_1280.jpg',
      category: 'drink',
      price: 2.2,
    },
    {
      goodsId: 4,
      name: '상품 4',
      thumbnailUrl:
        'https://cdn.pixabay.com/photo/2016/09/07/02/11/frogs-1650657_1280.jpg',
      category: 'drink',
      price: 0.1,
    },
];


// 상품 목록 조회 API
router.get('/goods', (req, res) => {
    return res.status(200).json({
        goods: goods,
    })
});

// 상품 상세 조회 API
// http://localhost:3000/api/goods/:goodsId
router.get('/goods/:goodsIds', (req, res) => {
    // 1. 상품 ID 조회
    // 2. 상풍 ID와 일치하는 데이터 찾기
    // 3. 조회된 상품 정보를 responese로 return
    const goodsIdd = req.params.goodsIds;

    const findGoods = goods.find((oneGoods) => oneGoods.goodsId === +goodsIdd);
    
    return res.status(200).json({goods: findGoods});
})

// 작성한 라우터를 내보내기 위해 사용
export default router;

 

 

 

상품 등록 API

상품 등록 API에서는 클라이언트가 요청 시 전달하는 상품의 정보를 서버로 전달받아야 한다.

또한 데이터를 저장하는 역할을 하므로, HTTP 메서드 중에서는 데이터를 생성하는 POST 메서드를 이용해 구현해야 한다

 

POST 메서드의 경우 클라이언트가 전달하는 데이터를 수신해야한다. 이때, 클라이언트가 전달하는 데이터는 Express.js에서 req.body로 접근하여 사용할 수 있다. 하지만 req.body로 접근하기 위해서는 Body Parser라는 미들웨어를 적용해야 한다.

 

Body Parser는 클라이언트가 전송하는 데이터를 해석하여 req.body 객체로 만들어주는 역할을 담당한다. 이를 통해 클라이언트가 전송하는 데이터를 쉽게 추출할 수 있다.

  • 요청값 예시
{
	"name": "상품 5",
	"thumbnailUrl": "https://cdn.pixabay.com/photo/2016/07/26/16/16/wine-1543170_960_720.jpg",
	"category": "drink",
	"price": 100.3
}

 

  • 응답 예시
    http://localhost:3000/api/goods/ URL에 POST 요청을 보냈을 때, 아래와 같은 응답이 객체형태로 전달된다.
{
	"goods": {
		"goodsId": 5,
		"name": "상품 5",
		"thumbnailUrl": "https://cdn.pixabay.com/photo/2016/07/26/16/16/wine-1543170_960_720.jpg",
		"category": "drink",
		"price": 100.3
	}
}

 

  • app.js
더보기
import express from 'express';
import goodsRouter from './routes/goods.js';
import newsRouter from './routes/news.js';

const app = express();
const PORT = 3000;

// Express에서 req.body에 접근하여, body 데이터를 사용할 수 있도록 설정하는 미들웨어
// JSON 형태로 서버에 body데이터를 전달하면, req.body에 데이터를 변환하여 넣어준다.
app.use(express.json());
// form content type에서 body 데이터를 전달하면, req.body에 데이터를 변환하여 넣어준다.
app.use(express.urlencoded({ extended: true }));

// 즉, 일반적으로 API 클라이언트를 통해서 테스트할 때는
// express.json() 형태로만 사용해도 되지만,
// 프론트엔드와 협업을 한다면 express.urlencoded() 방식을 통해서 해야한다.
// 하지만 보통 두 코드 동시에 사용해서 문제될 건 없다.


app.use('/api', [goodsRouter, newsRouter]);

// 1. Express.js의 서버를 연다.
app.listen(PORT, () => {
  console.log(PORT, '포트로 서버가 열렸어요!');
});

 

  • goods.js
더보기
import express from 'express';

const router = express.Router();

// /routes/goods.js

const goods = [
    {
      goodsId: 1,
      name: '상품 1',
      thumbnailUrl:
        'https://cdn.pixabay.com/photo/2016/09/07/19/54/wines-1652455_1280.jpg',
      category: 'drink',
      price: 6.2,
    },
    {
      goodsId: 2,
      name: '상품 2',
      thumbnailUrl:
        'https://cdn.pixabay.com/photo/2014/08/26/19/19/wine-428316_1280.jpg',
      category: 'drink',
      price: 0.11,
    },
    {
      goodsId: 3,
      name: '상품 3',
      thumbnailUrl:
        'https://cdn.pixabay.com/photo/2016/09/07/02/12/frogs-1650658_1280.jpg',
      category: 'drink',
      price: 2.2,
    },
    {
      goodsId: 4,
      name: '상품 4',
      thumbnailUrl:
        'https://cdn.pixabay.com/photo/2016/09/07/02/11/frogs-1650657_1280.jpg',
      category: 'drink',
      price: 0.1,
    },
];

// 상품 목록 조회 API
router.get('/goods', (req, res) => {
    return res.status(200).json({
        goods: goods,
    })
});

// 상품 상세 조회 API
// http://localhost:3000/api/goods/:goodsId
router.get('/goods/:goodsIds', (req, res) => {
    // 1. 상품 ID 조회
    // 2. 상풍 ID와 일치하는 데이터 찾기
    // 3. 조회된 상품 정보를 responese로 return
    const goodsIdd = req.params.goodsIds;

    const findGoods = goods.find((oneGoods) => oneGoods.goodsId === +goodsIdd);
    
    return res.status(200).json({goods: findGoods});
})


// 상품 등록 API
// localhost:3000/api/goods/
router.post('/goods', (req, res) => {
    // 1. name, thumbnailUrl, category, price를 req.body로 전달받는다.
    // 2. 해당하는 데이터를 바탕으로 상품을 등록한다.
    // 3. 등록된 상품 데이터를 클라이언트에게 반환한다.

    // req.body에 해당하는 데이터를 가지고 오는 역할을 해야한다.
    const name = req.body.name;
    const thumbnailUrl = req.body.thumbnailUrl;
    const category = req.body.category;
    const price = req.body.price;

    // +1 이 된 goodsId를 가져온다.
    const goodsId = goods[goods.length - 1].goodsId + 1;
    const goodsItem = {
        goodsId: goodsId,
        name: name,
        thumbnailUrl: thumbnailUrl,
        category: category,
        price: price
    };

    goods.push(goodsItem);

    return res.status(201).json({goods: goodsItem});
})

// 작성한 라우터를 내보내기 위해 사용
export default router;

 

확인을 위해 API Client로 확인을 해보자.

 

  1. 새로운 Request를 생성한다.
  2. URL 주소를 localhost:3000/api/goods로 설정한다.
  3. Body → JSON을 클릭한다.
  4. 그렇게 해서 나온 입력 창에 응답 예시를 붙여 넣고, 결과를 확인한다.

 

good.js를 리팩토링을 하면 다음과 같다.

리팩토링이란, 코드를 좀 더 간결하게 정리하는 것을 의미한다.
코드를 좀 더 단순하고 보기 좋게 정리함으로써 가독성을 올린다. 단, 기능에 변경은 없어야 한다.
/** 상품 등록 리팩토링 **/
// localhost:3000/api/goods POST
router.post('/goods', (req, res) => {
  // 객체 구조 분해 할당 적용하기.
  const { name, thumbnailUrl, category, price } = req.body;

  const goodsId = goods[goods.length - 1].goodsId + 1; // 현재 goodsId의 가장 큰 값 + 1

  // 객체 구조 분해 할당 적용하기.
  const goodsItem = {
    goodsId,
    name,
    thumbnailUrl,
    category,
    price,
  };
  goods.push(goodsItem);

  return res.status(201).json({ goods: goodsItem });
});