6) AWS S3를 이용한 웹 호스팅

Index

  1. 에밀리 시간표 웹 페이지
  2. AWS S3란?
  3. 도메인 등록
  4. S3 버킷 생성
  5. 웹 사이트 데이터 업로드
  6. 버킷 정책(Permissions) 설정
  7. 버킷 정적 웹 사이트 호스팅 기능 활성화
  8. 버킷 Record Set 설정

## 에밀리 시간표 웹 페이지

학생들에게 필요한 기능이 어떤게 있을까 생각하다 에밀리에 **시간표 기능**을 추가하게 되었습니다. 학생들이 수강하고 있는 수업 데이터를 얻을 수 있으면 좋겠지만 학교 DB에 접근할 수 있는 권한이 없기 때문에 학생들이 직접 에밀리를 통해 시간표를 등록하도록 만들어야 했습니다.

학교 홈페이지에서 제공하는 **수업 목록(엑셀 파일)**를 활용(파싱)하여 시간표 데이터를 만들고, 이를 학생들이 쉽게 등록 및 수정할 수 있도록 웹 페이지를 만들었습니다 (아무래도 전공도 다양하고 수업의 수도 많다보니 단순히 버튼 및 텍스트로 상호작용하여 시간표를 등록하는것은 불편하다고 판단해 시간이 좀 더 걸리더라도 웹 페이지를 만들기로 결정했었습니다.)

학생들이 많이 사용하는 여러 시간표 앱 및 웹 서비스를 참고해서 다음과 같은 **시간표 등록 및 수정 페이지**를 만들었습니다.

에밀리 시간표 등록 및 수정 페이지

참고로 에밀리 시간표 등록 및 수정 페이지는 **React(Starter-Kit)**를 이용해서 만들었습니다. 아무래도 회사에서 React Native를 사용해서 앱을 개발하다보니 React를 사용해 웹 개발을 함에 있어서도 도움이 많이 되었습니다.


## AWS S3란?

사용자들이 에밀리 시간표 페이지를 사용할 수 있도록 하기 위해서는 웹 페이지를 호스팅해야합니다. 간단히 호스팅할 수 있는 여러 방법들이 있지만, AWS 공부도 할겸 S3를 이용해 호스팅해보기로 생각했습니다.

먼저 S3에 대해 간단하게 알아보겠습니다. **S3(Simple Storage Service)**는 파일을 저장하기 위한 Storage입니다. 일반적인 파일시스템의 개념과는 약간 다르며, 파일 이름을 대표하는 key와 파일 자체로 구분되는 Object Storage입니다.

S3는 **정적 웹사이트 호스팅 기능**을 사용하는 S3 버킷을 **Route 53**을 통해 도메인과 연결해 사용할 수 있습니다. 여기서 동적 웹 사이트 PHP, JSP 등 서버 측 처리에 의존하는 사이트는 S3를 이용해 호스팅할 수 없습니다. 오직 개별 웹 페이지에서 정적 컨텐츠를 포함하며, 클라이언트 측 스크립트를 포함하고 있는 정적 웹 사이트만이 호스팅 가능합니다. (React의 경우 webpack을 통해 번들링된 파일을 호스팅하면 됩니다.)


## 도메인 등록

이미 등록된 도메인이 있다면 이 단계를 생략하면 됩니다. 그러나 inuemily.com과 같이 등록된 도메인 이름이 없는 경우, 원하는 도메인 이름을 만들어 등록해야 합니다.

도메인이 없으시다면 AWS의 Route53 - Registered domains를 통해 도메인을 생성하고, AWS가 아닌 다른 서비스로부터 도메인을 사용중 이라면 기존 도메인 DNS 서비스 역할을 하는 AWS Route53으로 마이그레이션을 해야 S3를 이용한 정적 웹사이트 호스팅이 가능합니다. 마이그레이션 관련해서는 AWS 문서를 참고하면 좋을것 같습니다.

저는 다음과 같이 inuemily.com 이라는 도메인을 갖고 있습니다.

에밀리 도메인


## S3 버킷 생성

inuemily.com과 같은 루트 도메인, www.inuemily.com과 같은 하위 도메인 양쪽의 요청을 모두 지원하려면 두 개의 버킷을 생성해야합니다. 하나의 버킷에 컨텐츠를 포함하고 다른 버킷은 컨텐츠를 포함하는 버킷에 redirection 하도록 버킷을 구성할 것입니다.

먼저 버킷 이름을 호스팅할 웹 사이트 이름과 일치하게 생성합니다. 저는 inuemily.comwww.inuemily.com 이름으로 버킷을 생성했습니다.

버킷 생성


## 웹 사이트 데이터 업로드

2개의 버킷을 모두 생상하였다면 루트 도메인 버킷(inuemily.com)에 컨텐츠를 업로드합니다. 두 번째 버킷(www.inuemily.com)은 추후에 이 루트 도메인 버킷으로 redirection 하도록 설정할 것입니다.

S3 버킷에 파일을 업로드하는 방법으로는 1) 드래그 앤 드롭, 2) AWS CLI 사용 과 같이 2가지 방법이 있습니다. 저는 드래그 앤 드롭을 이용해서 파일을 업로드 하였습니다. (webpack으로 번들링되어 나온 public 폴더의 파일들을 업로드 하였습니다.)

루트 버킷에 파일 업로드


## 버킷 정책(Permissions) 설정

버킷을 생성하고 파일을 업로드했지만 모든 사용자가 버킷에 업로드한 모든 컨텐츠에 접근할 수 있도록 **버킷 정책(Permissions)**을 설정해야 합니다.

다음의 코드를 복사하여 아래 사진과 같이 버킷 정책을 설정합니다. (10번째 라인의 inuemily.com을 자신의 버킷 이름으로 변경해야 합니다.) 두 번째 버킷에는 파일을 업로드하지 않기 때문에 따로 정책을 설정해주지 않아도 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"Version": "2012-10-17",
"Id": "Policy1484315866648",
"Statement": [
{
"Sid": "Stmt1484315864175",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::inuemily.com/*"
}
]
}

루트 버킷 정책 설정


## 버킷 정적 웹 사이트 호스팅 기능 활성화

파일 업로드와 정책 설정이 끝난 버킷을 정적 웹 사이트 호스팅으로 사용할 수 있도록 기능을 활성화 해야합니다.

루트 버킷 정적 웹 호스팅 기능 활성화

루트 버킷을 정적 웹 호스팅에 사용하기 때문에 User this bucket to host a website 에 체크를 하고, index document에는 자신이 작성한 웹 페이지의 index page의 파일명을 기재합니다. 저의 경우 좀 전에 버킷에 업로드한 파일을 보면 index.html이 있고, 이 파일이 index page의 파일이기 때문에 index.html을 기재했습니다.

이번에는 두번째 버킷으로 들어오는 요청을 루트 버킷으로 redirection 할 수 있도록 설정을 합니다. 루트 버킷과는 달리 **Redirect requests**에 체크를 하고, Target bucket or domain에 루트 버킷의 이름을 기재합니다.

두번째 버킷 정적 웹 호스팅 기능 활성화 및 Redirection 설정


## 버킷 Record Set 설정

이제 마지막 단계입니다. 지금까지 생성하고 설정을 마친 S3 버킷을 Route 53을 사용해 연결합니다. 도메인에 따른 호스팅 영역과 추가하는 별칭 레코드는 IP 주소 대신 S3 웹 사이트 엔드포인트를 사용함으로써 Route 53은 별칭 레코드와 S3 버킷이 존재하는 IP 주소 간 매핑을 유지합니다.

S3 버킷 Route 53 - Record Set 설정

Route 53의 Record Set 까지 설정을 마치면 해당 루트 도메인과 서브 도메인을 통해서 사이트에 접속할 수 있습니다.

참고
AWS - 사용자 지정 도메인으로 정적 웹 사이트 설정
아마존 웹 서비스를 다루는 기술 - 11장

5) AWS Lambda 사용하기

Index

  1. AWS Lambda란?
  2. CloudWatch와 Lambda 구성하기
  3. 정리하며

저번 포스팅에서 작성한 학식 메뉴 크롤링(학식 메뉴를 크롤링해 DB에 저장) 코드를 **AWS Labmda**를 사용해 일주일에 한번씩 실행되도록 스케쥴링을 해보겠습니다.

AWS(아마존 웹 서비스)에 대한 기본적인 내용은 생략합니다.

## AWS Lambda란?

어떠한 이벤트에 따라 Cron 프로세스를 구성하는데에는 다양한 방법이 존재합니다. 서버 인스턴스를 띄워놓고 일정 시간에 이벤트를 발생시키는 것이 일반적인 방법 중 하나이지만 이러한 방법으로 구성할 시에는 해당 시간에 Event를 발생시키는 일을 제외하고는 서버 인스턴스를 낭비하게 됩니다.

AWS Lambda이벤트에 응답하여 코드를 실행하고 자동으로 기본 컴퓨팅 리소스를 관리하는 서버 없는 컴퓨팅 서비스입니다. AWS Lambda를 사용하여 커스텀 로직으로 다른 AWS 서비스를 확장하거나, AWS 규모, 성능 및 보안으로 작동하는 자체 백엔드 서비스를 만들 수 있습니다. AWS Lambda는 Amazon S3 버킷의 객체에 대한 변경 또는 Amazon DynamoDB의 테이블 업데이트와 같은 다양한 이벤트에 대한 응답으로 코드를 자동 실행할 수 있습니다.

AWS Lambda는 코드가 실행되지 않을 때는 요금이 부과되지 않습니다. 즉, 서버 인스턴스를 계속 띄워놓는 것과 비교했을 때 더 저렴합니다. AWS Lambda의 자세한 요금 정책은 AWS 홈페이지에서 확인 가능합니다. (Lambda 함수가 다른 AWS 서비스를 사용하거나 데이터를 전송하는 경우 추가 요금이 부과될 수 있으므로 필히 확인바랍니다.)


## CloudWatch와 Lambda 구성하기
  1. AWS에 로그인 후 Lambda 서비스를 클릭합니다.

Lambda Function 생성하기 - 1

  1. 저와 같이 이미 생성한 Lambda 함수가 있는 경우는 아래와 같이 Create a Lambda Function 버튼을 클릭하고 처음으로 Lambda 함수를 생성하는 경우는 Get Started Now 버튼을 클릭합니다.

Lambda Function 생성하기 - 2

  1. Blueprint는 자주 사용되어지는 미리 준비된 Lambda 함수의 환경을 제공합니다. 저희는 Blank Function을 선택해서 직접 trigger(Event)와 환경을 설정합니다.

Lambda Function 생성하기 - 3

  1. Lambda를 일정 시간 규칙에 맞게 호출할 것이기 때문에 CloudWatch Events - Schedule를 선택합니다. CloudWatch는 AWS에서 실행되는 애플리케이션을 위한 모니터링 서비스 뿐만 아니라 Schedule도 제공합니다.
    (Lambda를 실행시키기 위한 trigger로 다양한 방법이 제공됩니다. 한가지 예시로 S3의 특정 Bucket에 이미지 파일이 업로드 될때마다 ReSizing을 하고싶다면 trigger로 S3를 설정한 후 Bucket과 Event type(Delete, Put, Post, Copy)을 설정하여 해당 Lambda를 수행하도록 할 수 있습니다.)

Lambda Function 생성하기 - 4

  1. CloudWatch Events - Schedule의 내용을 작성합니다. Rule nameRule description에는 해당 Schedule의 이름과 설명을 적고, Schedule expression에는 Event를 발생시키고자 하는 주기에 맞게 Cron 식을 작성합니다.
    Schedule expression에 작성한 Cron 식에 따라 Event가 발생하는 시간이 달라집니다. Cron 식을 처음 접하시는 분은 AWS 홈페이지1, AWS 홈페이지2를 참고해서 Cron 식을 작성합니다.

단, Cron 식에 사용되는 시간은 UTC를 기준으로 하기 때문에 유의해야 합니다. 제가 작성한 cron 식 (0 13 ? * SUN *)을 기준으로 하면 매주 일요일 22(13+9)시에 이벤트가 발생합니다.

Lambda Function 생성하기 - 5

6-1) 다음과 같이 Lambda function의 이름, 설명, 실행환경(포스팅 기준 Node.js 6.10 이 최신 버전입니다.) 을 작성합니다.

6-2) 다음으로 CloudWatch Events에 의해서 실행될 코드를 기재해야합니다. 코드를 기재하는 방식은 직접 작성, .ZIP 파일로 압축, S3를 이용하는 3가지 방식중 하나로 진행됩니다.
만약 작성한 코드가 aws-sdk를 제외한 외부 라이브러리를 사용하고 있다면 코드와 함께 라이브러리 파일(node_modules)을 압축하여 .ZIP 파일을 업로드 해야합니다.

6-3) 기존에 작성했던 코드에서 수정이 필요합니다. Lambda function의 코드가 에러없이 잘 완료되었는지 callback을 통해 결과를 반환해야합니다. 지난번에 작성했던 코드는 다음과 같이 수정됩니다. (예시를 들기위해 간단하게 작성했을 뿐, 실제 DB에 Insert 하는 부분은 생략되어 있습니다.)
29, 30번째 줄과, 34번째 줄에 추가된 코드에 주의하셔야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
exports.handler = (event, context, callback) => {
const request = require('request');
const cheerio = require('cheerio');

const url = "http://www.inu.ac.kr/com/cop/mainWork/foodList1.do?siteId=inu&id=inu_050110010000&command=week";

request(url, (error, response, body) => {
if (error) throw error;

let $ = cheerio.load(body);

try {
let krDay = '';
let corner = '';
let menu = '';

$('table').find('tr').each(function (index, elem) {
if (index % 6 === 0) {
krDay = $(this).find('th').text().trim();

console.log(`${krDay}`);
} else {
corner = $(this).find('th').text().trim();
menu = $(this).find('th').next().text().trim();

console.log(`${corner} -> ${menu}`);
}

callback(null);
context.succeed();
});
} catch (error) {
console.error(error);
callback(err);
}
});
};

Lambda Function 생성하기 - 6

6_4) 필요에 따라 환경변수(Environment variables)와 Advanced settings를 작성한 후 Role을 작성하고 다음단계로 넘어갑니다. (Handler에 index.handler 라고 작성했기 때문에 .ZIP파일로 압축할때 소스코드(.js)의 파일명은 index가 되어야 합니다)

Lambda Function 생성하기 - 6

  1. 지금까지 설정한 옵션을 확인하고 최종적으로 function을 생성합니다.

Lambda Function 생성하기 - 7

  1. 생성된 Lambda Function을 테스트할 수 있습니다. Actions -> Configure test event를 통해 테스트를 위한 이벤트를 설정합니다. trigger(Event)로 CloudWatch Events - Schedule를 설정했기 때문에 event template 목록 중 Schedule Event를 선택합니다.

Lambda Function 테스트 - 1
Lambda Function 테스트 - 2

  1. 테스트 후 CloudWatch의 로그를 통해서 lambda function 코드에서 출력했던 로그 및 실행결과를 확인할 수 있습니다.

Lambda Function 로그 확인 - 1
Lambda Function 로그 확인 - 2


## 정리하며

지금까지 진행한 작업은 다음의 이미지 한장으로 정리가 가능합니다. 이번기회에 lambda를 처음 사용해봤지만 앞으로 개인프로젝트에도 실무에서도 자주 사용하게될 것 같습니다.

AWS의 CloudWatch와 Lambda

4) 학식 메뉴 크롤링 하기

Index

  1. 크롤링이란?
  2. Scraping - 데이터 가져오기
  3. Parsing - 데이터 추출하기
  4. 크롤링 이후

인천대학교-에밀리에 처음 추가한 기능은 교내 식당의 메뉴를 알려주는 기능이였습니다. 학교 DB에 접근할 수 있는 권한이 없었기 때문에 하루마다 매일 갱신되는 식당의 메뉴를 알기 위해서는 학교 홈페이지에서 데이터를 참조하는 방법밖에 없었습니다. 이를 해결하기 위해 **크롤링**에 대해 공부하고 구현했던 방법에 대해 포스팅하려 합니다.

Node 버전은 6.6.0을 사용합니다.

## 크롤링이란?

우리가 흔히 부르는 **크롤링(crawling)**이란 **스크래핑(scraping)**이라고도 합니다. 즉 웹 페이지를 그대로 가져와 데이터를 추출해 내는 행위입니다. 크롤링 하는 소프트웨어를 크롤러(crawler)라고 부릅니다.

웹 크롤러(web crawler)는 조직적, 자동화된 방법으로 월드 와이드 웹(WWW)을 탐색하는 프로그램입니다. 검색 엔진과 같은 여러 사이트에서는 데이터의 최신 상태 유지를 위해 웹 크롤링을 합니다. 웹 크롤러는 대체로 방문한 사이트의 모든 페이지 복사본을 생성하는데 사용되며, 검색 엔진은 이렇게 생성된 페이지를 보다 빠른 검색을 위해 인덱싱합니다.

웹 크롤러는 시드(seeds)라고 불리는 URL 리스트에서부터 시작해서 페이지의 모든 하이퍼링크를 인식하여 URL 리스트를 갱신합니다. 갱신된 리스트를 재귀적으로 다시 방문합니다.

결국 웹 크롤러는 엄청난 분량의 웹문서를 사람이 직접 구별해서 모으는 일은 거의 불가능하기 때문에, 이를 자동으로 수행해 주는 것입니다.

웹은 기본적으로 HTML 형태로 되어 있습니다. 아래와 같이 **페이지 소스 보기**를 통해 확인할 수 있습니다.

페이지 소스 보기로 확인한 HTML 구조

위와 같은 HTML의 형태를 분석해서 원하는 정보만을 뽑아오는 것을 **웹 크롤링**작업이라고 합니다.

웹 사이트의 데이터를 수집하는데 있어서 2가지 단계만 기억하면 됩니다. 일반적으로는 이 2가지 과정을 합쳐 크롤링이라고 부르지만, 각각의 단계는 명확히 구분됩니다.

1.Scraping : 데이터를 어떻게 가져올 것인가?

2.Parsing : 가져온 데이터를 어떻게 추출할 것인가?


## Scraping - 데이터 가져오기

Javascript에서 웹 페이지 데이터를 가져오기 위해서 request 모듈을 사용합니다.

먼저 request 모듈을 설치합니다.

1
npm install request --save
1
2
3
4
5
6
7
8
9
const request = require('request');

const url = "http://www.inu.ac.kr/com/cop/mainWork/foodList1.do?siteId=inu&id=inu_050110010000&command=week";

request(url, (error, response, body) => {
if (error) throw error;

console.log(body);
});

위의 코드를 실행하면 아래와 같이 제가 얻고자 했던 학식 메뉴의 데이터를 포함한 웹 페이지 데이터를 가져옵니다. 해당 데이터는 크롬에서 **보기 - 개발자 정보 - 소스 보기**를 통해서도 확인이 가능합니다.

웹 페이지 Scraping


## Parsing - 데이터 추출하기

Scraping을 통해 가져온 데이터에서 원하는 데이터만을 추출(Parsing)하기 위해 cheerio 모듈을 사용합니다.

cheerio 모듈을 설치합니다.

1
npm install cheerio --save

cherrio 모듈을 사용하면 웹 클라이언트 자바스크립트 라이브러리인 jQuery 에서 사용하는 것 처럼 style을 통해 element를 선택할 수 있어, 기존의 선택자 방식을 그대로 사용할 수 있습니다.

따라서 cherrio를 사용하기 위해서는 Request 모듈을 통해 가져온 HTML 데이터에서 어떤 element가 우리가 원하는 데이터를 갖고 있는지 파악할 필요가 있습니다.

웹 페이지의 HTML 구조는 크롬의 **보기 - 개발자 정보 - 개발자 도구**를 통해 확인할 수 있습니다.

학식 메뉴 데이터를 포함하고 있는 table은 다음과 같은 구조를 갖고 있습니다.

HTML 구조 - 개발자 도구

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<table>
<thead>
<tr>
<th class="bdlrNone">29(월)</th>
</tr>
</thead>
<tbody>
<tr>
<th>1코너</th>
<td>햄순두부찌개 / 고추장삼겹살볶음 / 훈제오리철판구이 (2인)</td>
<td></td>
<td class="bdlrNone"></td>
</tr>
<tr>
<th>2코너</th>
<td>등심돈까스&해쉬포테이토 / 떡갈비난자완스덮밥</td>
<td></td>
<td class="bdlrNone"></td>
</tr>
<tr>
<th>3코너</th>
<td>냉모밀&닭갈비덮밥</td>
<td></td>
<td class="bdlrNone"></td>
</tr>
<tr>
<th>4코너</th>
<td>삼겹살스테이크,치즈함박스테이크,고르곤졸라피자,치즈오븐스파게티(토마토),빠네파스타,도리아(불닭,불고기),불고기샐러드,닭가슴살샐러드,훈제연어샐러드,옛날통닭</td>
<td></td>
<td class="bdlrNone"></td>
</tr>
<tr>
<th>5코너</th>
<td>신라면,신라면정식,만두라면,쫄면,해물짬뽕라면,폭탄라면,떡라면,치즈라면,크림치즈김밥,참치김밥,즉석떡볶이,짜파게티,냄비우동</td>
<td></td>
<td class="bdlrNone"></td>
</tr>
</tbody>
<thead>...</thead>
<tbody>...</tbody>
<thead>...</thead>
<tbody>...</tbody>
<thead>...</thead>
<tbody>...</tbody>
<thead>...</thead>
<tbody>...</tbody>
<thead>...</thead>
<tbody>...</tbody>
<thead>...</thead>
<tbody>...</tbody>
</table>

제가 필요한 데이터는 1. 날짜, 2. 코너, 3. 메뉴 인데 이 데이터는 다음과 같은 구조로 존재합니다.

table 태그 안에 thead > tr > th 에는 날짜, tbody > tr > th 에는 코너, tbody > tr > td 에는 메뉴가 있습니다.

thead와 tbody 태그를 사용해서 데이터를 추출할 수 있지만 테이블이 규칙적으로 만들어져 있기 때문에 table 태그의 tr 태그만 추출한 후 규칙을 이용해서도 쉽게 데이터를 추출 할 수 있습니다.

table에서 하루의 식단은 6개의 tr 태그마다 반복 되고 있습니다.
1번째 tr 태그는 th 태그 안에 날짜 데이터를 갖고 있고,
2번째~6번째 tr 태그는 th 태그 안에는 코너 데이터를, td 태그 안에는 메뉴 데이터를 갖고 있습니다.

이를 이용해 다음과 같이 코드를 작성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const request = require('request');
const cheerio = require('cheerio');

const url = "http://www.inu.ac.kr/com/cop/mainWork/foodList1.do?siteId=inu&id=inu_050110010000&command=week";

request(url, (error, response, body) => {
if (error) throw error;

let $ = cheerio.load(body);

try {
let krDay = '';
let corner = '';
let menu = '';

$('table').find('tr').each(function (index, elem) {
if (index % 6 === 0) {
krDay = $(this).find('th').text().trim();

console.log(`${krDay}`);
} else {
corner = $(this).find('th').text().trim();
menu = $(this).find('th').next().text().trim();

console.log(`${corner} -> ${menu}`);
}
});
} catch (error) {
console.error(error);
}
});

결과는 다음과 같습니다.

parsing 결과


## 크롤링 이후

크롤링을 통해 원하는 데이터를 추출했다면 이를 활용해야 합니다. 에밀리에서는 사용자의 요청(학생식당, 카페테리아, 사범대식당, 기숙사식당, 교직원식당)에 따라 해당하는 식당의 메뉴를 제공합니다.

메뉴라는 데이터 특성상 실시간으로 변경되는 데이터가 아니기 때문에 한번 크롤링 하고 나면 사용자의 요청(식당)에 따라 다른 응답해주면 될 뿐 데이터 자체는 요일이 변경되지 않는이상 계속 유효합니다. 따라서 학교 사이트에서 메뉴를 크롤링하는 행위는 학교 메뉴가 갱신되는 일주일 주기마다 한번씩만 하고 크롤링한 데이터를 DB에 저장한다면 사용자의 요청마다 반복적인 크롤링을 하지 않을 수 있습니다.

이를 위한 해결 방법으로는 node-schedule 모듈을 사용해서 정해진 시간마다 실행되도록 하는 방법이 있습니다.

혹은 AWS-Lambda를 사용할 수도 있습니다.

저는 AWS-Lambda 를 사용해서 스케쥴링을 하였는데 이와 관련된 내용은 다음에 포스팅하도록 하겠습니다.

3) Express 프로젝트 생성하기

Index

  1. 들어가기 앞서
  2. Express 프로젝트 생성하기
  3. 옐로아이디 API 문서 살펴보기
  4. API 작성하기
  5. 서버 실행 & API 연동하기
  6. 마치며

## 들어가기 앞서 **옐로아이디 자동응답API** 를 만드는데에는 여러가지 방법이 있습니다. 저는 **`Express generator`**를 사용하여 생성한 **`Express`** 기반의 프로젝트로 **Emily**를 만들었고 이 방식으로 설명할 예정입니다. 그러나 간단한 기능만 구현하는 경우 AWS의 Lambda와 API GateWay를 사용해서도 Serverless하게 만들 수 있습니다. 해당 내용은 [박상권의 삽질블로그](http://gun0912.tistory.com/63)를 참고하면 좋을것 같습니다.
## Express 프로젝트 생성하기 >Node 버전은 **6.6.0**을 사용합니다. Node가 설치되어 있지 않다면 글을 작성하는 기준으로 LTS 버전인 **6.10.3**이상의 버전을 설치합니다. 이왕이면 nvm을 이용하여 설치하는것을 권장합니다. ([nvm 관련글 참고](https://jongmin92.github.io/2016/09/20/Node/nvm-(Node-Version-Manager)) npm, Express 에 대해서는 해당 글에서 자세하게 설명하지 않겠습니다. 더 좋은 글들이 많으니 참고하시기 바랍니다!!

이제 Express 프로젝트를 생성하겠습니다.

npm을 통해 **Express generator**를 먼저 설치합니다. (global로 추가하기 위해 -g 옵션을 사용합니다.)

1
$ npm install express-generator -g

Express generator를 사용해서 프로젝트를 생성합니다. 저는 Emily라는 이름으로 프로젝트를 생성했습니다.

1
$ express Emily

생성된 프로젝트 폴더로 이동하여 모듈을 설치합니다.

1
2
$ cd Emily
$ npm install

생성된 프로젝트는 다음과 같은 구조입니다. 이제 옐로아이디 API 문서를 참고해서 간단한 봇을 만들어보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
├── error.jade
├── index.jade
└── layout.jade

## API 문서 살펴보기 카카오톡 이용자가 플러스친구, 옐로아이디에게 보낸 메세지를 카카오톡 서버를 통해 저희가 전달 받은 후 다시 카카오톡 서버로 응답 메시지를 전달해야하기 때문에 카카오톡에서 정의하고 있는 API 문서에 맞게 코드를 작성해야 합니다.

지금부터 옐로아이디 API 문서를 살펴보겠습니다. 본 포스팅에서는 **[이용에 대한 참고사항, 개인정보 수집 및 이용에 대한 주의사항]**에 대해서는 따로 설명하지 않습니다. 그러나 개발전에 한번쯤은 꼭 읽어보셔야합니다.

1. 용어

먼저 API 문서에서 사용되는 용어에 대해서 살펴보겠습니다.

(1) 수신 API
카카오톡 이용자가 플러스친구, 옐로아이디에게 보낸 메시지를 전달 받은 후 응답을 할 수 있는 API 입니다.
http(s) restful api를 통하여 카카오 API 서버 -> 파트너 서버를 호출합니다.

(2) app_key
플러스친구/옐로아이디에서 자동응답을 위한 앱 등록시 프로필별로 발급되는 고유 키 값입니다.
자동응답 기능만 이용하시는 경우 사용되지 않으며, 일부 app_secret을 통한 별도 인증이 필요한 일부 프로필에만 사용됩니다.

(3) app_secret
인증을 위해 app_key와 조합하여 사용되는 키 값입니다.
자동응답 기능만 이용하시는 경우 사용되지 않으며, 일부 app_secret을 통한 별도 인증이 필요한 일부 프로필에만 사용됩니다.

(4) user_key
특정 카카오톡 이용자를 구분하기 위한 key 입니다. 카카오에서는 이용자의 개인정보를 외부에 제공하지 않으므로, 외부 파트너사에서 카카오톡 이용자를 구분하기 위해서는 카카오로부터 API를 통해 user_key를 response로 받아야 합니다.
user_key는 특정 카카오톡 이용자에 대해 프로필별로 각기 다르게 발급됩니다. 따라서 user_key는 해당 프로필에 대해서만 유효합니다.
카카오톡 이용자가 프로필을 차단했다가 다시 추가한 경우에는 user_key가 갱신되지 않으며, 이용자가 카카오톡 탈퇴 후 재가입한 경우 갱신됩니다.

2. API

카카오에서 제공하고 있는 옐로아이디 API는 아래와 같이 5개 입니다. 각 API에 대한 자세한 내용은 문서를 참고합니다.

Action Method URL 설명
키보드 GET /keyboard 처음에 사용자가 채팅방에 들어왔을때 호출되는 API. 사용자에게 키보드에 보여질 내용을 리턴
메시지 POST /message 사용자가 보내는 메세지/사진/동영상 을 받아 처리해 응답
친구추가 POST /friend 사용자가 옐로아이디를 친구추가했을때 호출되는 API
친구차단(삭제) DELETE /friend 사용자가 옐로아이디를 친구차단(삭제)했을때 호출되는 API
채팅방 나가기(삭제) DELETE /chat_room 사용자가 채팅방 나가기(삭제)했을때 호출되는 API

## API 작성하기 API 문서를 참고하여 코드를 작성하겠습니다. 작성하는 코드는 사용자가 옐로아이디를 친구 추가 후 채팅방에서 입력하는 메시지를 받아 그대로 다시 전달해주는 코드입니다.

routes 폴더 아래에 **api.js**를 추가 후 다음 코드를 작성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const express = require('express');
const router = express.Router();

// 처음에 사용자가 채팅방에 들어왔을때 호출되는 API. 사용자에게 키보드에 보여질 내용을 리턴
router.get('/keyboard', function (req, res) {
res.status(200).json(
{
type: 'text'
}
);
});

// 사용자가 보내는 메세지/사진/동영상 을 받아 처리해 응답
router.post('/message', function (req, res) {
const { user_key, type, content } = req.body;

console.log(`"${user_key}"가 "${content}" 메시지 입력`);

res.status(200).json({
message: {
content
}
});
});

// 사용자가 옐로아이디를 친구추가했을때 호출되는 API
router.post('/friend', function (req, res) {
const userKey = req.body.user_key;

console.log(`"${userKey}"가 옐로아이디 친구 추가`);

res.status(200).json();
});

// 사용자가 옐로아이디를 친구차단(삭제)했을때 호출되는 API
router.delete('/friend/:userKey', function (req, res) {
const userKey = req.body.userKey;

console.log(`"${userKey}"가 옐로아이디 친구 삭제`);

res.status(200).json();
});

// 사용자가 채팅방 나가기(삭제)했을때 호출되는 API
router.delete('/chat_room/:userKey', function (req, res) {
const userKey = req.body.user_key;

console.log(`"${userKey}"가 옐로아이디 채팅방 삭제`);

res.status(200).json();
});

module.exports = router;

작성한 router를 **app.js**에 추가합니다.

1
2
const api = require('./routes/api');
app.use('/api', api);

## 서버 실행 & API 연동하기 서버는 해당 프로젝트 경로에서 **npm start** 혹은 **node bin/www** 로 실행할 수 있습니다. 그러나 해당 서버는 외부에서 접근 가능해야 합니다. 즉, AWS의 EC2와 같은 클라우드 자원을 사용하거나(추천), 본인의 컴퓨터 혹은 노트북에서 서버를 실행한다면 포트포워딩을 통해 **외부에서 접근할 수 있도록 설정이 필요합니다.**

외부에서 접근할 수 있도록 설정이 끝난 후에는 옐로아이디 홈페이지에서 해당 서버의 URL을 기재합니다.

옐로아이디 자동응답 API 등록-1

옐로아이디 자동응답 API 등록-2

앱 URL을 기재하고 API TEST 버튼을 누르면 카카오에서 해당 URL에 test request를 요청합니다. test request의 결과를 통해 잘 작동하는지 확인이 가능합니다.


## 마치며 이번 포스팅에서는 Express 프로젝트를 생성하고 API 문서를 토대로 간단한 봇을 만들어보았습니다. 해당 API를 토대로 앞으로 구현하고자 하는 기능을 구현하면 될것입니다.

다음번에는 제가 Emily를 만들며 구현하고자 했던 부분들을 어떤식으로 해결했는지에 대해 포스팅하겠습니다.

2) 카카오 옐로아이디 만들기

Index

  1. 옐로아이디란?
  2. 옐로아이디 만들기

## 옐로아이디란? [옐로아이디](https://yellowid.kakao.com/login)란 `고객과 소통할 수 있는 카카오톡 비즈니스 아이디입니다.` 옐로아이디를 무료로 만들어 비즈니스에 활용가능하며 옐로아이디는 별도의 전화번호가 필요없고, 한글 아이디 검색이 가능합니다.

옐로아이디 검색결과

옐로아이디는 API형 자동응답이 가능하기 때문에 사용자의 요청에 따라 자유롭게 답변 시나리오 및 이벤트를 구성할 수 있습니다.

카카오 뿐만 아니라 페이스북, 텔레그램 또한 API형 자동응답 기능을 제공하기 때문에 굳이 옐로아이디를 사용하지 않아도 자동응답 챗봇을 만들 수 있습니다.


## 옐로아이디 만들기 옐로아이디를 사용하기위해서는 먼저 정해진 절차에 따라 가입신청을 해야합니다. [Yellow ID 신규가입](https://yellowid.kakao.com/join)을 통해 옐로아이디 신청이 가능합니다. 모바일이라면 [Yellow ID App](https://play.google.com/store/apps/details?id=com.kakao.yellowid) 설치 후 신청이 가능합니다.

옐로아이디 API 문서는 공개되어 있기 때문에 옐로아이디 가입신청 후 기획이 되어있다며 바로 개발이 가능합니다. 가입신청이 완료되면 발급받은 API 키만 적용해주면 됩니다.

신청시 주의사항

  • 프로필 이름옐로아이디개설 후 변경이 불가능합니다.(위의 사진에서 인천대학교 에밀리가 프로필 이름, @인천대학교-에밀리가 옐로아이디를 의미합니다.
  • 주소는 입력하지 않아도 상관없습니다.
  • 홈페이지 주소를 입력하게 되는데 저는 처음 블로그 주소를 기재하였다가 옐로아이디의 목적과 맞지 않다는 이유로 반려되었습니다. 옐로아이디에 대한 설명이 있는 홈페이지가 없다면 페이스북 페이지를 생성한 후 간단히 옐로아이디에 대한 목적과 서비스에 대해 간단한 글을 남긴 후 홈페이지 주소에 페이스북 페이지를 기재하는 방법이 좋습니다.
  • 심사는 2~3일 정도의 시간이 소요됩니다. 심사에서 반려될 경우 해당 반려사유를 참고하여 수정 후 재심사를 요청해야합니다. 재심사도 2~3일 정도의 시간이 소요되기 때문에 처음 신청시 반려되는 불상사가 일어나지 않도록 공들여 작성해야합니다.

1) 에밀리 개발 시작하기

Index

  1. 들어가기 앞서
  2. 인천대학교-에밀리?
  3. 에밀리와의 대화방식
  4. 현재의 에밀리
  5. 앞으로의 에밀리
  6. 마치며

## 들어가기 앞서 기술 블로그의 좋은 글과 책을 통해 배우는 것도 많지만, 개인 프로젝트를 하며 배우는 점도 많은것 같습니다. 이번에는 퇴근 후 저녁시간과 주말을 이용해 진행했던 [인천대학교-에밀리](http://plus.kakao.com/home/@인천대학교-에밀리) 프로젝트에 대해 포스팅하려 합니다. 이전에도 여러 개인 프로젝트를 해왔지만 대부분 중도 마무리되거나, 완성 후 실제적으로 운영하지는 않았습니다. 아무래도 완성을 해도 이용자가 없다보니 **내가만든걸 누군가가 사용한다는 기쁨**을 맛보지 못해 그만두었던것 같습니다. 그래서 그동안 개인 프로젝트를 하는데에는 공부했던 것을 토대로 무언가를 만들어보자는 목적이었다면, 이번에는 **사용자의 범위를 최대한 좁혀 모든 기능을 특정 사용자에 맞추고 두고두고 사용할 수 있는 그런 서비스**를 만들어 보고싶었습니다.

카카오 플러스친구 - 웨더뉴스

위의 사진은 카카오 플러스친구인 웨더뉴스에서 매일 오전 날씨정보를 제공해주는 모습입니다. 하루 한번 날씨 정보를 제공해주는데, 글을 작성하는 2017-03-10 기준으로 531,980명이 이용하고 있습니다. 날씨를 제공해주는 서비스는 이전부터 많이 제공되고 있었지만 제일 사용빈도가 높은 카카오톡 서비스와 결합하다 보니 더 사용자들이 많이 찾는것 같았습니다. 저 또한 굳이 인터넷 검색이나 다른 앱을 실행하지 않고도 정보를 얻을 수 있다는 점에 가장 큰 가치를 두고 사용하고 있었습니다. 이점이 큰 메리트라고 느꼈고 이를 토대로 인천대학교-에밀리 프로젝트를 기획하고 개발하게 되었습니다.


## 인천대학교-에밀리? 왜 하필 에밀리일까요? [네이버 웹툰 - 신의탑](http://comic.naver.com/webtoon/list.nhn?titleId=183559&weekday=mon)을 보면 다음과 같이 에밀리가 등장합니다.

신의탑 에밀리

신의탑 웹툰에서 에밀리는 사용자가 궁금해 하는것을 모두 알려줍니다. 이러한 에밀리의 특징과 사용자의 범위를 최대한 좁히기 위해 제가 다녔던 인천대학교의 학생을 사용자로 맞추어 인천대학교-에밀리를 개발하게 되었습니다.


## 에밀리와의 대화방식 `인천대학교 학생들을 위해 필요한 정보를 쉽게 제공해준다.`라는게 이번 서비스의 목표입니다. 후에 다시 한번 알아보겠지만 카카오톡의 플러스친구/옐로아이디는 `자동응답 API`를 제공하는데 이를 통해 `(1)사용자가 직접 메시지를 입력` 혹은 `(2)버튼을 통해 객관식으로 정해진 내용을 입력`하도록 할 수 있습니다. 사용자가 쉽고 빠르게 정보를 얻을 수 있게 하기 위해서는 메시지보다는 버튼을 통해 사용자가 이용할 수 있는 기능을 알려주고 직접 선택 할 수 있도록 하는게 편리하고 빠르다는 생각에 버튼 방식으로 개발하기로 하였습니다.
## 현재의 에밀리 현재 에밀리는 다음과 같은 사용 가능합니다. `열람실`, `메뉴`, `날씨` 정보를 제공하고 있습니다.
# 사용법 (에밀리의 간단한 사용방법을 알 수 있습니다.)
![인천대학교-에밀리(사용법)](/images/post/2017-03-10/emily-use.jpeg)
# 열람실 (실시간 지하 도서관의 열람실 정보와 좌석도(링크)를 제공합니다.)
![인천대학교-에밀리(사용법)](/images/post/2017-03-10/emily-readingroom.jpeg)
# 메뉴 (학생식당, 기숙사식당 등... 교내 식당의 당일 메뉴를 제공합니다.)
![인천대학교-에밀리(사용법)](/images/post/2017-03-10/emily-menu.jpeg)
# 날씨 (어제와 오늘 인천대학교 날씨 정보를 제공합니다.)
![인천대학교-에밀리(사용법)](/images/post/2017-03-10/emily-weather.jpeg)
## 앞으로의 에밀리 지금까지 제공하는 기능들은 대부분 학교 홈페이지 크롤링을 이용하거나 외부 API를 사용해 제공하는 기능였습니다. 앞으로도 정보제공에 관한 기능들도 추가할 것이지만, 단순한 정보제공 외에도 인천대학교 학생들이 생활하는데 있어 편리할만한 기능을 추가할 예정입니다. 가령 자신의 학교 시간표를 에밀리를 통해 제공받은 웹 페이지에서 작성한 후 저장하면, 그 후로는 에밀리를 통해 자신의 시간표에 대한 정보를 제공받을 수 있습니다. 다음 수업의 시간과 강의실 정보, 오늘 전체적인 시간표 보기 등... 사용자가 입력한 정보를 바탕으로 개개인에 맞게 정보를 제공할 생각입니다.
## 마치며 실제로 사용자가 이용할 수 있는 서비스를 만드는것은 정말 재밌는 일인것 같습니다. 앞으로 계속해서 기능을 추가할 것이며, 앞으로의 포스팅은 지금까지 에밀리를 만들었던 과정과, 앞으로 추가하며 작업하는 내용을 다룰 예정입니다. [인천대학교-에밀리](http://plus.kakao.com/home/@인천대학교-에밀리)를 사용해보시고 혹시나 궁금한 점이 있으시다면 댓글로 남겨주시면 답변드리겠습니다.