튜토리얼: React Apollo 2.1 의 Render Props (Tutorial: Render Props in React Apollo 2.1)

React Apollo Client 최신 버전이 출시되었습니다. 아래 fullstack tutorial 을 통해 새로운 API 사용 방법을 확인해 보세요.

최근 React 커뮤니티에서 Render props 가 큰 화제가 되고 있습니다. 지난주 Apollo는 React Client 최신 버전을 출시 했으며 새롭게 적용된 패턴을 통해 React component 간 code 를 재사용할 수 있게 되었습니다.
presentation
이번 fullstack tutorial 에서는 React & GraphQL fullstack boilerplate 프로젝트를 rebuild 하면서 Apollo 에 새롭게 추가된 API 사용법을 배우게 됩니다. 이는 GitHub 을 통해 최종 버전을 확인할 수 있습니다.
이번에 제작할 app은 아래 기능을 갖춘 간단한 블로그 어플리케이션 입니다:
  • 발행된(published) 게시글(posts) feed 보기
  • 게시글로 발행되지 않은(unpublished) 초안(draft) 보기
  • 새로운 초안 만들기
  • 초안을 발행하면(publishing) feed에 출력 되도록 함
  • 발행되지 않은 초안 및 발행된 게시글(post) 삭제

render props 는 뭔가요?

개요

Render props는 값(value)이 함수(function) 인 prop 을 사용해서 React component 간 code 를 공유하는 패턴 입니다.
공식 문서에 따르면: “render props 를 포함한 component는 render 로직을 구현하는 대신 React component를 반환하고 이를 호출합니다” 라고 소개하고 있습니다. 다음은 render prop 사용법에 대한 간단한 예제 입니다:

Render props vs Higher-order components

일반적으로 render props는 code 재사용이 가능하므로 React higher-order components (HOCs) 를 대신해 사용하곤 합니다. 하지만 React Apollo 2.1이 새로운 render props API 를 제공한다고 해서 꼭 이것만 사용해야 하는 것은 아닙니다. 전과 같이 이미 검증된 graphql HOC를 사용해도 전혀 문제 없습니다. 그렇다면 언제 어떤 것을 사용해야 하는 것이 좋을까요?
일반적으로, 새로운 render prop component 인 Query, Mutation 그리고 Subscription 은 HOC 에 비해 사용하기 쉽고 단순합니다. 일례로 JSX code 에 해당 tag(예를들면 <Query>) 작성이 가능하며 이는 다른 React component를 적용하듯이 사용 가능합니다. 이에비해 Higher-order component는 React component를 HOC 로 감싸면서 추가적인 정밀한 작업이 필요 합니다. 이런 작업은 초심자(newcomers)에겐 직관성이 떨어져 작업이 어려울 수 있습니다.
이를 뒷받침할 간단한 사용사례를 예를 들자면 하나의 React component가 단일 query 및 mutation 에 의존하기 때문에 Query 또는 Mutation render prop component 내에서 쉽게 래핑 될 수 있습니다.
추가적으로 유용한 사례를 살펴보면 서로 종속적인 component 간 다양한 query를 사용하는 경우 입니다. 이는 <Query> component를 서로 중첩하여 쉽게 해결할 수 있습니다.
이미 HOC 사용에 익숙하다면 굳이 새롭게 선보인 render props API 를 사용할 필요는 없습니다. graphql HOC 와 render props component는 동일한 기능을 제공하기에 사용자 취향에 맞춰 사용해도 무방합니다. 또는 새롭게 선보인 API를 여러분이 원하는 방식으로 어플리케이션 구조를 잡는데 도움이되는 또 다른 도구라고 생각해도 좋습니다.
이 글은 render props 그 자체(per se)가 아닙니다. 많은 개발자 들이 HOC 보다 render props 를 선호하는 이유를 더 알아보고 싶다면 Machael Jackon기고한 글을 확인 해보시기 바랍니다.

1. 시작하기

이 섹션은 React Apollo 2.1 에서 선보인 새로운 render props API 를 시작하기 위한 준비 과정 입니다. 새로운 render props API 만 확인하려는 경우 해당 섹션은 건너 뛰어도 무방합니다 (섹션 2 로 이동).

1.1. starter code 다운로드

tutorial을 시작하기 앞서 starter code를 다운로드 해야 합니다. terminal 을 통해 다음 명령어를 실행해주세요:
이는 GitHub repo의 starter branch code 를 다운로드 해서 react-apollo-tutorial-starter 디렉토리 로 넣는 과정 입니다.

1.2. frontend app 탐색

원활한 진행을 위해 codebase에 익숙해지는 것이 좋습니다. 시작하기 앞서 react-apollo-tutorial-starter 디렉토리에서 yarn start 를 실행 하세요. 실행 전 반드시 yarn install 을 통해 dependencies(의존성 이하 dependencies) 를 설치해야 합니다.
presentation
다운로드한 디렉토리에는 전체 UI 가 포함되어 있지만 모든 기능은 backend 에 종속되어 있기에 실제 기능은 작동하지 않습니다.
1.3. GraphQL server 탐색
backend 의 server 디렉토리에 GraphQL server code 가 있습니다. 자 이제 server/src/schema.graphql 에서 GraphQL schema 를 통해 어떤 API 가 지원되는지 확인 합니다.
server 디렉토리에서 yarn start 를 실행하여 server 를 시작할 수 있습니다.(실행전 반드시 yarn install 실행해야합니다.) server가 실행되면 URL http://localhost:4000 (이 또한 frontend가 연결할 endpoint 입니다.) 에서 GraphQL Playground 를 열고 query 및 mutation 을 server 로 전송 합니다.
GraphQL server 를 web에 배포하는 방법을 배우려면 이전에 작성한 blog tutorial을 확인해주세요. Zeit Now Apex Up 과 같은 on-click 배포 tool에 대한 tutorial이 있습니다.
하지만 Playground 를 열면 아래와 같은 error 가 출력 됩니다:
presentation
이는 GraghQL server가 Prisma service의 database layer에 종속되는데 아직 Prisma service가 배포(deployed)되지 않았기 때문에 발생하는 오류 입니다. 다음은 이를 해결하기 위한 step 입니다.

1.4. Prisma 의 database layer를 GraphQL server 로 설정하기

Prisma service 배포를 위해 server 디렉토리 로 이동 후 Prisma CLI 를 통해 service를 배포합니다:
만약 Prisma CLI 를 전역(globally)으로 설치한 경우(npm install -g prisma 를 통해 가능함) package.json 을 이용한 yarn 대신 prisma deploy 를 사용할 수 있습니다.
command 실행 후 CLI에 Prisma service 를 배포 할 cluster를 선택하라는 메시지가 출력 됩니다. 본 tutorial 에서 추천하는 가장 쉬운 방법은 cloud 계정을 만들 필요가 없는 무료로 사용 가능한 개발 cluster 를 선택하는 것이며 굳이 Prisma cloud 계정을 생성할 필요는 없습니다. 이후 CLI 에 메시지가 출력되면 대상 cluster 를 prisma-man1 또는 prisma-user1 을 선택하면 됩니다. (Docker 를 설치한 경우 로컬 cluster 에도 배포 가능합니다.)
완료 후엔 Prisma service 의 HTTP endpoint 를 출력됩니다. 이는 아래와 같을 것 입니다: https://eu1.prisma.sh/public-warpcrow-598/blogr/dev 에서 public-warpcrow-598 는 임의로 생성된 ID 입니다.
또한 해당 command 로 server/database/seed.graphql 에 정의된 mutation을 기반으로 datebase로 일부 초기(initial) data가 시드됨(seeded)을 유의 하세요.
마지막으로 이 endpoint 를 Prisma binding instance 가 생성된 src/server/index.js 에 선언된 placeholder 인 __PRISMA_ENDPOINT__ 를 아래와 같이 대체하는 것 입니다:
자 이제 됐습니다! 이제 GrahQL server 는 완벽하게 database 에 의해 동작되며 Playground 에서 query 및 mutation을 보낼 수 있습니다.
이 app 에 사용된 간략한 설계(architecture) 개요는 아래와 같습니다:
presentation
이 tutorial 의 backend 로 사용되는 GraphQL server 빌드 방법은 여기 작성된 tutorial 을 참고 바랍니다.

1.5. fronedend app 에 React Apollo dependencies 설치

starter project 와 Prisma service 를 함께하기 위한 다음 단계는 Apollo 에 필요한 dependencies를 설치하는 것 입니다. terminal 에서 project root 디렉토리로 이동한 뒤 아래와 같이 dependencies를 설치해 주세요:
아래는 Apollo 문서를 통해 확인한 dependencies에 포함된 각각의 설명 입니다:
  • apollo-boost: Apollo Client 설정에 필요한 모든 사항을 포함하고 있습니다
  • react-apollo: React 통합 View Layer
  • graphql-tag: GraphQL query 파싱(parsing)에 필요합니다
  • graphql: 이 또한 GraphQL query 를 파싱(parses) 합니다
apollo-boost 는 Apollo Client 를 수많은 configuration 을 오버헤드 없이 빠르게 시작할수 있게 도와줄 wrapper package 입니다.

1.6. `ApolloClient` 를 초기화 하고 `ApolloProvider` 로 app 을 랩핑하세요

이제부터 진짜로 code 를 작성할 수 있습니다! 🙌 우선 가장 먼저 해야할 일은 frontend 와 backend 연결을 위해 GraphQL server 의 endpoint로 ApolloClient instance 를 생성하는 것 입니다.
src/index.js 를 열고 아래와 같이 입력해 주세요:
앞서 언급했던 것처럼, 이는 ApolloClient 가 port 4000 에서 local 로 실행되는 GraphQL server 와 연결하게 됩니다.
다음으로 ReactDOM.render 의 JSX code 전체를 ApolloProvider component 로 랩핑하면 생성된 prop 에 client instance 을 받을 수 있습니다:
ApolloProvider 덕에 손쉽게 app 에서 Apollo Client 기능을 사용할 수 있게 됐습니다.
마지막으로 여기에 ApolloClientApolloProvider 를 import 해줍니다. 파일 상단에 아래와 같이 추가해주세요:

2. render props 및 새로운 `Query` component 로 초안(drafts) 로드 및 출력 하기

먼저 DraftPage component 를 찾을 수 있도록 /drafts 경로(route) 기능을 구현 해야 합니다.

2.1. `drafts` query 지정

backend 로 부터 초안(drafts)을 로드하려면 server의 GraphQL schema 에 정의된 drafts query 를 사용해야 합니다.
/src/components/DraftsPage.js 파일 마지막에 아래 code 를 추가하세요:
query만 export 한다는 점을 유의해 주세요. 그 이유는 mutation 후에 캐시(cache) 업데이트 할 때, 다른 파일에서 이를 재사용할 수 있기 때문 입니다.

2.2. `Query` 로 data 로드하기

다음으로 React Apollo 의 새로운 Query component 를 사용하여 data를 로드하고 화면에 렌더링할 것 입니다. 이를 위해 DraftsPage.js 의 render 에서 반환할 모든것을 랩핑합니다. 또한 현재 사용중인 dummy 게시글(post) data도 삭제 가능합니다. 준비된 component 는 아래와 같습니다:
자 그럼 여기서 무슨 일이 일아나는지 확인해 봅시다. Query 를 사용해 component 를 래핑한뒤 DRAFTS_QUERY 를 prop 으로 전달하면 Apollo Client 가 관리하는 network 호출 결과에 접근 할 수 있습니다. 이 결과에는 query 결과를 다시 전송하는 data object 가 포함되어 있습니다. 다음으론 수신된 data 를 출력해야 합니다. 이 code 역시 이미 준비되어 있습니다 🙌
마지막으로 Query component 를 파일 맨위에 import 합니다:
app 을 실행하면 server로 부터 전달받은 data를 DraftPage로 로드합니다. app 을 테스트할때마다 server 실행을 잊지마세요(src/sever 내부에서 yarn start 호출)—그렇지 않은 경우 app이 backend 와 연결되지 않아 정상적으로 동작하지 않습니다!
현재 초안(drafts) 페이지에는 아래와 같이 처음 시드된(seeded) 초안만 출력 됩니다:
presentation

2.3. loading 및 error 상태(states) 관련 설명

이제 app 에서 data 를 불러오고 출력합니다. 하지만 알수 없는 이유로 network 요청이 실패하면 어떤 일이 일어날까요? 또한 요청이 진행되는 동안 사용자에게 loading 상태를 표시하려면요?
Apollo 덕에 이런 상황은 매우 간단하게 처리 가능합니다. render props 함수는 입력 인수(arguments)를 통해 응답 data 를 수신할 뿐 아니라 동시에 server 응답이 없는 경우 loading boolean 값(value)을 통해 true 를 수신 합니다.
해결을 위해 다음과 같이 두가지 추가적인 component 상태를 고려하여 Query component 내부 code 를 업데이트만 해주면 충분합니다:
이전 버전 Apollo 를 접한 적이 있다면 위 API 가 익숙할 것 입니다. 이는 기본적으로 graphql HOC 에서 사용하는 법과 동일합니다(여기서 Apollo는 graphql로 랩핑된 component props 에 data, loading 그리고 error 를 삽입(injects)합니다.). 자 벌써 초안(drafts) 페이지가 준비 되었네요!

3. feed 로드(loading the feed)

feed 구현은 drafts query 대신 feed 를 사용하는 점을 제외하면 초안(drafts) 구현과 유사합니다. 구현은 아래와 같습니다. (아래 code를 FeedPage.js 에 삽입하여 전체 내용을 대체하도록 합니다.)

4. reder props 와 새로운 `Mutation` component 를 이용하여 신규 초안(drafts) 생성하기

신규 초안(drafts)은 CreatePage component 를 렌더링하는 /create 경로 아래에 생성 됩니다. 이는 titletext 를 입력할 수 있는 간단한 양식으로 출력 됩니다.

4.1 `createDraft` mutation 지정

CreatePage.js 아래에 createDraft mutation 을 추가하면 끝입니다:
이 mutation 을 server 로 보내려면 component 에서 전달해야 할 두개의 변수(variables)가 필요 합니다.

4.2. `Mutation` 으로 data 쓰기

React Apollo 2.1 의 QueryMutation component 는 매우 유사합니다. 가장 핵심적(core)으로 다른점은 Mutation 로 다른 component 를 랩핑하면 render prop 함수에는 Query 와 다르게 server 의 mutation을 보내는주는 함수를 받게 된다는 것 입니다.
Query component 와 같이 render 에 반환된 모든 것을 아래와 같이 Mutation 으로 랩핑합니다:
render prop 함수로 전달되는 data, loading 그리고 error 인수(argument)는 Query component 에서 확인한 것과 동일한 의미를 지닙니다. createDraft 의 첫번째 인수는 CREATE_DRAFT_MUTATION 을 server 로 전송되는데 사용 됩니다. 이는 formonSubmit callback 에서 호출 됩니다.
마지막으로 아래와 같이 Mutationgql 을 import 합니다.
이를 통해 app 실행 후 /create 경로로 이동 한 뒤 저장된 새로운 초안을 server-side database로 제출(submit)할 수 있게 되었습니다:
presentation

하지만 Create - button을 클릭하고 app 이 /drafts 로 이동하면 페이지가 업데이트 되지 않을 걸 알 수 있습니다. 새로 생성된 초안을 확인하려면 페이지를 새로고침해야 합니다🤔
이러한 이유는 초안(drafts) 페이지가 이미 캐싱된 data 만 출력하기 때문 입니다. 이 문제를 해결하려면 mutation 실행 후 캐시를 수동으로 업데이트 해야 합니다.

4.3. imperative store API 를 이용하여 캐시 업데이트

Apollo imperative store API 를 사용하면 Apollo 캐시 에서 직접 읽고/쓸수 있습니다.이미 이전 Apollo 버전을 통해 imperative store API 을 사용해봤다면 아래 내용이 익숙 할 것 입니다. 이전 버전과 가장 큰 차이점은 update 가 mutation 의 함수 인수(argument)로 전달되지 않고 Mutation component props 로 전달 된다는 것 입니다.
여기서 update 함수의 API는 동일하게 유지 됩니다: 이는 캐시의 인터페이스 역할을 하는 object 를 수신하고 server 응답 뿐 아니라 update 도 가능합니다.
다음은 이를 구현하는 방법 입니다:
update 내부를 들여다 보면 맨 처음으로 DRAFTS_QUERY 이전 결과를 캐시로 추출 합니다 (이전에 DraftsPage 에서 이를 export 했던 건 이런 이유 때문입니다.). 이후 server 에서 반환된(data.createDraft 에 저장된) 새로운 초안 object 를 직접 추가한뒤 writeQuery 를 이용하여 캐시 내용을 업데이트 합니다.
이제 app을 다시 테스트해보면 /drafts 페이지에서 mutation 수행 하면 즉시 업데이트 되는 것을 아래와 같이 확인할 수 있습니다.
presentation
빠른 설명을 위해 errorloading 상태(states) 설명은 생략 했습니다.

5. 게시글 발행(publishing posts)

초안을 게시글로 발행 완료하면 해당 feed에 완료된 글이 나타나게 됩니다. 이때 DetailPage component를 통해 출력될 초안에 두개의 button 이 출력 되는 것을 확인할 수 있습니다: Publish 그리고 Delete
5.1. 선택한 게시글 로딩(loading)
게시글을 클릭할 때마다(초안(drafts) 또는 feed 페이지 에서) DetailPage component 를 통해 출력 됩니다. 또한 network 에서 게시글 data 를 로드 합니다.
이전처럼 아래와 같이 query 를 추가해주세요:
id 변수(variable)는 Query component 의 prop 으로 전달 됩니다. 이는 선택된 게시글의 id 가 포함된 URL 에서 불러 옵니다.
업데이트 된 render 함수는 아래와 같습니다:
마지막으로 Querygql 을 아래와 같이 import 합니다:
자 이제 /drafts 경로에서 초안을 선택하면 해당 app 에 Publish- 그리고 Delete- button을 포함한 DetailPage 가 출력될 것 입니다.
presentation
아직 작동하지 않습니다. 이어지는 섹션에서 구현하도록 합니다!

5.2. 초안을 게시글로 발행(publishing a draft)

초안을 게시글로 발행하려면 server 의 GraphQL API 인 publish mutation 을 이용해야 합니다. 먼저 DetailPage.js 에 mutation 을 추가 합니다:
_renderAction 내부에 button이 생성되므로 여기에 Mutation component 를 추가해야 합니다. 자 그럼 계속해서 publishButton 변수 정의(definition)를 아래와 같이 변경해주세요:
위 code 는 새로운 개념이 사용되지 않습니다. 실제 button은 PUBLISH_MUTATION 뿐 아니라 props로 update 기능을 지원하는 Mutation component 안에 랩핑됩니다.
update 를 살펴보면 게시글 발행 전 DRAFT_QUERY 에 캐싱된 결과에서 수동으로 제거한뒤 FEED_QUERY 에서 새롭게 추가하고 있습니다.
또한 render prop 함수에 전달된 publish 함수는 Publish-button이 클릭될 때마다 호출 됩니다.
다음으론 _renderAction 함수에서 반환된 내용을 update해야 합니다:
마지막으로 Mutation component 및 참조하고 있는 Query 를 import 합니다:
자 이제 새롭게 적용된 기능을 테스트 해보세요! UI 를 통해 초안에서 게시글로 발행된 것이 feed 로 출력되는걸 확인할 수 있습니다:
presentation
5.3. 게시글 삭제(Deleting)
삭제 기능은 게시글 발행 기능과 매우 유사 합니다. 이는 tutorial 간소화 및 여러분이 직접 구현하고 연습할 수 있도록 남겨두려 합니다. 만약 구현이 어렵게 느껴진다면 GitHub 프로젝트에서 최종 버전 확인 바랍니다.

요약(Summary)

여러분은 이번 tutorial을 통해 React Apollo 2.1 의 새로운 API 를 사용하는 법을 확인 했습니다. 이 API는 React component 간 code 공유를 위해 render props 패턴을 사용하는 새로운 QueryMutation 기반으로 되어 있습니다.
이 tutorial 은 목적상 React & GraphQL fullstack boilerplate 에서 basic boilerplate 를 rebuild 했습니다.
향후 tutorial 을 통해 React Apollo 2.1 의 새로운 Subscription component를 통해 실시간 업데이트를 구현하는 방법을 배워 볼 것 입니다 ⚡️

Peggy Rayzis 에게 감사드립니다.

이글은 번역글입니다.
원문은 아래를 참고 바랍니다.