라이브러리, 프레임워크•2026.01.21
{ "data": { "createProduct": null }, "errors": [ { "path": [ "createProduct" ], "locations": [ { "line": 2, "column": 3 } ], "message": "Could not resolve to a node with the global id of 'shop-that-does-not-exist'" } ]}errors 키에는 이러한 스키마가 없습니다. 명세에는 정의되어 있지만, 인트로스펙션(Introspection)을 통해서는 클라이언트가 그 안에 무엇이 들어있는지 알 수 없으며, 발전시키거나 확장하기도 어렵습니다. extensions 키를 통해 제공자가 추가 필드를 더할 수 있지만, 이는 명세를 벗어나는 일이며 스키마 외부에서 별도로 문서화해야 합니다. 이는 이상적이지 않습니다.null이어야 한다는 점입니다. 뮤테이션(Mutation)의 일부로 에러를 반환받고 싶으면서도 결과 데이터에 대해 쿼리하고 싶은 경우, 이는 치명적인 문제가 될 수 있습니다. 흔한 예로, 에러가 발생한 뮤테이션 이후에 리소스의 실제 상태를 서버가 다시 보내주는 경우가 있습니다. 최상위 에러를 사용하면 뮤테이션 필드 전체가 null이 되어야 하므로 이를 수행할 수 없습니다!GraphQL 에러는 서비스 중단이나 기타 내부 장애와 같은 예외적인 시나리오를 인코딩합니다. API 도메인의 일부인 에러는 해당 도메인 내에서 캡처되어야 합니다.
일반적인 철학은 에러를 예외적인 것으로 간주하는 것입니다. 사용자 데이터가 에러로 표현되어서는 안 됩니다. 사용자가 부정적인 안내를 받아야 하는 행동을 한다면, 그 정보는 에러가 아닌 데이터로서 GraphQL에 표현해야 합니다. 에러는 항상 개발자 실수나 예외적인 상황(예: 데이터베이스 오프라인)을 나타내야 합니다.
type Mutation { createUser(input: CreateUserInput!): CreateUserPayload}# 💡 "MutationPayload" 래퍼 타입은 처음에 # GraphQL 클라이언트인 Relay에 의해 지정된 일반적인 컨벤션입니다.type CreateUserPayload { user: User userNameWasTaken: Boolean! userNameAlternative: String!}createUser는 생성하려는 사용자 이름이 이미 존재할 때 에러가 발생할 수 있습니다. 이 경우 에러를 표시하고 입력한 내용을 바탕으로 더 나은 사용자 이름을 제안하고 싶을 것입니다.createUser 뮤테이션의 경우에만 에러에 userNameAlternative 확장을 추가해야 했을 것입니다. 클라이언트가 이를 알아내고 사용하기란 꽤 어렵습니다.user 필드를 nullable로 만들었는데, 사용자 이름이 중복되면 user는 null이 되고 userNameWasTaken은 true가 되기 때문입니다.userNameWasTaken이 true이면서 user 데이터도 존재할 수 있습니다. 이것이 무엇을 의미할까요? 불분명합니다.type CreateUserPayload { user: User userErrors: [UserError!]!}type UserError { # 에러에 대한 설명 message: String! # 에러를 유발한 입력 값에 대한 경로 path: [String!]}UserError 타입에 path 필드가 있습니다. 이 필드는 에러를 유발한 입력 값(있는 경우)을 가리키는 문자열 리스트입니다.username으로 사용자를 생성하려고 한다고 가정해 보겠습니다.mutation { createUser(input: { username: "xuorig" }) { user userErrors { message path } }}{ "data": { "createUser": { "user": null, "userErrors": [{ "message": "Username `xuorig` was already taken.", "path": ["input", "username"] }] } }}path가 우리가 사용한 username 입력 필드를 어떻게 가리키는지 주목하세요. 이는 클라이언트가 에러를 일으킨 폼(Form) 필드를 가리키는 등의 처리를 할 때 매우 유용합니다. 자, 다시 에러 배열 이야기로 돌아가겠습니다.message와 path를 모두 가질 것임을 알게 되며, 우리는 원하는 대로 다른 메타데이터를 추가하여 확장할 수 있습니다.UserError 타입을 가지므로, 클라이언트는 좀 더 미래 지향적인 방식으로 에러를 처리할 수 있습니다. 서버가 새로운 가능한 에러를 추가할 때, 에러 리스트를 처리하는 클라이언트는 별도의 작업 없이도 이를 받아볼 수 있습니다. 임시 필드를 사용하면 클라이언트는 새로운 케이스를 처리하기 위해 종종 새로운 커스텀 로직을 구현해야 합니다.user가 있으면서 userErrors 리스트에도 항목이 존재하는 것과 같은 잠재적으로 불가능한 상태를 허용합니다.@mustSelect 스키마 지시어(Directive) / 에러 같은 것입니다. 클라이언트가 해당 필드를 선택하지 않으면 에러를 생성하도록 특정 필드에 주석을 달 수 있습니다.type CreateUserPayload { userErrors: [UserError!]! }mutation { createUser(input: {}) (fields: ["userErrors"]) { user }}userErrors 필드를 응답에 강제로 포함시키지 않아도 된다는 것입니다. 강제로 포함시키면 GraphQL의 선언적 특성을 해칠 수 있기 때문입니다. 대신 사용자가 개발 시점에 에러를 발견하여 명시적으로 거부하거나 필드를 선택하도록 유도할 수 있습니다.UserError 타입이 하나뿐이므로 특정 에러에만 특화된 필드를 지원하기가 어렵습니다. 예를 들어, 제네릭한 UserError 타입에 userNameSuggestion 필드를 추가하는 것은 별로 의미가 없습니다.UserError를 인터페이스 타입으로 만들고, 필요할 때 구체적인 구현체를 만드는 것입니다.type CreateUserPayload { user: User userErrors: [UserError!]!}type UserNameTakenError implements UserError { userNameSuggestion: String message: String! path: [String!]}type SomeOtherError implements UserError {}interface UserError { # 에러에 대한 설명 message: String! # 에러를 유발한 입력 값에 대한 경로 path: [String!]}mutation { createUser(input: {}) { user { id } userErrors { message path ... on UserNameTakenError { userNameSuggestion } } }}UserCreationError 리스트를 가집니다.UserCreation 인터페이스와 UserError 인터페이스를 함께 가집니다.type Mutation { createUser(input: CreateUserInput): CreateUserResult}union CreateUserResult = UserCreated | UserNameTakentype UserCreated { user: User!}type UserNameTaken { message: String! suggestion: String!}User 타입에 관한 것이며 쿼리 예시들입니다.mutation { createUser(input: {}) { ... on UserCreated { user { id } } ... on UserNameTaken { message } }}UserCreationError를 가집니다.userErrors 리스트 타입을 선택합니다.userErrors 리스트를 개선할 수도 있습니다.type CreateUserPayload { user: User userErrors: [CreateUserError!]!}union CreateUserError = UserNameTaken | PasswordTooShort | MustBeOver18type UserNameTaken { message: String! suggestion: String!}CreateUserError 타입에 새로운 항목을 추가하기가 어렵습니다.type CreateUserPayload { user: User userErrors: [CreateUserError!]!}union CreateUserError = UserNameTaken | PasswordTooShort | MustBeOver18type UserNameTaken implements UserError { message: String! path: String! suggestion: String!}interface UserError { message: String! path: String!}이 솔루션이 작동하려면 모든 유니온 멤버가 공통 인터페이스를 구현해야 합니다. 이를 보장하기 위해 스키마 린터(Linter)를 찾아보시는 것을 권장합니다. graphql-schema-linter가 이 용도로 훌륭합니다.
mutation { createUser(input: {}) { user { id } userErrors { # 특정 케이스 ... on UserNameTaken { message path suggestion } # 인터페이스 계약 ... on UserError { message path } } }}아직 댓글이 없습니다.
첫 번째 댓글을 작성해보세요!
![[번역] AI 에이전트를 위한 좋은 사양서(Spec) 작성법](https://bucket.rosetta.page/images/cmhvyjhbb00009kn3jj49qrbw/번역-ai-에이전트를-위한-좋은-사양서-spec-작성법-hPmB4-26eb7f29-f859-4be4-9096-e6da73a8a942.webp)
[번역] AI 에이전트를 위한 좋은 사양서(Spec) 작성법
Inkyu Oh • AI & ML-ops
![[번역] 인생 디버깅](https://bucket.rosetta.page/images/cmhvyjhbb00009kn3jj49qrbw/번역-하루-만에-인생-전체를-바로잡는-법-5KqoB-9882e2dd-40af-402c-a9bc-2187dc6467ed.webp)
[번역] 인생 디버깅
Inkyu Oh • Career
![[번역] Vim의 가장 생산적인 단축키는 무엇인가요?](https://bucket.rosetta.page/images/cmhvyjhbb00009kn3jj49qrbw/번역-vim의-가장-생산적인-단축키는-무엇인가요-2F6UV-f80bf20a-bded-4b7a-88a5-74e6831e0da9.webp)
[번역] Vim의 가장 생산적인 단축키는 무엇인가요?
Inkyu Oh • Computer Science