Apollo Server와 Express 연동하여 서버 맛보기

Seonup·2023년 11월 14일
0

본 시리즈는 정재남님의 풀스택 리액트 라이브코딩 - 간단한 쇼핑몰 만들기 강의 내용을 기반으로, 추가적인 학습을 통해 습득한 지식 또는 강의 코드를 다른 방법으로 구현한 경험을 작성하고 있습니다. 강의 코드(GitHub)를 확인하세요.

Apollo Server란?

  • Apollo Server란 Apollo Client를 포함한 모든 GraphQL 클라이언트와 호환되는 spec 호환 GraphQL Server이다.
  • Apollo Client: GraphQL을 이용하여 로컬 및 원격 데이터를 모두 관리할 수 있는 JavaScript용 종합 상태관리 라이브러리로, UI를 자동으로 업데이트하면서 애플리케이션 데이터를 가져오고 캐시하고 수정할 수 있다. @apollo/client는 React와의 기본 integration을 제공한다.

Apollo Server에서 express 연동하는 방법

참고 문서: Migrate from apollo-server-express

Apollo Server 3까지는 express를 사용하려면 apollo-server-express 패키지를 설치해야 했지만, Apollo Server 4에서는 expressMiddleware 함수로 GraphQL 서버를 설정하여 apollo-server-express 패키지 대신에 사용할 수 있다.

설정 단계

  1. @apollo/server, cors, body-parser 패키지를 설치한다.
    • cors를 TypeScript 환경에서 사용하려면 @types/cors도 함께 설치해야 한다.
  2. @apollo/server에서 심볼(ApolloServer)을 가져와야 한다. (apollo-server-express, apollo-server-core에서 가져오는 것이 아님)
  3. 서버 설정에 corsbodyParser.json()을 추가해야 한다.
  4. Apollo Server 3의 apollo-server-express와 apollo-server-core 패키지를 제거해야 한다.
  5. apollo-server-express의 기본 /graphql URL 경로를 사용하는 경우 (즉, path 옵션을 사용하여 다른 URL을 지정하지 않는 경우), expressMiddleware/graphql에 마운트하여 동작을 유지할 수 있다. 다른 URL 경로를 사용하려면 app.use를 사용하여 지정된 경로에 서버를 마운트한다.

구현 코드

// npm install @apollo/server express graphql cors body-parser
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
import express from "express";
import http from "http";
import cors from "cors";
import bodyParser from "body-parser";
import { typeDefs, resolvers } from "./schema";

interface MyContext {
  token?: String;
}

// Express와의 통합에 필요한 로직
const app = express();
// httpServer는 Express 앱으로 들어오는 요청을 처리합니다.
// 아래에서는 Apollo 서버에 이 httpServer를 drain하도록 지시하여 서버를 정상적으로 종료할 수 있도록 한다.
const httpServer = http.createServer(app);

// 이전과 동일한 ApolloServer 초기화와 httpServer용 drain 플러그인.
const server = new ApolloServer<MyContext>({
  typeDefs,
  resolvers,
  plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});

// 서버가 시작될 때까지 기다리도록 await를 사용한다.
await server.start();

// CORS, body parsing, `expressMiddleware` 기능을 처리하도록 Express 미들웨어를 설정한다.
app.use(
  "/",
  cors<cors.CorsRequest>(),
  // 50mb는 `startStandaloneServer`가 사용하는 제한이지만 필요에 맞게 구성할 수 있다.
  bodyParser.json({ limit: "50mb" }),
  // `expressMiddleware`는 `ApolloServer` 인스턴스와 optional configuration options: { context: asynchronousFunction }과 같은 동일한 인수를 허용한다.
  expressMiddleware(server, {
    context: async ({ req }) => ({ token: req.headers.token }),
  })
);

// 수정된 서버 시작
await new Promise<void>((resolve) =>
  httpServer.listen({ port: 4000 }, resolve)
);
console.log(`🚀 Server ready at http://localhost:4000/`);

GraphQL 통신 구현 방법

  1. Schema 정의
    • Apollo Server를 포함한 모든 GraphQL Server는 스키마를 사용하여 클라이언트가 쿼리할 수 있는 데이터 구조를 정의한다.
    • 스키마는 type이 정의된 모음(typeDefs)이며, 함께 실행되는 쿼리의 모양을 정의한다.
  2. Data set 정의
    • 클라이언트가 쿼리할 수 있는 간단한 data set를 정의할 수 있으며, data set은 스키마에서 정의한 데이터 type과 동일해야 한다.
  3. Resolver 정의
    • Resolver를 생성하여 Apollo Server에게 특정 유형과 관련 데이터를 가져오는 방법을 알려줄 수 있다. (Apollo Server은 쿼리를 실행할 때 정의해둔 Data Set을 사용해야 한다는 것을 모른다.)
  4. ApolloServer 인스턴스 생성
    const server = new ApolloServer({
      typeDefs,
      resolvers,
    });
    • new 연산자와 함께 ApolloServer를 호출할 때 typeDefs 속성과 resolvers 속성이 정의된 객체를 인수로 전달한다.
    • 기존에는 context 속성 또한 ApolloServer를 호출할 때 전달했으나, 4버전에서는 expressMiddleware 또는 startStandaloneServer에 전달하는 것으로 변경되었다.

expressMiddleware

expressMiddleware(ApolloServer Instance, { context: context function })
  • expressMiddleware는 Apollo Server를 Express 서버에 연결할 수 있도록 하는 함수이다.
  • expressMiddleware를 사용하기 위해서는 웹 프레임워크에 대한 HTTP body parsing 및 CORS 헤더를 설정해야 한다. (cors, body-parser 패키지 설치 필요)

인수

  • ApolloServer Instance: expressMiddleware의 첫 번째 인수로, ApolloServer의 Instance를 전달한다.

  • { context: context function }

    • expressMiddleware의 두 번째 인수로, context 속성에 context function을 값으로 가지는 객체를 전달한다.

    • context function는 비동기 함수로 객체를 반환해야 한다.

    • operation 실행 중에 서버의 모든 resolver가 공유하는 객체를 반환한다. 이를 통해 resolver는 데이터베이스 연결과 같은 유용한 context value를 공유할 수 있다.

    • TypeScript를 사용하는 경우, ApolloServercontextValue에 대한 타입이 정의된 context를 전달해야 한다.

       interface MyContext {
         // You can optionally create a TS interface to set up types for your contextValue
         authScope?: String;
       }
      
       const server = new ApolloServer<MyContext>({
         typeDefs,
         resolvers,
       });
      
       const { url } = await startStandaloneServer(server, {
         // async context function should async and return an object
         context: async ({ req, res }) => ({
           authScope: getScope(req.headers.authorization),
         }),
       });
      			```
    • context 함수는 express.Request 및 express.Response 객체인 req 및 res 옵션을 받는다.

위에서 언급된 Express 관련 용어 정리

미들웨어란?

미들웨어는 HTTP 요청과 응답 사이(middle)에서 단계별 동작을 수행하는 함수이다.

Express 미들웨어

const middelware = (req, res, next) => {
  // ...
};
  • 미들웨어는 Express의 핵심 기능으로, HTTP 요청이 들어오는 순간 순차적으로 시작되며 HTTP 요청(req)과 응답 객체(res)를 처리하거나 다음 미들웨어를 실행(next())할 수 있다.
  • HTTP 응답이 마무리될 때까지 미들웨어 동작 사이클이 실행된다.
  • next 함수를 호출하지 않으면 미들웨어 동작 사이클이 멈춘다.
  • 미들웨어는 적용되는 위치에 따라서 애플리케이션 미들웨어, 라우터 미들웨어, 오류처리 미들웨어로 분류가 가능하다. 따라서 필요한 동작 방식에 따라 미들웨어의 위치를 지정해야 한다.

오류 처리 미들웨어

  • 오류 처리 미들웨어는 다른 미들웨어와 달리 (err, req, res, next), 4개를 인수로 받는다.

  • 모든 매개변수를 사용하지 않아도 4가지 모두 선언해주어야 Express가 오류 처리 미들웨어로 식별한다.

  • 동일한 경로(path)에 요청되는 미들웨어를 처리하는 메서드를 모두 작성한 뒤, 오류 처리 미들웨어는 마지막에 메서드와 함께 정의해야 한다.

    var bodyParser = require("body-parser");
    var methodOverride = require("method-override");
    
    app.use(bodyParser());
    app.use(methodOverride());
    app.use(function (err, req, res, next) {
      // 생략
    });

app.use([path,] callback [, callback...])

지정된 미들웨어의 기능을 지정된 경로에 마운트하는 메서드로, 요청된 path 경로의 기준이 일치할 때 미들웨어가 실행된다. 즉, path로 들어오는 요청에 대한 공통 미들웨어를 적용하기 위해 사용되는 메서드이다.

Arguments

  • [path,]
    • 미들웨어의 기능이 호출되는 경로로, 기본값은 root(/)이다.
    • 배열에 문자열, 경로 pattern, 정규표현식, 경로 조합을 전달할 수 있다.
    • path와 매칭할 때, path의 하위 path들 또한 함께 매칭된다.
      • 예를 들어, /fruitspath로 등록하면 /fruits/apple 또한 매칭된다.
      • path를 작성하지 않을 경우 기본값인 root(/)를 path로 매칭하기 때문에 미들웨어는 애플리케이션에 접근하는 모든 요청에 마운트된다.
  • callback
    • callback에는 미들웨어 function이나 미들웨어 function이 쉼표(,)를 기준으로 나열된 list, 미들웨어 function의 배열, 이들의 조합을 전달할 수 있다.

app.listen()

listen(port: number, hostname: string, backlog: number, callback?: () => void): http.Server;
listen(port: number, hostname: string, callback?: () => void): http.Server;
listen(port: number, callback?: () => void): http.Server;
listen(callback?: () => void): http.Server;
listen(path: string, callback?: () => void): http.Server;
listen(handle: any, listeningListener?: () => void): http.Server;
  • node의 http.Server.listen() 메서드와 동일한 동작을 하는 express 메서드로, http.Server를 반환한다.
  • Apollo Server 공식문서에서 http 모듈의 createServer() 메소드를 이용하여 서버를 생성하는 것과 동일한 역할을 하나, express에서는 http 모듈과 달리 미들웨어, 라우팅, 세션 관리, 에러 핸들링 등을 미리 구현해둔 서버를 제공하여 사용이 간단하다.

cors package

cors package는 CORS와 관련된 옵션들을 설정할 수 있는 Express 미들웨어를 제공하는 패키지로, npm에서 설치 가능하다.

cors()

cors(options) 메서드는 CORS와 관련된 설정을 할 수 있도록 만들어진 미들웨어로, 아래 속성을 포함한 options 객체를 인수를 전달할 수 있다.

  • origin: Access-Control-Allow-Origin CORS 헤더를 구성할 수 있다. 작성 가능한 값은 아래와 같다.
    • Boolean: req.header('Origin')에 정의된 대로 request origin(요청 출처)를 반영하려면 origin을 true로 설정하고, CORS를 비활성화하려면 false로 설정한다.
    • String: origin을 하나의 특정 origin으로 반영한다.
    • RegExp: origin 요청을 테스트하는 데 사용할 정규식 패턴으로 origin을 설정한다. 일치할 경우 request origin이 반영된다.
    • Array: origin을 유효한 origin의 배열, 즉 여러개의 origin을 설정한다. 각 origin은 String 또는 RegExp일 수 있다.
  • credentials: Access-Control-Allow-Credentials CORS 헤더를 구성한다. 헤더를 전달하려면 true로 설정하고 그렇지 않으면 생략한다.
profile
박선우

0개의 댓글