로컬과 리모트 데이터 상태 관리를 동시에 Apollo Client
Apollo Client는 GraphQL을 이용하여 로컬과 리모트 데이터를 다룰 수 있는 자바스크립트의 포괄적인 상태 관리 라이브러리이다. 이를 이용해서 데이터를 가져오거나, 캐시, 애플리케이션 데이터의 수정 이 모든 것을 UI를 업데이트하면서 할 수 있다.
Apollo Client는 현대의 개발 방식과 동일하게 코드를 경제적이고, 예상가능하며 선언적인 방식으로 짤 수 있게 해준다. @apollo/client
라이브러리는 리액트와의 통합성을 제공하며, 다른 유명 라이브러리/프레임워크를 위한 통합도 거대한 아폴로 커뮤니티를 통해 유지되고 있다.
특징
- 선언적인 데이터 패칭: 직접 로딩 상태를 따라갈 필요 없이 쿼리를 작성하고 데이터를 가져올 수 있다.
- 휼륭한 개발자 경험: 타입스크립트, 크롬 / 파이어폭스 개발자 도구, VS Code 등을 위한 편의 도구를 제공해준다.
- 최신 리액트를 위해 디자인된 라이브러리: 최신 리액트 문법인 React Hooks를 지원하는 useQuery, useMutation을 사용하는 등 리액트와 찰떡궁합
- 점진적 적용 가능성: 아폴로를 자바스크립트 앱에 몇 방울 뿌려가면서 기능별로 적용해나갈 수 있다.
- 보편적 호환성: 어떠한 빌드 설정이나 어떠한 GraphQL API도 사용할 수 있다.
- 커뮤니티 중심: 관련 지식을 GraphQL 커뮤니티에 잇는 수 많은 개발자들과 나눌 수 있다.
왜 Apollo Client인가?
왜 데이터를 다루는 데, Apollo Client를 선택해야하는가?
데이터 관리는 자고로 어려워선 안된다고 생각한다! 만약 리액트 앱에서 리모트와 로컬 데이터를 어떻게하면 간단하게 관리할 수 있는 지 궁금하다면, 잘 왔다. 해당 문서를 통해, 아폴로의 똑똑한 캐싱과 데이터 패칭에 대한 선언적 접근이 어떻게 코드는 적게 사용하면서 반복적인 작업은 도울 수 있는 지 배우게 될 것이다. 바로 출발해보자! 🚀
선언적 데이터 패칭
데이터 패칭에 대한 아폴로의 선언적 접근을 통해, 데이터를 받는 것, 로딩과 에러 상태를 추적하는 것 그리고 UI를 업데이트하는 것까지 모든 로직을 useQuery 훅을 통해 캡슐화할 수 있다. 이 캡슐화를 통해 쿼리 결과를 프레젠테이션 컴포넌트에 쉽게 통합할 수 있다! Apollo Client와 리액트를 통해 실제로 어떤 모습인지 알아보자:
function Feed() {
const { loading, error, data } = useQuery(GET_DOGS);
if (error) return <Error />;
if (loading) return <Fetching />;
return <DogList dogs={data.dogs} />;
}
여기서는 useQuery 훅을 사용하여 GraphQL 서버에서 몇 개의 개 데이터들을 가져와 목록에 표시하고 있다. useQuery는 리액트의 Hooks API를 활용하여 쿼리를 구성 요소에 바인딩하고 쿼리 결과에 따라 렌더링한다. 데이터가 반환되면 구성 요소는 필요한 데이터에 따라 반응적으로 업데이트된다.
Apollo Client는 로딩 및 오류 상태 추적을 포함하여 처음부터 끝까지 요청의 사이클을 처리한다. 첫 번째 요청 전에는 작성할 미들웨어나 상용 소프트웨어가 없으며, 응답의 변환 및 캐싱에 대해 걱정할 필요가 없다. 컴포넌트에 필요한 데이터를 설명하고 Apollo Client가 어려운 작업을 수행할 수 있도록 하면 된다. 💪
Apollo Client로 전환하면 데이터 관리와 관련된 불필요한 코드를 많이 삭제할 수 있다. 애플리케이션에 따라 정확한 양은 달라지겠지만, 일부 팀에서는 최대 수천 코드를 줄였다고 한다. 여러분이 아폴로로 코드를 덜 쓰겠지만, 그것이 여러분이 기능들과 타협해야 한다는 것을 의미하지 않는다! useQuery 옵션을 통해 옵티미스틱 UI, 리페칭 및 페이지네이션과 같은 고급 기능도 모두 쉽게 사용할 수 있다.
설정이 전혀 필요없는 캐싱
Apollo Client가 다른 데이터 관리 솔루션과 차별화되는 주요 기능 중 하나는 표준화된 캐시이다. Apollo Client에는 지능형 캐시가 포함되어 있어, 초기 구성이 거의 필요없는 수준이다.
import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
cache: new InMemoryCache()
});
그래프를 캐싱하는 것은 쉬운 일은 아니지만, Apollo Client 팀은 그래프 해결에 2년을 집중했다고 한다. 여러 경로를 통해 동일한 데이터를 얻을 수 있으므로 여러 컴포넌트에 걸쳐 데이터를 일관되게 유지하려면 정규화가 필수적이다. 몇 가지 실제 사례를 살펴보자.
const GET_ALL_DOGS = gql`
query GetAllDogs {
dogs {
id
breed
displayImage
}
}
`;
const UPDATE_DISPLAY_IMAGE = gql`
mutation UpdateDisplayImage($id: String!, $displayImage: String!) {
updateDisplayImage(id: $id, displayImage: $displayImage) {
id
displayImage
}
}
`;
쿼리 GET_ALL_DOGS
는 개의 목록과 개의 displayImage
를 가져온다. UPDATE_DISPLAY_IMAGE
이라는 뮤테이션은 단일 개의 displayImage
를 업데이트한다. 만약 특정 개의 displayImage
를 업데이트 한다면, 새로운 데이터를 반영하기 위해 모든 개의 목록에 있는 해당 항목이 필요하다. Apollo Client는 __typename
및 id
속성이 있는 GraphQL 결과의 각 개체를 Apollo 캐시에서 자체 항목으로 분할한다. 이렇게 하면 id
가 있는 뮤테이션에서 값을 반환하면 동일한 id로 개체를 가져오는 모든 쿼리가 자동으로 업데이트된다. 또한 동일한 데이터를 반환하는 두 개의 쿼리가 항상 동기화되도록 한다.
일반적으로 실행이 복잡한 기능은 아폴로 캐시를 사용하여 구축하기에는 매우 간단하다. 개 목록을 표시하는 이전 예제의 GET_ALL_DOGS
쿼리로 돌아가 보자. 특정 개의 세부 페이지로 전환하고 싶다면 어떻게 해야 할까? 이미 각 개의 정보를 가져왔기 때문에 서버에서 동일한 정보를 다시 가져오고 싶지는 않을 것이다. Apollo Client의 캐시 정책 API 덕분에 우리는 두 쿼리 사이에 개를 연결할 수 있으므로 이미 사용 가능한 정보를 가져올 필요가 없다.
개 한 마리에 대한 정보를 가져오는 쿼리는 다음과 같다.
const GET_DOG = gql`
query GetDog {
dog(id: "abc") {
id
breed
displayImage
}
}
`;
여기서는 캐시된 Dog
데이터에 대한 참조를 반환하는 사용자 지정 FieldPolicy
를 정의한다. 이렇게 하면 쿼리에서 해당 데이터를 검색하는 데 사용할 수 있다.
import { ApolloClient, InMemoryCache } from '@apollo/client';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
dog(_, { args, toReference }) {
return toReference({
__typename: 'Dog',
id: args.id,
});
}
}
}
}
});
const client = new ApolloClient({ cache });