WordPress 사이트·테마·플러그인을 위한 GraphQL 스키마 매핑
기존 WordPress 사이트에 GraphQL을 도입하기로 결정하셨군요. 훌륭합니다! 새로운 기능이든 기존 기능이든, GraphQL은 기반이 되는 데이터 계층과 연동되어야 합니다. 이를 위해 애플리케이션의 데이터 모델(WordPress 사이트의 커스텀 PHP 코드, 테마, 또는 플러그인)을 GraphQL 스키마에 매핑해야 합니다.
매핑은 어떻게 진행해야 할까요? 한 번에 모두 완료해야 할까요? 기존 데이터 모델의 완전한 복제본을 만들어야 할까요? 그 과정에서 부적절한 이름을 수정하는 것은 어떨까요? 기술적 부채는 유지해야 할까요, 아니면 해결해야 할까요?
기존 WordPress 애플리케이션의 데이터 모델을 GraphQL 스키마에 매핑하기 위한 몇 가지 전략을 살펴보겠습니다.
자신의 속도에 맞게 스키마를 매핑하세요
애플리케이션에 GraphQL을 추가하는 것은 전부 아니면 전무의 선택이 아닙니다. 동일한 애플리케이션이 여러 API로 동시에 작동할 수 있으며, 그 경우 GraphQL은 필요한 기간 동안 다른 API와 공존하게 됩니다. 예를 들어, 기존 기능은 REST로 계속 운영하면서 새로운 기능에만 GraphQL을 도입할 수 있습니다.
GraphQL로 완전히 마이그레이션하고 싶더라도, 한 번에 모두 진행할 필요는 없습니다. 기존 기능을 조금씩, 꾸준히 GraphQL로 마이그레이션해 나가다 보면 언젠가 GraphQL이 애플리케이션의 유일한 API가 되는 날이 올 것입니다.
따라서 첫날부터 완전한 GraphQL 스키마를 만들 수도 있지만, 반드시 그럴 필요는 없습니다. 어느 시점에서든 기능에 필요한 엔티티만 스키마에 존재하면 충분합니다(타입, 필드, 인터페이스를 통해). 시간이 지남에 따라 점진적으로 매핑해 나갈 수 있습니다.
인터페이스에 구현의 부담을 지우지 마세요
GraphQL 서버는 애플리케이션의 데이터에 접근하기 위한 로직을 구현합니다. 이는 WordPress의 기능을 호출함으로써 이루어집니다. 예를 들어, 게시물 데이터를 가져오기 위해 get_posts를 호출합니다. 이 계층에는 리졸버를 충족하기 위한 PHP 코드가 존재합니다.
하지만 GraphQL 스키마는 인터페이스입니다. API에서 데이터에 접근하기 위한 계약을 선언합니다. 구현 세부 사항에는 관여하지 않습니다. WordPress에 대해서도, get_posts 함수에 대해서도, DB 테이블 wp_posts에 대해서도, SQL 쿼리에 대해서도 아무것도 알지 못합니다.
따라서 가능한 한 계층 간에 정보가 누출되지 않도록 해야 합니다.
이것이 중요한 이유는 데이터 모델이 구현에 의해 왜곡되는 경우가 많기 때문입니다. WordPress는 이에 대한 명확한 예를 "attachment" CPT에서 보여줍니다. 이는 이미지와 같은 미디어 파일을 나타내기 위해 사용됩니다.
Custom Post Type이기 때문에, 이미지는 게시물로 처리됩니다. 그래서 Post 타입을 사용해 미디어 파일을 표현하고 싶어질 수 있습니다. 이 타입에는 다음 필드가 포함됩니다:
type Post {
id: ID!
title: String
content: String
excerpt: String
}하지만 이것은 애플리케이션에 적합하지 않을 수 있습니다. "content" 필드의 의미는 게시물에서는 명확하지만, 이미지에서는 명확하지 않습니다. 아마도 거기에 속하지 않아야 할 것입니다.
이미지가 WordPress에서 CPT로 모델링된 것은 기존 로직을 재사용하고 기존 wp_posts 테이블에 저장할 수 있다는 편의성 때문이었습니다.
하지만 편리하다는 것이 적절하다는 것을 의미하지는 않으며, 결국 기술적 부채로 이어질 수 있습니다(즉, 파괴적인 변경 없이는 수정할 수 없는 결함 있는 코드가 원래보다 오래 애플리케이션에 남아 있는 상황입니다).
가능한 한 애플리케이션에 기술적 부채를 쌓고 싶지 않습니다. 기회가 있을 때마다 수정해야 합니다. 데이터 모델을 GraphQL 스키마에 매핑하는 것은 그러한 기회를 제공하며, 데이터 인터페이스 계층에서 문제를 수정할 수 있게 해줍니다.
(단, 기술적 부채는 애플리케이션 수준에서는 여전히 남아 있으므로, 문제를 완전히 해결하는 것은 아니지만 우리가 할 수 있는 범위 내에서 줄이는 것입니다.)
이 아이디어를 실천해 봅시다. 미디어 파일을 표현하기 위해 Post 타입을 사용하는 대신, 이미지 엔티티에 실제로 의미 있는 속성만 포함하는 Media 타입을 갖는 것이 더 합리적입니다:
type Media {
id: ID!
src: String!
width: Int
height: Int
}내부 구현 수준에서는 필드 리졸버가 여전히 get_posts 함수를 실행하여 Media 타입의 항목을 해결하지만, 그것은 GraphQL 스키마와는 무관한 일입니다.
GraphQL 스키마를 DB 다이어그램으로부터 분리하세요
WordPress는 이 DB 엔티티 관계 다이어그램 위에 구현되어 있습니다:

GraphQL 스키마는 DB 다이어그램을 기반으로 해야 하지만, 1대1 복제본을 만들려고 시도해서는 안 됩니다. GraphQL 스키마와 DB 다이어그램은 각각 서로에게 적용되지 않는 특정 전제 조건이나 제약 하에 구축되기 때문입니다.
이전 섹션에서 그 예를 보여드렸습니다. 테이블 wp_posts는 이미지 CPT의 데이터를 저장하지만, GraphQL에서는 Post와 Media라는 두 가지 별개의 타입이 존재합니다.
또 다른 예로 카테고리를 생각해 봅시다. WordPress에서 게시물은 카테고리(하나 이상)를 가질 수 있으며, 임의의 CPT도 자체 카테고리를 만들 수 있습니다. 예를 들어, "event"라는 CPT는 "event_category"를 갖습니다.
게시물 카테고리와 이벤트 카테고리는 모두 wp_terms 테이블에 저장됩니다. 이를 통해 WordPress는 SQL 쿼리를 실행할 때 어느 카테고리 타입의 행이든 쉽게 가져올 수 있습니다.
따라서 게시물과 이벤트 모두에서 참조되는 Category 타입으로 카테고리를 매핑하고 싶어질 수 있습니다:
type Category {
id: ID!
name: String!
}
type Post {
categories: [Category]!
}
type Event {
categories: [Category]!
}하지만 게시물은 항상 게시물 카테고리를 갖고, 이벤트는 항상 이벤트 카테고리를 갖습니다. 이 두 카테고리 타입의 데이터는 동일한 DB 테이블에 저장되지만, 애플리케이션 수준에서는 혼재되지 않습니다. 게시물 카테고리와 이벤트 카테고리는 두 가지 별개의 엔티티입니다.
GraphQL은 정적 타입 시스템을 갖추고 있습니다. GraphQL을 최대한 활용하려면, 애플리케이션 수준에서 서로 다른 엔티티는 GraphQL 스키마에서 서로 다른 타입을 사용해 모델링해야 합니다.
이 경우, 카테고리를 GraphQL 스키마에 매핑할 때는 각각에 대해 별개의 타입을 만들어야 합니다: PostCategory와 EventCategory. 그러면 Post 타입은 PostCategory만 참조하고, Event 타입은 EventCategory만 참조합니다:
type PostCategory {
id: ID!
name: String!
}
type Post {
categories: [PostCategory]!
}
type EventCategory {
id: ID!
name: String!
}
type Event {
categories: [EventCategory]!
}스키마 내에서 모든 카테고리를 포함하는 엔티티가 필요하다면, 인터페이스 Category로 구현할 수 있습니다:
interface Category {
name: String!
}
type PostCategory implements Category {
id: ID!
name: String!
}
type EventCategory implements Category {
id: ID!
name: String!
}이렇게 하면 API에 접근하는 사용자는 DB 다이어그램에서 어떻게 매핑되는지, DB에 어떻게 저장되는지와 무관하게 어떤 데이터가 반환될지 명확하게 이해할 수 있습니다.
최종 GraphQL 스키마가 완성되면, 그 형태가 WordPress DB 다이어그램과 어느 정도 닮아 있지만 명확히 다르다는 것을 알 수 있습니다:

정적 타이핑에 따라 필드 이름을 적절히 조정하세요
필드는 가능한 한 애플리케이션 내의 이름을 존중해야 합니다.
예를 들어, wp_insert_post 함수로 게시물을 만들 수 있으며, 게시물에는 "title"과 "content"라는 속성이 있습니다. 이 이름들은 GraphQL 스키마에도 적합하므로(약간의 수정이 필요할 수 있지만) 유지해야 합니다:
type MutationRoot {
insertPost(title: String, content: String): Post
}
type Post {
id: ID!
title: String
content: String
}하지만 항상 그런 것은 아닙니다. 앞서 살펴본 것처럼, 커스텀 게시물은 자체 엔티티로 분리되어야 합니다. 따라서 get_posts 함수는 임의의 CPT 목록을 가져오지만, 스키마의 루트 타입에 있는 동등한 필드 posts는 Post 타입의 엔티티만 가져오며 Page(역시 CPT임)는 포함하지 않습니다:
type QueryRoot {
posts: [Post]!
}그렇다면 모든 게시물과 페이지의 목록을 어떻게 가져올까요? 또 다른 필드 customPosts를 사용합니다. 이는 유니온 타입 CustomPostUnion 아래에 매핑된 임의의 CPT 엔티티를 가져옵니다:
union CustomPostUnion = Post | Page
type QueryRoot {
customPosts: [CustomPostUnion]!
}중요한 교훈은 이것입니다: GraphQL 스키마에 선택하는 이름은 가져오는 엔티티의 타입에 맞게 조정되어야 합니다. 그리고 GraphQL의 강한 타입 시스템 때문에, 그 타입은 애플리케이션 계층과 API 계층에서 다를 수 있습니다.
이 경우 WordPress에서 "post"는 임의의 "custom post type"을 의미할 수 있지만, GraphQL에서 "post"는 반드시 Post입니다. 필드가 커스텀 게시물을 가져온다면, GraphQL 스키마의 필드는 posts가 아니라 customPosts로 명명되어야 합니다. 마찬가지로, 인풋이 커스텀 게시물의 ID를 받는다면 postID가 아니라 customPostID라고 불러야 합니다.

이 교훈은 댓글에도 적용됩니다. 댓글은 게시물뿐만 아니라 임의의 CPT에 추가될 수 있습니다. 따라서 Comment 타입은 이를 명확히 하기 위해 post가 아닌 customPost 필드를 포함해야 합니다:
type Comment {
id: ID!
customPost: CustomPostUnion!
}미리 정의된 문자열 값을 enum으로 변환하고, 가능하면 대문자를 사용하세요
열거형(enum)은 관례상 대문자로 정의됩니다. 예를 들어, graphql.org의 문서에서는 다음과 같은 예를 제공합니다:
enum Episode {
NEWHOPE
EMPIRE
JEDI
}새로운 enum 타입을 만들 때는 정의된 상수에 대문자를 사용해야 합니다. 하지만 애플리케이션에서 데이터 모델을 마이그레이션할 때, enum으로 매핑할 수 있는 미리 정의된 값의 집합이 존재하지만 그 값이 소문자 문자열인 경우가 있습니다.
예를 들어, WordPress의 게시물에는 "status" 속성이 있으며, 다음 값 중 하나를 가집니다:
"publish""pending""draft""trash"
스키마에서 이 속성을 매핑할 때, Post.status 필드는 다음과 같이 String을 반환할 수 있습니다:
type Post {
status: String!
}하지만 상태는 반드시 그 미리 정의된 값 중 하나여야 하므로, enum으로 매핑하는 것이 더 바람직합니다:
enum Status {
PUBLISH
DRAFT
PENDING
TRASH
}
type Post {
status: Status!
}여기서 문제가 생길 수 있습니다: enum PUBLISH는 애플리케이션에서 문자열 값 "PUBLISH"로 변환되며, "publish"가 되지 않습니다.
기대하는 소문자가 아닌 대문자 값을 사용하면 애플리케이션의 로직이 방해받을 수 있습니다. 실제로 WordPress에서 다음 코드를 실행해도 동작하지 않습니다:
// This will retrieve all posts, not only the published ones
$published_posts = get_posts([
"post_status" => "PUBLISH",
]);이 경우, 관례와 편의성 사이에서 트레이드오프를 고려하여, 여전히 enum을 사용해 상수를 매핑하되 소문자로 기술하는 방법을 선택할 수 있습니다:
enum Status {
publish
draft
pending
trash
}다시 말해, 적절함과 실용성 사이의 중간 지점을 찾을 수 있습니다. GraphQL 스키마를 구축할 때는 모범 사례를 사용해야 하지만, 이치에 맞는 경우에는 그로부터 벗어나는 것도 허용해야 합니다.