안녕하세요. 볼드나인 백엔드 개발자 박연호 입니다.
저희 회사에서는 GraphQL 서버를 Nexus를 사용하여 code first한 방법으로 개발하고 있습니다. Nexus를 사용했을 때의 장점 중 하나는 생성된 GraphQL schema의 타입스크립트 타입을 자체적으로 만들어 준다는 점입니다.
schema first한 방법을 사용할 때는 Graphql-Codegen을 통해 타입을 생성해줘야 합니다. 하지만 Nexus를 사용하면 makeSchema 함수를 호출할 때 outputs 옵션을 주면 입력한 경로에 타입이 생성됩니다.
import { makeSchema } from 'nexus';
export const schema = makeSchema({
...
outputs: {
typegen: __dirname + '/generated/nexus.ts',
schema: __dirname + '/generated/schema.graphql',
}
...
});
JavaScript
복사
생성된 타입에는 schema.graphql에서 생성된 모든 타입에 대한 타입스크립트 타입이 정의되어 있으며, 그 중에 NexusGenObjects와 NexusGenFieldTypes에 대해 알아보겠습니다.
type Profile {
author: User!
authorId: Int!
id: Int!
url: String!
}
type User {
id: Int!
name: String!
profile: Profile!
}
type Query {
users: [User!]
}
JavaScript
복사
위처럼 GraphQL schema를 만들었을 때 생성되는 타입은 아래와 같습니다.
export interface NexusGenObjects {
Profile: { // root type
authorId: number; // Int!
id: number; // Int!
url: string; // String!
}
Query: {};
User: { // root type
id: number; // Int!
name: string; // String!
}
}
export interface NexusGenFieldTypes {
Profile: { // field return type
author: NexusGenRootTypes['User']; // User!
authorId: number; // Int!
id: number; // Int!
url: string; // String!
}
Query: { // field return type
users: NexusGenRootTypes['User'][] | null; // [User!]
}
User: { // field return type
id: number; // Int!
name: string; // String!
profile: NexusGenRootTypes['Profile']; // Profile!
}
}
JavaScript
복사
자세히 보면 NexusGenObjects와 NexusGenFieldTypes의 생김새가 비슷한 것 같지만 조금 다른 부분이 있습니다. 실제로 위의 타입이 어디서 사용되고, 왜 그렇게 정의되어 있는지를 이해한다면 단순히 Nexus를 넘어 GraphQL이 동작하는 방식을 이해하는 데 도움이 될 거라 생각합니다.
NexusGenObjects
export type NexusGenRootTypes = NexusGenObjects
JavaScript
복사
사실 NexusGenObjects는 NexusGenRootTypes와 같으며 보통 NexusGenRootTypes를 사용하는 경우가 많습니다.
그렇다면 NexusGenObjects는 무엇일까요 ? 공식 문서에는 다음과 같이 나와있습니다.
NexusGenObjects는 결국 GraphQL field resolver의 첫 번째 인자의 타입이다. 첫 번째 인자는 field의 부모 객체입니다. 아래의 코드를 보면…
const user = objectType({
name: "User",
definition(t) {
t.nonNull.int("id");
t.nonNull.string("name");
t.nonNull.field("profile", {
type: "Profile",
resolve: (root, args, ctx, info) => {
const profile = profiles.find((v) => root.id === v.authorId);
if (!profile) throw new Error("프로필이 없음.");
return profile;
},
});
},
});
JavaScript
복사
여기서 우리가 확인해야 하는 것은 profile resolver함수의 첫 번째 매개변수 root 입니다. apollo server 공식문서에서는 첫 번째 인자를 profile field의 부모에서 반환한 값이라고 되어 있습니다. 즉 root는 User 값이 되며 root의 타입은 NexusGenObjects[”User”]가 됩니다.
root 값을 확인하면 이는 NexusGenObjects[”User”] 값과 같은 것을 확인할 수 있습니다.
export interface NexusGenObjects {
Profile: { // root type
authorId: number; // Int!
id: number; // Int!
url: string; // String!
}
Query: {};
User: { // root type
id: number; // Int!
name: string; // String!
}
}
JavaScript
복사
NexusGenFieldTypes
NexusGenFieldTypes는 생성된 GraphQL type의 field에서 반환되는 값을 정리한 타입입니다. Profile에는 author, authorId, id, url field가 있으며 각 필드에서 반환되어야 하는 타입을 정의했습니다.
export interface NexusGenFieldTypes {
Profile: { // field return type
author: NexusGenRootTypes['User']; // User!
authorId: number; // Int!
id: number; // Int!
url: string; // String!
}
Query: { // field return type
users: NexusGenRootTypes['User'][] | null; // [User!]
}
User: { // field return type
id: number; // Int!
name: string; // String!
profile: NexusGenRootTypes['Profile']; // Profile!
}
}
JavaScript
복사
NexusGenFieldTypes와 NexusGenObjects의 차이점
생성된 타입을 자세히 보면, NexusGenFieldTypes에는 Profile.author, User.profile, Query.users에 대한 타입 정의가 있지만 NexusGenObjects는 없습니다.
앞서 설명한 것처럼 NexusGenFieldTypes는 GraphQL type에 대한 filed의 반환 값을 정의했지만, NexusGenObjects는 이전 depth에서 반환한 값을 받기 때문에 해당 resolver에서 반환하는 값을 가지고 있지 않기 때문입니다.
export interface NexusGenObjects {
Profile: { // root type
authorId: number; // Int!
id: number; // Int!
url: string; // String!
}
Query: {};
User: { // root type
id: number; // Int!
name: string; // String!
}
}
export interface NexusGenFieldTypes {
Profile: { // field return type
author: NexusGenRootTypes['User']; // User!
authorId: number; // Int!
id: number; // Int!
url: string; // String!
}
Query: { // field return type
users: NexusGenRootTypes['User'][] | null; // [User!]
}
User: { // field return type
id: number; // Int!
name: string; // String!
profile: NexusGenRootTypes['Profile']; // Profile!
}
}
JavaScript
복사
여기서 재미있는 점은, Query.users의 반환 타입인 NexusGenObjects['User'][]에는 profile이 없다는 것입니다. 하지만 GraphQL schema에는 profile이 nonNull로 정의되어 있습니다.
type User {
id: Int!
name: String!
profile: Profile! <--- ?
}
JavaScript
복사
Graphql schema에는 profile이 필수값이지만, NexusGenObjects['User']에는 profile값이 없다는게 좀 이해가 안되는 데요…
NexusGenObjects['User'][]에 profile이 없는 이유는 GraphQL의 특성과 연관이 있는데, scalar type이 아닌 field는 실제로 클라이언트에서 호출했을 때만 resolver가 실행됩니다. 먼저 users쿼리에서는 profile이 없는 유저만 반환합니다. 클라이언트에서 profile이 필요 없을 수도 있기 때문입니다. 만약 클라이언트에서 profile이 필요하면 그때 User.profile resolver가 실행되면서 profile이 조회되게 됩니다.
query{
users {
id
name
profile { ---> profile을 질의하면 그때서야 서버에서 profile resolver 실행
url
}
}
}
query{
users { ---> profile을 질의하지 않았기 때문에 profile resolver가 실행되지 않음
id
name
}
}
JavaScript
복사
아마 users쿼리는 클라이언트에게 이렇게 말할 것입니다.
나는 클라이언트가 유저의 profile의 선택 여부를 너한테 맡기겠어. profile이 없는 유저만 반환할 테니깐 만약 profile이 필요하다면 쿼리로 질의해줘. 그러면 그때 profile 값을 전달해줄게
만약 users쿼리의 반환타입인 NexusGenRootTypes['User'][]에 profile이 필수값이었다면 항상 profile과 함께 유저 데이터가 반환되며, 이는 클라이언트에서 원하지 않는 데이터를 항상 받게 될 것입니다.
지금까지 NexusGenRootTypes와 NexusGenFieldTypes에서 유저의 profile 필드의 차이점을 알아 보았습니다. 중요한 점은 scalar type을 제외하고 profile처럼 objectType을 반환하는 필드는 NexusGenRootTypes에서 제외되며, 이는 클라이언트에서 해당 필드가 필요할 때만 질의했을 때, 필드의 resolver가 실행되도록 의도된 것입니다.
이러한 GraphQL resolver의 특징을 알고 사용한다면 GraphQL을 좀 더 풍부하게 사용할 수 있을 거라 생각됩니다.