Thinking in GraphQL (번역)
(이 글은 Relay 문서 중 Thinking in GraphQL 장을 번역한 것입니다. 245df1e0 리비전을 기준으로 하고 있으며, 저장소의 라이센스를 따라 BSD 라이센스입니다. 원작자의 허락으로 별도로 받지는 않았음을 밝힙니다.)
GraphQL은 클라이언트에서 데이터를 가져오는 새로운 방식을 제시합니다. 제품 개발자와 클라이언트 애플리케이션의 요구사항에 중점을 둔 방식입니다. 뷰에서 정확히 어떤 데이터가 필요한지 명시할 수 있는 방법을 제공하며, 클라이언트는 필요한 데이터를 단 한번의 네트워크 요청으로 가져올 수 있습니다. GraphQL을 사용하면 REST 등 기존 접근 방식에 비해 애플리케이션이 데이터를 더욱 효율적으로(리소스 중심 REST 방식 대비) 가져올 수 있고 (커스텀 엔드 포인트를 만들 때 발생할 수 있는) 서버 로직의 중복을 줄일 수 있습니다. 그에 더해 GraphQL은 제품 코드와 서버 로직의 결합도를 낮추는 데 도움을 줍니다. 예를 들어 제품에서 필요한 정보가 많아지거나 적어질 때 서버의 관련 엔드포인트를 모두 수정하지 않아도 됩니다. It's a great way to fetch data.
이 글에서는 GraphQL 클라이언트 프레임워크를 만든다는 것의 의미를 알아보고, 그것이 기존의 REST 시스템을 위한 클라이언트와 어떻게 다른지 비교해볼 것입니다. 그 과정에서 Relay의 설계 결정사항에 어떤 배경이 있는지 살펴보며 Relay가 단지 GraphQL 클라이언트일 뿐만 아니라 선언적으로 데이터를 가져올 수 있는 프레임워크임을 보여드리겠습니다. 자, 이제 데이터를 가져오기 시작해볼까요!
데이터 가져오기
스토리의 목록과 각 스토리의 상세 정보를 가져오는 간단한 애플리케이션을 생각해봅시다. 리소스 중심 REST 방식에서는 다음과 같을 것입니다.
// 스토리 ID 목록을 가져온다. 단, 상세 정보는 가져오지 않음.
rest.get('/stories').then(stories =>
// 결과값은 연결된 리소스의 목록
// `[ { href: "http://.../story/1" }, ... ]`
Promise.all(stories.map(story =>
rest.get(story.href) // 링크 따라가기
))
).then(stories => {
// 결과값은 스토리의 목록
// `[ { id: "...", text: "..." } ]`
console.log(stories);
});
이렇게 하면 서버에 n+1개의 요청을 보내야 합니다. 리스트를 가져올 때 1번, 각 항목을 가져올 때 n번. GraphQL에서는 같은 데이터를 단 하나의 네트워크 요청으로 가져올 수 있습니다. (별도의 엔드포인트를 만들지 않고 가능합니다. 만약 만든다면 계속 관리해야겠지요)
graphql.get('query { stories { id, text } }').then(
stories => {
// 스토리의 목록
// `[ { id: "...", text: "..." } ]`
console.log(stories);
}
);
여기까지는 GraphQL을 그저 일반적인 REST 방식의 효율적인 버전으로만 사용했습니다. GraphQL 버전의 중요한 장점 두가지를 알 수 있습니다.
- 모든 데이터를 한번의 라운드트립으로 가져올 수 있습니다.
- 클라이언트와 서버가 서로 독립적입니다. 서버 엔드포인트가 정확한 데이터를 돌려주는 것에 의존하는 대신 클라이언트가 필요한 데이터를 명시합니다.
간단한 애플리케이션에서는 이미 이것만으로도 큰 개선일 것입니다.
클라이언트측 캐싱
서버로부터 같은 정보를 반복해서 읽어오면 느릴 수 있습니다. 예를 들어 스토리 목록에서 하나의 항목으로 갔다가 다시 목록으로 돌아온다고 하면, 전체 목록을 다시 가져와야 합니다. 일반적으로 쓰이는 방법인 캐싱으로 이러한 문제를 해결해 보겠습니다.
리소스 중심 REST 시스템에서는 URI에 기반한 응답 캐시를 관리할 수 있습니다.
var _cache = new Map();
rest.get = uri => {
if (!_cache.has(uri)) {
_cache.set(uri, fetch(uri));
}
return _cache.get(uri);
};
응답 캐싱 방식은 GraphQL에도 적용됩니다. 기초적인 방법은 REST 버전과 비슷하게 작동할 것입니다. 쿼리 문자열 자체를 캐시 키로 사용할 수 있습니다.
var _cache = new Map();
graphql.get = queryText => {
if (!_cache.has(queryText)) {
_cache.set(queryText, fetchGraphQL(queryText));
}
return _cache.get(queryText);
};
이제 이전에 캐시된 데이터를 요청하면 네트워크 요청 없이도 바로 답을 받을 수 있습니다. 이는 애플리케이션의 체감 성능을 향상시키는 실용적인 접근법입니다. 그러나 이러한 방식은 데이터 일관성 문제를 일으킬 수 있습니다.
캐시의 일관성
GraphQL에서는 여러 쿼리의 결과가 겹치는 경우가 자주 있습니다. 그런데 위에서 사용한 응답 캐시는 이렇게 결과가 겹치는 성질을 고려하지 않고 개별 쿼리에 대해서만 캐싱합니다. 예를 들어 다음과 같이 스토리를 가져오는 쿼리를 날리고,
query { stories { id, text, likeCount } }
가져온 스토리 중 하나의 likeCount
가 증가한 이후에 다시 가져오면
query { story(id: "123") { id, text, likeCount } }
이제 스토리가 어떤 방법으로 접근되는지에 따라 다른 likeCount
를 보게 될 것입니다. 첫번째 쿼리를 사용하는 뷰는 오래된 카운트를 사용하는데 두번째 쿼리는 업데이트된 카운트를 표시합니다.