🦾

Apollo Server 3에서 Apollo Server 4로 마이그레이션

Created
2023/06/02 06:32
Tags
안녕하세요. 서버개발자입니다.
이번에 속도 개선 작업을 위해 프로세스를 재정리하면서 subscription 구현이 필요하게 되었습니다.
subscriptions-transport-ws는 더 이상 관리되지 않고, graphql-ws가 Apollo Server 4 지원을 한다는 글을 보고,
Apollo Server 3이 2023년 10월 22일에 deprecated 되어 Apollo Sever 4로 Migration을 하려고 했던 계획이 있었기에,
이번에 Migration을 하며 정리한 내용을 공유드립니다.

 Apollo Server 4

 The new @apollo/server package

apollo-serverapollo-server-coreapollo-server-expressapollo-server-plugin-base 등 사용하고 있었던 관련 패키지를 제거하고 @apollo/server 사용하도록 변경
// import { ApolloServer } from 'apollo-server-express' import { ApolloServer } from '@apollo/server'
TypeScript
복사

 Node

Apollo Server 4 supports Node.js 14.16.0 and later.

 graphql

Apollo Server has a peer dependency on graphql (the core JS GraphQL implementation). Apollo Server 4 supports graphql v16.6.0 and later.

 TypeScript

If you use Apollo Server with TypeScript, you must use TypeScript v4.7.0 or newer.
업그레이드하면서 영향이 있는 패키지들을(graphql-middleware, graphql-upload, jest, jest-mock-extended, ts-jest, typescript, jsonwebtoken, nexus-shield) 최신으로 변경
 graphql-upload은 최신으로 하면 오류가 나기 때문에 13.0.0으로 머물러 있도록 변경
Apollo Server 4 removes both ApolloError and toApolloError in favor of using GraphQLError.
formatError improvements
// Apollo Server 3 supports the formatError hook, which has the following signature: (error: GraphQLError) => GraphQLFormattedError // In Apollo Server 4, this becomes: (formattedError: GraphQLFormattedError, error: unknown) => GraphQLFormattedError
TypeScript
복사
In Apollo Server 4, there is no exception extension. The stacktrace is provided directly on extensions.
In Apollo Server 4, the default development landing page is the embedded Apollo Sandbox. Note that nothing changes about the default production landing page.
// 설정 부분 제거 if (isStaging) plugins.push(ApolloServerPluginLandingPageLocalDefault({ footer: false })) else plugins.push(ApolloServerPluginLandingPageGraphQLPlayground({}))
TypeScript
복사
If you used the apollo-server-express package in Apollo Server 3, use the expressMiddleware function in Apollo Server 4 (i.e., instead of using server.applyMiddleware or server.getMiddleware).
// Apollo Server 3 server.applyMiddleware({ app, path: '/' }) // Apollo Server 4 app.use('/', expressMiddleware(server, { context }))
TypeScript
복사

 Subscription

 graphql-ws

Apollo Server 4로 마이그레이션을 하고 나서 subscription을 구현하기 위해 graphql-ws로 시작해 보기
import express from 'express' import { createServer } from 'http' import { WebSocketServer } from 'ws' import { useServer } from 'graphql-ws/lib/use/ws' const app = express() const httpServer = createServer(app) const wsServer = new WebSocketServer({ server: httpServer, path: '/' }) const wsServerCleanup = useServer({ schema, context }, wsServer) const apolloServer = new ApolloServer({ ... plugins: [ // Proper shutdown for the HTTP server. ApolloServerPluginDrainHttpServer({ httpServer }), // Proper shutdown for the WebSocket server. { async serverWillStart() { return { async drainServer() { await wsServerCleanup.dispose() }, } }, }, ], ... }) app.use( '/', ... expressMiddleware(apolloServer, { context }) ) httpServer.listen({ port }, () => { console.log(`🚀 Query endpoint ready at http://localhost:${port}`) console.log(`🚀 Subscription endpoint ready at ws://localhost:${port}`) })
TypeScript
복사
webSocketServer을 따로 띄워줘야 하는 것이 중요하며, 같은 context를 넣어줘야 동일한 context를 사용할 수 있음
 context에서 header 정보를 사용하고 있다면 수정이 필요
const isSubscriptionConnection = !!params.connectionParams const authorization = isSubscriptionConnection ? params.connectionParams?.Authentication : params.req.get('Authorization')
TypeScript
복사

 Pubsub

pub/sub을 적용하기 위해서 기본 제공하는 기능을 사용해도 되지만 여러 개의 instance를 위해 redis를 이용한 pubsub으로 진행
// Context import { RedisPubSub } from 'graphql-redis-subscriptions' import Redis from 'ioredis' return { ... pubsub: new RedisPubSub({ publisher: new Redis(API_REDIS_URL), subscriber: new Redis(API_REDIS_URL), }), } // Subscription import { subscriptionType } from 'nexus' export default subscriptionType({ definition(t) { t.int('truths', { subscribe: (_, _args, ctx) => { return ctx.pubsub.asyncIterator('truths') }, resolve(eventData: number) { return eventData }, }) }, }) // Publish await ctx.pubsub.publish('truths', 1)
TypeScript
복사
 기존에 쓰고 있었던 redis가 있어서 ioredis을 안 쓰고 기존에 있던걸 쓰거나, 기존의 redis를 ioredis를 사용하도록 변경하려다가 일단 둘 다 쓰는 걸로 작업
문서는 더 많은 내용을 담고 있으며, 번역을 하기보다는 하면서 겪었던 이슈들을 위주로 정리해 봤습니다.
지금 저희 서버에서 필요한 부분들만 코드 위주로 설명드려서 맞지 않을 수도 있고 부족한 부분이 있겠지만 봐주셔서 고맙습니다.
지금 아직 테스트 중이라서 계속해서 발견해나가는 부분 업데이트하도록 하겠습니다.