이번 튜토리얼에서는 Sass 를 사용하여 컴포넌트 스타일링을 연습삼아 해보고, NASA 에서 Open API 로 제공하는 Astronomy Picture of the Day (오늘의 우주사진)들을 가져와서 화면에 띄워주는 작업을 진행해보겠습니다.
리액트 프로젝트에서는 다양한 방법으로 컴포넌트를 스타일링 해줄 수 있는데요 (리액트 컴포넌트 스타일링 포스트), 그 중에서 우리는 Sass +CSS Module 의 조합으로 프로젝트를 만드는 방법을 실습해보겠습니다. 이 과정에서 프로미스기반의 HTTP Client 인 axios 를 사용하여 간단한 웹 요청을 처리하는 방법도 배워보겠습니다.
sass 를 잘 모르신다면 다음 자료들이 큰 도움이 될 것 입니다.
- https://velopert.com/1712
- https://sass-guidelin.es/ko/
추가적으로, CSS Module 이 익숙하지 않다면, 리액트 컴포넌트 스타일링 포스트를 쭉 훑어보고 오세요 🙂
이 포스트는 Fastcampus 의 오프라인 리액트 강의 에서 사용된 자료로서, 부연설명이 조금 생략되어있습니다.
프로젝트 코드는 Github 에서 확인 하실 수 있습니다.
작업환경 설정
create-react-app 으로 프로젝트를 생성하세요.
$ create-react-app nasa-apod
그 다음에는, 우리는 Sass 를 사용하기 위하여 프로젝트 설정을 커스터마이징 해주어야 하니 yarn eject
를 통하여 내부 설정들이 우리에게 보여질 수 있도록 밖으로 꺼내주세요.
$ cd create-react-app
$ yarn eject
필요한 모듈 설치
우리가 앞으로 프로젝트를 진행하면서 필요하게 될 라이브러리들을 설치하겠습니다.
$ yarn add axios classnames sass-loader node-sass include-media open-color better-react-spinkit react-icons moment
- axios: Promise 기반 웹 요청 클라이언트
- classnames: CSS Module 과 조건부 className 을 설정 하는 것을 도와주는 라이브러리
- sass-loader, node-sass: 프로젝트에서 Sass 를 사용하기 위하여 필요한 도구
- include-media, open-color: Sass 라이브러리 (반응형 디자인, 색상 팔레트)
- better-react-spinkit: 로딩 시 보여줄 컴포넌트
- react-icons: SVG 형태의 리액트 컴포넌트 모음 라이브러리
- moemnt: 날짜 관련 라이브러리
Sass + CSS Module 적용
우리의 프로젝트에는 Sass 와 CSS Module 을 함께 사용하겠습니다.
config 디렉토리 내부의 webpack.config.dev.js 파일을 열어서 style-loader 를 검색해보세요.
config/webpack.config.dev.js
– css 설정 부분
{
test: /\.css$/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebookincubator/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
'>1%',
'last 4 versions',
'Firefox ESR',
'not ie < 9', // React doesn't support IE8 anyway
],
flexbox: 'no-2009',
}),
],
},
},
],
},
이런 부분이 보여질 것입니다. 해당 부분을 그대로 복사하여 바로 아래에 붙여넣으세요. 그리고, 확장자를 scss 로 변경하고, css-loader 의 options 를 설정하고, 배열의 끝에 sass-loader 를 설정하세요.
config/webpack.config.dev.js
{
test: /\.css$/,
(...)
},
{
test: /\.scss$/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
modules: true,
localIdentName: '[name]__[local]__[hash:base64:5]'
},
},
{
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebookincubator/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
'>1%',
'last 4 versions',
'Firefox ESR',
'not ie < 9', // React doesn't support IE8 anyway
],
flexbox: 'no-2009',
}),
],
},
},
{
loader: require.resolve('sass-loader'),
options: {
includePaths: [paths.styles]
}
}
],
},
여기서, sass-loader 쪽에 includePaths 를 넣어주었는데, 이 값은 sass 에서 공통적으로 사용되는 유틸 함수들을 필요할 때 import ../../styles/utils
형식으로 작성 할 필요 없이 @import 'utils';
형태로 불러 올 수 있게 해주는 설정입니다.
그러려면, paths.styles 를 설정해주어야 하는데요, 이 값은 config.paths.js 파일 내부에 있습니다.
다음과 같이 파일의 맨 끝에 styles 값도 설정하세요.
config/paths.js
(...)
// config after eject: we're in ./config/
module.exports = {
dotenv: resolveApp('.env'),
appBuild: resolveApp('build'),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveApp('src/index.js'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveApp('src/setupTests.js'),
appNodeModules: resolveApp('node_modules'),
publicUrl: getPublicUrl(resolveApp('package.json')),
servedPath: getServedPath(resolveApp('package.json')),
styles: resolveApp('src/styles')
};
이제 yarn start
를 통하여 개발서버를 구동시키세요. 문제 없이 실행 되나요?
프로젝트 초기화
불필요한 파일들을 제거하겠습니다. 다음 파일들을 제거하세요:
- src/App.css
- src/App.test.js
- src/logo.svg
- src/index.css
이에 따라 코드들도 바꿔주어야겠죠? 우선 App 컴포넌트부터 비워줍시다.
src/App.js
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<div>
App
</div>
);
}
}
export default App;
그리고, index.js 에서 index.css 를 불러오는 코드를 지워주겠습니다.
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();
스타일 유틸과 base 스타일 설정
페이지의 기반 스타일링을 할 base 파일과, 추후 유틸로 불러와서 사용할 utils 파일을 생성해주겠습니다.
src 디렉토리에 styles 디렉토리를 만드세요. 그리고, 그 안에 utils.scss 파일을 작성하세요.
src/styles/utils.scss
@import '~open-color/open-color';
@import '~include-media/dist/include-media';
경로에 ~ 이 들어간다는 것은, node_modules 내부의 디렉토리를 사용한다는 것 입니다.
우리는 색상 팔레트와 반응형 디자인을 쉽게 해주는 라이브러리를 적용하였습니다. 현 프로젝트에서 위 라이브러리들이 필수적인 것은 아니지만, Sass 를 사용하면서 이런식으로 사용 할 수 있다는 것을 알아보고자 적용 한 것입니다.
그 다음에는 base.scss 파일을 작성하세요.
src/base.scss
@import 'utils';
body {
margin: 0;
background: $oc-gray-8;
box-sizing: border-box;
}
* {
box-sizing: inherit;
}
여기서 사용한 $oc-gray-8
이 open-color 에 등록된 색상 변수입니다. open-color 에 등록된 변수를 사용하려면, 가이드를 참고하여 거기서 나타나는 색상값과 명암값을 다음과 같은 형식으로 변수명으로 작성하면 됩니다: $oc-[색상]-[명암]
이 파이릉ㄹ 다 작성하셨다면, index.js 에서 불러오세요.
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import './styles/base.scss';
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();
이렇게 하고나면 페이지가 회색으로 변할 것입니다.
컴포넌트 틀 준비하기
우리는 앞으로 3가지의 컴포넌트를 만들게 됩니다. 그리고, 우리는 CSS Module 과, Sass 를 적용하여 컴포넌트를 만들기에, 각 컴포넌트마다 디렉토리를 하나씩 만들어 줄 것이고, 각 디렉토리에 3종류의 파일을 만들게됩니다.
- ComponentName.js
- ComponentName.scss
- index.js
index.js 의 경우엔, 우리가 나중에 컴포넌트를 불러오게 될 때 src/components/ComponentName/ComponentName
이 아니라 src/components/ComponentName
형식으로 불러올 수 있도록, 컴포넌트를 불러와서 바로 내보내주는 파일입니다. 다음과 같이 말이죠:
export { default } from './ComponentName';
각 컴포넌트 파일은 다음과 같은 형식을 갖추고 있습니다:
import React from 'react';
import styles from './ComponentName.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const ComponentName = () => {
return (
<div className={cx('component-name')}>
</div>
);
};
export default ComponentName;
그러면, 각 컴포넌트들의 틀을 준비해볼까요?
ViewerTemplate
이 컴포넌트는 템플릿 컴포넌트로서 JSX 형태의 props 인 viewer, spaceNavigator 를 받아와서 적당한 위치에 렌더링해줍니다.
다음 파일들을 생성하세요:
- src/components/ViewerTemplate/ViewerTemplate.js
- src/components/ViewerTemplate/ViewerTemplate.scss
- src/components/ViewerTemplate/index.js
src/components/ViewerTemplate/ViewerTemplate.js
import React from 'react';
import styles from './ViewerTemplate.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const ViewerTemplate = ({ viewer, spaceNavigator }) => {
return (
<div className={cx('viewer-template')}>
</div>
);
};
export default ViewerTemplate;
src/components/ViewerTemplate/ViewerTemplate.scss
.viewer-template {
}
src/components/ViewerTemplate/index.js
export { default } from './ViewerTemplate';
Viewer
이 컴포넌트는 이미지 혹은 동영상을 보여주는 컴포넌트입니다. 데이터의 형식은 mediaType 에 “video” 혹은 “image” 라는 값으로 전달이 될 것이고, 이에 따라 url 을 사용하여 동영상이나 이미지를 보여주게 됩니다. 추가적으로, loading 값은 데이터를 불러올 때 로딩표시를 하기 위하여 사용되는 props 입니다.
- src/components/Viewer/Viewer.js
- src/components/Viewer/Viewer.scss
- src/components/Viewer/index.js
src/components/Viewer/Viewer.js
import React from 'react';
import styles from './Viewer.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const Viewer = ({mediaType, url, loading}) => {
return (
<div className={cx('viewer')}>
</div>
);
};
export default Viewer;
src/components/Viewer/Viewer.scss
.viewer {
}
src/components/Viewer/index.js
export { default } from './Viewer';
SpaceNavigator
이 컴포넌트는 앞, 혹은 뒤로 넘기는 버튼들을 내장하고 있습니다. 각 버튼에 연결 될 함수 onPrev 와 onNext 를 props 로 받습니다.
Navigator 가 아닌 SpaceNavigator 라는 이름을 붙여준 이유는 브라우저 상에 이미 Navigator 값이 존재하기 때문입니다.
- src/components/SpaceNavigator
/SpaceNavigator
.js - src/components/SpaceNavigator
/SpaceNavigator.scss - src/components/SpaceNavigator/index.js
src/components/SpaceNavigator/SpaceNavigator.js
import React from 'react';
import styles from './SpaceNavigator.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const SpaceNavigator = ({ onPrev, onNext }) => {
return (
<div className={cx('space-navigator')}>
</div>
);
};
export default SpaceNavigator;
src/components/SpaceNavigator/SpaceNavigator.scss
.space-navigator {
}
src/components/SpaceNavigator/index.js
export { default } from './SpaceNavigator';
후! 드디어 끝났습니다. 이렇게 컴포넌트를 만들 때 마다 세가지의 파일을 만드는 것이 번거롭다면, 도구의 힘을 빌려보는 것도 좋습니다. VS Code 의 generate-react-component 라는 패키지를 사용하면 컴포넌트를 좀 더 편리하게 만들 수 있습니다. (이 부분은 나중에 알아보겠습니다. 커스터마이징이 조금 필요합니다.)
ViewerTemplate 컴포넌트 완성하기
ViewerTemplate 컴포넌트를 완성해봅시다.
`src/components/ViewerTemplate/ViewerTemplate.js
import React from 'react';
import styles from './ViewerTemplate.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const ViewerTemplate = ({ viewer, spaceNavigator }) => (
<div className={cx('viewer-template')}>
<header>
Astronomy Picture of the Day
</header>
<div className={cx('viewer-wrapper')}>
{viewer}
<div className={cx('space-navigator-wrapper')}>
{spaceNavigator}
</div>
</div>
</div>
);
export default ViewerTemplate;
space-navigator-wrapper 를 viewer-wrapper 내부에 넣어준 이유는, 추후 SpaceNavigator 컴포넌트에서 위치선정을 하게 될 때 viewer-wrapper 의 크기에 기반하여 설정 할 것이기 때문입니다.
src/components/ViewerTemplate/ViewerTemplate.scss
@import 'utils';
.viewer-template {
header {
background: $oc-gray-9;
height: 5rem;
color: white;
padding: 1rem;
display: flex;
align-items: center;
font-size: 2rem;
font-weight: 600;
// 태블릿 사이즈에서 폰트 크기 줄이기
@include media("<tablet") {
font-size: 1.25rem;
}
}
.viewer-wrapper {
position: relative;
width: 100%;
height: calc(100vh - 5rem); // 페이지에서 헤더를 제외한 영역 모두 채우기
}
}
그럼 이 컴포넌트를 App 에서 불러와서 사용해볼까요?
src/App.js
import React, { Component } from 'react';
import ViewerTemplate from './components/ViewerTemplate';
class App extends Component {
render() {
return (
<ViewerTemplate/>
);
}
}
export default App;
그럼, 이러한 모양이 완성됩니다:
SpaceNavigator 완성하기
이 컴포넌트에선 좌측과 우측에 버튼을 만들어 줄 것인데요, 이 과정에서 우리는 react-icons 에서 받은 아이콘을 사용해보게 됩니다.
react-icons 에서 제공 가능한 아이콘 목록은 여기 에서 확인 하실 수 있습니다.
src/components/SpaceNavigator/SpaceNavigator.js
import React from 'react';
import styles from './SpaceNavigator.scss';
import classNames from 'classnames/bind';
import LeftIcon from 'react-icons/lib/md/chevron-left';
import RightIcon from 'react-icons/lib/md/chevron-right';
const cx = classNames.bind(styles);
const SpaceNavigator = ({onPrev, onNext}) => (
<div className={cx('space-navigator')}>
<div className={cx('left', 'end')}>
<div className={cx('circle')} onClick={onPrev}>
<LeftIcon/>
</div>
</div>
<div className={cx('right', 'end')}>
<div className={cx('circle')} onClick={onNext}>
<RightIcon/>
</div>
</div>
</div>
);
export default SpaceNavigator;
src/components/SpaceNavigator/SpaceNavigator.scss
@import 'utils';
.space-navigator {
.end {
position: absolute;
top: 0;
height: 100%;
display: flex;
align-items: center;
color: white;
&.left {
padding: 1rem;
}
&.right {
right: 1rem;
}
.circle {
width: 3rem;
height: 3rem;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
cursor: pointer;
&:hover {
background: rgba(0,0,0,0.25);
}
&:active {
background: rgba(0,0,0,0.5);
}
svg {
font-size: 2rem;
}
}
}
}
컴포넌트를 다 작성하셨다면, SpaceNavigator 를 App 에서 불러와서 ViewerTemplate 안에 spaceNavigator 값으로 설정해주세요.
src/App.js
import React, { Component } from 'react';
import ViewerTemplate from './components/ViewerTemplate';
import SpaceNavigator from './components/SpaceNavigator';
class App extends Component {
render() {
return (
<ViewerTemplate
spaceNavigator={<SpaceNavigator/>}
/>
);
}
}
export default App;
그러면 이제 페이지의 양 쪽에 화살표가 보여질것입니다.
Viewer 컴포넌트 완성하기
우리가 나중에 호출 할 NASA Open API 는 두가지 형태의 데이터를 반환합니다.
{
"media_type": "video",
"url": "https://www.youtube.com/embed/uj3Lq7Gu94Y?rel=0"
}
이렇게 유튜브 비디오를 반환 할 때도 있고,
{
"media_type": "image",
"url": "https://apod.nasa.gov/apod/image/1712/GeminidsYinHao1024.jpg"
}
이렇게 이미지를 반환 할 때도 있습니다. 우리의 Viewer 컴포넌트에서는 각 상황에 따라 알맞는 뷰를 보여주도록 설정하겠습니다.
우선, Viewer 컴포넌트를 작성하기 전에, 먼저 App 에서 불러와서 이미지 형태의 데이터를 주입해보세요.
src/App.js
import React, { Component } from 'react';
import ViewerTemplate from './components/ViewerTemplate';
import SpaceNavigator from './components/SpaceNavigator';
import Viewer from './components/Viewer';
class App extends Component {
render() {
return (
<ViewerTemplate
spaceNavigator={<SpaceNavigator/>}
viewer={(
<Viewer
url="https://apod.nasa.gov/apod/image/1712/GeminidsYinHao1024.jpg"
mediaType="image"/>
)}
/>
);
}
}
export default App;
이렇게 props 를 직접 주입해주었습니다. 이제 Viewer 컴포넌트에서 이에 따라 렌더링을 해봅시다.
src/components/Viewer/Viewer.js
import React from 'react';
import styles from './Viewer.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const Viewer = ({mediaType, url, loading}) => {
if(!url) return null;
return (
<div className={cx('viewer')}>
{
mediaType === 'image' ? (
<img onClick={() => window.open(url)} src={url} alt="space"/>
) : (
<div/>
)
}
</div>
);
};
export default Viewer;
src/components/Viewer/Viewer.scss
@import 'utils';
.viewer {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
img {
display: block;
width: auto;
max-width: calc(100% - 10rem);
max-height: calc(100% - 10rem);
cursor: pointer;
transition: all 0.3s ease-in;
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
&:hover {
box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
}
}
}
여기까지 작성을 하고, 페이지를 새로 불러오면 다음과 같은 결과가 나타납니다:
멋진 우주 사진이 나타났지요? 이번에는, 동영상을 렌더링 해봅시다. App 컴포넌트에서 전달하는 props 를 다음과 같이 변경하세요.
src/App.js
import React, { Component } from 'react';
import ViewerTemplate from './components/ViewerTemplate';
import SpaceNavigator from './components/SpaceNavigator';
import Viewer from './components/Viewer';
class App extends Component {
render() {
return (
<ViewerTemplate
spaceNavigator={<SpaceNavigator/>}
viewer={(
<Viewer
url="https://www.youtube.com/embed/uj3Lq7Gu94Y?rel=0"
mediaType="video"/>
)}
/>
);
}
}
export default App;
이렇게 video 형태로 전달이 되었을땐, iframe 태그를 사용해서 보여줍니다.
src/components/Viewer/Viewer.js
import React from 'react';
import styles from './Viewer.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const Viewer = ({mediaType, url, loading}) => {
if(!url) return null;
return (
<div className={cx('viewer')}>
{
mediaType === 'image' ? (
<img onClick={() => window.open(url)} src={url} alt="space"/>
) : (
<iframe title="space-video" src={url} frameBorder="0" gesture="media" allow="encrypted-media" allowFullScreen></iframe>
)
}
</div>
);
};
export default Viewer;
그리고, iframe 부분을 위한 스타일링도 해줘야겠지요?
src/components/Viewer/Viewer.scss
@import 'utils';
.viewer {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
img {
display: block;
width: auto;
max-width: calc(100% - 10rem);
max-height: calc(100% - 10rem);
cursor: pointer;
transition: all 0.3s ease-in;
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
&:hover {
box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
}
}
iframe {
background: black;
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
width: calc(100% - 10rem);
height: calc(100% - 10rem);
}
}
여기까지 하시면 동영상도 적당한 사이즈로 나옵니다
API 사용하기
API Key 요청
현재 NASA OpenAPI 는 하루에 1000회의 실행제한이 설정되어있습니다. 만약에 이 튜토리얼을 진행하는 사람들이 모두 같은 API 키를 사용하게 된다면, 1000번의 요청 횟수를 초과하면 그 날은 작동하지 않을 것입니다. 그러므로, API 키를 따로 신청하여 진행하세요.
NASA API 페이지 에 들어가서 스크롤을 조금 내리면 다음과 같이 API Key 요청 폼이 있습니다. 이 양식을 채우시면 이메일로 키가 전달됩니다.
신청 양식
이메일
위에 빨간색 텍스트가 바로 API Key 입니다.
Promise 란?
우리는 Promise 기반으로 웹 요청 라이브러리 axios 를 사용해볼것입니다. Promise
란 ES6 에서 비동기 처리를 다루기 위해 사용되는 객체입니다. 예를들어서, 숫자를 1초뒤에 콘솔에 기록하는 코드를 작성해보겠습니다. (해당 코드를 크롬 개발자 도구를 통하여 따라해보세요. 새 줄 입력은 SHIFT + Enter)
function printLater(number) {
setTimeout(
function() {
console.log(number);
},
1000
);
}
printLater(1);
이렇게 printLater 함수를 호출하면 1초뒤에 콘솔에 기록합니다. 이번엔, 1초에 걸쳐서 숫자를 더해가며 1,2,3,4 를 호출하는 코드를 작성해보세요.
function printLater(number, fn) {
setTimeout(
function() { console.log(number); fn(); },
1000
);
}
printLater(1, function() {
printLater(2, function() {
printLater(3, function() {
printLater(4);
})
})
})
비동기적으로 해야 할 작업이 많아진다면, 코드의 구조는 자연스레 깊어질 것이고 그러면 코드를 읽기 힘들어지겠죠? 이를 콜백 지옥이라고도 부릅니다.
기존의 자바스크립트의 이러한 문제에서 구제해주는것이 바로 Promise 입니다. 한번 위 코드를 Promise 로 해결해보겠습니다. 추가적으로, 코드를 더 읽기 쉽게 작성하기위해서 화살표 함수도 사용해볼게요.
function printLater(number) {
return new Promise( // 새 Promise 를 만들어서 리턴함
resolve => {
setTimeout( // 1초뒤 실행하도록 설정
() => {
console.log(number);
resolve(); // promise 가 끝났음을 알림
},
1000
)
}
)
}
몇번 하던간에 코드의 깊이는 일 합니다. 따라서 콜백지옥에 빠질일이 없겠죠?
Promise 에서는 값을 리턴 하거나, 에러를 발생 시킬 수도 있습니다.
코드를 다음과 같이 입력해보세요.
```javascript
function printLater(number) {
return new Promise( // 새 Promise 를 만들어서 리턴함
(resolve, reject) => { // resolve 와 reject 를 파라미터로 받습니다
setTimeout( // 1초뒤 실행하도록 설정
() => {
if(number > 5) { return reject('number is greater than 5'); } // reject 는 에러를 발생시킵니다
resolve(number+1); // 현재 숫자에 1을 더한 값을 반환합니다
console.log(number);
},
1000
)
}
)
}
printLater(1)
.then(num => printLater(num))
.then(num => printLater(num))
.then(num => printLater(num))
.then(num => printLater(num))
.then(num => printLater(num))
.then(num => printLater(num))
.then(num => printLater(num))
.catch(e => console.log(e));
결과:
1
2
3
4
5
number is greater than 5
API 요청 함수 만들기
src 디렉토리 내부에 lib 라는 디렉토리를 만들고, 그 안에 api.js 를 만드세요. 현재 프로젝트의 경우엔 해당 요청 함수를 바로 App 에서 선언 하여도 무방하지만, 대부분의 프로젝트에서는 다루는 API 의 갯수가 많으므로 이를 따로 파일로 분리하여 관리하면 조금 편해집니다.
src/lib/api.js
import axios from 'axios';
export function getAPOD(date = '') {
return axios.get(`https://api.nasa.gov/planetary/apod?api_key=5q6uswo7lQPq6HcC05xDRdcoikRkPCVdIqk6mbxe&date=${date}`);
}
api_key= 부분에 여러분이 받은 key 를 직접 넣어주세요. 추가적으로, 위 함수에서는 date 의 기본값을 공백으로 설정하였습니다. 만약에 date 가 주어지지 않았는데 ES6 template literal 문법 ` 가 사용된다면 undefined 가 전달됩니다. undefind 가 전달되면 서버쯕에서 처리하지 못하므로, 해당 값이 비어있을 땐 공백을 넣도록 설정하세요.
API 함수 사용하기
자, 우리가 만든 API 를 사용해봅시다. 컴포넌트에서 API 를 요청을 하고, API 가 응답되었을때 특정 작업을 하려면 다음과 같이 Promise 의 then 을 사용 할 수 있습니다
src/App.js
import React, { Component } from 'react';
import ViewerTemplate from './components/ViewerTemplate';
import SpaceNavigator from './components/SpaceNavigator';
import Viewer from './components/Viewer';
import * as api from './lib/api';
class App extends Component {
getAPOD = (date) => {
api.getAPOD(date).then((response) => {
console.log(response);
});
}
componentDidMount() {
this.getAPOD();
}
render() {
return (
<ViewerTemplate
spaceNavigator={<SpaceNavigator/>}
viewer={(
<Viewer
url="https://www.youtube.com/embed/uj3Lq7Gu94Y?rel=0"
mediaType="video"/>
)}
/>
);
}
}
export default App;
페이지를 열어서 다음과 같이 response 객체가 잘 기록되는지 확인해보세요.
비동기 작업을 하게 될 때, Promise 도 이미 충분히 편하지만, 더욱 편하게 사용하는 방법이 있습니다. 바로 async/await 를 사용하는 것이죠.
async 를 사용하려면 함수 선언을 하게 될 때 앞부분에 async 키워드를 붙여주어야 합니다. 그리고, 내부에서는 Promise 의 앞부분에 await 키워드를 넣어서 사용하면 됩니다.
src/App.js
import React, { Component } from 'react';
import ViewerTemplate from './components/ViewerTemplate';
import SpaceNavigator from './components/SpaceNavigator';
import Viewer from './components/Viewer';
import * as api from './lib/api';
class App extends Component {
getAPOD = async (date) => {
try {
const response = await api.getAPOD(date);
console.log(response);
} catch (e) {
// 오류가 났을 경우
console.log(e);
}
}
componentDidMount() {
this.getAPOD();
}
render() {
return (
<ViewerTemplate
spaceNavigator={<SpaceNavigator/>}
viewer={(
<Viewer
url="https://www.youtube.com/embed/uj3Lq7Gu94Y?rel=0"
mediaType="video"/>
)}
/>
);
}
}
export default App;
결과가 아까처럼 나타나는지 확인하세요.
상태 관리하기
그럼, 요청 값에 따라 컴포넌트의 상태를 관리해봅시다.
src/App.js
import React, { Component } from 'react';
import ViewerTemplate from './components/ViewerTemplate';
import SpaceNavigator from './components/SpaceNavigator';
import Viewer from './components/Viewer';
import * as api from './lib/api';
class App extends Component {
state = {
loading: false,
maxDate: null,
date: null,
urL: null,
mediaType: null
}
getAPOD = async (date) => {
if (this.state.loading) return; // 이미 요청중이라면 무시
// 로딩 상태 시작
this.setState({
loading: true
});
try {
const response = await api.getAPOD(date);
// 비구조화 할당 + 새로운 이름
const { date: retrievedDate, url, media_type: mediaType } = response.data;
if(!this.state.maxDate) {
// 만약에 maxDate 가 없으면 지금 받은 date 로 지정
this.setState({
maxDate: retrievedDate
})
}
// 전달받은 데이터 넣어주기
this.setState({
date: retrievedDate,
mediaType,
url
});
} catch (e) {
// 오류가 났을 경우
console.log(e);
}
// 로딩 상태 종료
this.setState({
loading: false
});
}
componentDidMount() {
this.getAPOD();
}
render() {
const{ url, mediaType, loading } = this.state;
return (
<ViewerTemplate
spaceNavigator={<SpaceNavigator/>}
viewer={(
<Viewer
url={url}
mediaType={mediaType}
loading={loading}/>
)}
/>
);
}
}
export default App;
- 요청이 시작하면 loading 값을 true 로, 끝났다면 false 로 설정합니다.
- 나중에 다음 이미지를 보여주게 될 때, 오늘 이후의 데이터는 존재하지 않기 때문에 maxDate 를 설정해둡니다.
- 비구조화 할당이 되는 과정에서, 새로운 이름으로 값을 지정했습니다. 예를들어, 위의 코드는 response.data 안에 있는 media_type 이란 값을 mediaType 이라고 부르겠다 라는 의미와 동일합니다.
- 상태의 url, mediaType, loading 값을 Viewer 로 전달해줍니다.
이렇게 입력해놓고, 페이지에서 이미지 혹은 동영상이 제대로 나타나는지 확인하세요.
다음 / 이전 이미지로 넘기기
이제 다음 / 이전 이미지로 넘기는 작업을 진행하겠습니다. 다음 혹은 이전 이미지로 넘길 때에는, API 에서 처음 주어진 날짜 (오늘) 를 기준으로 하루전, 이틀전 데이터를 불러 올 수 있으며, 과거의 이미지를 보고 있을 때에는 다시 다음날, 그 다음날 사진으로 넘길 수도 있습니다. 날짜의 파라미터는 YYYY-MM-DD 형태로 전달이 되어야 하는데됴, 직접 Date 객체를 사용하여 날짜를 더하고 빼고 원하는 형식으로 프린트를 할 수 있긴 하겠지만, 편의상 moment 라는 라이브러리를 사용하겠습니다.
handleNext 와 handlePrev 메소드를 구현 후, SpaceNavigator 의 onNext 와 onPrev 로 연결하세요.
src/App.js
import React, { Component } from 'react';
import ViewerTemplate from './components/ViewerTemplate';
import SpaceNavigator from './components/SpaceNavigator';
import Viewer from './components/Viewer';
import moment from 'moment';
import * as api from './lib/api';
class App extends Component {
state = {
loading: false,
maxDate: null,
date: null,
urL: null,
mediaType: null
}
getAPOD = async (date) => {
if (this.state.loading) return; // 이미 요청중이라면 무시
// 로딩 상태 시작
this.setState({
loading: true
});
try {
const response = await api.getAPOD(date);
// 비구조화 할당 + 새로운 이름
const { date: retrievedDate, url, media_type: mediaType } = response.data;
if(!this.state.maxDate) {
// 만약에 maxDate 가 없으면 지금 받은 date 로 지정
this.setState({
maxDate: retrievedDate
})
}
// 전달받은 데이터 넣어주기
this.setState({
date: retrievedDate,
mediaType,
url
});
} catch (e) {
// 오류가 났을 경우
console.log(e);
}
// 로딩 상태 종료
this.setState({
loading: false
});
}
handlePrev = () => {
const { date } = this.state;
const prevDate = moment(date).subtract(1, 'days').format('YYYY-MM-DD');
console.log(prevDate);
this.getAPOD(prevDate);
}
handleNext = () => {
const { date, maxDate } = this.state;
if(date === maxDate) return;
const nextDate = moment(date).add(1, 'days').format('YYYY-MM-DD');
this.getAPOD(nextDate);
}
componentDidMount() {
this.getAPOD();
}
render() {
const{ url, mediaType, loading } = this.state;
const { handlePrev, handleNext } = this;
return (
<ViewerTemplate
spaceNavigator={<SpaceNavigator onPrev={handlePrev} onNext={handleNext}/>}
viewer={(
<Viewer
url={url}
mediaType={mediaType}
loading={loading}/>
)}
/>
);
}
}
export default App;
자, 이제 페이지를 열어서 왼쪽 버튼을 눌러보고, 오른쪽 버튼도 눌러보세요. 화면이 잘 바뀌고 있나요?
로딩시 로더 보여주기
데이터를 로딩중일 때에는 로더를 보여주도록 설정할건데요, 로더를 보여주는 방법은 SVG 를 사용하거나, CSS 를 사용하거나, 정말 여러가지 방법이 있겠지만, 우리는 최소한의 공수를 사용하여 구현하기 위하여 better-react-spinkit 이라는 라이브러리를 사용하겠습니다.
구현방법은 정말 간단합니다. Viewer 컴포넌트에서 loading 값이 참이라면 로더를 렌더링하세요. 색상과 크기는 props 로 지정해줄 수 있습니다. (로더의 종류가 여러개 있으니 링크에 들어가서 원하는걸 골라보세요)
src/components/Viewer/Viewer.js
import React from 'react';
import styles from './Viewer.scss';
import classNames from 'classnames/bind';
import {
ChasingDots
} from 'better-react-spinkit'
const cx = classNames.bind(styles);
const Viewer = ({mediaType, url, loading}) => {
if(loading) {
// 로딩중일때 로더 보여주기
return <div className={cx('viewer')}>
<ChasingDots color="white" size={60}/>
</div>
}
if(!url) return null;
return (
<div className={cx('viewer')}>
{
mediaType === 'image' ? (
<img onClick={() => window.open(url)} src={url} alt="space"/>
) : (
<iframe title="space-video" src={url} frameBorder="0" gesture="media" allow="encrypted-media" allowFullScreen></iframe>
)
}
</div>
);
};
export default Viewer;
이제 로딩을 하게 될 땐 멋진 로더가 나타날 것입니다!
마치면서
이번 튜토리얼에서는 리액트의 컴포넌트 활용법과 스타일링 방법들을 배워가면서, 웹연동을 간단히 해보는 시간을 가졌습니다. 이번 프로젝트처럼, 규모가 작은 프로젝트는 App.js 에서 모든 상태를 관리하는 것이 충분하지만, 규모가 좀 커지면 모든 상태를 App 에서 관리하려면 조금 버거워질수도있습니다. 코드의 양이 App 쪽에 너무 많이 쏠릴 수도 있구요.
예를 들어서, 우리가 만들 웹 어플리케이션이 여러 페이지로 구성이 되어있다면, 각 페이지에서 공유하는 상태가 존재 할 수도 있습니다.
그럴 땐, 리덕스를 배울 차례입니다! (리덕스 관련 포스트는 여기에서 확인 하실 수 있으며, 조만간 새로 개선된 포스트가 올라올 예정입니다)