목록 보기
GraphQL Mutation 설계하기
기타

GraphQL Mutation 설계하기

원문: Caleb Meredith, https://www.apollographql.com/blog/graphql/basics/designing-graphql-mutations/ 좋은 GraphQL API를 설계하는 것은 까다롭습니다. API가 미래에 어떻게 발전될지에 대한 고려와 함께 활용성, 편의성의 균형을 맞춰야 하기 때문입니다. GraphQL mutation을 설계할 때 고려해야 할 주요 사항들은 다음과 같습니다. 작명. mutation 이름에 동사를 먼저 작성한다. 가능하다면 그 뒤에 목적어나 명사를 사용한다. 그리고 camelCase를 사용한다. 명확성. 가능한 구체적으로 만든다. mutation은 사용자가 취할 수 있는 의미있는 행동을 나타내야 한다. 입력 객체. 클라이언트에서 더 쉽게 mutation을 실행할 수 있도록 단일하고(single), 필수적이고(required), 고유한(unique) 입력(input) 객체 타입으로 작성한다. 고유한 Payload 타입. 각 mutation에 맞는 고유한 payload 타입을 사용하고 mutation 결괏값을 payload 타입에 필드로 추가한다. 중첩. 가능하면 어디서든 중첩을 사용한다. 이 글은 위 주요 사항들에 대한 근거를 설명하고 API에 대한 효과적인 GraphQL mutation 시스템을 설계할 수 있도록 도와 줄 것입니다. mutation 이름 고르기 요약하면 mutation 이름에 동사를 먼저 사용합니다. mutation은 행동(action) 을 나타내므로 그 mutation이 하는 일을 가장 잘 설명하는 행동에 대한 단어를 먼저 작성합니다. createUser, likePost, updateComment, reloadUserFeed 와 같은 이름이 userCreate, postLike, commentUpdate, userFeedReload 보다 더 좋습니다. Shopify의 GraphQL 팀과 같은 일부 팀들은 이름을 반대로 쓰는 것을 선호합니다. (createUser 가 아닌 userCreate로 사용합니다.) 이는 mutation을 알파벳순으로 정렬할 때 (Ruby GraphQL gem은 항상 알파벳순으로 필드를 정렬합니다.) 그리고 데이터 모델이 대부분 객체 지향적인 CRUD(create, read, update and delete) method인 경우에 유용합니다. 모든 mutations들이 소수의 객체 유형으로 중심이 되어 있는 경우 이 규칙이 의미가 있습니다. 그러나 많은 어플리케이션이 데이터 모델의 객체에서 수행되는 작업과 직접 매핑되지 않는 mutation을 가집니다. 예를 들어, 비밀번호 초기화 기능을 만든다고 가정 해봅시다. 초기화 이메일을 보내기 위해 sendPasswordResetEmail 이라는 mutation을 가질 수 있습니다. 이 mutation은 간단한 CRUD 작업 보다 RPC call에 더 가깝습니다. 비밀번호 초기화 이메일의 경우 일반적인 mutation보다 명시적인 mutation이 필요한 이유를 설명하는 좋은 예시입니다. sendEmail(type: PASSWORD_RESET)과 같이 mutation을 만들고 싶을수 있고 이 mutation을 다양한 이메일 타입으로 호출하고 싶을 수 있습니다. 이것은 좋은 설계가 아닙니다. GraphQL 타입 시스템에서 올바른 input을 강제하기 어렵기 때문입니다. 또한, GraphiQL에서 가능한 동작이 무엇인지 이해하기 어렵기 때문입니다. 예를 들어, 나중에 이메일 중 하나를 새로운 input 인자로 보내고 싶을 수 있습니다. 만약 일반적이고 “범용적”인 mutation으로 시작한다면, 여분의 input을 추가하는 것이 훨씬 어렵습니다. UI가 만들 수 있는 업데이트와 정확히 일치하는 mutation을 만드는 것을 겁내지 말아야 합니다. 의미있는 사용자 행동과 일치하는 명시적인 mutation은 범용적인 mutation보다 훨씬 강력합니다. 명시적인 mutation은 UI 개발자가 작성하기 쉽고, 백엔드 개발자가 최적화 하기 쉽고, mutation의 특정 하위 요소만 제공하면 공격자가 API를 악용하기 어렵기 때문입니다. 네이밍 컨벤션은 팀에 따라 다양합니다. 만약 다른 네이밍 규칙을 선택하고 싶다면, 그것을 사용하면 됩니다. 위 방식은 다양한 사례에서 제일 적합한 방식인 네이밍컨벤션입니다. mutation input 설계하기 mutation은 input 인자가 하나만 있어야 합니다. 이 인자는 input 이라는 네이밍이어야 하고 null이 아닌 고유한 입력 객체 타입이어야 합니다. 즉, mutation은 다음과 같은 형태입니다. // good updatePost(input: ( id: 4, newText: "..." )) ( ... )

// bad updatePost(id: 4, newText: "...") ( ... ) 이유가 뭘까요? 첫번째 방식이 클라이언트에서 더 사용하기 쉽습니다. 클라이언트는 mutation의 모든 인자에 변수를 하나씩 보내는 것 대신에 mutation 마다 하나의 변수만 보내면 되기 때문입니다. 이것은 작은 차이 같아 보이겠지만 10개 이상의 인자가 필요한 mutation이 있을 때, GraphQL file은 더 작아질 것입니다. mutation MyMutation($input: UpdatePostInput!) ( updatePost(input: $input) ( ... ) )

vs

mutation MyMutation($id: ID!, $newText: String, ...) ( updatePost(id: $id, newText: $newText, ...) ( ... ) ) 다음으로 할 일은 입력(input) 객체를 최대한 중첩하는 것입니다. GraphQL 스키마 설계에서 중첩을 이용하면 좋은 점이 있습니다. 중첩을 이용하면 GraphQL의 강점을 완전히 이용하여 버전이 없는 API가 될 수 있습니다. 중첩은 시간이 지남에 따라 object 타입에 새로운 스키마 설계를 고려할 수 있는 여지를 제공합니다. API의 영역을 쉽게 deprecate 하고 충돌이 많은 객체 타입에서 새 이름을 찾으려고 싸우는 것 대신에 충돌 없이 새로운 이름을 추가 할 수 있습니다. 중첩을 미래의 API에 대한 투자라고 생각합니다. 다음 예제를 살펴봅시다. mutation ( createPerson(input: ( # 중첩을 이용하여 input의 상위 레벨에서 # 'password' 혹은 clientMutationId와 같은 필드를 추가할 수 있는 여유 공간을 가질 수 있습니다. # 나중에 'partialPerson' 같은 상위 레벨 필드를 사용하기 위해, 'person'을 deprecate할 수 있습니다. password: "qwerty" person: ( id: 4 name: "Budd Deey" ) )) ( ... ) updatePerson(input: ( id: 4 patch: ( name: "Budd Deey" ) )) ( ... ) ) mutation payload 설계하기 input을 설계할 때와 같이 GraphQL payload에서 중첩을 이용하면 좋은 점이 있습니다. 각 mutation에 대해 항상 custom object 타입을 생성합니다. 그리고 원하는 모든 반환값을 custom object type으로 추가합니다. 이렇게 하면 추후에 여러개의 반환값 그리고 clientMutationId 혹은 userErrors와 같은 메타데이터 필드를 추가할 수 있습니다. mutation ( createPerson(input: ( ... )) ( # mutation payload에 다른 어떤 필드든 추가할 수 있다. # 'clientMutationId' 혹은 'userErrors' person ( id name ) ) updatePerson(input: ( ... )) ( person ( id name ) ) ) mutation에서 단 하나의 값만 반환하더라도 그 타입을 직접 반환하고 싶은 욕심을 참아야 합니다. 미래는 예측하기 어렵고 만약 단일 타입으로 반환하기로 선택한다면, 나중에 다른 반환 타입이나 메타데이터를 추가하기 어렵습니다. 버전 없는 GraphQL API를 디자인 할 때 새로운 설계에 대한 여지를 제거하는 것은 우리가 원하는 것이 아닙니다. 종합해서 보기 이 글에서 제공된 권장사항들을 이용하여 TodoMVC 어플리케이션에 대한 잘 만들어진 GraphQL mutation 시스템 예제 입니다. type Todo ( id: ID! text: String completed: Boolean )

schema (

mutation에 집중하기 위해 query type은 생략합니다

mutation: RootMutation )

type RootMutation ( createTodo(input: CreateTodoInput!): CreateTodoPayload toggleTodoCompleted(input: ToggleTodoCompletedInput!): ToggleTodoCompletedPayload updateTodoText(input: UpdateTodoTextInput!): UpdateTodoTextPayload completeAllTodos(input: CompleteAllTodosInput!): CompleteAllTodosPayload )

'id'는 백엔드에서 생성되었고 'completed'는 자동으로 false로 세팅됩니다.

input CreateTodoInput (

현재는 'text' 단일 필드 밖에 없지만 중첩하여 작성했습니다.

앞으로 더 많은 필드가 있다고 판단을 내린다면, 'text'를 nullable로 만들고 deprecate하는 것은 어렵지 않습니다.

text: String! )

type CreateTodoPayload (

생성된 todo 입니다.

이것은 nullable 해서 만약 error가 있다면 todo는 null 값을 가집니다.

todo: Todo )

'id' 만을 허용하고 백엔드는 todo의 상태인 새로운 'completed'를 결정합니다.

이것은 다음과 같은 엣지 케이스를 방지합니다.

타입 시스템에서 "완료 상태가 이미 true 일 때, todo의 'completed' 상태를 true로 설정하기"

input ToggleTodoCompletedInput ( id: ID! )

type ToggleTodoCompletedPayload (

업데이트된 todo

todo: Todo )

범용적인 mutation이 아닌 명시적인 mutation입니다.

그래서 'patch' field를 중첩하지 않았습니다.

대신에 의도를 나타내는 'newText' 라는 새로운 field를 제공할 것입니다.

input UpdateTodoTextInput ( id: ID! newText: String! )

type UpdateTodoTextPayload (

업데이트된 todo

todo: Todo )

input CompleteAllTodosInput (

이 mutation은 어떠한 필드도 필요 없지만

나중에 input에 필드를 추가할 수 있는 여유 공간을 가져야 합니다.

)

type CompleteAllTodosPayload (

완료한 todo

todos: [Todo]

나중에 연결 된 것들을 사용할 경우 'todoConnection' field를 추가할 수 있습니다.

) 결론 이러한 설계 원칙으로 API에 대한 효과적인 GraphQL mutation 시스템을 설계할 수 있어야 합니다. 이 글은 여러 프로젝트에서 작업한 경험을 통해, 최근에 제가 생각한 GraphQL API의 mutation 설계에 대한 방식을 나타냅니다. API가 미리 설계된 팀, custom API를 구축한 프로젝트, 누구나 사용 가능한 범용 GraphQL API를 설계하는 PostGraphQL 작업이 있었고, Facebook과 기타 제품에 대한 GraphQL 유저와의 대화가 있었습니다. 이 디자인 원칙은 또한 Relay 입력 객체 mutation 명세 에서 많은 영감을 얻었습니다. GraphQL의 mutation 작업에 대한 깊은 기술적 이해가 필요하다면, 이 블로그에 곧 올라올 “GraphQL Muations 이해하기” 라는 제목의 글을 작성 중입니다. mutation 설계에 대한 다른 의견이 있다면 그 의견을 듣고 싶습니다! mutation과 GraphQL API의 다른 부분을 설계할 때, 그리고 미래에 대한 계획은 항상 중요하다는 것을 알고 있어야 합니다.

댓글 0

댓글을 작성하려면 로그인이 필요합니다.

댓글을 불러오는 중...