Node.js에서 GraphQL vs. tRPC - API 패러다임 선택
Wenhao Wang
Dev Intern · Leapcell

소개
견고하고 확장 가능한 API를 구축하는 것은 현대 웹 개발의 초석입니다. Node.js의 방대한 생태계에서 개발자는 백엔드와 프론트엔드 애플리케이션 간의 인터페이스를 만드는 데 있어 다양한 선택을 할 수 있습니다. 상당한 주목을 받은 두 가지 강력한 경쟁자는 Apollo Server를 사용한 GraphQL과 tRPC입니다. 둘 다 API 개발에 대한 고유한 접근 방식을 제공하며 데이터 가져오기, 타입 안전성 및 개발자 경험과 같은 일반적인 과제를 해결하지만 근본적으로 다른 철학을 통해 이를 수행합니다. 프로젝트의 특정 요구 사항과 미래 성장에 부합하는 정보에 입각한 결정을 내리려면 각 기술의 미묘한 차이를 이해하는 것이 중요합니다. 이 글에서는 이 두 가지 패러다임을 살펴보고, 메커니즘을 탐구하고, 코드 예제를 통해 실제 적용을 보여주고, 궁극적으로 Node.js 백엔드에 가장 적합한 옵션을 선택하도록 안내합니다.
핵심 개념
비교에 앞서 논의의 기초가 되는 핵심 개념을 간략하게 정의해 보겠습니다.
GraphQL: API를 위한 쿼리 언어이자 기존 데이터로 해당 쿼리를 처리하기 위한 런타임입니다. REST보다 훨씬 효율적이고 강력하며 유연한 대안을 제공합니다. 클라이언트는 필요한 데이터를 정확하게 요청할 수 있어 과도한 가져오기 또는 불충분한 가져오기를 방지합니다.
Apollo Server: GraphQL 스키마를 데이터 소스에 연결하는 데 도움이 되는 인기 있는 오픈 소스 GraphQL 서버입니다. 스키마 스티칭, 캐싱, 다양한 Node.js 프레임워크와의 통합을 포함한 광범위한 기능을 제공합니다.
tRPC: "TypeScript Remote Procedure Call"의 약자입니다. 스키마나 코드 생성 없이 엔드투엔드 타입 안전 API를 자신 있게 구축할 수 있는 프레임워크입니다. TypeScript의 강력한 추론 기능을 활용하여 서버 측 정의부터 클라이언트 측 호출까지 직접 타입 안전성을 달성합니다.
Apollo Server를 사용한 GraphQL
GraphQL의 핵심은 클라이언트가 쿼리할 수 있는 모든 가능한 데이터를 설명하는 스키마를 정의하는 것입니다. 이 스키마는 클라이언트와 서버 간의 계약 역할을 합니다.
원칙 및 구현
GraphQL의 핵심 원칙은 클라이언트가 필요한 데이터를 정확하게 지정할 수 있도록 하는 것입니다. 이는 클라이언트가 상호 작용하고 쿼리를 보내는 단일 엔드포인트를 통해 달성됩니다.
Apollo Server를 사용하여 GraphQL API를 설정하는 기본적인 예는 다음과 같습니다.
먼저 GraphQL 스키마를 정의합니다. 이 스키마는 GraphQL 스키마 정의 언어(SDL)를 사용하여 유형, 쿼리 및 변이(mutation)를 지정합니다.
// src/schema.ts import { gql } from 'apollo-server'; export const typeDefs = gql` type Book { id: ID! title: String! author: String! } type Query { books: [Book!]! book(id: ID!): Book } type Mutation { addBook(title: String!, author: String!): Book! } `;
다음으로 스키마의 각 필드에 대한 데이터를 가져오는 함수인 리졸버를 구현합니다.
// src/resolvers.ts const books = [ { id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }, { id: '2', title: 'To Kill a Mockingbird', author: 'Harper Lee' }, ]; export const resolvers = { Query: { books: () => books, book: (parent: any, { id }: { id: string }) => books.find(book => book.id === id), }, Mutation: { addBook: (parent: any, { title, author }: { title: string, author: string }) => { const newBook = { id: String(books.length + 1), title, author }; books.push(newBook); return newBook; }, }, };
마지막으로 Apollo Server를 설정하고 시작합니다.
// src/index.ts import { ApolloServer } from 'apollo-server'; import { typeDefs } from './schema'; import { resolvers } from './resolvers'; const server = new ApolloServer({ typeDefs, resolvers }); server.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`); });
그런 다음 클라이언트는 다음과 같은 쿼리를 보낼 수 있습니다.
query GetBooks { books { id title } }
또는 변이:
mutation AddNewBook { addBook(title: "1984", author: "George Orwell") { id title author } }
GraphQL 적용 시나리오
GraphQL은 다음과 같은 상황에서 탁월합니다.
- 복잡한 데이터 구조: 데이터 모델이 상호 연결된 리소스가 많은 복잡한 경우.
- 다중 클라이언트: 다양한 데이터 요구 사항을 가진 다양한 클라이언트(웹, 모바일, IoT)를 지원해야 하며 여러 REST 엔드포인트를 버전 관리하는 것을 피하고 싶을 때.
- 과도한/불충분한 가져오기 방지: 클라이언트가 받는 데이터를 정확하게 제어하여 불필요한 데이터 전송을 최소화하고 싶을 때.
- 빠른 반복: 새 필드를 기존 쿼리에 영향을 주지 않고 유형에 추가할 수 있으므로 기존 클라이언트를 중단하지 않고 API를 빠르게 발전시켜야 할 때.
tRPC
tRPC는 급진적으로 다른 접근 방식을 취합니다. 별도의 스키마를 정의하는 대신 TypeScript를 활용하여 백엔드 코드에서 직접 API 계약을 추론합니다. 이는 API 유형이 서버 측 함수에서 자동으로 파생됨을 의미합니다.
원칙 및 구현
tRPC의 핵심 원칙은 중간 스키마 계층이나 코드 생성 없이 엔드투엔드 타입 안전성을 제공하는 것입니다. 서버에서 API 경로를 TypeScript 함수로 정의한 다음 클라이언트 측 라이브러리를 사용하여 이러한 함수를 타입 안전한 방식으로 호출함으로써 이를 달성합니다.
tRPC API를 설정하는 기본적인 예는 다음과 같습니다.
먼저 서버에서 tRPC 라우터 및 절차를 정의합니다.
// src/server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; // 입력 유효성 검사용 const t = initTRPC.create(); // tRPC 초기화 const books = [ { id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }, { id: '2', title: 'To Kill a Mockingbird', author: 'Harper Lee' }, ]; export const appRouter = t.router({ book: t.router({ list: t.procedure.query(() => { return books; }), byId: t.procedure .input(z.object({ id: z.string() })) .query(({ input }) => { return books.find(book => book.id === input.id); }), add: t.procedure .input(z.object({ title: z.string(), author: z.string() })) .mutation(({ input }) => { const newBook = { id: String(books.length + 1), ...input }; books.push(newBook); return newBook; }), }), }); export type AppRouter = typeof appRouter; // 라우터 유형 내보내기
그런 다음 tRPC 라우터를 노출하는 HTTP 어댑터를 설정합니다. Node.js의 경우 @trpc/server/adapters/fastify
또는 @trpc/server/adapters/standalone
을 사용할 수 있습니다.
// src/index.ts (@trpc/server/adapters/standalone 사용 - 단순화) import { createHTTPServer } from '@trpc/server/adapters/standalone'; import { appRouter } from './server'; const server = createHTTPServer({ router: appRouter, // 선택 사항: createContext는 모든 절차에서 `ctx`를 사용할 수 있도록 보장합니다. createContext() { return {}; }, }); server.listen(3000, () => { console.log('🚀 tRPC server listening on http://localhost:3000'); });
클라이언트 측에서는 tRPC 클라이언트를 생성하고 타입 안전성이 완벽한 절차를 호출할 수 있습니다.
// src/client.ts (예: React 구성 요소) import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; // 서버 측 유형 가져오기 const trpcClient = createTRPCProxyClient<AppRouter>({ links: [ httpBatchLink({ url: 'http://localhost:3000/trpc', // tRPC 엔드포인트 }), ], }); async function fetchBooks() { const allBooks = await trpcClient.book.list.query(); console.log('All books:', allBooks); const book = await trpcClient.book.byId.query({ id: '1' }); console.log('Book by ID:', book); const newBook = await trpcClient.book.add.mutate({ title: 'Dune', author: 'Frank Herbert' }); console.log('Added book:', newBook); } fetchBooks();
trpcClient.book.list.query()
및 trpcClient.book.byId.query()
가 자동으로 타입 안전하며 byId
의 input
인수가 엄격하게 적용된다는 점에 유의하십시오.
tRPC 적용 시나리오
tRPC는 다음과 같은 맥락에서 빛을 발합니다.
- 풀스택 TypeScript: 프론트엔드와 백엔드 모두에서 TypeScript를 사용하여 풀스택 애플리케이션을 구축하는 경우.
- 개발자 경험이 최우선: 즉각적인 타입 안전성과 자동 완성 기능을 갖춘 타의 추종을 불허하는 개발자 경험을 우선시하는 경우.
- 보일러플레이트 코드 감소: API 정의 및 클라이언트 측 사용을 위한 보일러플레이트 코드를 최소화하고 싶을 때.
- 내부 모노레포: 프론트엔드와 백엔드가 유형을 쉽게 공유하는 모노레포 설정에서 특히 효과적입니다.
- REST와 같은 단순성, 타입 안전성 장점: 간단하고 직접적인 RPC 스타일 상호 작용을 선호하지만 강력한 타입 검사가 필요한 경우.
결론
Apollo Server를 사용한 GraphQL과 tRPC는 Node.js API 구축을 위한 매력적인 솔루션을 제공하지만, 서로 다른 철학과 사용 사례를 제공합니다. GraphQL은 복잡한 다중 클라이언트 환경에서 정확한 데이터 가져오기가 필요한 상황에 탁월한 강력하고 유연한 쿼리 언어와 스키마 기반 계약을 제공합니다. 반면에 tRPC는 TypeScript를 활용하여 타의 추종을 불허하는 엔드투엔드 타입 안전성과 개발자 경험을 제공하며, 특히 모노레포 내에서 단순성과 개발 속도가 중요한 풀스택 TypeScript 애플리케이션에 탁월한 선택입니다. 궁극적으로 귀하의 선택은 프로젝트의 특정 요구 사항, 각 기술에 대한 팀의 익숙도, 유연성, 타입 안전성 및 개발 속도 간의 원하는 균형에 따라 달라집니다. 두 도구 모두 각자의 강점에서 매우 효과적이며 개발자가 견고하고 유지 관리 가능한 API를 구축할 수 있도록 지원합니다.