[React.JS] 강좌: React 컴포넌트 구성 & AJAX 비동기 작업 처리하기 & CSS 애니메이션 처리


이 포스트에서는 React 프로젝트에서 Ajax 와 같은 비동기 작업을 효율적으로 처리하는 방법을 다뤄보겠습니다. Ajax 처리 외에도, 컴포넌트 구성 및 응용, 애니메이션 처리도 다루니, React 입문자들은 한번 따라서 진행해보시면 많은 도움이 될거에요. 만약에 이해가지 않는 부분이 있거나 틀린 부분이 있다면 언제든지 덧글로 남겨주세요.

목차

이 포스트는 조금 깁니다.

만약에 강의를 진행하신다면 끈기를 가지고 진행하시길 바랍니다 😀

시작하기 전에
React 에서의 Ajax 요청
사전지식
준비물
미리보기

코드

1. 프로젝트 준비
2. 사용 할 API 알아보기
3. 컴포넌트 구성 – 기본 구조 잡기
4. 컴포넌트 구성 – 핵심 컴포넌트 만들기
5. Ajax 구현하기
6. Navigate 기능 구현하기
7. Warning 컴포넌트 만들기
8. 포스트 전환시 애니메이션 효과 넣어주기
9. 구형 브라우저 지원하기
10. 빌드하고 surge.sh 에 deploy 하기

시작하기 전에..

React 를 사용하여 프로젝트를 몇번 진행해본 결과, 제가 느낀 React의 장점이자 단점인 특징은, 뷰 레이어를 다룰때를 제외하고는, 뭔가를 할때 딱 정해진 가이드라인이 없다는 것입니다.

라우팅 부분을 예를 들자면, 이를 구현 할 땐 많은 개발자들이 react-router 라는 써드파티 라이브러리를 사용합니다. 하지만 이 외에도, react-enroute 라는 라이브러리도 있고, 라우팅 라이브러리를 따로 사용하지 않고도 자체적으로 구현하는 방법도 있습니다.

그리고, 상태 관리 부분만 해도 마찬가지입니다. React 라이브러리를 사용하는 개발자라면 한번쯤 사용해봤을 redux 라는 유명한 라이브러리도 있고 MobX 라는 라이브러리도 요새 정말 많이 사용되고 있습니다.

HTTP 요청을 할 때도, 딱 정해진 방법이 있는게 아니라서 jqueryaxios, fetch, superagent, 등등.. 중에서 개발자 자신이 골라서 사용해야합니다.

이런 이야기를 하면 정말 밑도 끝도 없죠. React 개발팀이 “ㅇㅇㅇ할땐 이렇게 하고, ㅇㅇㅇ할땐 저렇게 하세요” 라고 지정을 안해주다보니, 초보자들이 ‘내가 지금 하는 방식이 맞나? 이것보다 더 제대로 하는 방법이 있진 않을까?’ 라는 생각을 하면서 어려움을 느낄 때도 있습니다. 하지만, 장점으로는 프로젝트의 스택을 개발자 자신의 입맛에 맞게 자유롭게 설정 할 수 있습니다. 이 점은 React 가 지닌 중요한 매력 중 하나이기도 합니다.

React 에서의 Ajax 요청

React 프로젝트에서 Ajax 요청을 할 때, 특히 초보일땐, 어떤방식으로 해야할지.. 조금은 막막합니다. React 매뉴얼에선 “네트워크 요청을 할 땐 componentDidMount 에서 하세요.” 라고 알려주는게 전부니까요. React 로 개발을 하면서 저는 참 여러가지 방식, 구조, 문법으로 ajax 시도를 해본 것 같습니다. 그 중, 괜찮은 방법인것 같다… 싶은 방법들을 한번 공유해보도록 하겠습니다. 이 포스트에서 사용하는 방식이 언제나 정답은 아닙니다. 더 편하게 하는 방법이 있을수도 있습니다.

이 포스트에선 상태 관리 라이브러리인 redux사용하지 않는 환경에서의 ajax 요청을 하는 방법을 알아보도록 하겠습니다.

Redux 를 사용하는 예제는 다음 포스트에서 계속됩니다. 우선 이 포스트를 따라 튜토리얼을 진행해주세요.

사전지식

이 강의는 기존에 올린 리액트 강좌 에서 배운 지식들을 기반으로, 응용하는 과정을 다룬 강의입니다.

따라서, HTML/CSS/JAVASCRIPT 의 이해와, React 의 기초적인 지식이 있어야 어려움 없이 진행 할 수 있습니다.

하지만, 이 강의에서는 프로젝트 생성 과정부터 상세하게 진행하기 때문에, 제가 쓴 이전 강의를 읽지 않아도, 따라하실수는 있습니다. 진행을 하시면서 도중에 궁금한게 있으면 덧글로 달아주세요.

준비물

이 튜토리얼을 진행하기 위하여 필요한 준비물은 다음과 같습니다.

  • Node.js LTS (현재 기준 v6.9.1)
  • NPM v3 이상
  • 본인이 가장 좋아하는 코드 에디터 (VS Code, Atom 등)
  • 크롬 브라우저

미리보기

URL:  https://react-async-example.surge.sh

코드

이 프로젝트에 사용된 코드는 GitHub 에서 열람 하실 수 있습니다.

각 섹션별로 tag 가 생성되어있으니, 만약에 여러분 PC에서 직접 코드를 보고 싶으시다면,

다음과 같이해보세요.

git clone https://github.com/velopert/react-ajax-tutorial.git
git checkout 06 # 섹션 6 완성 후 상태 코드 열람
git checkout 07 # 섹션 7 완성 후 상태 코드 열람

또한 커밋 페이지에서 각 커밋별로 비교 할 수도 있습니다.

 

1 .프로젝트 준비

우리는, 이 튜토리얼을 진행하기 위하여, create-react-app 을 사용하여 React 프로젝트를 생성하도록 하겠습니다.

다른 boilerplate 를 사용하거나 프로젝트를 직접 구성하셔도 됩니다. 그런 경우에는, 다음 babel 플러그인들을 별도로 설치하고 적용해주세요

  • babel-plugin-transform-class-properties
  • babel-plugin-transform-runtime

1-1. create-react-app 설치

만약에 여러분의 컴퓨터에 create-react-app 이 설치되어있지 않다면, npm 을 통하여 설치해주세요.
create-react-app 은 React 프로젝트를 손쉽게 자동으로 만들고 설정해주는 페이스북 개발팀이 만든 도구입니다. (create-react-app 사용기)

터미널을 열어서 다음 명령어를 입력하세요

$ npm install -g create-react-app

UNIX 기반 운영체제를 사용한다면 필요에 따라 명령어 앞에 sudo 를 붙여주세요.

설치가 완료되면 다음 명령어를 입력하여 리액트 프로젝트를 생성하세요 (1~3분 정도 소요됩니다)

$ create-react-app react-ajax-tutorial

리액트 프로젝트 생성이 완료되면 npm start 를 입력하여 프로젝트를 시작하세요.

이 명령어를 입력하면 개발용 서버가 실행됩니다.

서버를 실행 후, 브라우저로 http://localhost:3000/ 에 접속해보세요.

%ec%9d%b4%eb%af%b8%ec%a7%80-2

이미지 1. 브라우저로 개발서버 접속

1-2. HTTP Client – axios 설치

ajax 작업을 할 때, 많은 개발자들이 jQuery 를 사용합니다. 만약에 여러분의 프로젝트에서 이미 jQuery 를 사용한다면, 따로 HTTP Client 를 받을 필요 없이 jQuery 를 사용해도 됩니다. 하지만, HTTP Client 만을 위해서 jQuery 기능을 사용하는건 좀 낭비겠죠. 필요없는 기능들이 많으니까요 (물론 그 부분만 따로 추출해서 사용 할 수도 있긴 합니다)

자바스크립트 라이브러리중, 오직 AJAX 기능만을 위하여 만들어진 라이브러리들이 있는데요, 그 중 유명한것들이 axios, fetch, superagent, request 등이 있습니다. (fetch 는 모던 웹브라우저에 내장되어있는 기능이고, GitHub 에서 만든 polyfill 을 적용해야 하위 브라우저에서도 작동합니다)

그 중에서, 우리는 axios 를 사용하겠습니다.

제가 axios 를 사용하는 주요 이유는 다음과 같습니다:

  • Promise 기반
  • 클라이언트 / 서버에서 동일하게 작동함
  • 다양한 브라우저 지원
  • 편리함

무조건 axios 를 사용 할 필요는 없습니다

기타 라이브러리를 사용해보고, 본인의 사용 용도에 맞다! 라고 생각이 드신다면 그 라이브러리를 사용하셔도 됩니다. 저는 여러 라이브러리를 사용해본 결과, 저에게는 axios 가 가장 편하다는 결론을 내렸습니다.

그럼, npm 을 사용하여 axios 를 설치하세요.

$ npm install --save axios

1-3. Semantic-UI 설치

예제 프로젝트를 만드는것이지만, 그래도 좀 그럴싸하게 만들어볼까요? Semantic-UI CSS 프레임워크의 도움을 얻어 예제 프로젝트를 예쁘게 만들어봅시다.

Semantic-UI 를 그대로 가져다가 써도 되지만, 기존 CSS 프레임워크는 드롭다운, 사이드바 등에서 jQuery 를 사용합니다.

그런게 맘에 들지 않는다면, semantic-ui-react 모듈을 설치하셔서 사용하면 됩니다. Semantic-UI 의 엘리먼트들이 모두 React 컴포넌트들로 구성되어있고, jQuery 를 사용하지 않습니다. (오예!)

그렇게 무겁지도 않고, 한번 사용해볼만한 가치가 있기 때문에, 이 튜토리얼에서 이 멋진 라이브러리를 사용해보도록 하겠습니다.

이 또한, 터미널에서 npm 으로 설치하시면 됩니다

$ npm install --save semantic-ui-react semantic-ui-css

2. 사용 할 API 알아보기

우리는 이 튜토리얼에서 프론트엔드 단에 집중하기 위해서, 이미 만들어진 가짜 API 들을 사용하도록 하겠습니다.

%ec%9d%b4%eb%af%b8%ec%a7%80-2

이미지 2. JSONPlaceholder

https://jsonplaceholder.typicode.com/

여기서 제공하는 가짜 API 중, 저희는 post 와 comment API 를 사용하겠습니다.

API 1. 포스트 읽기

GET https://jsonplaceholder.typicode.com/posts/:id

GET https://jsonplaceholder.typicode.com/posts/1

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

설명: id 를 가진 게시물의 내용을 불러옵니다. :id 부분에는 1~100 사이의 숫자가 들어갑니다.

 

API 2. 포스트의 덧글 읽기

GET https://jsonplaceholder.typicode.com/posts/:id/comments

GET https://jsonplaceholder.typicode.com/posts/1/comments

[
  {
    "postId": 1,
    "id": 1,
    "name": "id labore ex et quam laborum",
    "email": "Eliseo@gardner.biz",
    "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
  },
  {
    "postId": 1,
    "id": 2,
    "name": "quo vero reiciendis velit similique earum",
    "email": "Jayne_Kuhic@sydney.com",
    "body": "est natus enim nihil est dolore omnis voluptatem numquam\net omnis occaecati quod ullam at\nvoluptatem error expedita pariatur\nnihil sint nostrum voluptatem reiciendis et"
  },
  {
    "postId": 1,
    "id": 3,
    "name": "odio adipisci rerum aut animi",
    "email": "Nikita@garfield.biz",
    "body": "quia molestiae reprehenderit quasi aspernatur\naut expedita occaecati aliquam eveniet laudantium\nomnis quibusdam delectus saepe quia accusamus maiores nam est\ncum et ducimus et vero voluptates excepturi deleniti ratione"
  },
  {
    "postId": 1,
    "id": 4,
    "name": "alias odio sit",
    "email": "Lew@alysha.tv",
    "body": "non et atque\noccaecati deserunt quas accusantium unde odit nobis qui voluptatem\nquia voluptas consequuntur itaque dolor\net qui rerum deleniti ut occaecati"
  },
  {
    "postId": 1,
    "id": 5,
    "name": "vero eaque aliquid doloribus et culpa",
    "email": "Hayden@althea.biz",
    "body": "harum non quasi et ratione\ntempore iure ex voluptates in ratione\nharum architecto fugit inventore cupiditate\nvoluptates magni quo et"
  }
]

설명: id 를 가진 게시물의 덧글들을 불러옵니다. :id 부분에는 1~100 사이의 숫자가 들어갑니다.

3. 컴포넌트 구성 – 기본 구조 잡기

3.1 파일 구조

%ec%9d%b4%eb%af%b8%ec%a7%80-4

이미지3. 디렉토리 구조

우리 프로젝트의 구조는 위와 같습니다. 위 이미지는 참고용으로 봐주세요. 앞으로 파일들을 하나하나 만들 것 입니다.

우리는 컴포넌트 파일들을 똑똑한 컴포넌트 container 와, 멍청한 컴포넌트 components 디렉토리로 나눠서 구분 할 것입니다.

Q. 멍청하고 똑똑하다니 그게 뭔뜻이죠?

물론 실제로 멍청하거나 똑똑한건 아니고.. 추상적인 의미입니다. 이는 리액트 프로젝트에서 사용하면 유용한 패턴인데요, 조금 더 제대로 된 표현으로 쓰자면, 이 패턴에선 컴포넌트들이 presentational (멍청한) 컴포넌트와 container (똑똑한) 컴포넌트로 분류됩니다.

멍청한 컴포넌트들은 오직, props 로 전달받은 값을 렌더링하는것을 목표로 합니다. 이 컴포넌트들은 자신들만의 CSS 스타일을 가지고 있을 수 있구요, state 를 갖고있지 않습니다. 뭔가 처리를 해야 할 때는, 똑똑한 컴포넌트에서 선언된 함수를 props 로 전달받아서 실행합니다.

반대로 똑똑한 컴포넌트는 멍청한 컴포넌트들을 관리하는 녀석입니다. state 를 지닐 수 있고, 작업을 프로세싱 할 수 있죠. 그리고 기본적인 틀을 갖추기 위한 CSS 스타일만을 가지고있고, 복잡한 스타일을 갖고있지 않습니다.

이렇게 컴포넌트를 분류하면, 데이터의 흐름이 간편해진답니다. 추가적으로 컴포넌트의 재사용률도 높여주죠.

자세한 내용: Smart and dumbcomponents

저희 예제 프로젝트의 레이아웃은 다음과 같이 설계하겠습니다

%ec%9d%b4%eb%af%b8%ec%a7%80-1

이미지 4. 프로젝트 레이아웃

위 레이아웃을 컴포넌트별로 구분을 해볼까요?

%ec%9d%b4%eb%af%b8%ec%a7%80-7
이미지 4. 컴포넌트 구조

이미지 3과 이미지 4를 비교해 보면서, 컴포넌트 구조를 이해해보세요.

자, 이제, 컴포넌트들을 하나하나 만들어봅시다.

3.2 Semantic-UI CSS 불러오기

우선, src/index.js 파일에서 Semantic-UI의 CSS 파일을 불러와야합니다.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import 'semantic-ui-css/semantic.min.css';
import './index.css';

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

아까 npm 을 통하여 설치한 semantic-ui-css 를 불러오세요.

 

3.3 배경색 설정 및 App 비우기

저희 어플리케이션의 배경색을 회색으로 설정하겠습니다. 기존에 만들어져있던 src/index.css 파일을 다음과 같이 수정하세요.

body {
    background: #E0E0E0;
}

그리고, src/App.js 의 코드를 다음과 같이 필요없는걸 다 제거해주세요.

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <div>
            </div>
        );
    }
}

export default App;

다음과 같이 회색 화면이 보여졌나요?

gray

이미지 5. 회색 화면

 

3.4 Header 컴포넌트 만들기

컴포넌트 선언

자, 이제 저희의 첫 컴포넌트, Header 를 만들겠습니다.

헤더 컴포넌트는, 그냥 간단하게, 텍스트가 적혀있는 청록색의 바 입니다.

src/components/Header 디렉토리를 생성하세요, 그리고 그 안에 Header.js 파일을 만들어서 다음과 같이 코드를 입력하세요.

import React from 'react';
import './Header.css';

const Header = () => (
    <div className="Header">
        POSTS
    </div>
)

export default Header;

Q. 엥? 컴포넌트가 함수형태로 선언이 되어있네요?

독자분들중 일부분은 이런 코드를 처음 보시는 분들도 계실 수 있습니다. (특히.. 제 블로그만 보고 리액트를 공부하신분이라면요.. 아직 블로그에서 이 내용을 다룬적이 없습니다. 조만간 내용을 업데이트 할 때 다루게 될 것 같군요) 이런 형식으로 선언된 컴포넌트는 함수형 컴포넌트 (Functional Component) 라고 부릅니다. 만약에 state 가 없고, life cycle 메소드가 필요없는 멍청한 컴포넌트라면, 함수형 컴포넌트로 선언을 하는것이 좋은 패턴입니다. 보기에도 깔끔하고, 컴포넌트의 로직을 컴포넌트 바깥으로 옮기므로, 나중에 테스팅하기에도 편하죠.

함수형 컴포넌트는 this 에 접근하는것이 불가능하며, lifeCycle api 들을 사용하는것이 불가능합니다.

함수형 컴포넌트는 오직 전달받는 props 에만 의존합니다. props 는 어떻게 전달받는지 볼까요?

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

ES6 의 destructure 기능을 사용하면 다음과 같이 선언 할 수 있습니다.

function Welcome({ name }) {
  return <h1>Hello, {name}</h1>;
}

꽤 편하죠? 앞으로 멍청한 컴포넌트를 선언할때는 이 문법이 계속해서 사용 될 것입니다.

컴포넌트 스타일링

Header.css 파일을 만들어서 다음 스타일을 넣어주세요.

.Header {
    background-color: #00B5AD;
    color: white;
    font-size: 2rem;
    padding: 1.2rem;
    text-align: center;
    font-weight: 600;
    margin-bottom: 2rem;
}

보시다시피, 저희는 이렇게 컴포넌트 디렉토리 안에, 각 컴포넌트마다 디렉토리를 만들어주고, 그 안에 JS 파일과 CSS 파일을 따로따로 저장 할 계획입니다.

컴포넌트와 스타일을 관리하는 방법은 여러가지가 있습니다. 저의 경우엔 scss 를 사용하는것을 선호하고, 스타일을 위한 디렉토리를 따로 만들어서, 컴포넌트는 컴포넌트 디렉토리에만 넣고, 스타일은 스타일 디렉토리에만 넣는것을 좋아합니다. 하지만 이 글을 읽고계신분들 중 scss 를 모르는 분들을 고려하여 이 포스트에선 그냥 css 를 사용하도록 하겠습니다.

컴포넌트 인덱스 설정

우리가 components 디렉토리를 따로 만든 이유는, 추후 컴포넌트를 불러올 때, 특히 여러개의 컴포넌트를 불러올때 좀 더 편리하게 불러오기 위함입니다 (이 방법 또한 개발자마다 다릅니다)

우선 src/components/index.js 파일을 만들어서 다음과 같이 코드를 작성해주세요.

import Header from './Header/Header';

export {
    Header
};

이 파일에서 Header 컴포넌트를 불러오고, 바로 다시 내보냈습니다.

우리는 앞으로 새 컴포넌트를 만들때마다, 여기에 한줄한줄 추가를 할것입니다.

컴포넌트 불러오기

이렇게 인덱스 파일을 만들고 나면 src/App.js 에서 다음과 같이 이 Header 컴포넌트를 불러올 수 있게 됩니다.

import React, { Component } from 'react';
import { Header } from './components';

class App extends Component {
    render() {
        return (
            <div>
                <Header/>
            </div>
        );
    }
}

export default App;

자, 여기까지 작성하고 나면 다음과 같은 화면이 완성됩니다.

header

이미지 6. Header 컴포넌트 완성

 

3.5 PostWrapper 컴포넌트 만들기

PostWrapper 컴포넌트는 나중에 만들 Navigate 와 Post 를 감싸주는 컴포넌트입니다. 이 컴포넌트는 뷰를 페이지의 가운데에 정렬하는 역할을 담당합니다. 사실상 PostContainer 에서 이 작업을 해도 되긴 하지만, 똑똑한 컴포넌트는 CSS 스타일 갖지 않는다는 규칙을 준수하기 위하여 이 컴포넌트를 만들었습니다.

앞으로 새 디렉토리를 만들라는 안내는 생략하겠습니다. 코드 스니펫의 상단부분의 디렉토리를 참고하시고, 만약에 그 디렉토리가 존재하지 않는다면 생성해주세요.

컴포넌트 선언

PostWrapper.js 파일을 생성하여 다음 코드들을 입력하세요.

import React from 'react';
import './PostWrapper.css'

const PostWrapper = ({children}) => {
    return (
        <div className="PostWrapper">
            {children}
        </div>
    );
};

export default PostWrapper;

함수의 파라미터로는 children 을 받습니다. 이 값은, 클래스형태로 할 때의 this.props.children 과 동일합니다. 컴포넌트를 사용 할 때 <Component>여기에 있는 내용이 children 입니다</Component>

컴포넌트 스타일링

PostWrapper.css 파일을 작성하세요

.PostWrapper {
    width: 50rem;
    margin: 0 auto;
}

@media (max-width: 768px) {
    .PostWrapper {
        width: 90%;    
    }
}

위 스타일은 저희가 보여줄 뷰의 가로 사이즈를 50rem 으로 설정하고, 화면이 768px 미만일시에는 가로 사이즈를 비율에 맞춰 90% 로 설정하게 합니다..

컴포넌트 인덱스 설정

Header 컴포넌트를 만들 때 했던 것 처럼, index.js 파일을 설정하세요.

import Header from './Header/Header';
import PostWrapper from './PostWrapper/PostWrapper';

export {
    Header,
    PostWrapper
};

 

3.6 똑똑한 컴포넌트, PostContainer 만들기

위에서 만든 PostWrapper 컴포넌트를 렌더링 할, PostContainer 컴포넌트를 만들겠습니다.

이 컴포넌트는 똑똑한 컴포넌트이므로, components 디렉토리가 아닌 containers 디렉토리에 저장을 할것입니다.

컴포넌트 선언

PostContainers.js 파일을 만들어서 다음과 같이 코드를 작성하세요.

import React, {Component} from 'react';
import { PostWrapper } from '../../components';

class PostContainer extends Component {
    render() {
        return (
            <PostWrapper>
                Hello, Post
            </PostWrapper>
        );
    }
}

export default PostContainer;

이번에는 함수형 컴포넌트가 아닌 클래스 형식으로 컴포넌트를 선언했습니다. 그 이유는 나중에 여기에 state 를 추가할것이기 때문이죠!

렌더링 부분에선 PostWrapper 컴포넌트를 불러와서 렌더링 해줍니다. PostWrapper 의 children 을 시험 삼아 Hello, Post 라는 텍스트를 넣어줍시다.

 

이 컴포넌트는 똑똑한 컴포넌트이기에, 규칙을 따라 스타일이 따로 존재하지 않습니다.

컴포넌트 인덱스 설정

이제, src/containers 디렉토리에도 index.js 파일을 만드세요. 용도는 컴포넌트의 인덱스파일과 동일합니다.

물론, 이 프로젝트에서는 똑똑한 컴포넌트가 한개밖에 없기 때문에, 인덱스를 파일을 굳이 만들 필요는 없지만, 구조를 컴포넌트 디렉토리와 동일하는게 좋지 않을까요?

만약에 맘에 들지 않는다면 이 과정을 생략하셔도 됩니다.

import PostContainer from './PostContainer/PostContainer.js';

export {
    PostContainer
};

컴포넌트 불러오기

src/App.js 에서 방금 만든 컴포넌트를 불러와서 Header 하단에 렌더링하세요.

import React, { Component } from 'react';
import {PostContainer} from './containers';
import { Header } from './components';

class App extends Component {
  render() {
    return (
        <div>
            <Header/>
            <PostContainer/>
        </div>    
    );
  }
}

export default App;

%ec%9d%b4%eb%af%b8%ec%a7%80-5

이미지 7. PostContainer 렌더링

저희 프로젝트의 컴포넌트 기본구조를 잡았습니다. 여기까지 문제없이 따라오셨나요? 앞으로 컴포넌트를 만들땐 지금까지 했던 패턴 그대로, 계속해서 진행하면 됩니다.

  1. 컴포넌트 선언
  2. 컴포넌트 스타일링
  3. 인덱스 설정

4. 컴포넌트 구성 – 핵심 컴포넌트 만들기

섹션 4에선, 멍청한 컴포넌트 / 똑똑한 컴포넌트를 만드는 과정을 익혔습니다. 이제 계속해서 저희 예제 프로젝트에서 필요한 컴포넌트를 만들어봅시다.

4.1 Navigate 컴포넌트 만들기

포스트를 앞 뒤로 넘기는 Navigate 컴포넌트를 만들어봅시다.

컴포넌트 선언

Navigate.js 파일을 만들어서 다음 코드를 작성하세요

import React from 'react';
import {Button} from 'semantic-ui-react';
import './Navigate.css'

const Navigate = () => (
    <div className="Navigate">
        <Button
            color="teal"
            content="Previous"
            icon="left arrow"
            labelPosition="left"
        />
        <div className="Navigate-page-num">
            1
        </div>
        <Button
            color="teal"
            content="Next"
            icon="right arrow"
            labelPosition="right"
            className="Navigate-right-button"
        /> 
    </div>
);

export default Navigate;

여기서 버튼은 semantic-ui-react 의 Button 컴포넌트가 사용됐습니다. 이 컴포넌트에 대한 사용법은 여기를 참고해주세요.

버튼의 설정으론 색상, 내용, 아이콘, 아이콘 위치를 설정하였습니다.

왼쪽엔 Previous 버튼, 우측엔 Next 버튼, 그리고 그 가운데엔 현재 포스트의 번호를 위치하였습니다. (나중에 이 부분은 props 를 받아와서 값을 렌더링 할 것입니다. 지금은 임시로 숫자를 직접 넣어주세요.

컴포넌트 스타일링

Navigate.css 파일을 만들어서 다음 코드를 작성하세요

.Navigate {
    position: relative;
}

.ui.button.Navigate-right-button {
    float: right;
    margin: 0px;
}

.Navigate-page-num {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
}

컴포넌트 인덱스 설정

import Header from './Header/Header';
import PostWrapper from './PostWrapper/PostWrapper';
import Navigate from './Navigate/Navigate';

export {
    Header,
    PostWrapper,
    Navigate
};

Navigate 컴포넌트를 불러온 후 다시 내보냈습니다.

컴포넌트 사용하기

이제 이 컴포넌트를 PostContainer 에서 불러와서 렌더링하세요.

import React, {Component} from 'react';
import { PostWrapper, Navigate } from '../../components';


class PostContainer extends Component {
    render() {
        return (
            <PostWrapper>
                <Navigate/>
            </PostWrapper>
        );
    }
}

export default PostContainer;

이 컴포넌트를 렌더링 하고 나면 다음과 같은 화면이 나타납니다.

%ec%9d%b4%eb%af%b8%ec%a7%80-8

이미지 8. Navigate 렌더링

4.2 Post 컴포넌트 만들기

포스트의 내용과, 덧글들을 담을 Post 컴포넌트를 만들어봅시다.

컴포넌트 선언

import React from 'react';
import './Post.css';

const Post = () => (
    <div className="Post">
        <h1>Title</h1>
        <p>
            Body
        </p>
    </div>
);

export default Post;

컴포넌트의 제목과 내용 부분을, 지금은 그냥 텍스트를 보여주도록 설정했습니다. 나중에 저 부분에 props 가 들어가도록 할 것입니다.

컴포넌트 스타일링

.Post {
    padding: 1rem;
    margin-top: 1rem;
    margin-bottom: 2rem;
    background-color: #FAFAFA;
    box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}

.Post h1 {
    font-size: 3rem;
}

.Post p {
    font-size: 2rem;   
}

컴포넌트의 padding 을 설정해주고, 그림자를 그려주었습니다.

그리고, p 태그와 h1 태그의 글씨크기를 지정하였습니다.

컴포넌트 인덱스 설정

import Header from './Header/Header';
import PostWrapper from './PostWrapper/PostWrapper';
import Navigate from './Navigate/Navigate';
import Post from './Post/Post';

export {
    Header,
    PostWrapper,
    Navigate,
    Post
};

컴포넌트 사용하기

Post 컴포넌트를 PostContainer 에서 Navigate 하단에 렌더링해주세요.

import React, {Component} from 'react';
import { PostWrapper, Navigate, Post } from '../../components';


class PostContainer extends Component {
    render() {
        return (
            <PostWrapper>
                <Navigate/>
                <Post/>
            </PostWrapper>
        );
    }
}

export default PostContainer;

저장을 하고, 브라우저를 확인해보세요

%ec%9d%b4%eb%af%b8%ec%a7%80-12

이미지 9. Post 렌더링

짠! 뭔가 갖춰져가고 있는 것 같군요.

4.3 CommentList 컴포넌트 만들기

자, 이제 CommentList 컴포넌트를 만들겠습니다. 이 컴포넌트는, 나중에 덧글정보들을 담은 배열을 props 로 전달 받아서 곧 이어 만들 여러개의 Comment 컴포넌트들로 매핑해주는 역할을 합니다.

컴포넌트 선언

지금은 props 를 받는 부분과, 매핑하는 과정을 생략하고 지금은 일단 비어있는 컴포넌트를 스타일링만 하겠습니다.

import React from 'react';
import './CommentList.css';

const CommentList = () => {
    return (
        <ul className="CommentList">

        </ul>
    );
};


export default CommentList;

이 컴포넌트에서는 리스트를 보여줄 것이기 때문에, ul 태그를 사용하도록 하겠습니다.

컴포넌트 스타일링

근데, 이 태그는 기본적으론 왼쪽에 bullet 포인트를 보여주니 스타일에서 bullet 포인트를 안보여지게 설정을 하겠습니다. 추가적으로 배경설정과, 둥근 테두리도 설정하겠습니다.

.CommentList {
    list-style-type: none;
    background-color: #F0F0F0;
    padding: 1rem;
    border-radius: 5px;
}

컴포넌트 인덱스 설정

import Header from './Header/Header';
import PostWrapper from './PostWrapper/PostWrapper';
import Navigate from './Navigate/Navigate';
import Post from './Post/Post';
import CommentList from './CommentList/CommentList';

export {
    Header,
    PostWrapper,
    Navigate,
    Post,
    CommentList
};

컴포넌트 사용

CommentList 컴포넌트는 Post 컴포넌트에서 불러와서 렌더링됩니다.

import React from 'react';
import './Post.css';
import { CommentList } from '../';

const Post = () => (
    <div className="Post">
        <h1>Title</h1>
        <p>
            Body
        </p>
        <CommentList/>
    </div>
);

export default Post;

파일을 저장하고나면, 비어있는 회색박스가 보여질것입니다.

%ec%9d%b4%eb%af%b8%ec%a7%80-13

이미지 10. CommentList 렌더링

4.4 Comment 컴포넌트 만들기

이 컴포넌트는 CommentList 내부에 들어가는 컴포넌트이며, li 태그로 이뤄져있습니다.

컴포넌트 선언

import React from 'react';
import './Comment.css';

const Comment = () => {
    return (
        <li className="Comment">
            <p>
                <b>name</b> body
            </p>
        </li>
    );
};

export default Comment;

위의 name 과 body 부분엔, 지금은 텍스트를 보여주고, 나중에 props 가 렌더링되도록 변경하겠습니다.

컴포넌트 스타일링

.Comment p {
    font-size: 1.2rem;
    color: #868e96;
}

.Comment p b {
    color: #212529;
}

컴포넌트 인덱스 설정

import Header from './Header/Header';
import PostWrapper from './PostWrapper/PostWrapper';
import Navigate from './Navigate/Navigate';
import Post from './Post/Post';
import Comment from './Comment/Comment';
import CommentList from './CommentList/CommentList';

export {
    Header,
    PostWrapper,
    Navigate,
    Post,
    CommentList,
    Comment
};

컴포넌트 사용

이 컴포넌트는 CommentList 컴포넌트에서 불러와서 사용하세요. 3개를 렌더링하고 어떻게 보여지나 한번 봅시다.

import React, {Component} from 'react';
import {Comment} from '../';

import './CommentList.css';


class CommentList extends Component {
    render() {
        return (
            <ul className="CommentList">
                <Comment/>
                <Comment/>
                <Comment/>
            </ul>
        );
    }
}

export default CommentList;

42

이미지 11. Comment 3개 렌더링

호오… 보여지는 부분은 완성을 했습니다. 컴포넌트를 구성 할 땐, 이렇게 부분별로 여러개로 나누시면됩니다.

5. Ajax 구현하기

자, 이제 진짜 시작입니다.

f

이미지 12. 분위기 전환을 위한 이미지 (이것도 굳이 레이블링을)

우리는 ajax 를 구현하기 전에 해야 할 컴포넌트 구성을 모두 마쳤습니다. 우리의 뷰가 어떻게 보여질지, 다 준비를 했죠. 이제 서버에서 데이터를 가져와서 필요한곳에 뿌려줄 차례입니다.

5.1 axios 사용법 익히기

본격적으로 시작하기 전에, axios 의 간단한 사용법을 알아보겠습니다. 다음 내용을 한번 훑어보세요.

이 포스트에서는 간단한 사용방법만 알아보겠으니, 자세한 내용은 메뉴얼을 참조하세요.

GET 요청

axios.get('/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

PROMISE ALERT!

위에서 나온 .then(...) 이런 코드를 처음 보시나요? 이 문법은 ES6 문법 중 비동기 작업을 좀 더 효율적이고 깔끔한 코드로 작성 할 수 있게 해주는 기능입니다. 자세한 내용은 여기를 참조해주세요.

POST 요청

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

위에서 다룬 예제들과 같이, axios. 뒤에 get 과 post 외에도, delete, head, post, put, patch 메소드를 뒤에 붙여서 사용 할 수 있습니다. 예: axios.delete(...)

요청에 옵션을 설정 할 때

// Optionally the request above could also be done as
axios.get('/user', {
    params: {
      ID: 12345
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

메소드타입을 옵션으로 지정

// Send a POST request
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});

Response 스키마 형식

{
  // `data` 는 서버에서 반환한 데이터입니다. 
  data: {},

  // `status` 는 서버에서 반환한 HTTP 상태입니다
  status: 200,

  // `statusText` 는 HTTP 상태 메시지입니다
  statusText: 'OK',

  // `headers` 는 서버에서 반환한 헤더값입니다
  headers: {},

  // `config` 는 axios 요청시 전달했던 설정값입니다
  config: {}
}

요청이 끝나고 받는 response 는 위와 같은 형식으로 되어있습니다.

 

5.2 API 함수 모듈화하기

대충 어떤형식으로 요청하는지 배웠죠? 그렇다면 이제 실행해 옮겨봅시다. axios 를 컴포넌트에서 불러와서 바로 사용을 해도 되는데요, 저희는 좀 더 코드를 정리해가면서 작성하기 위해서, ajax 요청하는 함수들을 따로 만들어서 모듈화하여 src/services 디렉토리에 저장하겠습니다.

모듈화 하는 과정은 선택적입니다. 코드를 좀 더 체계적으로 작성하고자 거치는 과정이며, 본인의 필요에 따라 나중에 컴포넌트 내부에서 바로 요청을 해도 됩니다.

우리가 사용 할 api 들은 포스트 관련 api 니까, post.js 라는 파일을 생성하고 다음 코드를 입력하세요.

import axios from 'axios';

export function getPost(postId) {
    return axios.get('https://jsonplaceholder.typicode.com/posts/' + postId);
}

export function getComments(postId) {
    return axios.get(`https://jsonplaceholder.typicode.com/posts/${postId}/comments`)
}

 

Q. ${...} 은 무슨 표현이죠?

위 표현은 ES6 의 Template Literal 이라는 문법입니다. 문자열 내부에 변수를 넣을 때 사용합니다.

주의 하실 점은 문자열을 감싸는 따옴표가 숫자 1키 왼쪽에있는 키 입니다

 

5.3 컴포넌트에서 API 사용하기

똑똑한 컴포넌트인 PostContainer 을 열어서 포스트 내용과 해당 포스트의 덧글들을 불러오는 fetchPostInfo 메소드를 만들어봅시다.

import React, {Component} from 'react';
import { PostWrapper, Navigate, Post } from '../../components';
import * as service from '../../services/post';


class PostContainer extends Component {

    fetchPostInfo = async (postId) => {
        const post = await service.getPost(postId);
        console.log(post);
        const comments = await service.getComments(postId);
        console.log(comments);
    }

    render() {
        return (
            <PostWrapper>
                <Navigate/>
                <Post/>
            </PostWrapper>
        );
    }
}

export default PostContainer;

babel 을 사용하여 프로젝트를 몇번 진행해보신분들이라면, 아, 이렇게하는구나. 라고 생각하시겠지만,

그렇지 않은분들이라면, 으악!! 이게뭐야! 라는 반응이 나타날 수도 있습니다.

진정하세요. 하나하나 설명을 해드릴게요

 

다음 코드는 아까 작성한 post.js 에서 export 한 함수를 모두 불러와서 service 안에 담습니다.

import * as service from '../../services/post';

 

다음 부분은 좀.. 새롭게 느껴지는 분들도 있을겁니다.

    fetchPostInfo = async (postId) => {
        const post = await service.getPost(postId);
        console.log(post);
        const comments = await service.getComments(postId);
        console.log(comments);
    }

 

화살표 함수로 컴포넌트 메소드 선언

우선, 컴포넌트의 메소드가 만들어지는데에 화살표 함수가 사용되었습니다. 일반적인 방식으로는 컴포넌트에서 메소드 선언을 다음과 같은 방식으로 하죠.

class MyComponent extends Component {
    constructor(props) {
        super();
        this.myMethod = this.myMethod.bind(this);
    }
    myMethod() { ... }
    render() { ... }
}

보시다시피 메소드에서 this 에 접근하기 위해 constructor 에서 bind 를 해주었습니다.

 

하지만! 만약에 화살표 함수로 메소드를 선언해주면, binding 을 따로 하지 않아도 자동으로 됩니다.

이는 babel 플러그인 transform-class-properties 가 적용되어있기 때문이구요, create-react-app 으로 만든 프로젝트는 자동으로 적용이 되어있답니다.

 

비동기 작업을 좀 더 쉽게, async-await

이 코드에선 async-await 이 사용됐는데, 이 개념을 이 포스트에서 설명을 하려면 너무나 길기에, 정말, 정말, 간단하게 설명을 드리도록 하겠습니다. 나중에 이 문법에 관한 자세한 내용을 다루는 포스트를 따로 작성해보겠습니다. 이번 포스트에서는 이 문법의 사용법만 배우도록 하겠습니다. 사용법만 알아도 충분해요.

이 문법은, 비동기 작업을 마치 동기 작업을 하듯이 코드를 작성 할 수 있게 해줍니다. 보시다시피 callback 이나 promise 가 사용되지 않았죠.

하지만, 코드는 비동기적으로 작동합니다.

여기서 await 키워드는 Promise 를 기다려주는 역할을합니다. 그리고, 이 키워드를 사용하는 함수는 다음과 같이 함수를 선언 할 때 async 키워드가 함수 앞에 붙어있어야합니다.

async function foo() {
  await bar();
}

// OR

const foo = async () => { 
    await bar();
};

이렇게 작성된 코드는 babel 플러그인을 통하여 generator 코드로 변형됩니다.

var _asyncToGenerator = function (fn) {
  ...
};
var foo = _asyncToGenerator(function* () {
  yield bar();
});

자, 이제 제너레이터라는 개념을 이해해야 하는데, 여기에 상세한 설명이 있습니다.

async-await 은 내부적으로 파고들어서 어떻게 작동하는지 원리를 이해를 하려면 조금 어렵습니다.. 이것 저것 많이 읽어보고 실습해봐야 큰 그림이 그려지구요..

하지만, 사용하기는 쉽습니다. 몇가지만 기억하세요.

  • await 키워드로 Promise 를 기다린다
  • 함수앞에 async 키워드를 붙여준다
  • 에러 처리는 try-catch 로 한다
  • async 함수의 반환값은 Promise 형태이다

요청의 에러처리를 할 땐 (403, 등의 에러코드 포함) try-catch 문을 사용합니다. 하지만 저희 예제 프로젝트에서는 오류가 날 일이 없으므로

 

5.4 컴포넌트의 componentDidMount 에서 메소드 호출

컴포넌트가 로드되고 나서 데이터를 불러오려면, componentDidMount LifeCycle API 에서 데이터를 불러와야합니다. 따라서, componentDidMount 메소드를 만들어서 아까 만든 fetchPostInfo 메소드를 호출하겠습니다.

import React, {Component} from 'react';
import { PostWrapper, Navigate, Post } from '../../components';
import * as service from '../../services/post';


class PostContainer extends Component {

    componentDidMount() {
        this.fetchPostInfo(1);
    }
    
    // (...)
}

export default PostContainer;

그 다음, 브라우저의 개발자도구 (F12) 를 열어서 콘솔을 확인해보세요.

as

이미지 13. AJAX 요청 후 개발자도구 확인

요청이 성공했군요.

fetchPostInfo 메소드의 코드를 보고 이미 눈치채신분들도 있겠지만, 조금 비효율적입니다.

그 이유는, 두번의 비동기 요청을 하는데, 첫번째 요청을 기다린다음에 두번째 요청을 기다리기 때문이죠.

만약에 첫번째와 두번째 요청을 한꺼번에 요청한 다음에 둘 다 기다리면 더 좋지 않을까요?

코드를 다음과 같이 수정하면 해결됩니다.

    fetchPostInfo = async (postId) => {
        const info = await Promise.all([
            service.getPost(postId),
            service.getComments(postId)
        ]);
        
        console.log(info);
    }

여러개의 Promise 를 한꺼번에 처리하고 기다릴 때는, Promise.all 을 사용하면 됩니다.

Promise.all 에 promise 의 배열을 전달해주면, 결과값으로 이뤄진 배열을 반환합니다.

%ec%9d%b4%eb%af%b8%ec%a7%80-9

이미지 14. 배열 형태의 결과값

결과값의 순서는 Promise.all 에 전달한 배열의 순서와 동일합니다. 지금의 경우 첫번째는 포스트 제목/내용, 두번째는 덧글 내용을 지니고 있겠죠.

5.5 데이터를 하위 컴포넌트로 전달하기

자, 그럼 전달받은 데이터를 하위 컴포넌트에 전달을 해줄 차례입니다.

컴포넌트 state 초기값 설정

우선 이 데이터들을 똑똑한 컴포넌트의 state 에 넣어줍시다.

// (...)

class PostContainer extends Component {

    constructor(props) {
        super();
        // initializes component state
        this.state = {
            postId: 1,
            fetching: false, // tells whether the request is waiting for response or not
            post: {
                title: null,
                body: null
            },
            comments: []
        };
    }

    // (...)
}

export default PostContainer;

postId 값은 현재 포스트의 번호를 가르키고, fetching 은 요청의 완료 여부를 알려줍니다.

 

요청 전 후 setState 하기

import React, {Component} from 'react';
import { PostWrapper, Navigate, Post } from '../../components';
import * as service from '../../services/post';


class PostContainer extends Component {

    // (...)
    
    fetchPostInfo = async (postId) => {
        
        this.setState({
            fetching: true // requesting..
        });

        // wait for two promises
        const info = await Promise.all([
            service.getPost(postId),
            service.getComments(postId)
        ]);
        
        // Object destructuring Syntax,
        // takes out required values and create references to them
        const {title, body} = info[0].data; 
                                            
        const comments = info[1].data;

        this.setState({
            postId,
            post: {
                title, 
                body
            },
            comments,
            fetching: false // done!
        });

    }

    // (...)
}

export default PostContainer;

요청이 시작하기 전에 fetch 값을 true 로 설정합니다. 요청이 끝난다음엔 false 로 설정하고, postId 값도 설정해줍니다.

const { a, b } = c 의 형식의 코드는 ES6 의 Object Destructuring (객체 비구조화 할당)문법입니다. 필요한 값을 객체에서 꺼내서, 그 값을 가지고 레퍼런스를 만들어주죠.

 

하위 컴포넌트에 props 로 값 전달하기

// (...)

class PostContainer extends Component {

    // (...)

    render() {
        const {postId, fetching, post, comments} = this.state;

        return (
            <PostWrapper>
                <Navigate 
                    postId={postId}
                    disabled={fetching}
                />
                <Post
                    title={post.title}
                    body={post.body}
                    comments={comments}
                />
            </PostWrapper>
        );
    }
}

export default PostContainer;

이 코드에서도, state 부분에 비구조화 할당 문법을 사용했습니다. 이렇게 함으로 서, this.state.post.title 이렇게 해야되는거를 바로 post.title 로 할 수 있으니까 훨씬 보기 편하지 않나요?

<Navigate/> 컴포넌트엔 현재 포스트의 번호를 알려줄 postId, 그리고 데이터를 불러오는 중일 땐 버튼을 비활성화 하도록 fetching 값을 disabled 로 전달하도록 설정하였습니다.

<Post/> 컴포넌트엔 post 의 정보를 info 로 전달해주고, 덧글 리스트를 담고 있는 comments 도 전달 해 주었습니다.

 

5.6 전달받은 props 렌더링하기

Post 컴포넌트 수정

위에서 전달받은 props 값들을 화면에 렌더링해줍시다. Post 컴포넌트부터 시작 해 볼까요?

import React from 'react';
import './Post.css';
import { CommentList } from '../';

const Post = ({title, body, comments}) => (
    <div className="Post">
        <h1>{title}</h1>
        <p>
            {body}
        </p>
        <CommentList comments={comments}/>
    </div>
);

export default Post;

title 과 body 를 정해진 위치에 렌더링 해주고, comments 는 그대로 <CommentList/> 컴포넌트로 전달해주었습니다.

 

CommentList 컴포넌트 수정

import React from 'react';
import {Comment} from '../';

import './CommentList.css';

const CommentList = ({comments}) => {

    // map data to components
    const commentList = comments.map(
        (comment, index)=>(
            <Comment 
                name={comment.name}
                body={comment.body} 
                key={index}
            />
        )
    );

    return (
        <ul className="CommentList">
            {commentList}
        </ul>
    );
};

export default CommentList;

전달받은 comments 배열을 컴포넌트 배열로 map 해줍니다. (이 개념을 모른다면 여기를 참조하세요)

 

Comment 컴포넌트 수정

이어서, Comment 컴포넌트를 수정해줍시다.

import React from 'react';
import './Comment.css';

const Comment = ({name, body}) => {
    return (
        <li className="Comment">
            <p>
                <b>{name}</b> {body}
            </p>
        </li>
    );
};

export default Comment;

자, 여기까지 진행을 하셨다면, 우리가 ajax 를 통하여 전달받은 데이터를 모두 화면에 보여 줄 수 있게 됩니다.

%ec%9d%b4%eb%af%b8%ec%a7%80-1

이미지 15. 렌더링 결과

생략해도 되는 추가작업..

그런데, 렌더링을 하고 나니, 이름이 기네요.. 왜 예제 데이터에서 이름이 저렇게 여러 단어일까요? 이게 좀 맘에 안드니까, API 요청을 할 때 받아오는 email 값의 아이디 부분만 파싱하여 전달해주도록 하겠습니다.

CommentList 컴포넌트를 다음과 같이 수정하세요.

import React from 'react';
import {Comment} from '../';

import './CommentList.css';

const CommentList = ({comments}) => {

    // map data to components
    const commentList = comments.map(
        (comment, index)=>(
            <Comment 
                name={comment.email.split('@')[0]}
                body={comment.body} 
                key={index}
            />
        )
    );

    return (
        <ul className="CommentList">
            {commentList}
        </ul>
    );
};

export default CommentList;

%ec%9d%b4%eb%af%b8%ec%a7%80-2

이미지 16. 덧글에서 이메일의 아이디 보여주기

훨씬 더 그럴싸해보입니다. 이 과정은 사실상 생략해도 됩니다…

6. Navigate 기능 구현하기

컴포넌트 로딩 시에 데이터를 불러오는것은 성공적으로 완료하였습니다. 이제 앞, 뒤 포스트로 이동하는 Navigate 컴포넌트의 기능을 구현해보겠습니다.

원리는 간단합니다. 똑똑한 컴포넌트, PostContainer 에서 현재 postId 에 1 을 더하거나 뺀 포스트를 불러오도록 하는 메소드를 정의하면 됩니다.

6.1 handleNavigateClick 메소드 만들기

// (...)

class PostContainer extends Component {

    // (...)

    handleNavigateClick = (type) => {
        const postId = this.state.postId;

        if(type === 'NEXT') {
            this.fetchPostInfo(postId+1);
        } else {
            this.fetchPostInfo(postId-1);
        }
    }

    // (...)
}

export default PostContainer;

간단하죠? 파라미터가 ‘NEXT’ 면 다음 포스트를 읽어오고 ‘PREV’ 면 이전 포스트를 읽어오도록 작성했습니다.

이 메소드를, <Navigate/> 컴포넌트에 전달해줍시다.

 

 

6.2 Navigate 컴포넌트 onClick 설정

// (...)

class PostContainer extends Component {

    // (...)

    render() {
        const {postId, fetching, post, comments} = this.state;

        return (
            <PostWrapper>
                <Navigate 
                    postId={postId}
                    disabled={fetching}
                    onClick={this.handleNavigateClick}
                />
                <Post
                    title={post.title}
                    body={post.body}
                    comments={comments}
                />
            </PostWrapper>
        );
    }
}

export default PostContainer;

onClick props 에 방금 만든 this.handleNavigateClick 을 전달해줍시다.

 

6.3 Navigate 컴포넌트에서 onClick props 사용

import React from 'react';
import {Button} from 'semantic-ui-react';
import './Navigate.css'

const Navigate = ({onClick, postId, disabled}) => (
    <div className="Navigate">
        <Button
            color="teal"
            content="Previous"
            icon="left arrow"
            labelPosition="left"
            onClick={
                () => onClick('PREV')
            }
            disabled={disabled}
        />
        <div className="Navigate-page-num">
            {postId}
        </div>
        <Button
            color="teal"
            content="Next"
            icon="right arrow"
            labelPosition="right"
            className="Navigate-right-button"
            onClick={
                () => onClick('NEXT')
            }
            disabled={disabled}
        /> 
    </div>
);

export default Navigate;

각 Button 컴포넌트에서, 새로운 함수를 만들어서 그 내부에서 onClick 함수에 특정 파라미터를 포함하여 실행하도록 설정합니다.

만약에 이벤트에서 실행 할 함수에 파라미터가 필요하다면 이런식으로 함수를 따로 만들어주어야합니다.

그리고, postId 값을 중간에 렌더링 해주고,

요청중 일 때 true 로 설정되는 disabled 값도 버튼에 그대로 전달해줍시다.

여기까지 완성이 되었다면, 저장을 하고 페이지에서 Next / Previous 버튼을 눌러보세요.

awervaerv

이미지 17. Next / Previous 버튼 클릭

잘 되죠? 잘 되긴 하는데 1 에서 0 페이지로 가면 오류가 발생합니다. 이 때, 아예 멈춰버리는데요, 개발자 콘솔을 보면 다음과 같이 나타납니다.

%ec%9d%b4%eb%af%b8%ec%a7%80-4이미지 17. 오류 발생

포스트 0이 존재하지 않으므로, 위와같이 404 에러가 나타나게 됩니다. 한번 이 에러를 처리해볼까요?

async-await 을 사용 할 때 에러처리는 try-catch 로 하면 됩니다.

Q. 그냥 숫자가 1 일때 요청을 거부하게 하면 되지 않나요? 

네 물론이죠. 그렇게 하는게 일반적인 방법이지만, 우리는 에러 처리 방법을 배우기 위하여 굳이 존재하지 않는 페이지에 요청을 시도하는거랍니다.

6.4 에러 처리하기

이 에러를 처리하기 위해선 fetchPostInfo 코드를 try-catch 로 감싸주면 됩니다.

import React, {Component} from 'react';
import { PostWrapper, Navigate, Post } from '../../components';
import * as service from '../../services/post';


class PostContainer extends Component {

    // (...)
    
    fetchPostInfo = async (postId) => {
        this.setState({
            fetching: true // requesting..
        });

        try {
            // wait for two promises
            const info = await Promise.all([
                service.getPost(postId),
                service.getComments(postId)
            ]);

            // Object destructuring Syntax,
            // takes out required values and create references to them
            const {title, body} = info[0].data; 
                                                
            const comments = info[1].data;

            this.setState({
                postId,
                post: {
                    title, 
                    body
                },
                comments,
                fetching: false // done!
            });

        } catch(e) {
            // if err, stop at this point
            this.setState({
                fetching: false
            });
            console.log('error occurred', e);
        }
    }

    // (...)
}

export default PostContainer;

이렇게하면, 어플리케이션이 먹통이 되는것까진 막아주지만, 사용자에게 에러가 났다는것을 따로 알려주지는 않습니다.

ezgif-com-701bc79d01

이미지 18. 오류 처리

경고 메시지를 보여주면 더 좋겠죠? 단순히 javascript.alert 는 너무 구리니까, 연습삼아 경고 메시지 컴포넌트를 만들어봅시다.

7. Warning 컴포넌트 만들기

경고 메시지를 띄우기 위하여, 간편하게 라이브러리를 설치하여 사용해도됩니다. (저는 Notify.js 라는 라이브러리를 애용합니다) 하지만 우리는, 공부를 목적으로, 간단한 경고 메시지를 띄우는 컴포넌트를 직접 만들어보겠습니다.

앞으로 만들 컴포넌트는 다음과 같이 생겼습니다:

ezgif-com-6a6dca828a

이미지 19. Warning 컴포넌트

7.1 Animation 설정하기

React 컴포넌트에서 애니메이션을 사용 할 때, CSSTransitionGroup 혹은 react-motion 이 자주 사용됩니다. 아마, 들어본분들은 꽤 있을거에요.

보통 CSS 만으로 처리 할 수 있는 애니메이션은 CSSTransitionGroup 을 사용하고, 좀 복잡한 애니메이션들은 react-motion 을 사용합니다.

우리는 단순 효과만 줄 것이므로, CSSTransitionGroup 을 사용하는게 맞겠지만.. 저는 이 애드온을 딱히 좋아하지 않습니다. 그 이유는, 불필요한 복잡도가 올라가기 때문인데요, 이 강좌에서는 저 애드온을 사용하지 않고, CSS 와 리액트의 state 만을 사용하여 애니메이션을 구현하는 방법을 알아보겠습니다.

사용 할 애니메이션 고르기

애니메이션 CSS 코드를 직접 작성해도 되겠지만, 이미 만들어진것들을 사용하면 시간을 많이 단축할수있습니다.

다음 사이트에 들어가보세요: http://anicollection.github.io/#/

 

screenshot-2016-12-23-130226

이미지 20. Warning 컴포넌트

필요한 애니메이션을 클릭해서 추가한다음에 우측 상단에서 CSS 코드를 받을 수 있습니다.

그 코드를 src 디렉토리에 Animation.css 파일을 추가해서 붙여넣고, index.js 에서 불러오세요.

/*base code*/

.animated {
  -webkit-animation-duration: 1s;
  animation-duration: 1s;
  -webkit-animation-fill-mode: both;
  animation-fill-mode: both;
}

.animated.infinite {
  -webkit-animation-iteration-count: infinite;
  animation-iteration-count: infinite;
}

.animated.hinge {
  -webkit-animation-duration: 2s;
  animation-duration: 2s;
}

/*the animation definition*/

@-webkit-keyframes bounceIn {
  0%, 100%, 20%, 40%, 60%, 80% {
    -webkit-transition-timing-function: cubic-bezier(0.215, .61, .355, 1);
    transition-timing-function: cubic-bezier(0.215, .61, .355, 1)
  }
  0% {
    opacity: 0;
    -webkit-transform: scale3d(.3, .3, .3);
    transform: scale3d(.3, .3, .3)
  }
  20% {
    -webkit-transform: scale3d(1.1, 1.1, 1.1);
    transform: scale3d(1.1, 1.1, 1.1)
  }
  40% {
    -webkit-transform: scale3d(.9, .9, .9);
    transform: scale3d(.9, .9, .9)
  }
  60% {
    opacity: 1;
    -webkit-transform: scale3d(1.03, 1.03, 1.03);
    transform: scale3d(1.03, 1.03, 1.03)
  }
  80% {
    -webkit-transform: scale3d(.97, .97, .97);
    transform: scale3d(.97, .97, .97)
  }
  100% {
    opacity: 1;
    -webkit-transform: scale3d(1, 1, 1);
    transform: scale3d(1, 1, 1)
  }
}

@keyframes bounceIn {
  0%, 100%, 20%, 40%, 60%, 80% {
    -webkit-transition-timing-function: cubic-bezier(0.215, .61, .355, 1);
    transition-timing-function: cubic-bezier(0.215, .61, .355, 1)
  }
  0% {
    opacity: 0;
    -webkit-transform: scale3d(.3, .3, .3);
    -ms-transform: scale3d(.3, .3, .3);
    transform: scale3d(.3, .3, .3)
  }
  20% {
    -webkit-transform: scale3d(1.1, 1.1, 1.1);
    -ms-transform: scale3d(1.1, 1.1, 1.1);
    transform: scale3d(1.1, 1.1, 1.1)
  }
  40% {
    -webkit-transform: scale3d(.9, .9, .9);
    -ms-transform: scale3d(.9, .9, .9);
    transform: scale3d(.9, .9, .9)
  }
  60% {
    opacity: 1;
    -webkit-transform: scale3d(1.03, 1.03, 1.03);
    -ms-transform: scale3d(1.03, 1.03, 1.03);
    transform: scale3d(1.03, 1.03, 1.03)
  }
  80% {
    -webkit-transform: scale3d(.97, .97, .97);
    -ms-transform: scale3d(.97, .97, .97);
    transform: scale3d(.97, .97, .97)
  }
  100% {
    opacity: 1;
    -webkit-transform: scale3d(1, 1, 1);
    -ms-transform: scale3d(1, 1, 1);
    transform: scale3d(1, 1, 1)
  }
}

.bounceIn {
  -webkit-animation-name: bounceIn;
  animation-name: bounceIn
}

@-webkit-keyframes bounceOut {
  20% {
    -webkit-transform: scale3d(.9, .9, .9);
    transform: scale3d(.9, .9, .9)
  }
  50%,
  55% {
    opacity: 1;
    -webkit-transform: scale3d(1.1, 1.1, 1.1);
    transform: scale3d(1.1, 1.1, 1.1)
  }
  100% {
    opacity: 0;
    -webkit-transform: scale3d(.3, .3, .3);
    transform: scale3d(.3, .3, .3)
  }
}

@keyframes bounceOut {
  20% {
    -webkit-transform: scale3d(.9, .9, .9);
    -ms-transform: scale3d(.9, .9, .9);
    transform: scale3d(.9, .9, .9)
  }
  50%,
  55% {
    opacity: 1;
    -webkit-transform: scale3d(1.1, 1.1, 1.1);
    -ms-transform: scale3d(1.1, 1.1, 1.1);
    transform: scale3d(1.1, 1.1, 1.1)
  }
  100% {
    opacity: 0;
    -webkit-transform: scale3d(.3, .3, .3);
    -ms-transform: scale3d(.3, .3, .3);
    transform: scale3d(.3, .3, .3)
  }
}

.bounceOut {
  -webkit-animation-name: bounceOut;
  animation-name: bounceOut
}
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import 'semantic-ui-css/semantic.min.css';
import './index.css';
import './Animation.css';

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

 

7.2 Warning 컴포넌트 생성 및 디자인하기

컴포넌트 기본 틀 만들기

import React, {Component} from 'react';
import "./Warning.css";

class Warning extends Component {
    render() {
        const { message, visible } = this.props;

        return (
            <div className="Warning-wrapper">
                <div className="Warning animated bounceIn">
                    {message}
                </div>
            </div>
        );
    }
}

export default Warning;

이 컴포넌트는 messagevisible props 를 받습니다.

message 값은 경고 메시지 텍스트를 설정하고 visible 은 컴포넌트의 가시성 (visibility) 를 설정해줍니다, 가시성 부분은 추후 처리하겠습니다.

컴포넌트 스타일링

.Warning-wrapper {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 10;
}
.Warning {
    white-space: nowrap;
    font-size: 1.5rem;
    padding-top: 2rem;
    padding-bottom: 2rem;
    padding-left: 2rem;
    padding-right: 2rem;
    background-color: rgba(0,0,0,0.8);
    border-radius: 5px;
    color: white;
    font-weight: 600;
}

여기서 Warning-wrapper 를 따로 만든 이유는, 애니메이션 CSS 코드에서 transform 을 사용하는데, 만약에 Warning 클래스와 함께 fadeIn 을 넣어주면, 기존의 가운데 정렬을 도와주는 transform 이 풀리기 때문에, 가운데 정렬은 Warning-wrapper 에서 하고 애니메이션은 Warning 클래스에서 하게 한 것 입니다.

컴포넌트 인덱스 설정

import Header from './Header/Header';
import PostWrapper from './PostWrapper/PostWrapper';
import Navigate from './Navigate/Navigate';
import Post from './Post/Post';
import Comment from './Comment/Comment';
import CommentList from './CommentList/CommentList';
import Warning from './Warning/Warning';

export {
    Header,
    PostWrapper,
    Navigate,
    Post,
    CommentList,
    Comment,
    Warning
};

컴포넌트 사용

import React, {Component} from 'react';
import { PostWrapper, Navigate, Post, Warning } from '../../components';
import * as service from '../../services/post';

class PostContainer extends Component {
   
    // (...)

    render() {
        const {postId, fetching, post, comments} = this.state;

        return (
            <PostWrapper>
                <Navigate 
                    postId={postId}
                    disabled={fetching}
                    onClick={this.handleNavigateClick}
                />
                <Post
                    title={post.title}
                    body={post.body}
                    comments={comments}
                />
                <Warning message="That post does not exist"/>
            </PostWrapper>
        );
    }
}

export default PostContainer;

방금 만든 컴포넌트를 PostContainer 에 렌더링 해줍니다. Warning 컴포넌트의 message 에 보여줄 메시지를 설정하세요. 

지금은 이 경고 메시지가 언제나 렌더링 됩니다. 페이지에서 렌더링이 잘 됐나 확인해보세요.

screenshot-2016-12-23-132155

이미지 21. Warning 컴포넌트 초기작업

7.3 컴포넌트의 가시성 설정

이제, 컴포넌트가 오류가 날 때만 보여지도록 설정을 해보겠습니다.

PostContainer 기본 state 수정

// (...)

class PostContainer extends Component {

    constructor(props) {
        super();
        // initializes component state
        this.state = {
            postId: 1,
            fetching: false, // tells whether the request is waiting for response or not
            post: {
                title: null,
                body: null
            },
            comments: [],
            warningVisibility: false
        };
    }

    // (...)
}

export default PostContainer;

warningVisibility 라는 state 를 추가했습니다. 기본값은 false 입니다.

 

showWarning 메소드 작성, 에러발생시 호출

// (...)
class PostContainer extends Component {

    // (...)
    
    showWarning = () => {
        this.setState({
            warningVisibility: true
        });

        // after 1.5 sec

        setTimeout(
            () => {
                this.setState({
                    warningVisibility: false
                });
            }, 1500
        );
    }
    
    fetchPostInfo = async (postId) => {
        this.setState({
            fetching: true // requesting..
        });

        try {
            // (...)

        } catch(e) {
            this.setState({
                fetching: false
            });
            this.showWarning();
        }
    }

    // (...)
}

export default PostContainer;

showWarning 메소드는 warningVisibility 값을 true 로 설정 한 다음에 1.5 초 후에 다시 false 로 설정합니다.

fetchPostInfo 메소드에서는 에러가 발생 하면 showWarning 메소드를 호출합니다.

지금 프로젝트에서는 에러의 종류가 하나이므로 showWarning 에 파라미터가 없지만, 만약에 여러 에러를 처리해야한다면, 이 함수에 파라미터를 넣고 state 에 메시지를 담은다음에 렌더링 부분에서 넣어주면 되겠죠?

 

Warning 컴포넌트에 상태 전달

// (...)

class PostContainer extends Component {

    // (...)

    render() {
        const {postId, fetching, post, comments, warningVisibility} = this.state;

        return (
            <PostWrapper>
                { /* (...) */ }
                <Warning visible={warningVisibility} message="That post does not exist"/>
            </PostWrapper>
        );
    }
}

export default PostContainer;

 

Warning 컴포넌트 수정

import React, {Component} from 'react';
import "./Warning.css";

class Warning extends Component {
    constructor(props) {
        super(props);
        this.state = {
            closing: false
        };
    }

    componentWillReceiveProps (nextProps) {
        if(this.props.visible && !nextProps.visible) {
        // visible props is changing from true -> false
           
           this.setState({
               closing: true
           });

           // 1 sec after
           setTimeout(
               () => {
                   this.setState({
                       closing: false
                   });
               }, 1000
           );
        }
    }
    

    render() {
        const { visible, message } = this.props;
        const { closing } = this.state;

        if(!visible && !closing) return null;
        return (
            <div className="Warning-wrapper">
                <div className={`Warning ${closing?'bounceOut':'bounceIn'} animated`}>
                    {message}
                </div>
            </div>
        );
    }
}

export default Warning;

만약에 visible 값이 false 일 때 바로 아무것도 렌더링을 하지 않게 한다면 저희는 bounceOut 애니메이션을 주지 못하게됩니다. 따라서, closing 이라는 state 를 만들고, componentWillReceiveProps 에서 visible 값이 true 에서 false 로 변환될 때, closing 값을 true 로 설정 하고 1초 후에 (애니메이션이 1초입니다) 다시 false 로 되돌리도록 코드를 작성합니다.

그 후, 렌더링 부분에서, visible 값과 closing 값이 둘 다 값이 거짓 일 때 아무것도 렌더링 되지 않도록하고, 둘 중 하나라도 참인게 있으면 계속해서 렌더링을 진행합니다.

렌더링을 할 때는, closing 값이 참 일때는 bounceOut 클래스를 설정하고 그렇지 않을땐 bounceIn 클래스를 설정합니다.

이렇게 하면, 경고가 나타나고 사라질때마다 애니메이션이 정상적으로 작동하게됩니다.

여기까지 하셨으면, 경고 메시지 구현이 끝납니다 🙂

 

8. 포스트 전환시 애니메이션 효과 넣어주기

저희 프로젝트가 거의 다 끝났습니다. 뭐.. 이제 부족한 기능은 없는 것 같은데, 재미삼아서 포스트 전환 시 애니메이션을 넣어보도록 하겠습니다.

ezgif-com-09cf989ad2

이미지 22. 포스트 트랜지션 애니메이션 효과

8.1 애니메이션 설정

AniCollection 에서 bounceInLeft, bounceInRight, bounceOutLeft, bounceOutRight 코드를 가져와서 Animation.css 코드에 붙여넣습니다

animated, animated.infinite, animated.hinge 는 중복되므로 그 부분은 복사하지 않으셔도 됩니다

/* (...) */

@-webkit-keyframes bounceInLeft {
  0%, 100%, 60%, 75%, 90% {
    -webkit-transition-timing-function: cubic-bezier(0.215, .61, .355, 1);
    transition-timing-function: cubic-bezier(0.215, .61, .355, 1)
  }
  0% {
    opacity: 0;
    -webkit-transform: translate3d(-3000px, 0, 0);
    transform: translate3d(-3000px, 0, 0)
  }
  60% {
    opacity: 1;
    -webkit-transform: translate3d(25px, 0, 0);
    transform: translate3d(25px, 0, 0)
  }
  75% {
    -webkit-transform: translate3d(-10px, 0, 0);
    transform: translate3d(-10px, 0, 0)
  }
  90% {
    -webkit-transform: translate3d(5px, 0, 0);
    transform: translate3d(5px, 0, 0)
  }
  100% {
    -webkit-transform: none;
    transform: none
  }
}

@keyframes bounceInLeft {
  0%, 100%, 60%, 75%, 90% {
    -webkit-transition-timing-function: cubic-bezier(0.215, .61, .355, 1);
    transition-timing-function: cubic-bezier(0.215, .61, .355, 1)
  }
  0% {
    opacity: 0;
    -webkit-transform: translate3d(-3000px, 0, 0);
    -ms-transform: translate3d(-3000px, 0, 0);
    transform: translate3d(-3000px, 0, 0)
  }
  60% {
    opacity: 1;
    -webkit-transform: translate3d(25px, 0, 0);
    -ms-transform: translate3d(25px, 0, 0);
    transform: translate3d(25px, 0, 0)
  }
  75% {
    -webkit-transform: translate3d(-10px, 0, 0);
    -ms-transform: translate3d(-10px, 0, 0);
    transform: translate3d(-10px, 0, 0)
  }
  90% {
    -webkit-transform: translate3d(5px, 0, 0);
    -ms-transform: translate3d(5px, 0, 0);
    transform: translate3d(5px, 0, 0)
  }
  100% {
    -webkit-transform: none;
    -ms-transform: none;
    transform: none
  }
}

.bounceInLeft {
  -webkit-animation-name: bounceInLeft;
  animation-name: bounceInLeft
}

@-webkit-keyframes bounceOutLeft {
  20% {
    opacity: 1;
    -webkit-transform: translate3d(20px, 0, 0);
    transform: translate3d(20px, 0, 0)
  }
  100% {
    opacity: 0;
    -webkit-transform: translate3d(-2000px, 0, 0);
    transform: translate3d(-2000px, 0, 0)
  }
}

@keyframes bounceOutLeft {
  20% {
    opacity: 1;
    -webkit-transform: translate3d(20px, 0, 0);
    -ms-transform: translate3d(20px, 0, 0);
    transform: translate3d(20px, 0, 0)
  }
  100% {
    opacity: 0;
    -webkit-transform: translate3d(-2000px, 0, 0);
    -ms-transform: translate3d(-2000px, 0, 0);
    transform: translate3d(-2000px, 0, 0)
  }
}

.bounceOutLeft {
  -webkit-animation-name: bounceOutLeft;
  animation-name: bounceOutLeft
}

@-webkit-keyframes bounceInRight {
  0%, 100%, 60%, 75%, 90% {
    -webkit-transition-timing-function: cubic-bezier(0.215, .61, .355, 1);
    transition-timing-function: cubic-bezier(0.215, .61, .355, 1)
  }
  0% {
    opacity: 0;
    -webkit-transform: translate3d(3000px, 0, 0);
    transform: translate3d(3000px, 0, 0)
  }
  60% {
    opacity: 1;
    -webkit-transform: translate3d(-25px, 0, 0);
    transform: translate3d(-25px, 0, 0)
  }
  75% {
    -webkit-transform: translate3d(10px, 0, 0);
    transform: translate3d(10px, 0, 0)
  }
  90% {
    -webkit-transform: translate3d(-5px, 0, 0);
    transform: translate3d(-5px, 0, 0)
  }
  100% {
    -webkit-transform: none;
    transform: none
  }
}

@keyframes bounceInRight {
  0%, 100%, 60%, 75%, 90% {
    -webkit-transition-timing-function: cubic-bezier(0.215, .61, .355, 1);
    transition-timing-function: cubic-bezier(0.215, .61, .355, 1)
  }
  0% {
    opacity: 0;
    -webkit-transform: translate3d(3000px, 0, 0);
    -ms-transform: translate3d(3000px, 0, 0);
    transform: translate3d(3000px, 0, 0)
  }
  60% {
    opacity: 1;
    -webkit-transform: translate3d(-25px, 0, 0);
    -ms-transform: translate3d(-25px, 0, 0);
    transform: translate3d(-25px, 0, 0)
  }
  75% {
    -webkit-transform: translate3d(10px, 0, 0);
    -ms-transform: translate3d(10px, 0, 0);
    transform: translate3d(10px, 0, 0)
  }
  90% {
    -webkit-transform: translate3d(-5px, 0, 0);
    -ms-transform: translate3d(-5px, 0, 0);
    transform: translate3d(-5px, 0, 0)
  }
  100% {
    -webkit-transform: none;
    -ms-transform: none;
    transform: none
  }
}

.bounceInRight {
  -webkit-animation-name: bounceInRight;
  animation-name: bounceInRight
}

@-webkit-keyframes bounceOutRight {
  20% {
    opacity: 1;
    -webkit-transform: translate3d(-20px, 0, 0);
    transform: translate3d(-20px, 0, 0)
  }
  100% {
    opacity: 0;
    -webkit-transform: translate3d(2000px, 0, 0);
    transform: translate3d(2000px, 0, 0)
  }
}

@keyframes bounceOutRight {
  20% {
    opacity: 1;
    -webkit-transform: translate3d(-20px, 0, 0);
    -ms-transform: translate3d(-20px, 0, 0);
    transform: translate3d(-20px, 0, 0)
  }
  100% {
    opacity: 0;
    -webkit-transform: translate3d(2000px, 0, 0);
    -ms-transform: translate3d(2000px, 0, 0);
    transform: translate3d(2000px, 0, 0)
  }
}

.bounceOutRight {
  -webkit-animation-name: bounceOutRight;
  animation-name: bounceOutRight
}

 

8.2 Post 컴포넌트에 postId 전달

// (...)

class PostContainer extends Component {

    // (...)
    render() {
        const {postId, fetching, post, comments, warningVisibility} = this.state;

        return (
            <PostWrapper>
                <Navigate 
                    postId={postId}
                    disabled={fetching}
                    onClick={this.handleNavigateClick}
                />
                <Post
                    postId={postId}
                    title={post.title}
                    body={post.body}
                    comments={comments}
                />
                <Warning visible={warningVisibility} message="That post does not exist"/>
            </PostWrapper>
        );
    }
}

export default PostContainer;

저희는 애니메이션 처리를 Post 컴포넌트 내부에서 할 것입니다. 컴포넌트의 바깥에서 애니메이션 효과를 어떤걸 줄 지 props 로 전달하는게 아니라, 내용이 바뀜에 따라 컴포넌트의 내부 state 를 사용하여 관리하겠다는 말이지요. 그러기 위해서는, 애니메이션 방향을 알아내기 위해서 postId 값도 Post 컴포넌트에 전달을 해주어야합니다.

 

8.3 Post 컴포넌트 수정

import React, {Component} from 'react';
import './Post.css';
import { CommentList } from '../';

class Post extends Component {
    constructor(props) {
        super(props);
        this.state = {
            postInfo: {
                title: null,
                body: null,
                comments: []
            },
            animate: false,
            direction: 'left'
        }
    }
    

    componentWillReceiveProps (nextProps) {
        
        const { title, body, comments } = nextProps;
        
        if(this.props.postId !== nextProps.postId) {
            // identify the animation direction
            const direction = this.props.postId < nextProps.postId ? 'left' : 'right';
            
            this.setState({
                direction,
                animate: true
            });

            // sync the props to state 0.5 sec later
            setTimeout(
                () => {
                    this.setState({
                        postInfo: {
                            title, body, comments 
                        },
                        animate: false
                    })
                }, 500
            );
            return;
        }

        // sync the props to state directly (this is the first post)
        this.setState({
            postInfo: {
                title, body, comments 
            }
        })
    }
    
    render() {
        const { title, body, comments } = this.state.postInfo;

        const { animate, direction } = this.state;
        
        const animation = animate 
                          ? (direction==='left' ? 'bounceOutLeft' : 'bounceOutRight')
                          : (direction==='left' ? 'bounceInRight' : 'bounceInLeft');

        // show nothing when data is not loaded
        if(title===null) return null;

        return (
            <div className={`Post animated ${animation}`}>
                <h1>{title}</h1>
                <p>
                    {body}
                </p>
                <CommentList comments={comments}/>
            </div>
        );
    }
}

export default Post;

기존의 Post 컴포넌트는 함수형 컴포넌트였지만, 애니메이션을 처리하려면 내부 state 를 사용해야하기에, 클래스형 컴포넌트로 다시 코드를 작성했습니다.

우선, 포스트가 다음 혹은 이전으로 넘어갈 때, 애니메이션을 제대로 주기 위해서는 props 를 그대로 렌더링하면 않됩니다. 그 대신에, props 를 전달 받을 때 마다, state 와 동기화를 시키고, state 에 있는 값을 렌더링해줍니다.

이렇게 함으로서, 포스트가 페이지에서 사라지는 효과를 줄 때, 이전 포스트의 내용을 그대로 보여줄 수 있게 됩니다. 만약에 props 를 그대로 렌더링 한다면, 포스트가 사라지는데, 새로운 데이터를 보여주면서 사라지겠죠.

props 가 바뀔 때, 만약에 첫 포스트를 로딩하는거라면, props 내용을 state 에 바로 설정해줍니다. 하지만, 그 이후에는, 현재의 postId 와 다음 postId 를 비교하여, 애니메이션을 줄 방향을 설정합니다.

그 다음, animate, direction state 를 통해 애니메이션 클래스를 적용하고, 애니메이션이 끝나면 (0.5초뒤) 새 포스트가 로딩되게 합니다.

 

9. 구형 브라우저 지원하기

axios 는 Promise 를 사용하는 HTTP Client 라이브러리인데, 이 Promise 는 신형 브라우저에만 내장되어있습니다. Promise 기능을 갖고있지 않은 브라우저 자바스크립트 엔진들을 호환시켜주기 위하여, polyfill 을 적용해보겠습니다.

npm 을 통하여 promise-polyfill 을 다운로드 받으세요.

 npm install --save promise-polyfill

그 다음, src/index.js 코드에 다음 코드를 삽입하세요.

// (...)
import Promise from 'promise-polyfill'; 

// To add to window
if (!window.Promise) {
  window.Promise = Promise;
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

이렇게 코드를 삽입해주면 IE8 에서도 axios 가 문제없이 작동합니다.

 

10. 빌드하고 surge.sh 에 deploy 하기

마지막으로, 저희가 작업한 프로젝트를 퍼블리시 해보겠습니다.

Surge.sh 를 사용하면  html, css, js 등의 정적 파일들을 무료로 퍼블리시하고, 서브도메인 & SSL 을 받을 수 있습니다. Amazon 의 S3 와, Github의 Github pages 와 비슷한 개념이죠. 하지만, 무료이고, 더더욱 편리합니다.

원한다면 커스텀 도메인도 설정 할 수 있답니다, 그것도 무료로요! 트래픽에 제한도 없습니다.

유료 서비스인 Surge Plus 로 업그레이드를 하면 다음과 같은 혜택을 얻을 수 있게 됩니다

  • 커스텀 도메인에 SSL 설정
  • HTTP 로 들어오는 요청 모두 HTTPS로 리디렉트
  • 비밀번호로 프로젝트 보호 (클라이언트 사이드 어플리케이션 자체를 비밀번호로 보호해줍니다)
  • HTTP 접근 제어 (CORS)
  • 커스텀 리디렉트

Surge.sh 는 백엔드 REST API 서버 따로 운영하고, SPA 에 필요한 정적파일을 프로덕션용으로 제공해도 전혀 손색이 없는 멋진 서비스입니다. 인지도도 정말 높구요.

단! 한국에서 서비스하는거라면 어떨지는 모르겠습니다. 아직은 아시아에 CDN 서버가 없는것 같더라구요

그래도, 충분히 빠릅니다.

어서 리액트 프로젝트를 build 해봅시다… deploy 하는 과정이 얼마나 간단한지 알게된다면 정말 놀라실겁니다.

 

10.1 React 프로젝트 빌드하기

npm run build

이 명령어를 입력하고 잠시 기다리면 됩니다.

Q. 빌드 과정이 왜 필요한가요?

코드의 파일 사이즈를 축소하기 위해서 코드를 빌드합니다.

 

10.2 surge 글로벌 설치

npm install -g surge

그 다음, build 경로로 이동하여 surge 명령어를 입력하세요.

peek-2016-12-23-18-17

이미지 23. 매우 간단한 Surge 퍼블리싱

엄청나게 간단하죠?

생성 할 때, 중복되지 않는 랜덤한 도메인을 하나 만들어줍니다. 하지만 저 과정에서 수정해주면, 원하는 서브도메인으로도 만들 수 있습니다. 만약에 소유하고 계신 도메인이 있다면 여러분의 도메인으로도 deploy 할 수 있답니다 (여기 참조)

마치면서..

오랜만에 쓴 리액트 강좌였고.. 생각보다 길었습니다.

이번 강좌에선, 지금까지 제가 동영상 / 블로그 포스트에서 다뤘던 리액트 지식들을 응용하여 조금 그럴싸한? 예제 프로젝트를 만들었습니다.

이 포스트를 통하여 여러분들이 리액트에 조금 더 익숙해졌으면 좋겠네요!

다음 강좌에선 여기서 만든 이 프로젝트를, PostContainer 의 로컬 상태 대신에, Redux 의 스토어를 사용하여 구현하는 과정을 다뤄보겠습니다.

  • 편해걸

    안녕하세요.

    포스팅글이랑 강의동영상 항상 잘보고 있습니다 😀

    좋은 내용 잘 배우고 가요~

    감사합니다 ㅎㅎ

    • 즐거운 리액팅 되세요~ 😀

  • 강희룡

    안녕하세요 튜토리얼 보면서 리액트 열심히 배우고 있습니다. ㅎㅎ
    지금 이 글 참고하면서 회원가입 컴포넌트를 만들고 있는데요.
    해결이 안되는 오류가 있어서 한가지 질문드립니다…

    화살표 함수로 정의한 메소드에서 파라미터가 원하는대로 전달되지 않고 this 키워드를 사용할 수 없는데 짐작가는 원인이라도 있으시다면 답변 부탁드려요. 감사합니다.

    • 강희룡

      https://uploads.disquscdn.com/images/0097863b777aecc8696ecbf47d60727f88e3eb69f5de5f1f9cc885e3c04cea67.png

      코드 들여쓰기가 깨져서 수정하려고 했는데 수정에서는 이미지 첨부가 안되네요.

      • 혹시 transform-class-properties 플러그인이 잘 적용되어있는지 확인해보셨나요? create-react-app 없이 리액트 프로젝트 환경설정을 하셨는지요?

        • 강희룡

          네 프로젝트 환경설정 직접하면서 필요한 바벨 플러그인은 다 설정해도 해결이 안되길래 babel-preset-react-app 프리셋 적용하니까 해결됬네요.
          그냥 이거 써야겠어요 더 좋은거 같네요… ㅋㅋㅋ

  • 안녕하세요 velopert님 ^^
    종종 페북 메신저로 리액트로 프로젝트 진행중에 궁금증에 대한 질문을 주고 받았었는데, 간만에 좋은강좌 정독하고 갑니다!
    이번 포스팅으로 인해서 surge라는 무료 정적 퍼블리싱 서비스도 알게되서 좋네요.
    혹여나 회사내부나 몇명이 쓰지 않는 간단하게 사용하기 위한 프로젝트를 올릴경우,
    api서버만 따로 운영하고 surge를 이용해서 리액트앱을 퍼블리싱하기에도 좋을 것 같네요!

    앞으로도 좋은정보 많이 얻어가겠습니다 화이팅!

  • 박지성

    추천하실만한 ui framework가 있나요?
    react toolbox랑 materail ui를 써봤는데 편하고 또 깔끔하니 좋더라구요.
    요즘 대세가 어떤건지도 궁금하네요~

    • 저는 보통 UI를 직접 디자인할때가 많아서 semantic-ui 를 사용합니다. 지금까지는 리액트로 포팅하지 않은 버전을 잘 사용해왔었는데요, 이 포스트에서 버튼으로만 사용했던 리액트버전도 굉장히 쓸만한 것 같습니다.

      제가 아직 사용하진 못했지만 정말 눈여겨보는것은 Groomet 이라는 IBM에서 후원하는 프레임워크입니다.

      Groomet은 design practice 이지만, 자신만의 컴포넌트도 있답니다.

      react-md 라는것도 꽤 평이 좋습니다.

  • jinhoyim

    이번 강의 완전 재밌습니다. 애니메이션 계속 뒤로 미뤘는데 이제야 해보네요.
    ㅋㅋ 방식은 이해했는데 애니메이션은 너무 어려운것 같아요.
    감사합니다~

  • Time Spot

    안녕하세요 오늘 처음 여기 블로그를 읽었습니다. 다름아니라 샘플 데모페이지가 어느서버호스팅을 사용하시는지 궁금합니다. 헤로쿠의 경우 지역설정이 잘 안되어서 로딩속도가 다소 늦는데 엔터프라이즈로 가입해야 설정이 가능하다고 하고 가입경로는 알수없고 그러네요.. 아마존에 올리자니 배울것이 거창한듯하고..

    • 샘플페이지는 backend 서버가 없이 jsonplaceholder라는 서비스에서 가짜데이터를 가져오는거여서 surge.sh에 스태틱파일들을 올려서 했습니다.

      만약에 백엔드도 필요하시다면 digitalocean 이나 vultr를 추천드려요. 특히 vultr의 경우 일본서버를 사용할수있어 빠릅니다. digitalocean의 싱가폴 서버도 나쁘지 않아요

      Conoha란곳도 있는데, 가격대가 좀 싸긴 하지만 트래픽이 너무 많으면 차단처리 해버리고 백업도 힘들어서.. 비추입니다.

      규모가 커진다면 구글클라우드엔진이나 아마존에서 호스팅받는게 좋겠죠 ㅎㅎ

      • Time Spot

        네 감사합니다. 그런데 만약에 시연을 위해서 서버에 꼭 올려야하는게 아니라면 .. 그렇다고 해서 반드시 로컬에서 특정한 세팅된 컴퓨터에서만 시연이 가능하다면 그것도 번거롭고.. 결과물을 이것저것 설치안해도 되게끔 하나의 실행파일로 변환하는 방안은 없을까요. 모바일 앱이나 플래쉬 처럼 데스크탑용 설치형태로요..

  • 이방인

    스타일 적용할 때 저는 css와 js를 불리하지 않습니다. 한 컴퍼넌트안에 몰아 넣는게 요즘 흐름인거 같기도 하구요.. 뭐 그거랑 상관없이 편하기도 합니다.
    이런 상황에서는 @media를 어떻게 대체해야 할까요? 렌더링할때마다 devise의 width를 체크해서 style을 바꿔주는 방식으로 가는게 미디어쿼리랑 비교하면 많이 비효율적일까요? 일단 이렇게 하고 있습니다. 어떻게 생각하시는지요~

    componentDidMount = () => {
    window.addEventListener(‘resize’, () => {
    this.setState({
    width: document.documentElement.clientWidth
    })
    })
    }

    render() {
    return(
    768 ? styles.container : styles.container2}>
    {this.props.children}

    );
    }

  • 임기영

    안녕하세요.
    예전부터 벨로퍼트님 강좌를 읽기만 하다가 오늘에서야 따라해볼 결심이 생겨 천천히 작성 해보고 있습니다.

    내용 중 Navigate.css 작성하는 부분에서 파일명 및 경로가
    src/components/Navigator/Navigator.css 로 “Navigator” 로 되어있는데
    src/components/Navigate/Navigate.css 가 맞지 않을까 싶습니다.

    사소한거지만 저처럼 초보에겐 중요한거라 혹시나 하고 남겨봅니다.^^

    좋은 강좌 감사합니다.!
    너무 잘 보고 있습니다.

  • ggoban

    오 메모패드 끝내고 넘어왔습니다. 이번편은 구조가 훨씬 간단하고 좋은것 같습니다~!
    이 다음편에 Redux 편을 얼른 보고 싶네요. 이왕이면 Duck 구조로요 ㅎㅎㅎ

    • 고생하셨습니다! 요즘 바빠서 ㅎㅎㅎㅎ 쪼끔 기달려주세요~~ 🙂

  • moon

    막연하게 react를 어떻게 공부해야 할지 몰랐었는데… 너무 감사합니다!!! 큰 도움이 되었습니다.

    다음 강좌도 기대하겠습니다 .^^

  • Kyungbae Ro

    이번편 최고였음!
    redux 포스팅을 먼저보고,
    이 포스팅을 적용하여, 개인프로젝트를 진행하면서 컴포넌트를 만지다가 보니
    redux 필요성이 절실해 졌어요. ㅎㅎㅎ

  • AeroIsland

    안녕하세요 벨로퍼트님 궁금해서 그러는데 혹시 나이가 어떻게 되시나요?? 13년 입학이면 94년생이신가요? 만약 그렇다면 나이에 비해 기술스펙들이 ㅎㄷㄷ해서요.

  • HANSOO KIM

    좋은글 잘보고있습니다. !

  • 리액트화이팅

    감사합니다. Veloper님 강좌 덕분에 ReactJS 를 시작할수 있게 되었습니다.

  • 박다정

    여러페이지를 만들고 리액트 라우터로 URL 변경 처리한 후 surge.sh 올려봤는데, URL 이동하면 404에러가 나타납니다… 이부분은 어떻게 해야하나요?

  • dddog

    이번에 강좌가 순차적이라서 아주 이해하기 좋아서 많은 지식을 얻을 수 있었어요.. 감사합니다.

  • leecm

    우와 정말 감사합니다 !!!

  • Kyoung Rok Hwang

    좋은 강좌 감사드립니다. 군더더기 없고 정확한 설명에 이번에 처음 웹 개발에 도전해 보는 저에게 큰 도움이 되고 있습니다. 감사합니다!!!

  • 이상민

    좋은 강좌 잘 봤습니다.
    감사합니다!

  • 백재인

    함수형 에서 return 사용한 컴포넌트랑 안한거랑 잇던데 차이가 몬가여?
    둘다 랜더링은 되던데..

  • 백재인

    comoponents 디렉토리의 컴포넌트들 console.log 찍으면 두번씩 랜더링 되는데 이유를 알수있을까요??
    찾아보는데 해답이 없어서…ㅜ

  • 이지훈

    api 요청을 할때 velopert 님이 쓰신 그 api 말고는 api axios로 요청을 했을때 응답이 없고 오류가 납니다..
    cors 문제인 것 같은데 어떻게 해결하나요?

  • 김봉준

    감사합니다 ~ 책 출간하신것도 축하드려요~

  • Oscar won

    감사합니다 😉

  • 이보영

    몇일전부터 react에 관심이생겨 열심히 보고 있습니다. 세세한 부분까지 설명 또는 링크를 남겨주셔서 너무너무 감사합니다!
    좀 더 일찍 react에 관심이 생겼더라면 더 좋았을뻔 했습니다 ㅜ

  • boodafest

    훌륭한 강의 감사합니다.!!!!!!!!!!!! 한번 따라 쳐보면서 정말 많은 것을 배웠습니다.

  • 김준영

    감사합니다 블로그 너무 잘해주셔서 공부에 도움 많이 받고있습니다

  • 박상국

    좋은글 감사합니다. 궁금하던 부분이 많이 해결되었습니다.

  • kuki

    너무 잘봤습니다. axios를 공부하러 왔다가 재밌는 개념들 많이 익히고 가네요…

  • 너무나 잘 봤습니다. 좋은 강의 만들어 주셔서 감사합니다.
    잘되네요!