본문 바로가기
graphql

[Nexus] NexusGenObjects와 NexusGenFieldTypes의 차이

by 박연호의 개발 블로그 2023. 3. 24.

nexus는 graphql 서버를 code first하게 작성할 수 있게 도와주는 라이브러리 이다. nexus의 여러 장점중 하나는 생성된 graphql schema를 기반으로 typescript type을 제공해준다는 것이다. 덕분에 우리는 type safe하게 개발을 할 수 있다.

 

nexus schema를 만들 때 다음과 같이 작성해주면 된다.

const schema = makeSchema({
  types: [
    /* All schema types provided here */
  ],
  outputs: {
    schema: path.join(__dirname, '../../my-schema.graphql'),
    typegen: path.join(__dirname, '../../my-generated-types.d.ts'),
  },
})

 

만약 schema first하게 graphql 서버를 만든다고 한다면 code generator를 사용하여 따로 타입을 관리해야 한다. 하지만 nexus는 그럴 필요가 없다. 알아서 type을 다 만들어 주기 때문 !!


NexusGenObjects와 NexusGenFieldTypes

type Profile {
  author: User!
  authorId: Int!
  id: Int!
  url: String!
}

type User {
  id: Int!
  name: String!
  profile: Profile!
}

type Query {
  users: [User!]
}

위와 같은 graphql schema가 있다.  유저와 프로필은 서로를 반드시 가지고 있다. 이를 기반으로 type을 만들었을 때 생성되는 NexusGenObjets와 NexusGenFieldTypes는 다음과 같다.

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!
  }
}

잘보면 NexusGenObjets와 NexusGenFieldTypes의 생김새가 비슷한 것 같은데 조금 다른 부분이 있다. 실제로 위의 타입이 어디서 사용되고, 왜 그렇게 정의되어 있는지를 이해한다면 단순히 nexus를 넘어 graphql이 동작하는 방식을 이해하는데 도움이 될거라고 장담한다.


NexusGenObjects

export type NexusGenRootTypes = NexusGenObjects

사실 NexusGenObjects는 NexusGenRootTypes와 같다. 생성된 타입에도 보통 NexusGenRootTypes을 사용하는 경우가 많다. 

 

그렇다면 NexusGenRootTypes는 무엇일까 ? 공식문서에는 다음과 같이 나와있다.

 

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(parent, args, ctx, info) {
        const userProfile = profile.find((v) => v.authorId === parent.id);

        if (!userProfile) throw new Error("유저 프로필 없음");
        return userProfile;
      },
    });
  },
});

여기서 parent는 누구일까 ? profile field의 상위개념, 즉 User가 되며 parent의 타입은 User 이다. 그리고 이는 NexusGenObjects['User']로 표현할 수 있다. nexus문서의 설명처럼 NexusGenObjects은 모든 resolver의 첫번째 인자의 타입이다.

 

실제로 위의 parent위에 마우스 오버를 하면 아래와 같은 타입을 확인할 수 있고, 이는 정확히 NexusGenObjects['User']와 일치한다.

- parent 타입
(parameter) root: {
    id: number;
    name: string;
}

- NexusGenObjects['User'] 타입
  User: { // root type
    id: number; // Int!
    name: string; // String!
  }

NexusGenFieldTypes

NexusGenFieldTypes는 object type에 존재하는 field에서 반환하는 데이터의 타입이다.

type User {
  id: Int!
  name: String!
  profile: Profile!
}

User 타입에는 id, name, profile이 존재한다. 각각의 필드는 scalar type 또는 object type의 데이터를 반환하게 되는데, 이때 반환하는 데이터의 타입을 정의한다.

 

 실제로 코드상에서 보면 FieldResolver<'User', 'profile'>이라고 되어 있으며, 이 타입은 결국 NexusGenFieldTypes['User']['profile'], 즉 NexusGenRootTypes['Profile']이 된다. 

  User: { // field return type
    id: number; // Int!
    name: string; // String!
    profile: NexusGenRootTypes['Profile']; // Profile!
  }

 

User.profile에서 반환하는 데이터의 타입(NexusGenObjects['Profile'])을 한번 보자.

  Profile: { // root type
    authorId: number; // Int!
    id: number; // Int!
    url: string; // String!
  }

앞서 봤던 NexusGenObjects에 해당하는 타입이다.

 

앞에서 다루진 않았지만, 여기서 의문인 점은...위에서 정의한 graphql schema에는 Profile에 author field가 무조건(non null)있어야 한다. 하지만 NexusGenObjects['Profile']에는 존재하지 않는다는 것이다. 

 

이유는 클라이언트에서 Profile.author를 조회할 수도 있고 조회하지 않을 수도 있기 때문이다.

 

Profile의 author field가 non null이긴 하지만, 그 말이 noll이 될 수 없다는 것이지, 항상 조회해야 한다는 것은 아니다. graphql은 클라이언트에서 원하는 데이터만 가져올 수 있다. 아래처럼 질의해도 전혀 문제되지 않는다.

 

query{
  profiles{
    id
    url
    author{   -> 조회하면 그때서야 author를 orm에서 가져옴
       id
       name
       }
     }
   }
  
    
query{
  profiles{
    id
    url 
    }       -> author를 조회하지 않음
  }

근데 만약 type에서 NexusGenObjects['Profile']에 항상 author가 존재해야 한다면, profiles쿼리에서는 클라이언트에서 author를 요청 할 지도 안 할지도 모르는데 항상 ORM으로 profile와 author를 join해서 가져와야 한다. 그렇지 않으면 타입스크립트 에러가 날 것이다.

 

굳이 사용 안할 수도 있는데, 미리 조회해서 보내는건 비효율적이다. 단지 author를 조회할 때만 주면 된다. nexus는 그것을 의도한 것이고 NexusGenObjects['Profile']에 author field가 없는 이유이다. 이후에 클라이언트가 author를 조회하면 그때 Profile.author resolver 실행되어 author 데이터를 가져오게 된다.

 

다 알고서야 하는 얘기지만, 생각해보면 지극히 당연한 얘기이다. 조회할 지도 안할지도 모르는 author인데...미리 서버에서 조회해서 보내주는 것은 말이 안되며, 이는 type도 같은 맥락이다. 그렇다면 그냥 http api 서버와 다를게 없다. 

 

 

 

 

 

'graphql' 카테고리의 다른 글

[GraphQL] custom scalar type 사용하기  (0) 2022.10.09
[GraphQL] null propagation  (0) 2022.10.01
[Nexus] Source Types으로 parent 타입 수정하기  (0) 2022.04.09
[GraphQL] GraphQL와 N+1  (4) 2021.09.26