문제 상황

저희 조에서는 Vite를 이용하여 개발을 진행하고, 번들링을 하고 있습니다. 개발 도중에는 Vite의 dev 서버를 이용해서 프론트엔드 코드가 변경되었을 때 바로 재실행을 할 수 있도록 해서 진행했습니다. 그리고 실제로 배포를 할 때는 번들링된 정적 파일들을 express 서버에서 반환하는 방식으로 진행하고 있습니다. (참고로 배포 서버를 1개로 구성한 까닭은 개인 페이지 구축 측면에서 라우팅이 복잡하게 달라지는 부분이 존재할 것이라고 예상했기 때문입니다.)

Untitled

여기에서 생기는 문제점은 개발 서버는 프론트엔드와 백엔드 서버가 분리되어 있는데, 실제 서버는 프론트+백엔드 서버가 통합되어 있어서 개발 환경과 실제 환경 간의 괴리감이 존재한다는 점입니다. 해당 부분을 통합하기 위해 여러 노력을 하였습니다.

프록시를 이용한 분리된 서버 라우팅

Untitled

개발 서버의 프론트엔드 url은 http://(도메인 주소):5713이고, 백엔드 url은 http://(도메인 주소):3000입니다. 하지만 배포 서버에서는 페이지를 요청할 때나 api를 요청할 때나 모두 http://(도메인 주소):3000로 요청을 보내야 합니다. 원활한 개발을 위해 페이지를 요청할 때에도 http://(도메인 주소):3000으로 요청을 보내도록 하기 위해, 개발 모드에서 서버를 열면 express-http-proxy를 이용하여 백엔드 서버에서 페이지를 요청하는 url이 요청되었을 때 프론트엔드 서버로 요청을 넘겨주는 프록시를 추가하였습니다. 이렇게 프록시를 추가하면 백엔드 개발서버에서 프론트엔드 페이지를 요청받은 것을 넘겨줄 수 있기 때문에 일관성이 생기고, 프론트엔드 페이지에서 자신 도메인의 api 요청(ex. /api/data 등)을 보낼 때에도 CORS 에러가 뜨지 않습니다.

프록시의 원리는 단순합니다. 사용자가 보낸 요청을 다시 다른 서버에 요청하여, 다른 서버에서 응답받은 결과물을 그대로 사용자에게 넘겨주는 것입니다. 리다이렉트와의 차이는 리다이렉트는 사용자의 요청 자체를 다른 서버로 넘겨주는 식이라면, 즉 다른 서버와 직접 통신하는 형태라면, 프록시는 사용자가 요청한 값을 서버가 대신 요청하는 것의 차이라고 할 수 있습니다. 위의 예제에 빗대어 표현하자면, 사용자가 개발용 백엔드 서버에서 /를 요청하면, 백엔드 서버는 프론트엔드 서버에 페이지 요청을 하고, 응답받은 페이지 그 자체를 사용자에게 넘겨주게 됩니다. 클라이언트가 자신 도메인의 api 요청을 보내면 이 요청은 비록 받은 결과는 원래 프론트엔드 서버더라도, 백엔드 서버에 요청이 가게 되며, 동일한 origin을 갖게 되므로 cors 에러가 뜨지 않습니다.

Vite Dev server 라우팅

Untitled

문제는 express-http-proxy를 이용해서 페이지 요청을 받을 때 프론트엔드 개발 서버로 요청을 넘겨주는 것까지는 좋았지만, vite 개발 서버의 특성상 정확한 자원의 주소가 아니면 무조건 index.html을 반환한다는 것이었습니다. 서브 페이지의 주소는 /create이지 /create.html이 아니었기에, 우선은 /create로 요청이 오면 /create.html으로 리다이렉트를 하는 방식으로 express 서버에서 구현했습니다. 하지만 이렇게 구현해도 주소창에서는 /create.html이 뜨게 되었으며, 저는 이것이 불편했습니다. 그래서 vite 서버 자체적으로 라우팅을 해서 /create 요청이 오면 리다이렉트를 하지 않고 바로 /create.html 파일을 보내주는 것을 구현하고자 했습니다.

export function devRouter(rawRouteList) {
  // 문자열로 된 route list를 정규표현식으로 변환하고, 정규표현식이 아닌 것들을 제거합니다.
  const routeList = rawRouteList
    .map(([route, html]) => {
      if (typeof route === "string") {
        route = new RegExp(`^${route.replace(/\\*/g, ".*").replace(/\\?/g, ".")}/?$`);
      }
      return [route, html];
    })
    .filter(([route]) => route instanceof RegExp);

  // 요청한 패스가 설정된 라우팅 경로에 맞는지 확인합니다.
  function foundRoute(path) {
    for (let [route, html] of routeList) {
      if (route.test(path)) return html;
    }
    return null;
  }

  return {
    name: "route-server",
    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        const htmlPath = foundRoute(req.originalUrl);
        if (htmlPath === null) return next();
        req.url = htmlPath;
        req.originalUrl = htmlPath;
        next();
      });
    },
  };
}

vite는 플러그인을 설정할 때 객체를 반환하는 함수를 호출하는 방식으로 플러그인을 추가할 수 있습니다. 이 플러그인이 반환하는 객체에는 configureServer가 있는데, 이는 dev 서버의 동작 방식을 결정합니다. server.middleware.use()를 이용하여 dev 서버의 동작 방식을 트랩할 수도 있습니다. 미들웨어를 이용하여 주어진 조건에 맞으면 다른 html 문서를 응답으로 보내주는 방식으로 dev 서버 라우팅을 구현했습니다.

바닐라 환경에서는 괜찮았으나, 저희 프로젝트는 react를 사용하는 프로젝트였고, react 프로젝트에서는 html 문서를 바로 응답으로 보내면 에러가 뜨게 됩니다.

fix: should add globalPreamble to history fallback by csr632 · Pull Request #11 · vitejs/vite-plugin-react

원인은 @vitejs/plugin-react 플러그인에서 html 파일을 수정해서 수정된 파일을 보내는데, 미들웨어를 이용해 파일 내에 있는 html 파일을 직접 보내버리면 @vitejs/plugin-react 플러그인에서 수정한 기능이 없어서 에러가 뜨는 것이었습니다. 원본 html 대신 server.transformIndexHtml(html)을 사용하여 react에서 수정한 html을 반환하도록 만들었더니 해결되었습니다.