도움말 - 글감 수집하기 (인용)

도움말 - 부분 리뷰 작성하기

4편: React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - Apollo 스토어 업데이트, Flow

이전 편에서 우리는 graphql mutation을 이용해서 보드(Board)를 만들어 보았다. 하지만, 보드 생성 후 데이터가 바로 반영이 안되는 문제가 있었다. 이번 편에서는 이 문제를 해결 할 수 있는 다양한 방법에 대해서 알아보고, 이 문제를 해결해보자. 그리고 나서 Flowtype에 대해 다루도록 하겠다.

참고: 이번 튜토리얼을 진행 하기 위해 진행 되는 코드를 깃헙에 업로드 해놨으니, 이 튜토리얼을 진행하고 싶다면 아래에서 소스를 클론 받은 후 아래의 내용을 따라가기를 추천 드립니다. 

튜토리얼 소스:

https://github.com/simsim0709/react-apollo-flow-trello-clone/tree/ch-4 

이전 편:

0편: React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - 소개

1편: React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - 프로토타입

2편: React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - graphcool (GraphQL 서버 만들기)

3편: React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - Trello Board 생성


Apollo를 사용하여 mutation을 실행하면, Apollo에서 내부적으로 부여하는 ID가 동일한 경우 UI(리액트 컴포넌트)가 자동으로 업데이트가 된다. 하지만, 지금 같이 보드 리스트의 경우는 위의 경우(자동으로 업데이트 되는)가 아니기 때문에 이 부분을 해결해 주어야 화면 새로고침 없이 변경된 데이터가 리스팅 된다. 이를 해결 할 수 있는 방법은 크게 3가지다:

  1. Optimistic UI + update
  2. refetchQueries
  3. subscription 

먼저, Optimistic UI와 update를 사용해서 스토어(store)의 데이터를 직접 변경하는 방법이 있다. 이 방법의 장점은 네트워크 지연시간 없이 유저에게 바로 변경된 데이터를 보여 줄 수 있기 때문에 UX 측면에서 장점이 있다. 하지만, 스토어를 직접 변경해야 하기 때문에 코드량이 증가하고, 스토어에 데이터가 꼬일 수 있다는 단점이 있다.

두번째, refetchQueries를 통해 mutation후 query를 다시 요청하는 방법이 있다. refetchQueries가 하는 역할은 mutation이 완료되면 refetchQueries 배열에 있는 쿼리를 다시 요청 하는 것이다. 이 방법의 장점은 코드가 가장 간단하다는 데 있다. mutation 후에 데이터를 새로 받아와야 하는 쿼리명만 적어주면 되기 때문이다. 그렇기 때문에, 이 방법의 단점은 불필요한 네트워크 요청이 있을 수 있다는 것이다. 

마지막으로, subscription을 통해 실시간으로 데이터를 받아오는 방법이 있다. 이 튜토리얼에선 subscription은 다루지 않기 때문에 패스하도록 하겠다.

자, 우리는 이 중에서 refetchQueries를 사용해서 추가된 데이터를 반영 하도록 하자. 

graphql 함수에 refetchQueries 옵션을 추가하고, 배열로 `AllBoardsQuery`를 추가한다:

const withCreateBoard = graphql(CREATE_BOARD_MUTATION, {
name: 'createBoard',
options: {
refetchQueries: ['AllBoardsQuery'],
},
});

이렇게 option을 추가하면 mutation이 완료되고 나서, AllBoardsQuery를 다시 쿼리하게 된다.

이제 다시 한번 보드를 추가해보자. 그럼 정상적으로 새로 생긴 데이터가 보드리스트에 추가되는 것을 확인 할 수 있다. 

여기까지 진행하면서 우리는 보드 생성과 리스팅을 graphql query와 mutation을 사용해서 진행하는 방법에 대해서 다뤘다. 이제 컴포넌트에 flowtype을 적용 해보도록 하자.

Flowtype

우리는 지금부터 Flowtype(이하 flow)을 리액트 컴포넌트와 graphql에 적용하는 방법에 대해서 다룰 것이다. flow에 대한 이론적인 부분은 다루지 않기로 했으니 이 부분은 넘어가더라도 우리가 이 튜토리얼을 진행하기 위해 필요한 문법과 설정(flowconfig) 부분에 대해서는 간단히 다루도록 하겠다. 

참고: 이 글 아래에 더 읽을 거리에 자바스크립트 타입 시스템에 대한 자료를 링크 해놨으니 이 부분에 대해 알고 싶다면 참고하자.

먼저, flow-bin 패키지를 추가한다:

yarn add --dev flow-bin 

그 다음 package.json에 scripts에 "flow": "flow"를 추가한다:

"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"flow": "flow"
}

터미널에서 flow init 명령어로 flow 초기화를 진행한다:

yarn flow init

init이 완료 되면, 프로젝트 루트 폴더에 .flowconfig 파일이 생성 되는데, 이 부분은 밑에서 다루도록 하겠다.

이제 터미널에서 yarn flow 명령어를 실행하면, 어떤 파일에 어느 부분에 타입에러가 있는지를 쭉 리스팅 해준다. 하지만, 이렇게 매번 파일을 변경하고 yarn flow를 실행하는 것은 비효율 적이다. flow server를 별도로 띄워서 진행 할 수도 있지만, 에디터에서 실시간으로 에러 표시를 해주면 더 효과적으로 타입 체크를 진행 할 수 있을 것 같다.

에디터에 flow를 추가하는 방법은 flow 공식문서에 자세히 나와 있느니 이곳에서 설정을 하고 나서, 튜토리얼을 계속 진행 하도록 하자.

참고: flow를 도입 한다고 해서 모든 파일에 타입체크를 해야 하는 것은 아니다. 부분적으로 원하는 파일에만 flow를 적용 할 수 있다. flow로 타입체크를 하고 싶은 파일 상단에 // @flow 코멘트를 넣어 주면 된다.

.flowconfig

방금 전, yarn flow init을 실행하면  프로젝트 루트에 .flowconfig라는 파일이 생성 된 걸 알 수 있는데 이 파일을 열어보면 아래와 같을 것이다:

[ignore]
[include]
[libs]
[lints]
[options]
[strict]

.flowconfig에 대한 상세한 설명은 공식 문서를 참고하도록 하고, 우리는 [ignore]와 [options]에 아래와 같이 추가를 할 것이다:

[ignore]
<PROJECT_ROOT>/build/.*
.*/node_modules/jss/lib/.*\.js\.flow
.*/node_modules/material-ui/.*\.js\.flow
[include]
[libs]
[lints]
[options]
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=src
[strict]

[ignore]: 이름에서 유추 할 수 있듯이, 아래의 아래의 목록은 flow 체크를 하지 않겠다는 것이다.  build 폴더에 있는 파일들은 당연히 flow 체크를 할 필요 없기 때문에 추가했고, 그리고 node_modules/jss와 node_modules/material-ui를 제외 시킨 것은 이 튜토리얼 스코프에 벗어나는 UI 관련한 것이기 때문에 추가했다.

[options]: 설정 할 수 있는 옵션은 다양 하지만, 우리는 여기에 module.system.node.resolve_dirname 옵션에 node_modules와 src를 추가 했다. node_modules는 당연히 우리가 사용하는 패키지(특히, 이 튜토리얼에서는 graphql)를 추가해준 것이고, src는 우리의 소스 폴더 이다. (여기에 이것을 추가한 이유는 .env 파일에 NODE_PATH=./src 를 설정 함으로써, 소스내에서 상대경로를 사용하지 않고, 절대 경로로 파일을 import 하고 있기 때문이다.), flow에서 이것을 문제없이 처리하기 위해 resolve_dirname에 src를 추가 한 것이다. 

flow 적용 (BoardItem.js, MainPage.js)

이번 편에서 우리는 components/BoardItem.js와 pages/Mainpage.js에 flow를 적용 해볼 것이다.

BoardItem.js

BoardItem 컴포넌트는 stateless functional component로서, props만 타입 체크를 해주면 된다.

BoardItem 컴포넌트의 props는 현재 총 세개(classes, name, description)인데, 각각의 타입은 이렇다. description에 "?"가 헷갈릴 수 있는데, 이것은 Maybe 타입이다(값이 optional 하다면 Maybe 타입을 사용한다).

type Props = {
classes: Object,
name: string,
description?: string,
};

Props type을 정의 했다고 해서, flow 타입 체크가 되는 것은 아니고 정의 된 타입을 컴포넌트에 적용 해야 한다. 적용 하는 방법은 이렇다:

const BoardItem = ({ classes, name, description }: Props) => {

컴포넌트 props를 Props type으로 체크 하도록 했다.

MainPage.js

자, 이제 좀 까다로운 부분이다.

MainPage 컴포넌트는 UI(stateless functional component)와 react-apollo의 graphql HOC가 섞여 있다. 그렇기 때문에 UI 컴포넌트의 props를 타입체크 해주어야 하고, graphql HOC도 타입을 추가해줘야 한다.

먼저 MainPage UI 컴포넌트는 현재 boardsData prop만 있다:

type Board = {|
id: string,
name: string,
description?: string,
|};
type Props = {
boardsData: ?(Board[]),
};

위의 코드를 글로 풀어내면 이렇다. "boardsData prop은 값이 없거나 (null or undefined), 있다면 배열이어야 한다. 그리고, 배열의 각각의 값은 정확히 Board 타입에 정의된 형태의 객체어야 한다."라고 풀어 낼 수 있겠다.

자, 기본적인 UI 컴포넌트의 flow 타입체크를 추가 했다. 이제, graphql에 flow 를 추가해보자.

import type { OperationComponent } from 'react-apollo'; // 1
type Response = { // 2
allBoards: Board[],
};
const withAllBoards: OperationComponent<Response, {}, Props> = graphql( //3
ALL_BOARDS_QUERY,
{
props: ({ ownProps, data }) => { // 4
return {
boardsData: data.allBoards,
};
},
}
);
export default withAllBoards(MainPage);

(소스: 1) 먼저 react-apollo 패키지에서 제공하는 OperationComonent를 import 한다(참고: flow type을 import 할 때는 import type { xxx } 이렇게 import 다음에 type을 명시해주어야 한다.).

(소스: 3) 소스에서 보면 알 수 있듯이, graphql HOC에 OperationComponent<Response, {}, Props> 이렇게 타입 체크 해주는 것을 볼 수 있다(여기서 우리가 알아햐 할 flow의 타입은 generic 타입이다. 자세한 설명은 링크를 참고 하도록 하자.).

OperationComponent generic type의 첫 번째 파라미터는 필수로 query 또는 mutation 후에 결과 데이터 형태가 포함 되어야 한다. 여기서는 Response type (소스: 2)을 넣어 주었다. 두 번째 파라미터는 InputProps로 graphql query, mutation을 실행할 때, variables의 타입 형태를 넣어주는 것인데, MainPage 컴포넌트는 별도로 variables가 없기 때문에 빈 객체를 넣어 주었다. 마지막 파라미터는 Props인데(소스: 4), query 결과 데이터의 allBoards를 컴포넌트의 boardsData prop으로 매핑 해주는 부분을 graphql props 함수를 이용해서 진행 해주었기 때문에 이 마지막 파라미터에 타입을 추가 해주었다(이 부분 역시 optional이기 때문에, 만약 props 함수를 선언하지 않았다면 생략한다).  

참고: react-apollo에 flow를 추가하는 자세한 방법은 링크를 참조하자.

자, 우리는 이번편에서 react-apollo의 graphql 함수 options 중에서 mutation후에 스토어를 업데이트 하는 방법에 대해서 다뤘고, 컴포넌트와 graphql query에 flow를 추가하는 방법에 대해서 다뤘다. 다음편에선 mutation에 타입을 추가해보고, 카드를 만들어보자.


더 읽을 거리:



리뷰