노션클론 리팩토링 (15) - Middleware, SPA 새로고침 문제

김영현·2024년 12월 31일
1

미들웨어란?

위키피디아의 정의를 가져와보자.

미들웨어란 서로 다른 시스템, 애플리케이션이 원활하게 통신할 수 있도록 중간에서 매개 역할을 하는 소프트웨어를 뜻한다.

AWS공식문서에서는 이렇게 설명한다.

서로 다른 애플리케이션이 서로 통신하는데 사용되는 소프트웨어다.

예를들어 클라이언트서버는 통신을 한다.

이때 요청과 응답 사이에 중개자 역할을 하는 소프트웨어를 미들웨어라 볼 수 있다.

마지막으로 express문서에서는 미들웨어에 대해 이렇게 설명한다.

애플리케이션의 요청-응답 주기 중 다음 미들웨어 함수에 대한 액세스 권한을 갖는 함수.
request와 response 주기 사이에 다음 미들웨어 함수에 대한 액세스 권한을 갖는다.
즉, request와 response 사이에서 중개자 역할을 하며, 다음 미들웨어를 호출할 수 있는 권한을 갖는다.

그렇다면 미들웨어가 서버측에서 어떤역할을 하는지 간단하게 예시로 보자.

middleware in router

//express 예시

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

const myLogger = function (req, res, next) {
  console.log('LOGGED')
  next()
}

app.use(myLogger)

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

app.listen(3000)

myLogger()함수는 미들웨어다. 즉, 요청과 응답 사이에 호출되는 함수다.
실제로 /경로로 접속시 먼저 콘솔에 'LOGGED'문자열이 찍히고, 그다음 'Hello World'가 화면에 나온다.

간단한 미들웨어를 한 번 구현해보자.

  1. 미들웨어는 요청과 응답 사이에 존재한다.
  2. next()함수로 다음 미들웨어를 호출할 수 있어야 한다.
const middlewares = [];

  // 미들웨어 실행 함수
  const executeMiddlewares = (middlewares, req, res, done) => {
    let index = 0;

    const next = (err) => {
      if (err) {
        return done(err);
      }

      if (index >= middlewares.length) {
        return done();
      }

      const middleware = middlewares[index++];
      middleware(req, res, next);
    };

    next();
  };

//...
executeMiddlewares(middlewares, req, res, (err) => {
          if (err) {
            res.writeHead(500, { "Content-Type": "text/plain" });
            res.end("Internal Server Error");
            return;
          }

          // API 실행
          api(req, res);
});

핵심은 클로져를 이용해 인덱스를 저장하는 것이다. 미들웨어를 실행중인 컨텍스트 내에서 다음 미들웨어 함수에 대해 액세스 권한을 가져야 하기 때문이다.

또한 에러를 처리하기위해 만약 에러 발생시 콜백함수로 제어권을 넘겨

해당 조건문을 실행하게 한다!

실제로 미들웨어가 적용되는지 테스트해본다.

const logMiddleware = (req, res, next) => {
  console.log(`메서드:${req.method}는 링크:${req.url} 접속시 실행됨`);
  next();
};

const documentsRouter = router();

documentsRouter.get("/documents", getDocumentListController);
documentsRouter.use(logMiddleware);

미들웨어가 잘 실행되는 모습을 볼 수 있다.

cors

노션클론 리팩토링13편에서 CORS문제를 해결했던 로직을 미들웨어로 바꾸어 보겠다.

const CORS_HEADER = {
  "Content-Type": "application/json",
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
};

const cors = (req, res, next) => {
  for (const key in CORS_HEADER) {
    res.setHeader(key, CORS_HEADER[key]);
  }
  next();
};

module.exports = cors;

이렇게 모듈로 뺴주고

documentsRouter.use(cors);

다른 미들웨어처럼 사용하면 정상 작동하는 모습을 볼 수 있다.

body-parser

express는 node.js의 http모듈을 추상화하여 보다 더 편리하게 개발할수 있게 도와주는 프레임워크다.
해당 프레임워크 사용시 기본적으로 사용하는 미들웨어가 몇가지 있는데, 개중 하나가 req.body를 파싱해주는 body-parser이다.

저번 회차에서 해당문제를 겪어 해결한적이 있다. 따라서 req.bodybuffer에서 string으로 파싱하는 로직을 미들웨어로 꺼내보자.

const bodyParser = async (req, res, next) => {
  const method = req.method;
  if (method !== "POST" && method !== "PUT") return next();

  let body = [];

  req.on("data", (chunk) => {
    body.push(chunk);
  });

  await new Promise((resolve, reject) => {
    req.on("end", () => {
      try {
        const buffer = Buffer.concat(body);
        req.body = JSON.parse(buffer.toString());
        resolve();
      } catch (error) {
        reject(error);
      }
    });
  });

  next();
};

module.exports = bodyParser;

return을 사용할땐 주의해야한다. 다음 미들웨어를 호출하는 next()함수를 호출하지 않는다면, 영원히 미들웨어 컨텍스트에 갇히게 된다!

cors미들웨어와 마찬가지로 use를 이용하여 미들웨어를 부착한다.

documentsRouter.use(bodyParser);

문제없이 화면이 잘 나온다.


SPA 리로드 문제

SPA는 실제 주소가 바뀌지만, 해당 경로의 문서가 서버에 처음부터 존재하지 않는다.
클라이언트측 JS를 이용해 화면을 전부 그려내는 터라, 만약 SPA에 대응하는 작업을 하지 않고 특정 경로에서 새로고침시 404 NOT FOUND를 볼 수 도 있다.

이를 한 번 해결해보자.

현재 웹서버 로직은 대략 이렇다.

const http = require("http");
const fs = require("fs");
const path = require("path");
const PORT = 3000;

const server = http.createServer((req, res) => {
  let filePath = path.join(
    __dirname,
    req.url === "/" ? "/public/index.html" : req.url
  );
  const extname = path.extname(filePath);

  // MIME 타입 설정
  const mimeTypes = {
    ".html": "text/html",
    ".css": "text/css",
    ".js": "application/javascript",
  };

  const contentType = mimeTypes[extname] || "text/plain";

  // 파일 읽기
  fs.readFile(filePath, (err, data) => {
    if (!err) {
      //오류가 아닐시, 파일 반환
      res.writeHead(200, { "Content-Type": contentType });
      res.end(data);
      return;
    }

    if (err.code === "ENOENT") {
      // 파일을 찾을 수 없음
      res.writeHead(404, { "Content-Type": "text/plain" });
      res.end("404 Not Found");
      return;
    }
    // 기타 서버 오류
    res.writeHead(500, { "Content-Type": "text/plain" });
    res.end("Internal Server Error");
  });
});

server.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});
  1. 현재 디렉토리와 요청한 url을 합쳐서 파일 경로를 생성한다.
    a. 만약 /경로로 요청시 /public/index.html을 반환함.
  2. fs.readFile()과 경로를 이용해 파일을 가져온다.
    a. 오류가 아니라면, 파일을 반환한다.
    b. 만약 오류인데 'ENOENT'코드(파일을 찾을 수 없음)라면 404를 반환한다.
    c. 기타오류는 500을 반환.

여기서 2-b의 오류로 인하여 새로고침시 404 not found가 표시되게 된다.
예를들어 http://localhost:3000/documents/1주소에서 새로고침시, 서버측컴퓨터경로/documents/1로 파일을 요청하게 된다.
이때 당연히 documents/1이라는 폴더는 없을 것이므로, 2-b오류를 반환하게 된다.

그렇다면 'ENOENT'오류시 /public/index.html을 반환하게 하면 되지 않을까?

if (err.code === "ENOENT") {
      // html 요청이면 index.html 반환 (SPA 새로고침 대응)
      fs.readFile(
        path.join(__dirname, "/public/index.html"),
        (error, indexData) => {
          if (error) {
            res.writeHead(500, { "Content-Type": "text/plain" });
            res.end("Internal Server Error");
            return;
          }
          res.writeHead(200, { "Content-Type": "text/html" });
          res.end(indexData);
        }
      );
      return;
    }

해당 요청의 에러코드가 ENOENT라면 /public/index.html을 반환한다.

새로고침을 해도 문서가 잘 유지되는 모습을 볼 수 있다.

profile
모르는 것을 모른다고 하기

2개의 댓글

comment-user-thumbnail
2024년 12월 31일

영현님 2024년 마지막 날까지 열심히 하시는군요..! 항상 열심히 하시는 모습 덕분에 저도 많이 자극 받을 수 있네요ㅎㅎ
2024년 한 해 수고하셨어요 :) 내년에 영현님께 꼭 좋은 일이 있기를 기원할게요!!🙏
2025년도 화이팅하시구 새해 복 많이 받으세요~!

1개의 답글