서버리스 아키텍처 소셜미디어 개발기 5편

어느 새 5편이 되었네요.

서버리스 아키텍처 소셜미디어 개발기 4편
_지난 편에서는 Editor 에서 데이타를 어떻게 나눌지살펴보았고 Firebase 기초, 테스트 코드를 작성해 보았습니다. 이번에는 그 기반으로 어떻게 실제로 데이타를 입력하고 리스트를 작성하는지 확인해 보겠습니다._techstory.shma.so

사용자 스토리3

편집기를 끝을 내는 걸로~

김개발이 작성한 글이 목록으로 보여진다. 이렇게 함으로써 다른 사람들이 목록을 확인할 수 있다.

이번에는 글 목록과 편집기를 풍성하게 해 줄 카드를 만들도록 하겠습니다.

$git checkout day4

를 통해 day4 branch 부터 시작하는 것을 잊으면 안되겠지요.

1. Card 만들기

사이트 링크가 편집기에 작성이 되면 어떻게 보여질 것인지에 대한 정의가 이번에는 내려져야 할 것입니다.

[그림1] 카드 UI

그림1에서 보여지는 것과 같이 썸네일 이미지는 그 이미지의 url과 width와 height가 있어야 제대로 화면에 보여질 수 있을 것이고, 제목(title)과 설명(description) 그리고 사이트의 링크가 포함되어져 있어야 할 것입니다. 이렇게 카드를 만들고 나면 이 카드는 목록 작업에도 똑같이 이루어질 수 있을 것입니다.

그러면 일단, Article로 만들어 놓은 Mock 데이타를 바탕으로 진행을 해 보도록 하겠습니다.

먼저 카드를 한번 만들어 볼까요? src 폴더 아래 Card.js라는 파일을 만들고 아래와 같이 작성해 줍니다.

import React, { Component } from 'react'  
import './Card.css'

export default class Card extends Component {  
render(){  
let cardInfo = this.props.cardInfo;  
if(cardInfo){  
return(  
<a className="card" href={cardInfo.url} target="\_blank"\>  
<div className="card\_image" \>  
<img src={cardInfo.thumbnail\_url} alt={cardInfo.title} className="width100 card\_img"/\>  
</div\>  
<div className="borderTop"\>  
<div className="card\_text"\>  
<p className="card\_title"\>{cardInfo.title}</p\>  
<p className="card\_desc"\>{cardInfo.description}</p\>  
<p className="card\_provider"\>{cardInfo.provider\_url}</p\>  
</div\>  
</div\>  
</a\>  
)  
}else{  
return null;  
}  
}  
}

똑같이 Card.css를 다음과 같이 만들어 줍니다.

.card{  
color: \#222;  
text-decoration: none;  
position: relative;  
border: 1px solid rgba(79, 73, 75, 0.28);  
display: block;  
overflow: hidden;  
margin: 12px 12px 12px 12px;  
min-height: 83px;  
font-size: 10px;  
}  
.card\_image{  
overflow: hidden;  
left: 0;  
top: 0;  
max-height: 180px;  
}  
.width100{  
width: 100%;  
height:auto;  
background-size: 100% 100%;  
}  
.card\_img{  
height: 100%;  
transform: translateX(-50%);  
position: relative;  
left: 50%;  
border-bottom: 1px solid rgba(79, 73, 75, 0.28);  
}  
.card\_text{  
margin-left: 5px;  
min-height: 80px;  
padding: 5px;  
box-sizing: border-box  
}  
.card\_title{  
margin: 0;  
font-size: 12px;  
font-weight: bold;  
display: inline-block;  
white-space: nowrap;  
overflow: hidden;  
text-overflow: ellipsis;  
}  
.card\_desc{  
margin: 5px 0 0;  
font-size: 11px;  
display: inline-block;  
overflow: hidden;  
text-overflow: ellipsis;  
/\* 여러 줄 자르기 추가 스타일 \*/  
white-space: normal;  
line-height: 1;  
height: 3em;  
}  
.card\_provider {  
margin: 5px 0 0;  
font-size: 10px  
}  
.borderTop{  
border-top: 1px solid rgba(79, 73, 75, 0.28);  
}

이렇게 까지 적용하고 나면 카드를 위한 적용사항은 모두 진행 되었다고 볼 수 있습니다.한번 Mock 데이타를 가지고 확인해 볼까요? Test 를 눈으로 해 보기 위해서 이번에는 App.js를 변경해 보겠습니다.

그 전에 Article 은 다음과 같이 변경해서 불확실성을 좀 줄이도록 하겠습니다. urls를 배열로 받은 부분은cardInfo라는 단일 객체로 변경하겠습니다.

export default function getArticle(  
user = "Genji",  
content = "겐지가 함께한다.",  
url = "[https://namu.wiki/w/%EA%B2%90%EC%A7%80(%EC%98%A4%EB%B2%84%EC%9B%8C%EC%B9%98)][anchor2]",  
title="겐지(오버워치)",  
description = "블리자드 엔터테인먼트 사의 FPS 게임 오버워치의 영웅.기계가 되어버린 몸을 받아들여 내면의 평화를 찾은 강력한 사이보그 닌자.",  
thumbnail\_url = "[https://image-proxy.namuwikiusercontent.com/r/http%3A%2F%2Fi66.tinypic.com%2F10mpje9.jpg][anchor3]" ,  
thumbnail\_width = 80,  
thumbnail\_height =80,  
provider\_name = "namu wiki"  
){  
return {  
user : user,  
content : content,  
cardInfo:{  
url : url,  
title:title,  
description : description,  
thumbnail\_url : thumbnail\_url,  
thumbnail\_width : thumbnail\_width,  
thumbnail\_height :thumbnail\_height,  
provider\_name : provider\_name  
}  
}  
}

이후 App.js파일은 다음과 같이 변경합니다.

import Card from './Card'  
import getArticle from './Article'

class App extends Component {  
constructor(){  
//...중략..  
this.cardInfo = getArticle().cardInfo;  
}  
//...중략..  
render() {  
//...중략...  
<Card cardInfo = {this.cardInfo}/\>  
}  
}

이렇게 확인해 보면 다음과 같은 화면을 확인할 수 있습니다.

하지만 일단 Card를 확인했으면 App.js 파일은 되돌리고, Article.js파일은 __tests__ 파일 폴더 아래로 내립니다.

이럴때 사용하기 좋은 툴이 git 인데 App.js 파일 이외의 파일들은 커밋하고 reset 해 버리면 원래 파일로 원상복귀 됩니다.

//환경이 다를 수 있지만 참조하세요  
$standup\>mv Article.js ./\_\_tests\_\_/Article.js  
$standup\>git add Card.js Card.css ./\_\_tests\_\_/Article.js  
$standup\>git commit -m "Card added and Article moved"  
$standup\>git reset --hard

이렇게 Article.js파일을 테스트 파일로 빼 버리는 거는 Mock으로써 이제 그 역할을 다 했기 때문이고, 실제 돌아가는 수행에 필요한 역할을 다 했기 때문입니다. 이제 이 Card에 들어갈 데이타를 얻어올 방법을 찾아볼까요?

2.Embed.ly

Embed.ly 서비스는 oEmbed open format을 토대로 만들어진 contents 삽입(embed) 서비스 입니다.

예를들어 youtube 같은 provier의 경우

[http://www.youtube.com/oembed?url=http%3A//youtube.com/watch%3Fv%3DM3r2XDceM6A&format=json][anchor4]

의 형태로 질의를 보내면

{  
"version": "1.0",  
"type": "video",  
"provider\_name": "YouTube",  
"provider\_url": "[http://youtube.com/][anchor5]",  
"width": 425,  
"height": 344,  
"title": "Amazing Nintendo Facts",  
"author\_name": "ZackScott",  
"author\_url": "[http://www.youtube.com/user/ZackScott][anchor6]",  
"html":  
"<object width=\\"425\\" height=\\"344\\"\>  
<param name=\\"movie\\" value=\\"[http://www.youtube.com/v/M3r2XDceM6A&fs=1\\][anchor7]"\></param\>  
<param name=\\"allowFullScreen\\" value=\\"true\\"\></param\>  
<param name=\\"allowscriptaccess\\" value=\\"always\\"\></param\>  
<embed src=\\"[http://www.youtube.com/v/M3r2XDceM6A&fs=1\\][anchor7]"  
type=\\"application/x-shockwave-flash\\" width=\\"425\\" height=\\"344\\"  
allowscriptaccess=\\"always\\" allowfullscreen=\\"true\\"\></embed\>  
</object\>",  
}

의 형태로 답변을 주게 되어 있습니다.

이렇게 oEmbed API 포맷을 통해 다른 사이트의 컨텐츠를 쉽게 블로그나 게시판에 삽입(embed)할 수 있는 것이죠.

하지만, 우리가 하려고 하는 것은 각각의 사이트에서 사이트의 타이틀과 description, 이미지들을 뽑아 내는 일들을 해야합니다. 보통 오픈 소스들을 찾아서 개발을 하려고 보면 Facebook에서 사용중인 og meta 태그들을 많이 사용하고 있습니다. 페이스 북이 카드로 인식하게 하려면 og 태그를 써야하는 셈이 된 것인거죠.
게다가 og 태그를 아직 쓰지 않은 작은 사이트들은 이런 부분에 대한 지원이 없어서 카드에 이미지는 안나오는 경우를 종종 보곤 합니다.

이런 여타 사이트들을 돌아 다니면서 해 줘야 하는 작업들. 예를 들어 og 태그가 없을 때 어떻게 처리할 지,이미지가 없을때 어떻게 처리할지 등등, 그리고 결론적으로 내가 원하는 JSON 으로 받고 싶은 데이타 처리 작업 등의 일들을 oEmbed 포맷 API로 제공을 해 주는 사이트가 이 Embed.ly 입니다.

우리는 여기에서 우리가 Card를 만들 때 드는 정보를 얻어낼 것입니다.

그림3

이 API를 얻는 다는 것은 API key를 우선 얻는다는 것이겠죠?

회원가입이 꽤나 재밌는데 일단 개발자인지를 확인하는 문제를 제출합니다. 너무 쉬워서 하품이 날 지경이니 긴장은 안하셔도 됩니다.

가입을 하고 나면 처음은 프로젝트를 만들게 됩니다. standup 으로 만들어 보겠습니다.

그리고 팀 멤버를 초대하는 화면은 skip하셔도 됩니다. 이후 email 확인을 하고 나면 서비스를 이용할 수 있는데
Manage API key를 통해 발급받을 수 있습니다.

회원 가입을 하고 로그인 하면 다음과 같이 카드나 iframe 소스를 얻는 실습을 해볼 수도 있습니다.

카드 API

그리고 팀 멤버를 초대하는 화면은 skip하셔도 됩니다. 이후 email 확인을 하고 나면 서비스를 이용할 수 있는데 Manage API key를 통해 발급받을 수 있습니다.

회원 가입을 하고 로그인 하면 다음과 같이 카드나 iframe 소스를 얻는 실습을 해볼 수도 있습니다.

var firebase\_config = {  
apiKey: process.env.REACT\_APP\_FIREBASE\_KEY,  
authDomain: process.env.REACT\_APP\_AUTH\_DOMAIN,  
databaseURL: process.env.REACT\_APP\_DB\_URL,  
storageBucket: process.env.REACT\_APP\_STRG\_BKT,  
messagingSenderId: process.env.REACT\_APP\_MSG\_SENDER\_ID  
}  
export var embedlyKey = process.env.REACT\_APP\_EMBEDLY\_KEY;  
export default firebase\_config;

와 같이 저장하면 됩니다.

API가 잘 작동하는지 확인해 보기 위해 call을 직접해 보도록 하겠습니다.

[https://api.embedly.com/1/oembed?url=https:%2F%2Fgithub.com%2Fmzabriskie%2Faxios&key=][anchor8]

key 부분만 여러분이 얻은 키로 채워 주시면 다음과 같은 결과를 얻어올 수 있습니다.

{   
"provider\_url":"[https://github.com][anchor9]",  
"description":"axios - Promise based HTTP client for the browser and node.js",  
"title":"mzabriskie/axios",  
"thumbnail\_width":400,  
"url":"[https://github.com/mzabriskie/axios][anchor10]",  
"thumbnail\_url":"[https://avatars2.githubusercontent.com/u/199035?v=3&s=400][anchor11]",  
"version":"1.0",  
"provider\_name":"GitHub",  
"type":"link",  
"thumbnail\_height":400  
}

이제 URL을 호출해 직접 API를 통해 카드를 만들어 보도록 하겠습니다.

3. axios.js

request 를 만들 클라이언트 모듈로 무엇을 쓸까하는 고민은 많은 개발자들이 늘 겪는 고민일 거라고 생각합니다. 안드로이드의 경우는 retrofit 을 많이 쓰는 거 같은데 프론트엔드에서 많이 쓰는 거는 어떤게 있을까요? 가장 유명하고 잘 알려져 있는 예로는 jquery가 있습니다. $.get으로 대표되는 놀라운 간편성 및 DOM과 직접 연계 및 다양한 옵션이라는 강력한 무기를 가지고 있죠.

하지만, React 및 Node.js 환경에서는 이걸 쓰기에는 조금 꺼려집니다. 첫번째는 강점인 DOM 과의 궁합인데요. 이것이 왜 약점이 되는가 하면 React는 Virtual DOM을 이용해 웹 컴포넌트를 사용한다는 점에서 콜백 처리 및 DOM 의 중복 처리 여부의 단점이 있고 Node환경에서는 굳이 사용하지 않아도 될 DOM컴포넌트의의존성 때문에 JSDOM이라는 무거운 모듈을 같이 로딩해야 한다는 어려움이 있습니다.

그래서 superagent 를 포함한 가벼운 request용 프레임워크들이 최신 대세가 되었는데요. 제가 이번에 얘기하고 싶은 모듈은 axios.js입니다.

mzabriskie/axios
_axios - Promise based HTTP client for the browser and node.js_github.com

이 axios.js 의 강점은 뭐니뭐니 해도 Promise 기반의 HTTP 클라이언트라는데에 있습니다.

기존의 모듈들은 주로 jquery가 해 주던 내용에서 HTTP 클라이언트 부분만 유사하게 (혹은 호환되게) 코드를작성했지만 이 axios가 리턴해 주는 형태는 ES2015의 Promise 객체이기 때문에 현재의 자바 스크립트가 가야할 길에서 가장 맞는 선택지라고 보여지고 게다가 Jest 테스트의 경우는 리턴값을 undefined 와 Promise로만 받기 때문에 테스트 코드를 짤 때에도 적합합니다.

그래서 아래와 같은 코드가 가능해 집니다.

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

// 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);  
});

여기서 잠깐. Promise란?

Promise 패턴은 JavaScript에서 콜백을 다루는 패턴 중에 하나입니다.  
보통 콜백을 매개 변수로 다뤄서 함수의 매개변수로 보내는 코드  

function callback(){  
console.log("this is callback function");  
}  
function http(){  
request.get("[http://www.devpools.kr][anchor13]",callback)  
}

의 형태를 띄거나 이걸 익명함수로 바꾸면

function http(){  
request.get("[http://www.devpools.kr][anchor13]",()=\>{  
console.log("this is callback function");  
})  
}

의 모습을 띄게 됩니다.  
하지만 이런 콜백을 여러번 호출하게 되면 이른바 콜백 지옥에 떨어지게 됩니다. 가독성도 무척이나 떨어지게 되는 셈입니다.

콜백 아도겐

이런 일들에서 구원코자 여러 방법들이 동원 되었지만 ES2015에서 정립된 표준은 Promises 입니다. Promise는 위의 함수가 아래와 같이 변화하는 것입니다.

function http(){  
request.get("[http://www.devpools.kr][anchor13]")  
.then(()=\>{console.log("this is Promise!")})  
.catch((error)=\>{console.log(error)})  
}

성공을 했을 때 then으로 빠지고, 실패를 하면 catch에 들어가는 패턴.  
어디선가 보지 않았나요? 우리는 이미 Firebase DAO를 작성하며 이 패턴을 확인했습니다.

getArticle(key){  
return new Promise(resolve=\>{  
firebase.database().ref('/posts/'+key)  
.on('value',(articles)=\>{  
resolve(articles);  
})  
});  
}

firebase 에서 article을 하나 가져오는 것이 성공하면 resolve 를 호출해서 resolve 에 매개 변수를 건네주는 형식으로 작성이 되어 있습니다.

자 이제, axios를 통해 Embed.ly 에 요청을 하는 코드를 짜 볼까 합니다.

$npm install --save axios

src/__tests__/ 폴더 아래에 EmbedDao.js를 만들고 다음과 같이 작성합니다.

import getEmbedly from '../EmbedlyDao';  
it('Get Embedly Info From Embedly', () =\> {  
//일단 URL을 호출하면 response.data.url 은 같은 값을 줄 것이다~  
getEmbedly("[http://www.naver.com][anchor14]").then((response)=\>{  
expect(response.data.url).toEqual("[http://www.naver.com][anchor14]");  
}).catch((error)=\>{  
console.log(error);  
});  
});

이렇게 작성을 하면 getEmbedly 함수를 URL 을 받아서 Promise 객체에 respose를 담아 주면됩니다.
이런 일련의 작업을 axios가 할 것이기 때문에 src 폴더 아래에 EmbedlyDao.js파일을 만들어서 아래와 같이 작성해 줍니다.

import request from 'axios';  
import {embedlyKey} from './config';  
export default function getEmbedly(url){  
return request.get('[https://api.embedly.com/1/oembed',{][anchor15]  
params: {  
url : url,  
key : embedlyKey  
}  
});  
}

이렇게 작성을 하고

$npm test

를 보내면 훌륭한 결과를 보내 줍니다.

4. 편집기에 카드 집어 넣기

지금까지 한 작업은 카드를 만들었고 Axios를 이용해 Request 를 작성했으니 이번에는 편집기에 카드가 삽입 되도록 해 보겠습니다.

기존 Editor의 redner내의 JSX코드에 다음과 같이 추가해 보겠습니다.

<div className="textEditor"\>  
<div className="innerEdit"  
contentEditable="true"  
placeholder="글쓰기..."  
onPaste={this.onPaste}  
onKeyUp={this.editorChange}\></div\>  
<Card cardInfo={this.state.cardInfo}/\>  
</div\>

Article 을 통해 cardInfo를 셋팅해 주던 걸 편집기에서 request를 날릴 수 있도록 작업해 보겠습니다.
사실 여기서는 조금 고민이 드는게 request 라던지 중요한 데이타를 다루는 부분은 진입점으로 끌어와서 한 눈에 보이도록 하는 패턴을 주로 쓰기는 하는데 이 경우는 편집기에서만 온전히 사용하고 재사용할 확률이 낮아 보이므로 그냥 편집기에서 사용하겠습니다.

그럼 이 cardInfo를 constructor에서 초기 값으로 셋팅하도록 변경을 하고 기존 onPaste와 editorChange의 소스에서 URL을 detect를 부분에 URL을 받아서 Embedly에 요청을 하는 함수를 다음과 같이 만들어 보도록 하겠습니다.

getForcedState(embedlyUrl,content){  
//Promises를 사용합니다.  
return new Promise(resolve=\>{  
//URL이 들어 왔을 경우  
if(embedlyUrl){  
//Embedly API를 호출해서 response.data의 값을 받아온다.  
getEmbedly(embedlyUrl).then((response)=\>{  
let cardInfo = Object.assign({},response.data);  
//setState 해줄 객체를 만들어 준다  
resolve({  
embedlyUrl : embedlyUrl,  
content : content,  
cardInfo : cardInfo  
});  
}).catch((error)=\>{  
resolve({  
embedlyUrl : undefined,  
content : undefined,  
cardInfo : undefined  
});  
});  
}else{  
//URL 이 안들어 올 경우는 state에서 content만 업데이트 할 수 있도록 bypass 시켜 준다  
//호출하는 쪽에서 setState를 콜하도록 하는 패턴을 바꾸는 편이 낫다고 생각해서  
//이렇게 분기를 했는데, 리팩토링의 여지가 있음  
resolve({  
content : content  
});  
}  
})  
}

그러고 나서 onPaste와 editorChange를 아래와 같이 변경합니다.

onPaste(event){  
event.clipboardData.items\[0\].getAsString(text=\>{  
let checkText = this.detectURL(text);  
//복사 붙여넣기 할때 클립보드의 URL을 확인해서 있을 경우 카드를 만든다.  
//state가 변경이 되면 카드가 만들어진다.  
if(checkText){  
this.getForcedState(checkText).then((obj)=\>{  
this.setState(obj);  
});  
}  
})  
}  
editorChange(event){  
let checkText = this.detectURL(event.currentTarget.textContent);  
//편집기에 타이핑을 할때 URL을 확인해서 있을 경우 카드를 만든다.  
//state가 변경이 되면 카드가 만들어진다.  
if(!this.state.embedlyUrl&&  
(event.keyCode===32||event.keyCode===13)&&  
checkText){  
this.getForcedState(checkText,event.currentTarget.textContent)  
.then((obj)=\>{  
this.setState(obj);  
});  
}else{  
this.getForcedState(undefined,event.currentTarget.textContent)  
.then((obj)=\>{  
this.setState(obj);  
});  
}  
}

이렇게 하고나면 다음과 같은 카드가 만들어집니다.

한번 화면을 확인해 보면 아래 그림5 처럼 출력 됩니다.

그림5

런데.. submit을 하면 content의 상태(state)를 비워줘도 편집기는 reset되지가 않습니다.게다가 우리가 사용한 태그는 input 이 아니라 참조(ref)를 사용할 수도 없습니다.

이럴 경우를 위해서 편집기 소스에 약간의 수정이 필요합니다. 하지만 한줄이면 끝입니다.

<div className="textEditor"\>  
<div className="innerEdit"  
contentEditable="true"  
placeholder="글쓰기..."  
onPaste={this.onPaste}  
onKeyUp={this.editorChange}  
**_dangerouslySetInnerHTML={{\_\_html: this.state.content}}_**\></div\>  
<Card cardInfo={this.state.cardInfo}/\>  
</div\>

dangerouslySetInnerHTML 기능은 원래 DOM의 innerHTML과 같은 기능을 합니다.(하지만 Cross-Site scripting에 대한 우려가 있으니 접두어를 붙인 기능입니다. 그래서 저 변수가 최대한 노출되지 않게 작성하는 것은 중요합니다.)

5. 목록만들기

이제 편집과 외부에 저장되는 것은 확인했으니, 카드 목록을 리스트로 보는 기능이 남았네요.목록은 오히려 간단합니다.

src폴더 아래의 App.js 컴포넌트의 ComponentWillMount에 firebase 이벤트를 걸어 두겠습니다.

componentWillMount() {  
//최대 25건까지 목록을 가져와서  
this.dao.list(25,(articles)=\>{  
var items = \[\];  
//목록을 루프를 태워서  
articles.forEach(function(article){  
var item = article.val();  
item\['key'\] = article.key;  
items.push(item);  
})  
//state 값에 입력  
if(items && items.length\>0){  
this.setState({  
//최신 데이타가 위에 올라와야 하므로 데이타의 역순으로 확인  
articles: items.reverse()  
});  
}  
});  
}

이렇게 하면 상태(state)값에 firebase에서 가져온 데이타가 저장이 됩니다. 이상태 값을 바탕으로

<ul\>  
<li\>카드1</li\>  
<li\>카드2</li\>  
<li\>카드3</li\>  
<li\>카드4</li\>  
....  
</ul\>

형태로 코드는 나열되도록 만들겠습니다. 우선 src 폴더 아래 CardList.js와 CardList.css 라는 파일을 만들겠습니다.

import React, { Component } from 'react';  
import Card from './Card'  
import './CardList.css'

export default class CardList extends Component {  
createCard(item,index){  
return(<li className="list\_row" key={item.key}\>  
<pre className="common\_margin grey\_text"\>{item.content}</pre\>  
{  
(item.cardInfo)?<Card cardInfo={item.cardInfo}/\>:""  
}  
</li\>);  
}  
render() {  
return <ul\>{ this.props.articles.map(this.createCard) }</ul\>;  
}  
}

CSS는 다음과 같습니다.

ul{  
list-style-type: none;  
padding:10px;  
}  
.list\_row{  
margin-top: 3px;  
padding-top: 10px;  
padding-bottom: 5px;  
text-align: left;  
background-color: white;  
border:1px solid \#dddfe2;  
}  
.common\_margin{  
margin: 15px;  
}  
.grey\_text{  
color:\#666;  
font-size: 12  
}  
pre {  
white-space: pre-wrap; /\* Since CSS 2.1 \*/  
white-space: -moz-pre-wrap; /\* Mozilla, since 1999 \*/  
white-space: -pre-wrap; /\* Opera 4-6 \*/  
white-space: -o-pre-wrap; /\* Opera 7 \*/  
word-wrap: break-word; /\* Internet Explorer 5.5+ \*/  
}

Card 관련해서는 편집기에서 만들었으니 더 작업할 것이 없습니다. 만들고 저장하고 나면 아래 그림6 과 같은 결과를 확인할 수 있습니다.

전체 소스는

ehrudxo/standup
_standup project for newbi_github.com

에서 확인할 수 있습니다.

By Keen Dev on November 15, 2016.

Exported from Medium on May 31, 2017.

서버리스 아키텍처 소셜미디어 개발기 4편

지난 편에서는 Editor 에서 데이타를 어떻게 나눌지살펴보았고 Firebase 기초, 테스트 코드를 작성해 보았습니다. 이번에는 그 기반으로 어떻게 실제로 데이타를 입력하고 리스트를 작성하는지 확인해 보겠습니다.

서버리스 아키텍처 소셜미디어 개발기 3편
_지난 번 글에서는 기초적인 편집기를 만드는 작업을 했습니다. 사용자 스토리에 기반한 하나의 편집기와 버튼으로만 구현할 수 있도록 화면을 구성했구요._techstory.shma.so

2. 김개발이 사이트를 방문해서 자신이 어제 유심하게 읽은 글을 올릴 수 있다. 이렇게 하면 다른 사람들이 볼 수 있다.
* 에디터 창은 하나만 있고 거기서 글을 작성하고 업로드 하면 글이 외부 클라우드 공간에 저장이 된다.
* 아무런 내용도 김개발이 입력하지 않으면 업로드하지 않는다.( 버튼이 눌러지지 않는다. )
3. 김개발이 작성한 글이 목록으로 보여진다. 이렇게 함으로써 다른 사람들이 목록을 확인할 수 있다.

cloud data

1. Firebase DAO(Data Access Object) 만들기

JavaScript라서 DAO패턴을 좋아하지는 않지만 굳이 이해를 돕기 위해 DAO라는 이름을 명명하고 작업을 하겠습니다.

(인터페이스를 만들고 Implementation 소스를 만드는 작업까지는 굳이 하지 않겠습니다.)

테스트 코드를 짜 보자.

일단 Mocking을 할 데이터를 만들어야 하겠죠.

__tests__ 폴더 밑에 CloudDao.js 를 만듭니다.

//CloudDao.js 파일  
var article = {  
user : "Genji",  
content : "겐지가 함께한다.",  
urls:\[{  
url : "[https://namu.wiki/w/%EA%B2%90%EC%A7%80(%EC%98%A4%EB%B2%84%EC%9B%8C%EC%B9%98)][anchor2]",  
title:"겐지(오버워치)",  
description : "블리자드 엔터테인먼트 사의 FPS 게임 오버워치의 영웅.기계가 되어버린 몸을 받아들여 내면의 평화를 찾은 강력한 사이보그 닌자.",  
imageUrl : "[https://image-proxy.namuwikiusercontent.com/r/https%3A%2F%2Fd1u1mce87gyfbn.cloudfront.net%2Fmedia%2Fartwork%2Fgenji-concept.jpg][anchor3]",  
imgWidth: 640,  
imgHeight : 480,  
thumbnailUrl : "[https://image-proxy.namuwikiusercontent.com/r/http%3A%2F%2Fi66.tinypic.com%2F10mpje9.jpg][anchor4]" ,  
thumbnailWidth : 80,  
thumbnailHeight :80  
}\]  
}

Mocking을 위한 데이터를 만들어 보겠습니다. 이 데이터가 잘 입력이 되고 수정이 되고 삭제가 되면서 리스트를 확인하는 작업을 해 보겠습니다.

firebase를 사용하기 위해서 모듈을 설치합니다. 이미 설치된 모듈이 아니기 때문에 다음의 명령어를 사용합니다.

$npm install firebase --save

DAO 만들기

if code is coffee

자 이제 DAO를 어떻게 만들면 좋을까요?

먼저 DAO에는 입력, 수정, 삭제, 조회(insert, update, delete, list)는 기본적으로 들어갈 것이고,한 건 검색을 위한 getArticle이 필요할 것입니다. 일단 입력에 관한 유저 스토리 이지만 DAO는 한꺼번에 작성할 수 있는 것은 먼저 작성해 보도록 하겠습니다.

사용자 스토리의 기본 중 하나는 확인할 수 있어야 한다 인데, 입력이 되면 조회를 할 수 있어야 하기 때문입니다.

! 여기서 잠깐 !
사용자 스토리는 INVEST를 기반으로 하는데
Independent : 독립적이어야 한다
Negotiable : 조절 가능해야 한다
Valuable : 사용자에게 가치가 있어야 한다.
Estimatable : 측정가능해야 한다
Small : 작아야 한다
Testable : 테스트 가능해야 한다.
우리가 정해 놓은 사용자 스토리를 위에 맞춰 변경하는 작업도 중간 중간 이루어 질 것입니다.

일단 입력을 위한 테스트 코드를 아래와 같이 작성해 보겠습니다.

it('upload article',function(){  
let inserted = dao.insert( article );  
// 입력이 되었는지 key 값을 가지고 검색해서 확인  
dao.getArticle(key).on('value',(snapShot)=\>{  
//키 값이 같은지 테스트 케이스 작성  
expect(snapShot.key).toEqual(key);  
});  
return inserted;  
});

자, 이제 npm test를 실행하면 되는데, 다음의 셋팅을 하지 않으면 데이터 값이 로딩이 되지 않을 것입니다.
firebase는 인증 시스템을 갖고 있는데, 일단 그림과 같이 anonymous가 입력할 수 있도록 해 두고 이후 인증관련 모듈을 같이 붙인 후에 셋팅을 바꿔보도록 하겠습니다.

그림1

StandUp-> Database -> 규칙 메뉴를 따라가면 될 듯 합니다.

{  
"rules": {  
".read": true,  
".write": true  
}  
}

자 이제 npm test 를 실행하고 나면

그림2

아래와 같은 결과와 함께 외부 firebase 에 저장이 됩니다.

firebase 웹 콘솔에서 확인해 봐야겠죠?

그림3

그림과 같이 firebase 웹 콘솔에서 Database 의 데이터 탭에서 방금 올린 데이터를 확인할 수 있습니다.

이렇게만 데이터를 쌓아도 되지만 개인화 서비스를 위해 username 기반으로 데이터를 쌓이도록 변경해 보겠습니다.

이런 경우는 key값을 먼저 만들고 같은 key를 가지는 posts와 user-post 데이터 베이스를 만들어야 하는 관계로 update 함수를 작성하도록 하겠습니다. 아직 user 정보를 어떻게 가지고 올지 결정하지 않았으므로 똑같이 genji 폴더에만 작성하도록 하겠습니다.

해당 관련된 테스트 코드는 다음과 같습니다.

it('upload article and edit and delete',function(){  
//공통 키값  
let key = dao.newKey();  
//입력  
var updated = dao.update( key, article1 );  
dao.getArticle(key).on('value',(snapShot)=\>{  
//같은 값이 들어 갔는지 확인  
expect(snapShot.key).toEqual(key);  
//수정  
dao.update(key, article2);  
//삭제. 데이터의 확인을 위해서는 주석 처리.  
dao.remove(key);  
});  
return updated;  
});

먼저 key 값을 공통으로 만들고, key 값을 기준으로 입력, 수정, 삭제할 수 있습니다.

FirebaseDao 전체 소스는 다음과 같습니다.

import firebase from 'firebase';  
/\*  
\* initializeFirebaseApp  
\*/

export default class FirebaseDao {  
constructor(config){  
firebase.initializeApp(config);  
}  
//더 이상 입력에 사용하지 않습니다.  
insert(postData){  
return firebase.database().ref().child('posts').push(postData);  
}  
// 수정  
update(key,postData){  
var updates = {};  
updates\['/posts/' + key\] = postData;  
updates\['/user-posts/genji/' + key\] = postData;  
return firebase.database().ref().update(updates);  
}  
// 삭제, delete는 예약어 이므로 remove를 사용  
remove(key){  
firebase.database().ref('/posts/').child(key).remove();  
return firebase.database().ref('/user-posts/genji/').child(key).remove();  
}  
//database에 걸린 이벤트를 종료  
off(){  
return firebase.database().ref().off();  
}  
//새로 빈 데이터를 만들고 key값만 return  
newKey(){  
return firebase.database().ref().child('posts').push().key;  
}  
//한개의 글을 얻어 온다.  
getArticle(key){  
return firebase.database().ref('/posts/' + key);  
}  
}

자 이제 테스트 코드 하나로 입력-> 수정 -> 삭제를 하는 코드베이스가 완성되었습니다.

2. 사용자 스토리2 — 입력하기

업로드 하면 글이 외부 클라우드 공간에 저장이 된다.

부분을 위한 테스트 코드와 DAO를 완성했으니 프레젠테이션 레이어와 결합해 보도록 하겠습니다.

지금 에디터에서 글을 쓰고 스탠드업! 버튼을 누를 경우에

그림4

이루어지는 이벤트는 다음과 같습니다.

<button className="upload"  
disabled={!this.hasValue(this.state.content)}  
onClick={this.handleSubmit}\><span\>스탠드업!</span\></button\>

이 handleSubmit을 어떻게 작성하면 좋을까요? onClick 이벤트가 호출이 되는 시점부터는 데이터의 영역인데 데이터의 흐름은 한군데서 파악하는 것이 개발을 용이하게 합니다. 게다가 우리는 firebase 처럼 우리가 직접 컨트롤하지 않는 데이터의 흐름을 가지고 있기 때문에 내부의 데이터의 흐름은 App.js 같은 애플리케이션 진입점으로 위임하는 편이 현명합니다.

그럼 이 “데이터”의 기본 단위가 될 Article 을 우리는 미리 정의했었습니다. 처음에 Mocking을 위한 데이타라고 한 부분의 article을 반환하는 임시 파일을 만들겠습니다. 이렇게 하는 이유는 테스트의 완결성을 위해서입니다.

Article.js 라는 파일을 src 폴더 아래에 만들고 테스트 코드는 아래와 같이 작성합니다.

import Article from '../Article';  
var article1 = Article();  
it('Object assign', function(){  
var article2 = Object.assign({},article1);  
article2.user = "Genji";  
article2.content = "다음";  
article2.urls\[0\].url = "[http://www.daum.net][anchor5]";  
//article1의 값이 잘 전달되었는지 확인.  
expect(article1.urls\[0\].imgWidth).toEqual(article2.urls\[0\].imgWidth);  
})

왜 이런 작업을 했을까요? 간단히 말씀 드리면 4일차 작업에는 url로 카드를 만들 때 외부에서 URL에 해당하는 메타정보(title, description, thumbnail image)등의 작업을 하지 않기 때문입니다. 그래서 일단은 더미 작업을 하고 이 부분을 카드를 가져오는 방법으로 전환하는 과정을 5일차에 진행하려고 하기 때문입니다.

그런 과정중에 Article의 URL 이외의 부분은 자동으로 채우게 하도록Mocking 데이타를 활용해서 보도록 하는 것입니다.

자 이제 다시 에디터로 돌아가겠습니다.

그림5

Editor 안의 state는 보시다시피 embedlyUrl, content 두가지를 설정해 두었습니다.
이 embedlyUrl을 App.js에게 값을 전달해 이후 list 컴포넌트를 만드는 사용자 스토리까지 전달되기 전까지가 지금의 해결 과제입니다.

[그림 5] 에 묘사가 되어 있듯이 Editor.js 의 handleSubmit은 props 속성으로 App.js와 연결되어 있습니다. handleSubmit event는 App.js의 submit 을 가리키게 되어 있는 것이죠.

그래서 submit 은 Article을 변수로 받아서 firebase 에 저장하는 역할을 하면 됩니다.

App.js의 submit을 아래와 같이 변경해 줍니다.

import FirebaseDao from './FirebaseDao'  
import config from './config'  
(중략)  
class App extends Component {  
//중략  
constructor(){  
super();  
this.dao = new FirebaseDao(config);  
this.submit = this.submit.bind(this);  
}  
submit(article){  
if(article){  
let key = this.dao.newKey();  
let updated = this.dao.update( key, article );  
return updated;  
}  
}  
//후략  
}

Editor.js 파일은 아까 언급한 Article.js 를 이용해 handleSubmit을 아래와 같이 작업해 줍니다.

handleSubmit(event){  
let article = Object.assign({}, Article());  
article.user = "Genji";  
article.content = this.state.content;  
article.urls\[0\].url = this.state.embedlyUrl;  
this.props.submit(article);  
}

그림6

[그림6]과 같이 작성을 하고 업로드를 하면

그림7

훌륭하게 값이 들어간 것을 확인할 수 있습니다.

이제 입력한 목록만 간단하게 보여지는 것을 보고는 마무리 하겠습니다.

FirebaseDao에 list에 관련된 함수를 추가해 봅시다.

일단 테스트 코드를 아래와 같이 작성해 봅니다.

it('list article', function(){  
dao.list(25).once('value',(dataSnapshots)=\>{  
dataSnapshots.forEach((dataSnapshot)=\>{  
keys.push(dataSnapshot.key);  
var article = dataSnapshot.val();  
expect(article.user).toEqual("Genji");  
})  
});  
})

DAO 에서 25개까지의 결과만 가지고 와서 데이타의 값을 비교하는 형태이고 콜백함수는 firebase 문서에서 참조해서 작성했습니다.

list(pagesize){  
return firebase.database().ref('/posts/')  
.orderByKey().limitToLast(pagesize);  
}

위와 같이 작성하면 npm test를 통해 값이 훌륭하게 가져와 있는지 확인할 수 있습니다.

이제 App.js 파일을 고쳐 보겠습니다.

React 컴포넌트의 생명주기에서 mounting 과정은 [그림8] 과 같습니다.

그림8

componentWillMount 메쏘드를 이용해 데이타 이벤트를 등록합니다.

가지고 온 article들은 상태(state)값에 등록하기 위해 생성자 함수에 먼저 상태값을 등록해 둡니다.

this.state = {  
articles:\[\]  
}

ComponentWillMount와 componentWillUnmount 값을 정의합니다.

componentWillMount() {  
//데이터 조회 이벤트를 등록합니다.  
this.dao.list(25).on('value',(dataSnapshots)=\>{  
var items = \[\];  
dataSnapshots.forEach(function(dataSnapshot){  
//..중략.. 테스트코드와 같게 작성합니다.  
})  
if(items && items.length\>0){  
// state값에 셋팅  
this.setState({  
articles: items.reverse()  
});  
}  
});  
}  
componentWillUnmount(){  
//이벤트 삭제  
this.dao.off();  
}

render 메쏘드에도 약간의 변화가 있습니다.
기존의 Editor 에 스프레스 함수를 설명하게 넘긴 수많은 속성(props)은 애플리케이션의 견고성을 위해 필요한 값만 넘기고 firebase에 저장된 목록을 가져오는 작업을 해 보겠습니다.

HTML 에서 목록을 의미하는 태그는 UL, OL입니다. 그 태그를 이용해서 render내의 jsx 는 아래와 같이 작업해 줍니다.

Editor handleSubmit={this.submit} isAnonymous={this.isAnonymous}/\>  
<ul\>  
{this.getArticles()}  
</ul\>

getArticles 메쏘드는 Article의 상태를 가져오는 함수입니다.

getArticles(){  
let lis = \[\];  
for(let i=0;i<this.state.articles.length;i++){  
lis.push(<li key={this.state.articles\[i\].key}\>{this.state.articles\[i\].content}</li\>);  
}  
return lis;  
}

이렇게 작성하고 나면 목록이 다음 [그림7]과 같이 완성됩니다.

그림7

다음 번에는 이제 목록과 입력에 사용할 카드를 만들고 URL값을 어떻게 제대로 입력하는지 알아보도록 하겠습니다.

By Keen Dev on November 7, 2016.

Exported from Medium on May 31, 2017.