6편 - React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - 카드(Card) 상세 뷰

이전편에서 우리는 graphql의 가장 큰 장점 중 하나인 relational data를 graphcool에서 다루는 방법에 대해서 알아 보았다.
이번편에서 우리는 이렇게 만들어진 데이터를 카드 상세 뷰에 표시해주고, 카드 내용을 수정하는 로직을 개발해보도록 하겠다.
참고: 이번 튜토리얼을 진행 하기 위해 진행 되는 코드를 깃헙에 업로드 해놨으니, 이 튜토리얼을 진행하고 싶다면 아래에서 소스를 클론 받은 후 아래의 내용을 따라가기를 추천 드립니다.
튜토리얼 소스:
https://github.com/simsim0709/react-apollo-flow-trello-clone/tree/ch-6
이전 편:
0편: React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - 소개
1편: React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - 프로토타입
2편: React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - graphcool (GraphQL 서버 만들기)
3편: React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - Trello Board 생성
4편: React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - Apollo 스토어 업데이트, Flow
5편: React + GraphQL + Flowtype으로 트렐로 클론 웹앱 만들기 - 리스트(List), 카드(Card)

우리는 먼저 graphql Card type에 content field를 추가 할 것이다:
// graphcool-server/types.graphql type Card @model { id: ID! @isUnique createdAt: DateTime! updatedAt: DateTime! name: String content: String list: List! @relation(name: "CardOnList") }
type이 변경 되었기 때문에 graphcool에 deploy를 해준다:
// terminal graphcool deploy

Card.js

card의 id를 CardDialog 컴포넌트에 cardId prop으로 전달 해준다.
const { id, name, createdAt, classes } = this.props; // render <CardDialog cardId={id} open={this.state.open} onClose={() => this.setState({ open: false })} />
CardDialog.js
우리는 카드 상세 뷰에서 cardId prop을 통해 graphql에 query를 요청해서 데이터를 받아올것이다.
상세뷰에서 필요한 데이터는 id, name, content 이다:
const CARD_VIEW_QUERY = gql` query CardViewQuery($id: ID!) { Card(id: $id) { id name content } } `;
graphql(CARD_VIEW_QUERY, { skip: ({ cardId }) => !cardId, (1) options: ({ cardId }) => { return { variables: { id: cardId, }, }; }, props: ({ data: { Card } }) => { return { cardData: Card, (2) }; }, })
(1) cardId가 없을 경우 쿼리를 요청할 필요가 없기 때문에, skip option을 추가 해준다.
(2) 쿼리를 통해 받아온 데이터를 cardData prop으로 컴포넌트에 전달 해준다.
이 CardDialog 컴포넌트의 경우 graphql 관련한 로직은 심플하다. 하지만, UI 적인 부분을 이 컴포넌트에서 많이 다뤄야 하기 때문에 조금 복잡 할 수 있다. 그러니 차근차근 풀어 나가 보자.
먼저, CardDialog 컴포넌트에서 다뤄야 할 것은 아래와 같다:
  1. 카드 타이틀을 클릭하면 인풋으로 변경해서 유저가 타이틀을 변경 할 수 있어야 한다.
  2. 카드 타이틀을 수정하고, 엔터키를 누르면 변경된 타이틀로 변경 되야 한다.
  3. 컨텐츠 역시 위의 타이틀과 동일하게 컨텐츠를 클릭하면 컨텐츠를 수정 할 수 있도록 인풋으로 변경하고, SAVE 버튼을 클릭하면 컨텐츠가 저장된다.
  4. 타이틀과 다르게 컨텐츠는 마크다운 형식으로 저장 할 수 있고, 뷰를 할때는 마크다운 문법으로 작성된 컨텐츠를 HTML로 변경해서 보여 주어야 한다.
위의 요구 사항을 처리하기 위해서는 우리는 총 4개의 state가 필요하다:
state = { isEditingName: false, // 카드 타이틀 수정 상태 editedName: '', // 카드 타이틀의 수정된 값 isEditingContent: false, // 카드 컨텐츠 수정 상태 editedContent: '', // 카드 컨텐츠의 수정된 값 };
먼저, 타이틀을 수정하는 로직을 추가해보자:
handleNameClick = event => { event.preventDefault(); this.setState({ isEditingName: true, }); }; // render() const { isEditingName, editedName } = this.state; const { cardData = {}, classes } = this.props; const { name } = cardData; <DialogTitle onClick={this.handleNameClick}> {isEditingName ? ( // (1) <TextField fullWidth autoFocus value={editedName} onChange={this.handleNameChange} onKeyPress={this.handleNameKeyPress} onBlur={this.handleNameBlur} />) : (name)} </DialogTitle>
위의 로직은 isEditingName state가 true일때는 input(TextField)을 보여줘서 유저가 타이틀을 수정 할 수 있도록 하고, false일때는 CardData.name 값을 보여준다. 왜 여기서 this.state.editedName이 아니냐고 물을 수도 있는데, 그 이유는 editedName의 경우 submit을 하지 않으면 저장한게 아니기 때문에 기존의 name을 표시해주는 것이다.
이 CardDialog 컴포넌트에서 state의 역할은 UI 표현과 onChange에 의해 변경되는 input의 value 값을 담는 용도로만 사용 된다. 실제로 초기 데이터와 변경된 데이터 모두 prop으로 전달 받는 구조(graphql mutation을 실행하게 되면 자동적으로 rerendering이 된다)이다.
이러한 구조이기 때문에 우리는 editedName과 editedContent state를 prop과 동기화(sync) 시켜주는 로직을 추가해야 한다.
componentWillReceiveProps({ cardData }) { if ( cardData && cardData.name !== (this.props.cardData && this.props.cardData.name) ) { this.setState({ editedName: cardData.name, }); } if ( cardData && cardData.content !== (this.props.cardData && this.props.cardData.content) ) { this.setState({ editedContent: cardData.content, }); } }
위의 로직을 보면 nextProps(다음으로 들어오는 props)의 cardData의 name과 content 값을 현재의 props(this.props)와 비교해서 같지 않다면 this.setState 메소드를 통해 동기화 해주는 로직이다.
컨텐츠(content)는 타이틀(name)과 기본적인 프로세스와 로직은 동일하다. 한가지 다른점이 있다면 보기(view) 상태 일때는 일반 텍스트가 아닌 마크다운을 html로 렌더링 해주어야 한다는 점이 다르다.
그렇기 때문에 마크다운으로 작성된 글을 렌더링 해주는 패키지를 추가해서 처리하도록 하자.
react-markdown 패키지를 설치한다:
yarn add react-markdown
react-markdown 패키지를 import 해준다:
import Markdown from 'react-markdown';
renderMarkdown 메소드를 만들어서, this.state.editedContent가 false일때는 placeholder(Add description ...)를 렌더링 해주고, true일때는 마크다운 텍스트를 렌더링 한다:
renderMarkdown() { if (!this.state.editedContent) { return ( <Typography className={this.props.classes.descriptionCaptionText} type="body1"> Add description ... </Typography> ); } return <Markdown source={this.state.editedContent} />; }
그리고, render 메소드에서 컨텐츠가 에디팅 중일 때(this.state.isEditingContent가 true일 때)는 인풋 텍스트 필드를 보여주고, 아닐땐 renderMarkdown을 호출 한다:
{this.state.isEditingContent ? ( <Fragment> <TextField fullWidth autoFocus multiline rows="4" placeholder="Add description ..." className={classes.description} value={this.state.editedContent} onChange={this.handleContentChange} onBlur={this.handleContentBlur} /> <div className={classes.buttonWrapper}> <Button raised color="accent" onClick={this.handleSaveClick}> SAVE </Button> <IconButton> <CloseIcon onClick={this.handleOpen} /> </IconButton> </div> </Fragment> ) : ( this.renderMarkdown() )}
변경된 컨텐츠를 editedContent state에 저장하고 저장 버튼을 누르면 graphql mutation을 이용해서 서버의 데이터를 업데이트 해준다:
const CARD_NAME_MUTATION = gql` mutation CardNameMutation($id: ID!, $name: String!) { updateCard(id: $id, name: $name) { id name } } `; const CARD_CONTENT_MUTATION = gql` mutation CardContentMutation($id: ID!, $content: String!) { updateCard(id: $id, content: $content) { id content } } `;
graphql(CARD_NAME_MUTATION, { name: 'updateCardName', }), graphql(CARD_CONTENT_MUTATION, { name: 'updateCardContent', })
우리가 필요한 두개는 각각 다른 mutation을 적용한다. 하나는 card name을 업데이트 하는 mutation, 다른 하나는 card content를 업데이트 하는 mutation.
자, 우리는 여기서 mutation name을 각각 updateCardName과 updateCardContent로 이름지었다. 이렇게 한 이유는 첫째로 mutation prop네임을 mutate가 아닌 더 명확한 메소드 이름을 부여하는 것과 동시에 두개의 mutation이 CardDialog 컴포넌트에 매핑 되기 때문에 둘중의 하나의 mutate prop이 무시되기 때문에 이를 방지하기 위함이다.
자, 이렇게 카드 상세 부분을 마무리 했다. 당연히 트렐로에는 카드 상세에 다양한 기능이 있지만, 우리는 여기서 가장 기본적인 부분(타이틀, 컨텐츠 뷰/수정)만 구현했다.
다음 편에서는 auth0을 이용해 인증 로직을 추가해서 로그인 한 유저만 보드나 카드를 생성 할 수 있도록 로직을 추가해보도록 하겠다.