[Nexus] NexusGenObjects와 NexusGenFieldTypes의 차이
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는 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 서버와 다를게 없다.