프로젝트를 진행하면서, 이번에 처음으로 Graphql 및 Typescript 를 도입해보게 되었다. 일단 Grahpql에 대한 첫인상은 매우좋음 이었다. 먼저 Graphql에서 쿼리를 요청하는 방식은 다음과 같다.
이런식으로 원하는 유저의 id 값, 유저명, 프로필 url 등을 원하는 값들만 클라이언트에서 선택해서 가져올 수 있다.
기존의 REST 방식의 Overfetching, Underfetching 문제를 해결한 완전한 방식인 것 같았다.
그런데 서버쪽 코드를 작성하면서 점점 느낌이 싸해졌다.
일단 현재 백엔드 구조는 이렇다.
entity 디렉토리는 DB의 TypeORM엔티티들을 가지고 있다.
여기서 graphql 디렉토리는 다음과 같은 구조를 가지고 있다.
- inputTypes : Mutation 의 입력 형식 타입들
- mutations : Mutation 관련 ts파일들
- queries: Query 관련 ts파일들
- services: 서비스 로직
- types: graphql object types
일단 이 구조로도, 잘 돌아가긴 한다. 그런데 문제가 있다.
첫 번째 문제
Graphql 의 Object Type 과 TypeORM의 entity는 같거나 거의 유사한 구조를 가지고 있다. 예를 들어 User를 살펴보면,
이게 현재 User Entity의 구조이고,
이게 현재 Grahpql 의 UserType이다. 그냥 같다고 보면 된다. 여기서 Model이 하나 추가된다고 하면 Type을 하나 더 추가해줘야 하는 문제가 있다.
만약 Model 하나에 칼럼이 하나 추가된다고 하면, Entity Class 에 필드를 추가하고, UserType에도 필드를 추가해야 한다. 하나라도 빼먹으면 제대로 결과가 나가지 않는다.
두 번째 문제
이건 내가 typescript에 익숙하지 않아서 발생한 문제일수도 있겠지만, express-graphql의 너무 난잡한 인터페이스들이 너무 머리가 아팠다. 인터페이스 이름들을 조금 살펴보면,
GraphQLField
GraphQLInputField
GraphQLFieldConfig
GraphQLFieldConfigArgumentMap
GraphQLArgumentConfig
GraphQLArgument
GraphQLArgs
...
등등 하나하나가 다 인터페이스로 정의되어있는데, 어디까지 인터페이스를 구현해야하고, 어떤 인터페이스를 적용해야할지 계속 읽어보는것들이 힘들었다.
그렇게 힘든 나날을 보내다가 TypeGraphQL 이라는 것을 알게 되었다.
어.. 이거 난가?
역시 개발할때 모두 같은 고민을 하는 것 같다. 훨씬 더 전부터 이 고민을 하던 개발자들이 많았는지 이미 이를 위한 프레임워크가 존재했다. (리서치를 좀 더 열심히 했어야 했는데..)
어쨌든 이를 확인하자마자 도입하기 위해 공부하고 하나씩 바꾸는 과정을 거쳤다.
시작하기
TypeGraphQL 공식문서에서 어떻게 시작하는지에 대한 가이드가 나와 있다.
npm에서 해당 의존성을 설치하고, tsconfig 에서 데코레이터 관련 옵션을 켜주면 된다.
GraphqlObjectType과 TypeORM Entity 합치기
가장 먼저 기존 GraphqlObjectType과 TypeORM Entity를 하나로 합쳐야 한다. Entitiy 클래스에 데코레이터를 붙이기만 하면 된다!!
기존 UserType.ts 가 User.ts(entity)로 합쳐지는 과정을 살펴본다.
이게 기존 UserType으로 사용하던 GraphQLObjectType이다. 이걸 Entity로 합치기 위해 먼저 ObjectType() 데코레이터를 엔티티 클래스에 붙여준다.
이렇게 데코레이터를 붙여준 것만으로도 벌써 GraphQLObejctType이 된 것이다! 이후에 필드로도 사용할 칼럼 상단에 @Field() 데코레이터를 달아준다.
@Field() 데코레이터의 첫 번째 인자는, 해당 필드의 반환타입을 적어주면 된다. TypeGraphQL 에서는 ID와 Int Float이라는 스칼라 타입을 지원한다. string 같은 경우는 자동으로 지정이 되어서 써줄 필요가 없다.
(참고로 number 타입의 필드는 아무 반환타입을 주지 않으면 float으로 처리해버리니 필요시 지정해줘야 한다)
두번째 인자로는 config를 넘기면 된다. 자주 사용할 것 같은 property로는 description과 nullable 이 있을 것 같다.
description은 말 그대로 설명이다. graphiql에 노출된다.
nullable은 해당 필드가 null 이어도 되는지의 여부이다. 기본 false 이다.
타입과 프로퍼티에 관에서는 TypeGraphQL 공식문서를 참고하길 바란다. 영어긴 한데 읽을만 하다.
해당 작업을 완료했다면 이제 기존 UserType은 필요가 없다!!!
Resolver 구현하기
이제 해당 필드와 타입에 대해 리졸버를 만들어야 한다. resolvers 라는 디렉토리를 만들고 UserResolver.ts 를 생성한다.
@Resolver(Type) 데코레이터를 통해서 해당 클래스가 Resolver 이며, Type(여기선 User) 에 관한 Resolver라는 것을 알릴 수 있다. 이제 쿼리와 필드리졸버를 작성할 수 있다.
간단하게 User ID 를 통해 하나의 유저 객체를 받아오는 Query를 만들어 본다.
데코레이터와 그 옵션 하나하나가 직관적이어서 쓰기 참 좋은 것 같다.
먼저 @Query 데코레이터는 하나의 쿼리를 정의할 때 사용한다. 1쿼리 = 1함수로 매칭된다. 쿼리명은 함수명이 된다.
첫 번째 인자로 해당 쿼리의 반환타입을 지정하고, 두 번째 인자로 옵션객체를 넘기면 된다.
@Arg 데코레이터는 해당 쿼리가 받을 인자를 지칭한다. 함수의 인자 자리에 넣으면 된다.
첫 번째 인자로, 인자이름을 받는다.
두 번째 인자로, 받을 타입(여기서는 정수)를 받는다.
세 번째 인자로, 역시 옵션 객체를 넘긴다.
이제 해당 쿼리함수 내에서 서비스 로직을 호출해서 user data를 가져오고 반환해주면 된다.
(사실 여기서는 간단한 조회이기 때문에 repository를 호출하는게 맞지만 이 글 작성 시점에 repository가 구분이 안돼있었다. 현재는 Data Mapper 패턴을 적용하여 Repository를 분리하고 있다)
Field resolver 구현하기
필드 하나에 resolver를 등록해줘야 하는 경우는 @FieldResolver() 를 사용한다.
해당 유저가 작성한 질문글 리스트를 필드에 넣었다면, 다음과 같은 코드로 필드리졸버를 구현할 수 있겠다.
아까 사용했던 @Field() 데코레이터와 다른게 있다면, 로직이 있어야 하기 때문에 단일 프로퍼티 대신 함수가 들어온다.
적용하기
실제로 지금까지 구현한 모든 쿼리와 뮤테이션을 리팩터링 하고, 적용하기 위해서 schema를 만들고, express 미들웨어를 만들기 위해서 buildSchema 함수를 사용한다.
이제 반환된 미들웨어를 app.use를 통해 등록해주면 끝이다!
개선 후 구조
개선 후에는 백엔드 구조는, 일단 types 폴더가 entities 와 합쳐지면서 없어졌다.
그리고 resolver를 통해서 query와 mutation을 모두 처리하도록 바뀌었다.
실제로 코드를 확인하면 훨씬 가독성이 올라가고, 무분별한 any의 사용도 줄어서 아주 만족스러운 결과를 얻을 수 있었다!
이 구조에 만족하지 않고 Spring 의 MVC 패턴과 비슷하게 추후 개선할 예정이다.
'Projects > NPE' 카테고리의 다른 글
빠른 대응을 위한 실시간 모니터링/알림 툴 도입 (0) | 2022.01.07 |
---|---|
TypeDI를 통한 의존관계 주입과 관심사의 분리 (0) | 2021.11.29 |
[TypeORM] 쿼리빌더를 통해 데이터 가져오기 (0) | 2021.11.07 |
댓글