Next.js API Route로 GraphQL Proxy Server 만들(고군분투)기

Jung Wish·2024년 3월 8일
2

오늘의_코딩

목록 보기
5/11

일반적으로 CORS 처리를 할때는 1) 백엔드 서버단에서 허용할 origin에 대해 CORS 처리를 해주거나 2) 미들웨어 단에서 Proxy server를 통해 Origin을 인위적으로 바꿔서 요청해 회피하는 방법이 있다.
보통 production 서버라던지 확정된 호스트인 경우에는 백엔드 팀에 처리를 부탁하는 편이지만 origin이 바뀔 가능성이 있는 local dev 서버 origin의 경우 프론트에서 자체적으로 처리하기 위해 필요할때마다 proxy를 이용하곤 한다.

Next.js의 경우 정적(static) proxy는 설정 파일(next.config.js)에서 Rewrites 함수를 설정해주면 된다. 설정 자체가 어렵지도 않고 문서에도 잘 설명되어 있어서 왠만한 proxy는 추가적인 라이브러리나 서버 처리 없이도 해당 내용으로 커버가 가능하다.
https://nextjs.org/docs/pages/api-reference/next-config-js/rewrites#rewriting-to-an-external-url

module.exports = {
  async rewrites() {
    return [
      {
        source: '/blog',
        destination: 'https://example.com/blog',
      },
      {
        source: '/blog/:slug',
        destination: 'https://example.com/blog/:slug', // Matched parameters can be used in the destination
      },
    ]
  },
}

근데 이제 여기서 문제는 동적으로 proxy를 처리해야하는 경우다. 시나리오를 짧게 설명해보자면 다음과 같다.

사용자 서비스 등록 -> 서비스에 고유 endpoint(서버)가 존재 -> 클라이언트 단에서는 해당 endpoint 정보를 API를 통해 가져와서 해당 서버에 대한 API를 요청

동적으로 endpoint를 받아 API 호출을 해주고 있기 때문에 위의 rewrites로 커버가 되지 않는다. production에서는 백엔드에서 docker image build시 환경변수를 통해 서버를 띄울때 CORS 처리할 origin을 주입하여 배포해주고 있기 때문에 문제가 되지는 않았다.

그런데, 왜 갑자기 Proxy Server를 만들게 되었냐? 라고 한다면 이유는 이러하다.
현재 서비스 프론트를 monorepo로 운용하고 있는데 최근에 microfrontend 패턴을 적용하게 되면서 기존 origin과 다른 host app에서 GraphQL server 요청을 해야하는 case가 생겼다. 백엔드 팀에 부탁할까 하다가
1) local dev 서버의 endpoint가 또 바뀔 가능성이 있고
2) 서비스 특성상 Cross Origin으로 요청할 일이 계속 생길 것이며
3) 계속 origin을 추가하다보면 env 관리도 비효율적이게 될 가능성이 있고
4) 환경변수 주입을 위해 매번 build/restart를 해줘야 하는 문제가 있어서

Next app에 동적 proxy를 위한 route를 뚫어보기로 했다.
일종의 API Gateway처럼 다른 프론트 앱에서 User Auth 정보 접근과 정적 proxy용으로 사용하고 있었던 Next API route 전용 App이 있어서 해당 지점에 적용해보았다.

첫 시작은 middleware

기존 Next.js app은 v14의 app router를 차용하고 있었고, proxy 처리는 모두 rewrites case에 넣어 사용하고 있었다. 동적인 케이스를 어떻게 처리하지?라고 고민하다가 처음 생각했던 것은 middleware였다. 실제 서버로 가기전 동작을 제어하는 역할을 하는 proxy 성격과 맞고 Next.js의 middleware config 설정을 통해 특정 paths에 match하는 부분에만 동작하도록 제어할 수도 있기 때문이다.

proxy에 사용할 라이브러리를 찾아다 https://github.com/stegano/next-http-proxy-middleware 에 언급된 http-proxy 이용 케이스를 보고 해당 내용을 기반으로 코드를 작성해보기로 했다.

// pages/api/[...all].ts
import httpProxy from "http-proxy";

export const config = {
  api: {
    // Enable `externalResolver` option in Next.js
    externalResolver: true,
    bodyParser: false,
  },
};

export default (req, res) =>
  new Promise((resolve, reject) => {
    const proxy: httpProxy = httpProxy.createProxy();
    proxy.once("proxyRes", resolve).once("error", reject).web(req, res, {
      changeOrigin: true,
      target: process.env.NEXT_PUBLIC_API_PROXY_URL,
    });
  });

그런데 NextRequest 형식은 nodejs express request 형태가 아닌것 같았다. 라이브러리를 쓸 수가 없었다.🫠 그리고 query요소와 body도 읽을 수가 없었다. query string으로 target endpoint를 받아서 proxy origin으로 설정해주려고 했고, body값이 잘 넘어가는지 확인해보려고 했는데 계속 null만 찍히고 query 요소는 애초에 NextRequest 객체에 없었다. (대신 searchParam이라는 요소가 있는듯 하다)

이게 뭐지..? middleware의 Request는 nodejs express 환경이 아닌가...???하고 찾아봤는데 그것이 사실이었다. ^__^ Express middleware가 아니라 client-side navigation안에서만 동작한다고 한다. 실제로 NextRequest type을 살펴보면 Fetch API Request Class를 extends하고 있는 것을 찾을 수 있다. 클라이언트용 middleware였던 것이다... 보안취약점 가능성이 있어 실제 client단에서 호출한 request body가 포함된 response를 middleware단에서 제어할 수 없도록 만들었다고 한다.(stackoverflow 답변에 의하면 그렇다네요👻)

middleware NextResponse 파트 공식문서도 보면 middleware에서 생성할 수 있는 Response의 종류는 아래와 같은 2가지 케이스다.

  1. Page, Edge API Route로 만든 route를 rewrite하는 response.
  2. NextReponse (redirect, rewrite, cookie 및 header 제어, json body 생성 가능)

그래서 App router - Route Handler로 처리해봤는데 외않돼..?


일단 나는 이미 설치한 라이브러리를 최대한 사용하는 쪽으로 구현하고 싶었고 API Route는 express 서버단 처리를 할 수 있을테니 여기에 걸면 되겠지?라고 생각해서 middleware 내용을 옮겨봤는데 마찬가지로 제어가 안됐다.
App router의 Route Handler단에서도 middleware와 마찬가지로 Fetch Request와 Response를 사용하고 있었다.

🫠 뭔데..Good to know에는 pages router의 API Routes랑 동일하니까 pages와 API Route를 함께 사용할 필요가 없다더니...? request 형태가 다르쟈나요..흙흙 혹시 몰라 runtime 변수를 nodejs로도 줘봤는데 그런거 안통한다.ㅋㅋ(여기도 보안 취약점 이슈인걸까..)

(나만 바보인건가..?하고) 다시 또 열심히 찾아봤는데 다들 똑같은 에러를 경험하고 계셨다. app router에서는 http-proxy-middleware 구현이 안되네요..? client side용 proxy도 아마 만들 수 있지 않을까 싶긴한데….?

어쨌든 결론은 현 라이브러리 사용을 위해서 nodejs runtime환경의 request, reseponse를 사용하지 않는것 같으니 page router API Route로 만들면 된다고한다. (그냥 처음부터 예시코드 따라할걸...)
https://github.com/chimurai/http-proxy-middleware/issues/932

그래서 page route로 다시 처리해봤더니 호출은 되는데 이번엔 response가 안온다. 😊


proxy로 request 요청 자체를 잘 보내는건가? 싶어서 Graphql route말고 다른 path로 요청을 보내봤는데 응답이 온다! => 이 말은 proxy 자체는 문제가 없고 GraphQL 요청을 뭔가 잘못보내고 있다는 것인데..? 대체 어디가 문제일까!

나같은 사람 있게 해주세요 기도하며 또 구글링을 하다보니 드디어 해결책을 찾았다. (할렐루야)

https://github.com/vercel/next.js/discussions/11036

Next.js API Route에서 실행되는 automatic body parsing 때문인 것 같다고 한다. (그게 뭔지는 나도 모른다ㅎ. 뭐 대충 request body를 자동적으로 parsing해서 형태를 변형하나부다.) config 설정에 bodyParser 기능을 꺼주면 원하는대로 동작할 것이라는 말..!

드디어 원하는 결과 도출 🎊

response는 잘 살펴보면 GraphQL 요청 query가 잘못되서 error긴 하지만ㅋㅋㅋㅋㅋ 여기서 중요한건 response가 온다는 것이다.

완성된 next API Route단 코드는 이것

// pages/api/engine.ts
import { createProxy } from 'http-proxy';
import { NextApiRequest, NextApiResponse } from 'next';

export const config = {
  api: {
    bodyParser: false,
  },
};

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  new Promise((resolve, reject) => {
    const proxy = createProxy();
    proxy.once('proxyRes', resolve).once('error', reject).web(req, res, {
      changeOrigin: true,
      target: req.query.target?.toString(),
      ignorePath: true,
    });
  });
}

client단에서는 ApolloClient를 만들때 요청 uri가 proxy 서버에 전달되게끔 다음과 같이 설정해주었다. 결과적으로 성공!

 const httpLink = createHttpLink({
    uri: `/api/engine?target=${engineURL}/api/graphql`, // 이 부분!
  });
  const authLink = makeGraphqlAuthLink(APIToken);
  graphqlClient = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache({
      addTypename: false,
    }),
  });

근데 app router랑 page router랑 같이 써도 되나..?

권장되는 사항은 아닌것 같지만 같이 쓸 수는 있다고 한다. app router의 route handler 부분이 page router의 API Route와는 다르게 동작하는 부분이 분명히 존재하다보니 어쩔 수 없다고 결론지었다. nodejs runtime에서도 동작할 수 있게 만들어주시면 좋겠다...server component에 sql까지 쓸 수 있는 환경인데 어쩨서 nodejs 환경 동작은 못하는 것인가…!

profile
Frontend Developer, 올라운더가 되고싶은 잡부 개발자, ISTP, 겉촉속바 인간, 블로그 주제 찾아다니는 사람

0개의 댓글