블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 

'안드로이드'에 해당되는 글 22

  1. 2017.03.10 구글의 IOT 솔루션
  2. 2016.09.01 파이어베이스 애널러틱스를 이용한 모바일 데이타 분석- #3 빅쿼리에 연동하여 모든 데이타를 분석하기
  3. 2016.08.29 파이어베이스 애널러틱스를 이용한 모바일 데이타 분석 #1-Hello Firebase (3)
  4. 2015.11.27 모바일 앱 개발을 지원하는 - Twitter fabric overview
  5. 2015.10.16 안드로이드 채팅 UI 만들기 #2 - 나인패치 이미지를 이용한 채팅 버블 (1)
  6. 2015.10.13 안드로이드 채팅 UI 만들기 #1 - ListView를 이용한 채팅창 만들기 (8)
  7. 2015.10.06 안드로이드 Fragment 이해하기
  8. 2015.10.02 안드로이드에서 동영상 재생하기 (3)
  9. 2015.09.23 SharedPreference를 이용한 데이타 저장 및 애플리케이션간 데이타 공유
  10. 2015.09.16 안드로이드 웹뷰(Webview)의 이해와 성능 최적화 방안 (2)
  11. 2015.09.15 안드로이드에서 REST API 호출하기 (2)
  12. 2015.08.27 안드로이드 주요 레이아웃 (1)
  13. 2015.08.18 안드로이드 기초 - 컴파일,설치,실행 (커맨드라인)
  14. 2015.08.09 안드로이드 리소스파일 (Resource)
  15. 2015.08.09 안드로이드 인텐트를 통해서 새로운 액티비트에 데이타 전달하기 (2)
  16. 2015.07.23 안드로이드 기초-뷰와 뷰그룹의 개념
  17. 2015.06.22 안드로이드 액티버티 생명 주기와 BackStack (3)
  18. 2015.06.21 안드로이드 프로그래밍 구조의 기본 (1)
  19. 2015.06.15 안드로이드 플랫폼 기본 아키텍쳐
  20. 2015.06.14 안드로이드 프로그래밍 시작하기 (7)
 

구글의 IOT 솔루션

클라우드 컴퓨팅 & NoSQL/M2M & IOT | 2017.03.10 10:31 | Posted by 조대협


구글의 IOT 솔루션


조대협 (http://bcho.tistory.com)


오늘 샌프란시스코 구글 NEXT 행사에서 IOT 솔루션에 대한 소개가 있었는데, 내용이 괜찮아서 정리를 해놓는다.



구글의 특징은 안드로이드 플랫폼, 클라우드 , 분석 플랫폼, 개발자 에코 시스템  등 End to End 에 걸쳐서 상당히 다양한 포트폴리오를 가지고 있다는 것이 장점인데 이를 잘 녹여낸 아키텍쳐 구성이다.

디바이스 OS

IOT는 라즈베리파이와 같은 임베디드 디바이스를 사용하는 것이 일반적인데, 이런 임베디드 시스템 운용에 어려운 점중의 하나가 보안이다.

장비에 따라서 보안적인 문제가 없는지 체크를 해야 하고, 주기적으로 기능 및 보안에 대한 업데이트를 해줘야 하는데, 구글의 Android IOT (https://developer.android.com/things/index.html) 플랫폼은 이를 다 자동으로 해준다.


더구나, 기존의 모바일 안드로이드 플랫폼을 기반으로 하기 때문에, 안드로이드 개발자 풀을 그대로 사용할 수 있다는 장점이 있다.

이미 Android IOT 플랫폼은 인텔,라즈베리파이등 여러 디바이스 업체와 협업을 하면서 Certi 작업을 하고 있기 때문에 잘 알려진 플랫폼이라면 보안 테스트나 별도의 기능 테스트 없이 바로 사용이 가능하다.


백앤드

IOT의 백앤드는 구글 클라우드 플랫폼을 이용한다.

  • 디바이스로 부터 수집된 데이타는 Pub/Sub 큐에 저장된후

  • DataFlow 프레임웍을 통해서 배치나 실시간 스트리밍 분석이 되고

  • 분석된 데이타는 빅테이블이나 빅쿼리에 저장된다. 분석이나 리포팅을 위해서는 빅쿼리, 타임 시리즈 데이타나 고속의 데이타 접근을 위해서는 빅테이블이 사용된다.

  • 이렇게 저장된 데이타들은 구글의 머신러닝 프레임웍 텐서플로우의 클라우드 런타임인 CloudML을 사용해서 분석 및 예측 모델을 만들게 된다.



머신러닝을 등에 탑재한  디바이스

구글이 재미있는 점은 텐서플로우라는 머신러닝 프레임웍을 가지고 있다는 것인데, 애초부터 텐서플로우의 디자인은 서버 뿐만 아니라, 클라이언트 그리고 IOT 디바이스에서 동작하게 디자인이 되었다. 그래서 학습된 모델을 디바이스로 전송하여, 디바이스에서 머신러닝을 이용한 예측이 가능하다.

예를 들어 방범용 카메라를 만들었을때, 방문자의 사진을 클라우드로 저장하는 시나리오가 있다고 하자.

그런데 매번 전송을 하면 배터리나 네트워크 패킷 요금이 문제가 될 수 있기 때문에, 텐서 플로우 기반의 얼굴 인식 모델을 탑재하여 등록되지 않은 사용자만 사진을 찍어서 클라우드로 전송하게 하는 등의 시나리오 구현이 가능하다.


파이어 베이스 연동

동영상을 보다가 놀란점 중의 하나는 파이어 베이스가 Android IOT에 연동이 된다.

아래 그림은 온도를 측정해서 팬의 속도를 조정하는 시나리오인데, 우측 하단에 보면 파이어베이스가 위치해 있다.



센서로 부터 온도를 측정한 다음, 디바이스 컨트롤러로 온도 조정 명령을 내리는 것을 파이어베이스 메시징 서비스를 이용하도록 되어 있다.


결론

Android IOT 서비스 하나만 IOT 서비스로 내놓은 것이 아니라 구글 클라우드 플랫폼, 텐서플로우에 파이어베이스까지 구글의 기존의 노하우들을 묶어서 포트폴리오를 만들어 내었고, 더구나 이러한 기술들이 개발자 에코 시스템이 이미 형성이 되어 있는 시스템인 점에서, IOT 개발에 있어서 누구나 쉽게 IOT 서비스를 개발할 수 있게 한다는데, 큰 의미가 있다고 본다.


'클라우드 컴퓨팅 & NoSQL > M2M & IOT' 카테고리의 다른 글

구글의 IOT 솔루션  (0) 2017.03.10
TI의 IOT 개발용 센서 키트  (0) 2016.03.17
MQTT 서버 간단 공부 노트  (2) 2014.02.13

파이어베이스 애널러틱스를 이용한 모바일 데이타 분석

#3 빅쿼리에 연동하여 모든 데이타를 분석하기


조대협 (http://bcho.tistory.com)


파이어베이스 애널러틱스의 대단한 기능중의 하나가, 모바일에서 올라온 모든 원본 로그를 빅쿼리에 저장하고, 이를 빅쿼리를 통해서 분석할 수 있는 기능이다. 대부분의 매니지드 서비스 형태의 모바일 애널리틱스 서비스는 서비스에서 제공하는 지표만, 서비스에서 제공하는 화면을 통해서만 볼 수 있기 때문에, 상세한 데이타 분석이 불가능하다. 파이어베이스의 경우에는 빅쿼리에 모든 원본 데이타를 저장함으로써 상세 분석을 가능하게 해준다.


아울러, 모바일 서비스 분석에 있어서, 상세 로그 분석을 위해서 로그 수집 및 분석 시스템을 별도로 만드는 경우가 많은데, 이 경우 모바일에 설치될 로그 수집 에이전트에서 부터 로그를 수집하는 API 서버, 이를 저장하기 위한 분산 큐(카프카 Kafka)와 같은 복잡한 백앤드 시스템을 설계 구현해야 하는데, 파이어베이스 애널러틱스의 로깅 기능을 이용하면 별도의 이런 인프라 구현이 없이도 손쉽게 로그를 수집 및 분석할 수 있다. (일종의 무임 승차라고나 할까?)


가격 정책

그렇다면 가장 고민이 되는 것이 가격 정책일 것이다. 파이어베이스 애널러틱스에서 빅쿼리에 데이타를 저장하려면 파이어베이스 플랜중 무료가 아닌 유료 플랜인 Blaze 플랜을 사용해야 한다.

그러나, 다행이도 Blaze 플랜은 “Pay as you go” 모델로 사용한 만큼 비용을 지불하는 모델인데, “Google Cloud Integration”은 별도의 비용이 부과 되지 않는다.



단지 빅쿼리에 대한 비용만 부담이 되는데, 빅쿼리의 경우 데이타 로딩은 무료이고, 저장 요금 역시 GB당 월 0.02$ (약 22원)이며, 90일동안 해당 데이타를 사용하지 않으면 이 요금은 50%로 자동 할인되서 GB당 월 0.01$(약 11원)만 과금된다. 이외에 쿼리당 비용이 과금되는데, 쿼리당 비용은 쿼리에서 스캔한 데이타 용량 만큼만 과금이 된다. TB를 쿼리 했을때 5$가 과금이되는데, 이역시 전체 테이블을 스캔을 하는것이 아니라, 쿼리에서 스캔하는 컬럼에 대해서만 과금이 되고, 전체 테이블이 아니라, 쿼리에서 스캔하는 날짜만 과금이 되기 때문에, 실제 과금 금액은 미미하다고 볼 수 있다. 실제로 실 서비스에서 모 앱의 하루 데이타를 수집한 경우 17만건의 이벤트가 수집되었는데 저장 용량은 전체 350 MB에 불과하다. 전체 컬럼을 스캔한다고 하더라도 (전체 컬럼을 스캔할 일은 없겠지만….) 쿼리 비용은 0.00175$에 불과하다.


파이어베이스 애널러틱스와 빅쿼리를 연동하여 데이타 수집하기

파이어베이스 애널러틱스에서 데이타를 빅쿼리로 수집하기 위해서는 앞에서 언급한바와 같이 먼저 파이어베이스 플랜을 Blaze로 업그레이드 해야 한다. 파이어베이스 콘솔 좌측 하단을 보면 아래와 같이 UPGRADE 버튼이 있다. 이 버튼을 눌러서 Blaze 플랜으로 업그레이드를 하자


다음으로 파이어베이스 애널러틱스 프로젝트를 빅쿼리와 연결을 해줘야 한다.

파이어베이스 콘솔 좌측 상단에서 설정 버튼을 누른 후에, Project settings 메뉴를 선택한다.


프로젝트 세팅 메뉴에 들어가서 상단 메뉴중에 ACCOUNT LINKING이라는 메뉴를 선택한다.


그러면 구글 플레이나 광고 플랫폼등과 연결할 수 있는 메뉴와 함께 아래 그림처럼 빅쿼리로 연결할 수 있는 메뉴와 “LINK TO BIGQUERY”라는 버튼이 화면에 출력된다.


이 버튼을 누르면 작업은 끝났다. 이제부터 파이어베이스의 모든 로그는 빅쿼리에 자동으로 수집되게 된다.

만약에 수집을 중단하고 싶다면 위의 같은 화면에서 LINK TO BIGQUERY라는 버튼이 MANAGE LINKING으로 바뀌어 있는데, 이 버튼을 누르면 아래와 같이 App Details가 나온다.



여기서 스위치 버튼으로 Send data to BigQuery를 끔 상태로 변경해주면 된다.

이제 부터 대략 한시간 내에, 데이타가 빅쿼리에 수집되기 시작할 것이다.  

수집 주기

그러면 파이어베이스 애널러틱스에서는 어떤 주기로 데이타를 수집하고 수집된 데이타는 언제 조회가 가능할까? 이를 이해하기 위해서는 앱 로그 수집에 관여되는 컴포넌트와 흐름을 먼저 이해할 필요가 있다.

로그 수집이 가능한 앱은 크게, 구글 플레이 스토어에서 배포되는 앱, 구글 플레이 스토어를 통하지 않고 배포되는 앱 그리고 iOS 앱 3가지로 나눌 수 있다.

이 앱들이 파이어베이스 서버로 로그를 보내는 방식은 앱마다 약간씩 차이가 있다.


  • 플레이스토어에서 다운 받은 앱 : 각 개별 앱이 이벤트 로그를 수집하여 저장하고 있다가 1시간 주기로, 모든 앱들의 로그를 모아서 파이어베이스 서버로 전송한다.

  • 플레이스토어에서 다운받지 않은 앱 : 플레이스토어에서 다운로드 받은 앱과 달리 다른 앱들과 로그를 모아서 함께 보내지 않고 한시간 단위로 로그를 모아서 개별로 파이어베이스에 전송한다.

  • iOS 앱 : 앱별로 한시간 단위로 로그를 모아서 파이어베이스 서버로 전송한다.


이렇게 앱에서 파이어베이스 서버로 전송된 데이타는 거의 실시간으로 구글 빅쿼리에 저장된다.

그러나 파이어베이스 애널러틱스의 대쉬 보다는 대략 최대 24시간 이후에 업데이트 된다. (24시간 단위로 분석 통계 작업을 하기 때문이다.)


이 전체 흐름을 도식화 해보면 다음과 같다.



수집된 데이타 구조

그러면 빅쿼리에 수집된 테이블은 어떤 구조를 가질까?

테이블 구조를 이해하기 전에 테이블 종류를 먼저 이해할 필요가 있다.

앱에서 수집한 로그는 안드로이드와 iOS 각각 다른 데이타셋에 저장되며, 테이블 명은

  • app_events_YYYYMMDD

가 된다. 2016년 8월30일에 수집한 로그는  app_events_20160830 이 된다.



Intraday 테이블

여기에 intraday 테이블이라는 개념이 존재하는데, 이 테이블은 app_events_intraday_YYYYMMDD 라는 이름으로 저장이 되는데, 이 테이블은 실시간 데이타 수집을 목적으로 하는 테이블로 오늘 데이타가 저장된다. 예를 들어 오늘이 2016년9월1일이라면, app_events테이블은 app_events_20160831 까지만 존재하고, 9월1일자 데이타는 app_events_intraday_20160901 이라는 테이블에 저장된다.

9월1일이 지나면 이 테이블은 다시 app_events_20160901 이라는 이름으로 변환된다.

intraday 테이블의 특성중의 하나는 몇몇 필드들은 값이 채워지지 않고 NULL로 반환된다. 모든 데이타를 수집하고 배치 연산을 통해서 계산이 끝나야 하는 필드들이 그러한데, LTV 값과 같은 필드가 여기에 해당한다.


여기서 주의할점 중의 하나가 intraday 테이블이 하나만 존재할것이라는 가정인데. 결론 부터 이야기 하면 최대 2개가 존재할 수 있다. 9월1일 시점에  app_events_intraday_20160901 테이블이 존재하다가 9월2일이 되면 app_events_intraday_20160902 테이블이 생성된다. app_events_intraday_20160901 를 app_events_20160901 테이블로 변환을 해야 하는데, 단순히 복사를 하는 것이 아니라, 배치 연산등을 수행하기 때문에 연산에 다소 시간이 걸린다. 그래서 연산을 수행하는 동안에는 app_events_intraday_20160901 테이블과 app_events_intraday_20160902이 동시에 존재하고, 9월1일 데이타에 대한 연산이 종료되면 app_events_intraday_20160901 은 app_events_20160901 로 변환 된다.  

테이블 스키마

빅쿼리에 저장된 데이타의 테이블 구조를 이해하기 위해서 빅쿼리의 데이타 저장 특성을 이해할 필요가 있는데, 빅쿼리는 테이블 데이타 구조를 가지면서도 JSON과 같이 컬럼안에 여러 컬럼이 들어가는 RECORD 타입이나, 하나의 컬럼안에 여러개의 데이타를 넣을 수 있는  REPEATED 필드라는 데이타 형을 지원한다.



<그림. 레코드 타입의 예>

레코드 타입은 위의 그림과 같이 Name이라는 하나의 컬럼 내에 Last_name과 First_name이라는 두개의 서브 컬럼을 가질 수 있는 구조이다.

아래는 REPEATED 필드(반복형 필드)의 데이타 예인데, Basket이라는 컬럼에 Books,Galaxy S7, Beer 라는 3개의 로우가 들어가 있다.


<그림. 반복형 필드 예>

이런 구조로 인하여, 빅쿼리는 JSON과 같이 트리 구조로 구조화된 데이타를 저장할 수 있고, 실제로 파이어베이스 애널러틱스에 의해 수집되어 저장되는 데이타도 JSON과 같은 데이타 구조형으로 저장이 된다.

많은 데이타 필드가 있지만, 큰 분류만 살펴보면 다음과 같은 구조를 갖는다.



하나의 레코드는 하나의 앱에서 올라온 로그를 나타낸다. 앱은 앞의 수집 주기에 따라서 한시간에 한번 로그를 올리기 때문에, 하나의 레코드(행/로우)는 매시간 그 앱에서 올라온 로그라고 보면 된다.


가장 상위 요소로 user_dim과, event_dim이라는 요소를 가지고 있다.

user_dim은 사용자나 디바이스에 대한 정보를 주로 저장하고 있고, event_dim은 앱에서 발생한 이벤트들을 리스트 형태로 저장하고 있다.

user_dim에서 주목할만한 것은 userid에 관련된 것인데, userid는 사용자 id 이지만, 파이어베이스가 자동으로 수집해주지는 않는다. 개발자가 앱의 파이어베이스 에이전트 코드에서 다음과 같이 setUserId 메서드를 이용해서 설정해줘야 빅쿼리에서 조회가 가능하다. (앱 서비스의 계정을 세팅해주면 된다.)

mFirebaseAnalytics.setUserId(Long.toString(user.id));

다음 주목할 필드는 user_dim에서 app_info.app_instance_id 라는 필드인데, 이 필드는 각 앱의 고유 ID를 나타낸다. 파이어베이스가 자동으로 부여하는 id로 설치된 앱의 id이다.

예를 들어 내가 갤럭시S7과 노트7를 가지고 같은 앱을 설치했다고 하더라도 각각 다른 디바이스에 설치되었기 때문에 각각의 앱 id는 다르다.


다음은 event_dim인데, event_dim은 이벤트들로 레코드들의 배열(리스트)로 구성이 되고 각각의 이벤트는 이벤트 이름과 그 이벤트에 값을 나타내는 name 과 params라는 필드로 구성이 되어 있다.  params는 레코드 타입으로 여러개의 인자를 가질 수 있고, params내의 인자는 또 각각 key와 value식으로 하여 인자의 이름과 값을 저장한다. values는 string_value,int_value,double_value 3가지 서브 필드를 가지고 있는데, 인자의 타입에 따라서 알맞은 필드에만 값이 채워진다. 예를 들어 인자의 타입이 문자열 “Cho” 이고, 인자의 이름이 “lastname”이면, params.key “lastname”이 되고, params.value.string_value=”Cho”가 되고 나머지 필드인 params.value.int_value와 params.value.float.value는 null이 된다.


   "event_dim": [

     {

       "name": "Screen",

       "params": [

         {

           "key": "firebase_event_origin",

           "value": {

             "string_value": "app",

             "int_value": null,

             "float_value": null,

             "double_value": null

           }

         },

         {

           "key": "Category",

           "value": {

             "string_value": "Main",

             "int_value": null,

             "float_value": null,

             "double_value": null

           }

         },

      ]

    },

     {

       "name": "Purchase",

       "params": [

         {

           "key": "amount",

           "value": {

             "string_value": null,

             "int_value": “5000”,

             "float_value": null,

             "double_value": null

           }

         }

         },

      ]

    },


위의 예제는 빅쿼리에 저장된 하나의 행을 쿼리하여 JSON형태로 리턴 받은 후, 그 중에서 event_dim 필드 내용 일부를 발췌한 것이다.

Screen과 Purchase라는 두개의 이벤트를 받았고,

Screen은 firebase_event_origin=”app”, Category=”main” 이라는 두개의 인자를 받았다.

Purchase는 amount=5000 이라는 정수형 인자 하나를 받았다.


전체 빅쿼리의 스키마는 다음과 같이 되어 있다.




파이어베이스 애널러틱스에서 빅쿼리로 저장된 테이블 스키마에 대한 상세는 https://support.google.com/firebase/answer/7029846?hl=en 를 참고하기 바란다.


구글 빅쿼리에 대한 자료 아래 링크를 참고하기 바란다.


  1. 2016.08.01 빅쿼리를 이용하여 두시간만에 트위터 실시간 데이타를 분석하는 대쉬보드 만들기

  2. 2016.07.31 빅데이타 수집을 위한 데이타 수집 솔루션 Embulk 소개

  3. 2016.06.18 빅쿼리-#3 데이타 구조와 접근(공유) (3)

  4. 2016.06.16 구글 빅데이타 플랫폼 빅쿼리 아키텍쳐 소개

  5. 2016.06.15 구글 빅데이타 플랫폼 빅쿼리(BIGQUERY)에 소개

  6. 빅쿼리로 데이타 로딩 하기 http://whitechoi.tistory.com/25


다음은 데이타랩을 통하여 데이타를 직접 분석해보도록 하겠다.


파이어베이스 애널러틱스를 이용한 모바일 데이타 분석 #1-Hello Firebase

조대협 (http://bcho.tistory.com)


얼마전에 구글은 모바일 백앤드 플랫폼인 파이어베이스를 인수하고 이를 서비스로 공개하였다.

파이어 베이스는 모바일 백앤드의 종합 솔루션으로, 크래쉬 리포팅, 리모트 컨피그를 이용한 A/B 테스팅 플랫폼, 클라우드와 자동 동기화가 가능한 리얼타임 데이타 베이스, 사용자 인증 기능, 강력한 푸쉬 플랫폼 다양한 모바일 기기에 대해서 테스트를 해볼 수 있는 테스트랩 등, 모바일 앱 개발에 필요한 모든 서비스를 제공해주는 종합 패키지와 같은 플랫폼이라고 보면 된다. 안드로이드 뿐만 아니라 iOS까지 지원하여 모든 모바일 앱 개발에 공통적으로 사용할 수 있다.



그중에서 파이어베이스 애널러틱스 (Firebase analytics)는 모바일 부분은 모바일 앱에 대한 모든 이벤트를 수집 및 분석하여 자동으로 대쉬 보드를 통하여 분석을 가능하게 해준다.


이 글에서는 파이어베이스 전체 제품군중에서 파이어베이스 애널러틱스에 대해서 수회에 걸쳐서 설명을 하고자 한다.


파이어베이스 애널러틱스

이미 시장에는 모바일 앱에 대한 데이타 분석이 가능한 유료 또는 무료 제품이 많다.

대표적으로 야후의 flurry, 트위터 fabric, 구글 애널러틱스등이 대표적인 제품군인데, 그렇다면 파이어베이스가 애널러틱스가 가지고 있는 장단점은 무엇인가?


퍼널 분석 및 코호트 분석 지원

파이어베이스 애널러틱스는 데이타 분석 방법중에 퍼넬 분석과 코호트 분석을 지원한다.

퍼널 분석은 한글로 깔데기 분석이라고 하는데, 예를 들어 사용자가 가입한 후에, 쇼핑몰의 상품 정보를 보고  주문 및 결재를 하는 단계 까지 각 단계별로 사용자가 이탈하게 된다. 이 구조를 그려보면 깔데기 모양이 되는데,사용자 가입에서 부터 최종 목표인 주문 결재까지 이루도록 단계별로 이탈율을 분석하여 서비스를 개선하고, 이탈율을 줄이는데 사용할 수 있다.

코호트 분석은 데이타를 집단으로 나누어서 분석하는 방법으로 일일 사용자 데이타 (DAU:Daily Active User)그래프가 있을때, 일일 사용자가 연령별로 어떻게 분포가 되는지등을 나눠서 분석하여 데이타를 조금 더 세밀하게 분석할 수 있는 방법이다.


이러한 코호트 분석과 퍼넬 분석은 모바일 데이타 분석 플랫폼 중에서 일부만 지원하는데, 파이어베이스 애널러틱스는 퍼넬과 코호트 분석을 기본적으로 제공하고 있으며, 특히 코호트 분석으로 많이 사용되는 사용자 잔존율 (Retention 분석)의 경우 별다른 설정 없이도 기본으로 제공하고 있다.


<그림. 구글 파이어베이스의 사용자 잔존율 코호트 분석 차트>

출처 : https://support.google.com/firebase/answer/6317510?hl=en

무제한 앱 및 무제한 사용자 무료 지원

이러한 모바일 서비스 분석 서비스의 경우 사용자 수나 수집할 수 있는 이벤트 수나 사용할 수 있는 앱수에 제약이 있는데, 파이어베이스 애널러틱스의 경우에는 제약이 없다.

빅쿼리 연계 지원

가장 강력한 기능중의 하나이자, 이 글에서 주로 다루고자 하는 내용이 빅쿼리 연동 지원이다.

모바일 데이타 분석 서비스 플랫폼의 경우 대 부분 플랫폼 서비스의 형태를 띄기 때문에, 분석 플랫폼에서 제공해주는 일부 데이타만 볼 수 가 있고, 원본 데이타에 접근하는 것이 대부분 불가능 하다.

그래서 모바일 애플리케이션 서버에서 생성된 데이타나, 또는 광고 플랫폼등 외부 연동 플랫폼에서 온 데이타에 대한 연관 분석이 불가능하고, 원본 데이타를 통하여 여러가지 지표를 분석하는 것이 불가능하다.


파이어베이스 애널러틱스의 경우에는 구글의 데이타 분석 플랫폼이 빅쿼리 연동을 통하여 모든 데이타를 빅쿼리에 저장하여 간단하게 분석이 가능하다.

구글 빅쿼리에 대한 소개는 http://bcho.tistory.com/1116 를 참고하기 바란다.

구글의 빅쿼리는 아마존 S3나, 구글의 스토리지 서비스인 GCS 보다 저렴한 비용으로 데이타를 저장하면서도, 수천억 레코드에 대한 연산을 수십초만에 8~9000개의 CPU와 3~4000개의 디스크를 사용해서 끝낼만큼 어마어마한 성능을 제공하면서도, 사용료 매우 저렴하며 기존 SQL 문법을 사용하기 때문에, 매우 쉽게 접근이 가능하다.

모바일 데이타 분석을 쉽게 구현이 가능

보통 모바일 서비스에 대한 데이타 분석을 할때는 무료 서비스를 통해서 DAU나 세션과 같은 기본적인 정보 수집은 가능하지만, 추가적인 이벤트를 수집하여 저장 및 분석을 하거나 서버나 다른 시스템의 지표를 통합 분석 하는 것은 별도의 로그 수집 시스템을 모바일 앱과 서버에 만들어야 하였고, 이를 분석 및 저장하고 리포팅 하기 위해서 하둡이나 스파크와 같은 복잡한 빅데이타 기술을 사용하고 리포팅에도 많은 시간이 소요 되었다.


파이어베이스 애널러틱스를 이용하면, 손 쉽게, 추가 이벤트나 로그 정보를 기존의 로깅 프레임웍을 통하여 빅쿼리에 저장할 수 있고, 복잡한 하둡이나 스파크의 설치나 프로그래밍 없이 빅쿼리에서 간략하게 SQL만을 사용하여 분석을 하고 오픈소스 시각화 도구인 Jupyter 노트북이나 구글의 데이타스튜디오 (http://datastudio.google.com)을 통하여 시작화가 간단하기 때문에, 이제는 누구나 쉽게 빅데이타 로그를 수집하고 분석할 수 있게 된다.

실시간 데이타 분석은 지원하지 않음

파이어베이스 애널러틱스가 그러면 만능 도구이고 좋은 기능만 있는가? 그건 아니다. 파이어베이스 애널러틱스는 아직까지는 실시간 데이타 분석을 지원하고 있지 않다. 수집된 데이타는 보통 수시간이 지나야 대쉬 보드에 반영이 되기 때문에 현재 접속자나, 실시간 모니터링에는 적절하지 않다.

그래서 보완을 위해서 다른 모니터링 도구와 혼용해서 사용하는 게 좋다. 실시간 분석이 강한 서비스로는 트위터 fabric이나 Google analytics 등이 있다.

이러한 도구를 이용하여 데이타에 대한 실시간 분석을 하고, 정밀 지표에 대한 분석을 파이어베이스 애널러틱스를 사용 하는 것이 좋다.


파이어베이스 애널러틱스 적용해보기

백문이 불여일견이라고, 파이어베이스 애널러틱스를 직접 적용해보자.

https://firebase.google.com/ 사이트로 가서, 가입을 한 후에, “콘솔로 이동하기"를 통해서 파이어 베이스 콘솔로 들어가자.

프로젝트 생성하기

다음으로 파이어베이스 프로젝트를 생성한다. 상단 메뉴에서 “CREATE NEW PROJECT”를 선택하면 새로운 파이어 베이스 프로젝트를 생성할 수 있다. 만약에 기존에 사용하던 구글 클라우드 프로젝트등이 있으면 별도의 프로젝트를 생성하지 않고 “IMPORT GOOGLE PROJECT”를 이용하여 기존의 프로젝트를 불러와서 연결할 수 있다.



프로젝트가 생성되었으면 파이어베이스를 사용하고자 하는 앱을 등록해야 한다.

파이어베이스 화면에서 “ADD APP” 이라는 버튼을 누르면 앱을 추가할 수 있다.

아래는 앱을 추가하는 화면중 첫번째 화면으로 앱에 대한 기본 정보를 넣는 화면이다.

“Package name” 에, 파이어베이스와 연동하고자 하는 안드로이드 앱의 패키지 명을 넣는다.


ADD APP 버튼을 누르고 다음 단계로 넘어가면 google-services.json 이라는 파일이 자동으로 다운된다. 이 파일은 나중에 안드로이드 앱의 소스에 추가해야 하기 때문에 잘 보관한다.


Continue 버튼을 누르면 아래와 같이 다음 단계로 넘어간다. 다음 단계에서는 안드로이드 앱을 개발할때 파이어베이스를 연동하려면 어떻게 해야 하는지에 대한 가이드가 나오는데, 이 부분은 나중에 코딩 부분에서 설명할 예정이니 넘어가도록 하자.


자 이제 파이어베이스 콘솔에서, 프로젝트를 생성하고 앱을 추가하였다.

이제 연동을 할 안드로이드 애플리케이션을 만들어보자.

안드로이드 빌드 환경 설정

콘솔에서 앱이 추가되었으니, 이제 코드를 작성해보자, 아래 예제는 안드로이드 스튜디오 2.1.2 버전 (맥 OS 기준) 으로 작성되었다.


먼저 안드로이드 프로젝트를 생성하였다. 이때 반드시 안드로이드 프로젝트에서 앱 패키지 명은 앞에 파이어베이스 콘솔에서 지정한 com.terry.hellofirebase가 되어야 한다.

안드로이드 프로젝트에는 프로젝트 레벨의 build.gradle 파일과, 앱 레벨의 build.gradle 파일이 있는데



프로젝트 레벨의 build.gradle 파일에 classpath 'com.google.gms:google-services:3.0.0' 를 추가하여  다음과 같이 수정한다.


// Top-level build file where you can add configuration options common to all sub-projects/modules.


buildscript {

  repositories {

      jcenter()

  }

  dependencies {

      classpath 'com.android.tools.build:gradle:2.1.2'

      classpath 'com.google.gms:google-services:3.0.0'

      // NOTE: Do not place your application dependencies here; they belong

      // in the individual module build.gradle files

  }

}


allprojects {

  repositories {

      jcenter()

  }

}


task clean(type: Delete) {

  delete rootProject.buildDir

}



다음으로, 앱레벨의 build.gradle 파일도 dependencies 부분에    compile 'com.google.firebase:firebase-core:9.4.0' 를 추가하고, 파일 맨 아래 apply plugin: 'com.google.gms.google-services' 를 추가 하여 아래와 같이 수정한다.

apply plugin: 'com.android.application'


android {

  compileSdkVersion 24

  buildToolsVersion "24.0.2"


  defaultConfig {

      applicationId "com.terry.hellofirebase"

      minSdkVersion 16

      targetSdkVersion 24

      versionCode 1

      versionName "1.0"

  }

  buildTypes {

      release {

          minifyEnabled false

          proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

      }

  }

}


dependencies {

  compile fileTree(dir: 'libs', include: ['*.jar'])

  testCompile 'junit:junit:4.12'

  compile 'com.android.support:appcompat-v7:24.2.0'

  compile 'com.google.firebase:firebase-core:9.4.0'

}

apply plugin: 'com.google.gms.google-services'



그리고 파이어베이스 콘솔에서 앱을 추가할때 다운된 google-services.json 파일을 app디렉토리에 복사한다.




이 예제의 경우에는 /Users/terrycho/AndroidStudioProjects/HelloFireBase에 프로젝트를 만들었기 때문에,  /Users/terrycho/AndroidStudioProjects/HelloFireBase/app 디렉토리에 복사하였다.


Gradle 파일 수정이 끝나고, google-services.json 파일을 복사하였으면 안드로이드 스튜디오는 gradle 파일이 변경이 되었음을 인지하고 sync를 하도록 아래 그림과 같이 “Sync now”라는 버튼이 상단에 표시된다.


“Sync now”를 눌러서 프로젝트를 동기화 한다.

예제 코드 만들기

이제 안드로이드 스튜디오의 프로젝트 환경 설정이 완료되었다. 이제, 예제 코드를 만들어 보자.

이 예제 코드는 단순하게, 텍스트 박스를 통해서 아이템 ID,이름, 그리고 종류를 입력 받아서, 파이어베이스 애널러틱스에 이벤트를 로깅하는 예제이다.

파이어베이스 애널러틱스 서버로 로그를 보낼 것이기 때문에, AndroidManifest 파일에 아래와 같이  수정하여 INTERNET과 ACCESS_NETWORK_STATE 권한을 추가한다.

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

  package="com.terry.hellofirebase">

  <uses-permission android:name="android.permission.INTERNET" />

  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

 

  <application

      android:allowBackup="true"

      android:icon="@mipmap/ic_launcher"

      android:label="@string/app_name"

      android:supportsRtl="true"

      android:theme="@style/AppTheme">

      <activity android:name=".MainActivity">

          <intent-filter>

              <action android:name="android.intent.action.MAIN" />


              <category android:name="android.intent.category.LAUNCHER" />

          </intent-filter>

      </activity>

  </application>


</manifest>


다음으로 화면을 구성해야 하는데, 우리가 구성하려는 화면 레이아웃은 대략 다음과 같다.



각각의 EditText 컴포넌트는 tv_contentsId, tv_contentsName,tv_contentsCategory로 지정하였다.

위의 레이아웃을 정의한 activity_main.xml은 다음과 같다.


<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

  xmlns:tools="http://schemas.android.com/tools"

  android:layout_width="match_parent"

  android:layout_height="match_parent"

  android:paddingBottom="@dimen/activity_vertical_margin"

  android:paddingLeft="@dimen/activity_horizontal_margin"

  android:paddingRight="@dimen/activity_horizontal_margin"

  android:paddingTop="@dimen/activity_vertical_margin"

  tools:context="com.terry.hellofirebase.MainActivity">


  <LinearLayout

      android:orientation="vertical"

      android:layout_width="match_parent"

      android:layout_height="match_parent"

      android:layout_alignParentLeft="true"

      android:layout_alignParentStart="true">


      <TextView

          android:layout_width="wrap_content"

          android:layout_height="wrap_content"

          android:textAppearance="?android:attr/textAppearanceMedium"

          android:text="Contents ID"

          android:id="@+id/tv_contetnsId" />


      <EditText

          android:layout_width="match_parent"

          android:layout_height="wrap_content"

          android:id="@+id/txt_contentsId"

          android:layout_gravity="center_horizontal" />


      <TextView

          android:layout_width="wrap_content"

          android:layout_height="wrap_content"

          android:textAppearance="?android:attr/textAppearanceMedium"

          android:text="Contents Name"

          android:id="@+id/tv_contentsName" />


      <EditText

          android:layout_width="match_parent"

          android:layout_height="wrap_content"

          android:id="@+id/txt_contentsName" />


      <TextView

          android:layout_width="wrap_content"

          android:layout_height="wrap_content"

          android:textAppearance="?android:attr/textAppearanceMedium"

          android:text="Contents Category"

          android:id="@+id/tv_contentsCategory" />


      <EditText

          android:layout_width="match_parent"

          android:layout_height="wrap_content"

          android:id="@+id/txt_contentsCategory" />


      <Button

          android:layout_width="wrap_content"

          android:layout_height="wrap_content"

          android:text="Send Event"

          android:id="@+id/btn_sendEvent"

          android:layout_gravity="center_horizontal"

          android:onClick="onSendEvent" />

  </LinearLayout>

</RelativeLayout>


레이아웃 설계가 끝났으면, SEND EVENT 버튼을 눌렀을때, 이벤트를 파이어베이스 애널러틱스 서버로 보내는 코드를 만들어 보자.

MainActivity인 com.terry.hellofirebase.MainActivity 클래스의 코드는 다음과 같다.


package com.terry.hellofirebase;


import android.support.v7.app.AppCompatActivity;

import android.os.Bundle;

import android.view.View;

import android.widget.EditText;

import android.widget.Toast;


import com.google.firebase.analytics.FirebaseAnalytics;


public class MainActivity extends AppCompatActivity {


  // add firebase analytics object

  private FirebaseAnalytics mFirebaseAnalytics;


  @Override

  protected void onCreate(Bundle savedInstanceState) {

      super.onCreate(savedInstanceState);

      mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);

      setContentView(R.layout.activity_main);

  }


  public void onSendEvent(View view){

      String contentsId;

      String contentsName;

      String contentsCategory;


      EditText txtContentsId = (EditText)findViewById(R.id.txt_contentsId);

      EditText txtContentsName = (EditText)findViewById(R.id.txt_contentsName);

      EditText txtContentsCategory = (EditText)findViewById(R.id.txt_contentsCategory);


      contentsId = txtContentsId.getText().toString();

      contentsName = txtContentsName.getText().toString();

      contentsCategory = txtContentsCategory.getText().toString();


      Bundle bundle = new Bundle();

      bundle.putString(FirebaseAnalytics.Param.ITEM_ID, contentsId);

      bundle.putString(FirebaseAnalytics.Param.ITEM_NAME, contentsName);

      bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, contentsCategory);

      mFirebaseAnalytics.logEvent(FirebaseAnalytics.Event.SELECT_CONTENT, bundle);


      Toast.makeText(getApplicationContext(), "Sent event", Toast.LENGTH_LONG).show();

  }

}


MainActivity 클래스에 FirebaseAnalytics 객체를 mFirebaseAnalytics라는 이름으로 정의하고 onCreate메서드에서 FirebaseAnalytics.getInstance(this) 메서드를 이용하여 파이어베이스 애널러틱스 객체를 생성한다.


다음 onSendEvent라는 메서드를 구현한다. 이 메서드는 화면에서 “SEND EVENT”라는 버튼을 누르면 EditText 박스에서 입력된 값으로 SELECT_CONTENT라는 이벤트를 만들어서 파이어베이스 애널러틱스 서버로 보내는 기능을 한다.

컨텐츠 ID,NAME,CATEGORY를 EditText 박스에서 읽어온 후에, Bundle 이라는 객체를 만들어서 넣는다.

파이어베이스 애널러틱스 로그는 이벤트와 번들이라는 개념으로 구성이 된다.

이벤트는 로그인, 컨텐츠 보기, 물품 구매와 같은 이벤트이고, Bundle은 이벤트에 구체적인 인자를 묶어서 저장하는 객체이다. 위의 예제인 경우 SELECT_CONTENTS 라는 이벤트가 발생할때 컨텐츠 ID, 이름(Name), 종류(Category)를 인자로 하여, Bundle에 묶어서 전달하도록 하였다.

Bundle 클래스를 생성한후, bundle.putString(“인자명",”인자값") 형태로 Bundle 객체를 설정한 후에, mFirebaseAnalytics.logEvent(“이벤트명",”Bundle 객체") 메서드를 이용하여 SELECT_CONTENTS 이벤트에 앞서 작성한 Bundle을 통하여 인자를 전달하였다.


앱 개발이 모두 완료되었다. 이제 테스트를 해보자

실행하기

앱을 실행하고 아래와 같이 데이타를 넣어보자


컨텐츠 ID는 200, 컨텐츠 이름은 W, 그리고 컨텐츠 종류는 webtoon으로 입력하였다.

SEND EVENT 눌러서 이벤트를 보내서 파이어베이스 웹콘솔에 들어가서 Analytics 메뉴에 상단 메뉴인 “Events”를 선택하면 처음에는 아무런 값이 나오지 않는다.

앞에서 설명했듯이 파이어베이스 애널러틱스는 아직까지 실시간 분석을 지원하지 않기 때문에 수시간이 지난 후에야 그 값이 반영 된다.


본인의 경우 밤 12시에 테스트를 진행하고 아침 9시경에 확인을 하였더니 아래와 같은 결과를 얻을 수 있었다.



실제로 테스트 시에 select contents 이벤트를 3번을 보냈더니, Count가 3개로 나온다.

그러나 이벤트에 보낸 컨텐츠 ID, 이름 , 분류등은 나타나지 않는다. 기본 설정에서는 이벤트에 대한 디테일 정보를 얻기가 어렵다. 그래서 빅쿼리 연동이 필요한데 이는 후에 다시 다루도록 하겠다.


Dashboard 메뉴를 들어가면 다음과 같이 지역 분포나 단말명등 기본적인 정보를 얻을 수 있다.



이벤트와 이벤트 인자

앞서처럼 이벤트와 인자등을 정해줬음에도 불구하고 대쉬보드나 기타 화면에 수치들이 상세하지 않은 것을 인지할 수 있다. 정확한 데이타를 분석하려면 마찬가지로 정확한 데이타를 보내줘야 하는데, 화면 로그인이나 구매등과 같은 앱에서의 이벤트를 앱 코드내에 삽입해줘야 상세한 분석이 가능하다.

이벤트는 https://firebase.google.com/docs/reference/android/com/google/firebase/analytics/FirebaseAnalytics.Event 에 정의가 되어 있고, 각 이벤트별 인자에 대한 설명은 https://firebase.google.com/docs/reference/android/com/google/firebase/analytics/FirebaseAnalytics.Param 에 있는데, 이미 파이어베이스에서는 게임이나 미디어 컨텐츠, 쇼핑과 같은 주요 모바일 앱 시나리오에 대해서 이벤트와 인자들은 미리 정의해놓았다.

https://support.google.com/firebase/topic/6317484?hl=en&ref_topic=6386699

를 보면 모바일 앱의 종류에 따라서 어떠한 이벤트를 사용해야 하는지가 정의되어 있다.


또한 미리 정의되어 있는 이벤트 이외에도 사용자가 직접 이벤트를 정의해서 사용할 수 있다.  이러한 이벤트를 커스텀 이벤트라고 하는데 https://firebase.google.com/docs/analytics/android/events 를 참고하면 된다.


지금까지 간략하게 나마 파이어베이스 애널러틱스의 소개와 예제 코드를 통한 사용 방법을 알아보았다.

모바일 데이타 분석이나 빅데이타 분석에서 가장 중요한 것은 데이타를 모으는 것도 중요하지만, 모아진 데이타에 대한 지표 정의와 그 의미를 파악하는 것이 중요하다. 그래서 다음 글에서는 파이어베이스 애널러틱스에 정의된 이벤트의 종류와 그 의미 그리고, 대쉬 보드를 해석하는 방법에 대해서 설명하고, 그 후에 빅쿼리 연동을 통해서 상세 지표 분석을 하는 방법에 대해서 소개하고자 한다.


 

트위터 모바일 SDK 서비스 패브릭에 대한 소개


조대협 (http://bcho.tistory.com)




트위터에서는 2014년 부터, 모바일 생태계 지원을 위해서 다양한 기능을 제공하는 Fabric 서비스를 제공하고 있다. 데이타 분석 및 리포팅, 트위터 연동등 다양한 기능을 제공하고 있는데, 대략적인 프로덕트들에 대한 기능과 특징을 살펴보고자 한다.



Crashlytics - Crash Reporting (https://fabric.io/kits/android/crashlytics)


모바일앱에 대한 크래쉬 내용에 대한 수집 및 분석 기능을 제공한다.  

특이한 사항으로는 크래쉬 분석 뿐만 아니라, 베타 사용자나 테스터들에게 앱을 배포할 수 있는 기능을 제공하고 베타 테스트 사항을 추적할 수 있는 기능을 제공한다.

근래에는 게임 개발 SDK인 Unity를 지원하는 기능이 추가 되었다.

 


Answers - Mobile Analytics Kit (https://fabric.io/kits/android/answers/summary) 


Google Analytics나 Yahoo의 Flurry와 비슷한 앱 모니터링/리포팅 서비스이다. Google Analytics와는 다르게 완전 무료이다. (데이타 Limit가 없다.)

단 타 서비스와 차이점은 복잡한 형태의 분석이 불가능하다 Cohort, Funnel 분석이나 User Path등 복잡한 분석은 불가하고 DAU,MAU,Session등 단순한 분석만 가능하다.


단순하기 때문에 지표 이해가 쉬운것이 장점으로 볼 수 있고, 또 다른 장점은 타 서비스에 비해서 리얼타임이라는 것이다. 대쉬보드의 수치는 20~30초 정도의 지연이 있는 수치로, 실시간 이벤트를 하거나 PUSH에 대한 반응을 바로바로 봐야할때나 TV CF후에 반응등 실시간 반응 분석이 필요할때 유용하게 사용할 수 있다.


정확한 분석을 위해서는 Fabric 하나로만은 불가능하겠지만 실시간성을 지원하는 점을 보면, Fabric + Flurry와 같이 두개의 솔루션을 조합해서 사용하는 것을 고려하는 것이 좋다.


Answers에서 특이한 기능중에 하나는, 트위터의 사용자 정보를 기반으로, Fabric Answer 를 통해서 모니터링 되는 사용자에 대한 특성 파악이 가능하다는 것이다. 트위터는 컨텐츠 및 여러가지 종류의 계정 (스포츠, 코메디 등등)을 운영하고 있기 때문에, 트위터는 트위터 사용자의 특성이 어떤지를 알 수 있고, 이 정보를 바탕으로 Fabric이 연동된 서비스의 각 사용자들의 특성을 파악해줄 수 있기 때문에, 서비스 운영 입장에서 사용자에 대한 인사이트를 제공할 수 있다. 




Digit Kit


Digit Kit는 SMS를 이용한 인증 서비스 이다. SMS를 통해서 인증 번호를 전송해서 본인 여부를 확인하는 서비스인데, 200여개의 국가를 지원하고 있고, 가장 중요한건 무료다!!. 글로벌 서비스를 제공 하는 경우 글로벌 SMS 서비스를 고려해야 하고, 또 그에 대한 금액도 만만하지 않은데, 하나의 서비스로 글로벌 커버를 비용 부담없이 제공하는 것은 활용을 고려해볼만하다고 보다. 향후  Email Verification 서비스도 함께 제공할 예정이다. 




Twitter Kit


Twitter Kit은 트위터 기능을 사용하기 위한 모바일 SDK이다. 특이한 점은 트위터로의 공유하는 GUI등을 SDK로 제공해서 어렵지 않게 트위터로의 공유 기능을 구현할 수 가 있다. 




Curator - Twitter contents curation service


트위터 컨텐츠를 모아서 큐레이션 (기존의 컨텐츠들을 2차 가공하여 새로운 컨텐츠를 만드는 것) 해주는 서비스로, 주로 미디어 서비스나 컨텐츠 공급자, 큐레이터에게 유리한 서비스로 Curator라는 저작툴을 이용하여, 큐레이션할 컨텐츠를 골라서 특정 주제에 해당하는 피드를 만들 수 있다. 아래는 서울의 첫눈이라는 주제로 트윗을 검색한 후에, 이를 골라서 콜랙션을 만드는 저작도구 화면이다. 



다음은 큐레이트된 컨텐츠를 임베딩하기 위해서 퍼블리슁 화면이다. 




안드로이드 채팅 UI 만들기 #2 


나인패치 이미지를 이용한 채팅 버블


조대협 (http://bcho.tistory.com)


지난 글에서는 ListView를 이용하여 스크롤이 가능한 텍스트 기반의 간단한 채팅창을 만들어보았다.

이번글에는 채팅 메세지에 이미지로 채팅 버블을 입히는 방법을 알아보도록 한다.


채팅 버블 이미지를 입히는 방법이나 원리는 간단한데, 채팅 메세지를 출력하는 TextView에 백그라운드이미지를 입혀서 출력하면 된다. 그런데 여기서 문제가 생기는데, 채팅 메세지 버블의 크기는 메세지의 글자수에 따라 가변적으로 변경되기 때문에, 일반적인 이미지를 백그라운드로 넣어서 가로로 늘이거나 줄이게 되면 채팅창이 이상하게 가로로 늘어날 수 가 있다.. (아래 그림에서 가로로 늘렸을때 말꼬리 부분 삼각형이 원본과 다르게 늘어난것을 확인할 수 있다) 



< 원본 이미지 > 



<가로로 늘린 이미지 >



그래서 필요한 것이, 특정 부분만 늘어나게 하는 것이 필요한데. 이렇게 크기가 변경되어도 특정 구역만 늘어나게 하는 이미지를 나인패치 이미지 (9-patch image라고 한다.). 나인패치 이미지를 이용하여 말풍선을 느리게 되면, 말꼬리 부분은 늘어나지 않고 텍스트가 들어가는 영역만 늘어난다. 




< 나인패치 이미지를 가로로 늘린 경우> 


나인패치 이미지 만들기


나인 패치 이미지는 안드로이드 SDK에 내장된 draw9patch라는 도구를 이용해서 만들 수 있다.

보통 안드로이드 SDK 가 설치된 디렉토리인 ~/sdk/tools 아래 draw9patch라는 이름으로 저장되어 있다.

실행하면 아래와 같은 화면이 뜨는데, 

좌측은 일반 이미지를 나인패치 이미지로 만들기 위해서 늘어나는 영역을 지정하는 부분이고 (작업영역), 우측은 가로, 세로등으로 늘렸을때의 예상 화면 (프리뷰)을 보여주는 화면이다.




그러면 9 patch  이미지는 어떻게 정의가 될까? draw9patch에서 가이드 선을 드래그해서 상하좌우 4면에, 가이드 선을 지정할 수 있다.



좌측은 세로로 늘어나는 영역, 상단을 가로로 늘어나는 영역을 정의하고, 우측은 세로로 늘어났을때 늘어나는 부분에 채워지는 이미지를, 하단은 가로로 늘어났을때 채워지는 이미지 영역을 지정한다. 

이렇게 정의된 나인 패치 이미지는 어떻게 사용하는가? 일반 이미지 처럼 사용하면 되고, 크기를 조정하면 앞서 정의한데로, 늘어나는 부분만 늘어나게 된다.


나인패치 이미지를 채팅 메세지에 적용하기 


그러면 이 나인패치이미지를 앞에서 만든 채팅 리스트 UI에 적용하여 말 풍선을 만들어보도록 하자.

여기서는 채팅 버블을 좌측 우측용 양쪽으로 만들도록 하고, 서버에 연결된 테스트용이기 때문에, 메세지를 입력하면 무조건 좌/우 버블로 번갈아 가면서 출력하도록 한다.


앞의 코드 (http://bcho.tistory.com/1058) 에서 별도로 바위는 부분은 ChatMessageAdapter의 getView 메서드만 아래와 같이 수정하고 채팅 버블로 사용할 이미지를 ~/res/drawable/ 디렉토리 아래 저장해 놓으면 된다. 




그러면 수정된 getView 메서드를 살펴보도록 하자.


   @Override

    public View getView(int position, View convertView, ViewGroup parent) {

        View row = convertView;

        if (row == null) {

            // inflator를 생성하여, chatting_message.xml을 읽어서 View객체로 생성한다.

            LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            row = inflater.inflate(R.layout.chatting_message, parent, false);

        }


        // Array List에 들어 있는 채팅 문자열을 읽어

        ChatMessage msg = (ChatMessage) msgs.get(position);


        // Inflater를 이용해서 생성한 View에, ChatMessage를 삽입한다.

        TextView msgText = (TextView) row.findViewById(R.id.chatmessage);

        msgText.setText(msg.getMessage());

        msgText.setTextColor(Color.parseColor("#000000"));


        // 9 패치 이미지로 채팅 버블을 출력

        msgText.setBackground(this.getContext().getResources().getDrawable( (message_left ? R.drawable.bubble_b : R.drawable.bubble_a )));


        // 메세지를 번갈아 가면서 좌측,우측으로 출력

        LinearLayout chatMessageContainer = (LinearLayout)row.findViewById(R.id.chatmessage_container);

        int align;

        if(message_left) {

            align = Gravity.LEFT;

            message_left = false;

        }else{

            align = Gravity.RIGHT;

            message_left=true;

        }

        chatMessageContainer.setGravity(align);

        return row;


    }


수정 내용은 간단한데, 좌측/우측용 채팅 버블을 출력하는 부분과, 좌측 버블은 좌측 정렬을, 우측 버블은 우측 정렬을 하는 내용이다.


채팅 버블을 적용하는 방법은 TextView에서 간단하게 setBackground 메서드를 이용하여 백그라운드 이미지를 나인패치 이미지를 적용하면 된다.


msgText.setBackground(this.getContext().getResources().getDrawable( (message_left ? R.drawable.bubble_b : R.drawable.bubble_a )));


나인패치이미지가  resource아래 drawable 아래 저장되어 있기 때문에, getResource().getDrawable() 메서드를  이용하여 로딩 한다.


        LinearLayout chatMessageContainer = (LinearLayout)row.findViewById(R.id.chatmessage_container);

        int align;

        if(message_left) {

            align = Gravity.LEFT;

            message_left = false;

        }else{

            align = Gravity.RIGHT;

            message_left=true;

        }

        chatMessageContainer.setGravity(align);


다음으로는 채팅 버블을 번갈아 가면서 좌/우측에 위치 시켜야 하는데, 채팅 버블의 위치는 채팅 메세지를 담고 있는 LinearLayout을 가지고 온후에, LinearLayout의 Gravity를 좌우로 설정하면 된다.


나인패치 이미지를 이용한 완성된 채팅 버블 UI는 다음과 같다. 






안드로이드에서 ListView를 이용한 채팅 UI 만들기


조대협 (http://bcho.tistory.com)


안드로이드 프로그래밍 기본 개념이 어느정도 잡혀가기 시작하니, 몬가 만들어봐야겠다는 생각이 들어서 생각하던중에 결론 낸것이, 간단한 채팅 서비스, 기존에 node.js 하면서 웹용 채팅을 만들어보기도 했고, 찾아보니, 안드로이드용 SocketIO 라이브러리도 잘되어 있어서 서버 연계도 어려울것이 없을것 같고, 또한 메세지가 왔을때 푸쉬 알림을 써야 하는 등 이것저것 실습이 될것 같아서, 결국은 채팅으로 정했다.


서버나 연계 코드 구현보다, 가장 어려운게 역시나 UI 디자인과 프로그래밍인데, 가장 쉬운 방법으로는 ListView를 사용하는 방법이 무난하다. (결국 코딩을 하고 나니 여러가지 한계를 느껴서 다른  UI를 찾으려고 하고는 있지만)


궁극적으로 만들고자 하는 UI는 카카오톡 처럼 말풍선이 나오는 UI이다. 





말풍선은 Ninepatch (나인패치) 이미지 라는 것으로 만들 수 가 있는데, 나인 패치 이미지에 대해서는 나중에 알아보도록 하고, 여기서는 말풍선 없이, 화면에 스크롤되서 내려가는 텍스트 기반의 대화창을 만드는 것을 먼저 진행하도록 한다.  스크롤이 되는 채팅창은 ListView 컴포넌트를 사용해서 구현하는데, 아래 그림과 같이 지난 메세지는 화면에 나오지 않고 현재 대화되는 상황만 보이도록 한다. 





리스트뷰(ListView) 에 대해서


그렇다면 리스트뷰는 무엇인가? 리스트뷰는 안드로이드 뷰 그룹 (View Group)의 일종으로, 스크롤이 가능한 아이템들의 리스트를 출력해주는 그룹이다.



아답터(Adaptor) 의 개념


채팅용 리스트뷰를 만들기 위해서는 아답터(Adaptor)의 개념을 이해해야 하는데, 아답터는 크게 두가지 기능을 한다. 리스트뷰에서 보여질 아이템들을 저장하는 데이타 저장소의 역할과, 리스트뷰안에 아이템이 그려질때 이를 렌더링하는 역할을 한다.


add 메서드를 이용하여, 아이템을 추가하고

getItem(int index)를 이용하여, index  번째의 아이템을 리턴하며 (자바의 일반적인 List형과 유사하다)

View getView(int position, xxx )이 중요한데, position 번째의 아이템을 화면에 출력할때 렌더링하는 역할을 한다.


그러면 실제로 작동하는 코드를 만들어보자. 이 예제에서는 텍스트를 입력하면 리스트 뷰에 추가되고, 텍스트가 입력됨에 따라 쭈욱 아래로 리스트뷰가 자동으로 스크롤되는 예제이다. 





아답터 클래스 구현


제일 먼저 채팅 메세지 리스트를 저장할 아답터 클래스를 구현해보자


package com.example.terry.simplelistview;


import android.content.Context;

import android.graphics.Color;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import android.widget.ArrayAdapter;

import android.widget.TextView;


import java.util.ArrayList;

import java.util.List;


/**

 * Created by terry on 2015. 10. 7..

 */

public class ChatMessageAdapter extends ArrayAdapter {


    List msgs = new ArrayList();


    public ChatMessageAdapter(Context context, int textViewResourceId) {

        super(context, textViewResourceId);

    }


    //@Override

    public void add(ChatMessage object){

        msgs.add(object);

        super.add(object);

    }


    @Override

    public int getCount() {

        return msgs.size();

    }


    @Override

    public ChatMessage getItem(int index) {

        return (ChatMessage) msgs.get(index);

    }


    @Override

    public View getView(int position, View convertView, ViewGroup parent) {

        View row = convertView;

        if (row == null) {

            // inflator를 생성하여, chatting_message.xml을 읽어서 View객체로 생성한다.

            LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            row = inflater.inflate(R.layout.chatting_message, parent, false);

        }


        // Array List에 들어 있는 채팅 문자열을 읽어

        ChatMessage msg = (ChatMessage) msgs.get(position);


        // Inflater를 이용해서 생성한 View에, ChatMessage를 삽입한다.

        TextView msgText = (TextView) row.findViewById(R.id.chatmessage);

        msgText.setText(msg.getMessage());

        msgText.setTextColor(Color.parseColor("#000000"));


        return row;


    }

}


add나 getItem,getCount등은 메세지를 Java List에 저장하고, n 번째 메세지를 리턴하거나 전체 크기를 저장하는 방식으로 구현한다.


가장 중요한 부분은 getView 메서드인데,리스트의 n 번째 아이템에 대한 내용을 화면에 출력하는 역할을 하며 여기서는 두 단계를 거쳐서 렌더링을 진행한다.


첫번째로는 인플레이터 (inflator)를 사용하여, 아이템을 렌더링할 View 컴포넌트를 ~/layout/XX.XML 에서 읽어오는 역할을 한다.

인플레이터에 대해서 간략하게 짚고 넘어가면, View등 화면등을 디자인할 때 안드로이드에서는 XML 을 사용하여 쉽게 View를 정의할 수 있다. 이렇게 정의된 XML을 실제 View 자바 Object로 생성을 해주는 것이 인플레이터(inflator)이다. 


    LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 


를 통해서 인플레이터를 생성하고, 이 인플레이터를 통해서 각 아이템을 출력해줄 View를 생성하여 row라는 변수에 저장한다.


 row = inflater.inflate(R.layout.chatting_message, parent, false);


이때, 레이아웃을 정의한 XML 파일은 chatting_message.xml 로 안드로이드 프로젝트의 ~/layout 디렉토리 아래에 있으며, 인플레이트 할때는 R.layout.chatting_message라는 이름으로 지칭한다.


chatting_message.xml 의 내용은 다음과 같다.


<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical" android:layout_width="match_parent"

    android:layout_height="match_parent">


    <TextView

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:textAppearance="?android:attr/textAppearanceMedium"

        android:text="Chat message"

        android:id="@+id/chatmessage"

        android:gravity="left|center_vertical|center_horizontal"

        android:layout_marginLeft="20dp" />

</LinearLayout>



이렇게 아이템을 표시할 View가 생성되었으면, 그 안에 알맹이를 채워넣어야 하는데, 채팅 메세지를 저장하는 List 객체에서, position 번째의 채팅 메세지를 읽어온 후에, row 뷰 안에 있는 TextView에 그 채팅 메세지를 채워 넣는다. 


getView 코드에서 주의해서 봐야할 부분이


  View row = convertView;

        if (row == null) { …


인데, 가만히 보면 row가 null 일 경우에만 인플레이터를 이용해서 row를 생성하는 것을 볼 수 있다.


ListView의 특징중 하나는, 아이템을 랜더링 하는 View 객체가 매번 생성되는 것이 아니라, 해당 아이템에 대해서 이미 생성되어 있는 View가 있다면, getView( .. ,View convertView, ..) 를 통해서 인자로 전달된다.


그래서, convertView가 null 인지를 체크하고, 만약에 null이 아닌 경우에만 View를 생성한다. 


여기까지가 채팅 메세지를 저장하고, 각 메세지를 렌더링 해주는 ListView 용 아답터의 구현이었다. 

그러면 아답터를 이용한 ListView를 사용하여 채팅 메세지를 출력하는 부분을 구현해보자


아래는 ListView를 안고 있는 MainActivity이다.


package com.example.terry.simplelistview;


import android.database.DataSetObserver;

import android.support.v7.app.ActionBarActivity;

import android.os.Bundle;

import android.view.Menu;

import android.view.MenuItem;

import android.view.View;

import android.widget.AbsListView;

import android.widget.EditText;

import android.widget.ListView;



public class MainActivity extends ActionBarActivity {

    ChatMessageAdapter chatMessageAdapter;


    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

    }


    @Override

    public boolean onCreateOptionsMenu(Menu menu) {


        // Inflate the menu; this adds items to the action bar if it is present.

        getMenuInflater().inflate(R.menu.menu_main, menu);


        chatMessageAdapter = new ChatMessageAdapter(this.getApplicationContext(),R.layout.chatting_message);

        final ListView listView = (ListView)findViewById(R.id.listView);

        listView.setAdapter(chatMessageAdapter);

        listView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); // 이게 필수


        // When message is added, it makes listview to scroll last message

        chatMessageAdapter.registerDataSetObserver(new DataSetObserver() {

            @Override

            public void onChanged() {

                super.onChanged();

                listView.setSelection(chatMessageAdapter.getCount()-1);

            }

        });

        return true;

    }


    @Override

    public boolean onOptionsItemSelected(MenuItem item) {

        // Handle action bar item clicks here. The action bar will

        // automatically handle clicks on the Home/Up button, so long

        // as you specify a parent activity in AndroidManifest.xml.

        int id = item.getItemId();


        //noinspection SimplifiableIfStatement

        if (id == R.id.action_settings) {

            return true;

        }


        return super.onOptionsItemSelected(item);

    }


    public void send(View view){

        EditText etMsg = (EditText)findViewById(R.id.etMessage);

        String strMsg = (String)etMsg.getText().toString();

        chatMessageAdapter.add(new ChatMessage(strMsg));

    }

}


먼저 send는 “SEND” 버튼을 눌렀을때, 화면상에서 채팅 메세지를 읽어드려서 Adapter에 저장하는 역할을 한다.

가장 중요한 메서드는  onCreateOptionsMenu인데, 이 메서드의 주요 내용은, Adapter를 생성하여, Adapter를 listView에 바인딩한다.


  chatMessageAdapter = new ChatMessageAdapter(this.getApplicationContext(),R.layout.chatting_message);

        final ListView listView = (ListView)findViewById(R.id.listView);

        listView.setAdapter(chatMessageAdapter);


다음 기능으로는, 새로운 아이템이 listView에 추가되었을때, 맨 아래로 화면을 스크롤 하는 기능인데, 이는 Adapter에 DataObserver를 바인딩해서, 데이타가 바뀌었을때 즉 채팅 메세지가 추가되었을때, listView에서 리스트업 되는 아이템 목록을 맨 아래로 이동 시킨다.


        // When message is added, it makes listview to scroll last message

        chatMessageAdapter.registerDataSetObserver(new DataSetObserver() {

            @Override

            public void onChanged() {

                super.onChanged();

                listView.setSelection(chatMessageAdapter.getCount()-1);

            }

        });


이렇게 DataObserver를 추가하더라도, 아래로 스크롤이 안되는데, 새로운 아이템이 리스트뷰에 추가되었을 때 스크롤을 하게 해줄려면 TranscriptMode에 새로운 아이템이 추가되었을때 스크롤이 되도록 설정을 해줘야 한다. (디폴트는 새로운 아이템이 추가되더라도 스크롤이 되지 않는 옵션으로 설정되어 있다.)


listView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); // 이게 필수



간단하게, ListView를 이용한 채팅 UI를 만들어봤다. 다음에는 나인패치 이미지를 이용하여, 말풍선을 넣는 기능을 추가해보도록 한다.


참고 : http://javapapers.com/android/android-chat-bubble/



안드로이드 Fragement 이해하기


조대협 (http://bcho.tistory.com)


안드로이드에서 하나의 화면은 하나의 Activity로 대변된다.

이러한 Activity를 View등을 이용해서 여러개로 쪼갤 수 있지만, 타블렛이나 팬블릿과 같이 스마트 폰과 다른 해상도를 사용할때 일일이 View를 다시 디자인 해야 하며, 또한 해당 화면 레이아웃이나 배치등을 다른 Activity에서 사용하고자 하면 매번 다시 만들어야 한다.





<출처 : http://developer.android.com/guide/components/fragments.html >


왼쪽은 타블렛 화면 레이아웃으로, 좌측에 메뉴와 오른쪽에 다른 형태의 컨텐츠가 나타난다.

이를 스마트 폰에서 볼때는 화면이 작기 때문에, 한 화면에서는 메뉴를 출력하고, 메뉴를 클릭하면 컨텐츠 화면으로 이동하도록 하는 형태인데, 이를 그냥 각각 구현하려면, 각 Activity에서 중복된 코드를 일일이 개발해야 한다. 

만약에 이러한 메뉴와 컨텐츠 화면이 포터블한 컴포넌트 형태로 묶이게 된다면 이를 재 사용해서 쉽게 만들 수 있다. 즉 타블릿 화면에는 메뉴와 컨텐츠 컴포넌트를 출력하고, 스마트 폰에는 메뉴 화면에는 메뉴 컴포넌트를, 컨텐츠 화면에는 컨텐츠 컴포넌트를 출력하면 쉽게 개발이 가능하다.


그래서 Activity안에 다른 Activity를 구현하는 것과 같은 형태의 컴포넌트를 제공하는 데 이를  Fragment라고 한다.


Fragment를 사용하는 방법은 단순하다. Activity의 Layout XML 안에, <fragment> 라는 태그를 이용하여, <fragment>를 추가해주면 된다. ( 또는 자바 코드를 이용하여 fragment를 추가할 수 있다.)



다음은 간단한 TextView를 포함한 Fragement를 정의한 fragement_main.xml 이다.


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"

    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivityFragment">


    <TextView android:text="Hello!! I&apos;m fragment" android:layout_width="fill_parent"

        android:layout_height="wrap_content"

        android:id="@+id/tvHello" />


</RelativeLayout>


Activity의 Layout을 정의하는 방법과 유사하게, fragment의  layout도 정의한다.

다음으로 Fragment class를 상속받아서 MainActivityFragement를 정의한다.


public class MainActivityFragment extends Fragment {


    public MainActivityFragment() {

    }


    @Override

    public View onCreateView(LayoutInflater inflater, ViewGroup container,

                             Bundle savedInstanceState) {

        return inflater.inflate(R.layout.fragment_main, container, false);

    }

}


여기까지 진행했으면 Fragment를 만든것이다. 다음으로, 이 Fragement를 Activity 위에 배치해보자.

아래는 MainActivity에서 사용하는 Layout을 정의한 activity_main.xml 이다.


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"

android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"

android:paddingRight="@dimen/activity_horizontal_margin"

android:paddingTop="@dimen/activity_vertical_margin"

android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivityFragment">


    <fragment xmlns:android="http://schemas.android.com/apk/res/android"

        xmlns:tools="http://schemas.android.com/tools"

        android:id="@+id/fragment1"

        android:name="com.example.terry.simplefragment1.MainActivityFragment"

        tools:layout="@layout/fragment_main"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        />


    <fragment xmlns:android="http://schemas.android.com/apk/res/android"

        xmlns:tools="http://schemas.android.com/tools"

        android:id="@+id/fragment2"

        android:name="com.example.terry.simplefragment1.MainActivityFragment"

        tools:layout="@layout/fragment_main"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:layout_below="@+id/fragment1"

        android:layout_alignParentEnd="true" />


    <fragment xmlns:android="http://schemas.android.com/apk/res/android"

        xmlns:tools="http://schemas.android.com/tools"

        android:id="@+id/fragment3"

        android:name="com.example.terry.simplefragment1.MainActivityFragment"

        tools:layout="@layout/fragment_main"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:layout_below="@+id/fragment2"

        android:layout_alignParentEnd="true" />



</RelativeLayout>


안에 <fragment ..> 라는 이름으로 앞서 정의한 MainActivityFragment를 3개 순차적으로 배치했음을 볼 수 있다.





안드로이드에서 비디오 플레이 하기


조대협 (http://bcho.tistory.com)


동영상 서비스를 하다보니, 실제로 비디오 플레이어를 어떻게 구현하는 지가 궁금해서 찾아봤는데, 생각보다 안드로이드의 비디오 플레이어 구현은 간단해 보인다. 

내부적으로 MediaPlayer라는 컴포넌트를 사용하는데, 이 MediaPlayer는 조금 더 미세한 컨트롤이 가능하고 이를 한단계 더 쌓아 놓은 VideoView라는 것이 사용방법이 쉽다. 또한 구버전의 안드로이드에서는 MediaPlayer 기능이 잘 작동하지 않는 부분이 있기 때문에, VideoView를 사용하는 것이 조금 더 안정적일 수 있다.


내장된 플레이어로 지원되는 프로토콜은 다음과 같다.

RTSP

HTTP/HTTPS Progressive Streaming

HTTP/HTTPS Live Streaming (HLS)

MPEG-2 TS 

Protocol version 3 (Android 4.0 & above)

Protocol version 2 (Android 3.X)

3.0 이하 버전은 지원 안됨


 


MediaPlayer는 실시간 방송을 위한 RTMP나 HTTP 기반의  DASH등 다양한 프로토콜을 지원하지 않고, 기능상에 제약이 있는데, 이러한 기능상의 제약을 없애고 플레이어를 확장하려면, MediaCodec과  MediaExtractor 클래스를 이용하여 직접 플레이어를 만들어야 하는데, 번거롭고 다른 대안으로는 조금 더 다양한 기능을 제공하는 오픈 소스 플레이어를 활용하는 방법이 있다.

https://developer.android.com/guide/topics/media/exoplayer.html

를 보면 ExoPlayer를 소개하고 있는데, 안드로이드 공식 개발자 페이지에서 소개하는 것을 보면 신뢰성이 꽤 높은 듯 하다.


배경 설명은 이정도로 하고, 여기서는 간단하게 VieoView를 이용한 간단한 플레이어를 만들어 보도록 한다.


다음과 같은 기능을 구현한다.


URL 입력 (Progressive download 또는 HLS)

재생

일시 멈춤

네비게이션 바 출력

전체 시간 출력


완성된 앱의 UI는 다음과 같다. 




가장 기본적인 기능인데, 앞으로 가기나, 뒤로 가기,음소거,음켜기 등도 구현이 어렵지 않으니 쉽게 추 가 할 수 있다.


참고 

미디어 플레이어 예제  http://www.tutorialspoint.com/android/android_mediaplayer.htm

전체화면 보기 예제

http://javaexpert.tistory.com/365


레이아웃 코드는 다음과 같다. 

activity_main.xml



다음은, MainActivity.java  코드이다.


MainActivity.java



코드를 하나하나 뜯어 보면, 먼저 OnCreate에서 객체가 생성될때, VideoView 객체와 비디오 플레이 진행상황을 표시할 SeekBar에 대한 레퍼런스를 저장해놓는다.


    final static String SAMPLE_VIDEO_URL = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4";

    VideoView videoView;

    SeekBar seekBar;

    Handler updateHandler = new Handler();


    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        EditText tvURL = (EditText)findViewById(R.id.etVieoURL);

        tvURL.setText(SAMPLE_VIDEO_URL);


        videoView = (VideoView)findViewById(R.id.videoView);

        // MediaController mc = new MediaController(this);

        // videoView.setMediaController(mc);


        seekBar = (SeekBar)findViewById(R.id.seekBar);

    }


다음 “LOAD” 버튼을 누르면 지정된 URL에서 MP4 파일을 읽어와서 플레이를 시작하도록 한다.


    public void loadVideo(View view) {

        //Sample video URL : http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_2mb.mp4

        EditText tvURL = (EditText) findViewById(R.id.etVieoURL);

        String url = tvURL.getText().toString();


        Toast.makeText(getApplicationContext(), "Loading Video. Plz wait", Toast.LENGTH_LONG).show();

        videoView.setVideoURI(Uri.parse(url));

        videoView.requestFocus();


        // 토스트 다이얼로그를 이용하여 버퍼링중임을 알린다.

        videoView.setOnInfoListener(new MediaPlayer.OnInfoListener() {


            @Override

            public boolean onInfo(MediaPlayer mp, int what, int extra) {

                switch(what){

                    case MediaPlayer.MEDIA_INFO_BUFFERING_START:

                        // Progress Diaglog 출력

                        Toast.makeText(getApplicationContext(), "Buffering", Toast.LENGTH_LONG).show();

                        break;

                    case MediaPlayer.MEDIA_INFO_BUFFERING_END:

                        // Progress Dialog 삭제

                        Toast.makeText(getApplicationContext(), "Buffering finished.\nResume playing", Toast.LENGTH_LONG).show();

                        videoView.start();

                        break;

                }

                return false;

            }

        }


        );


        // 플레이 준비가 되면, seekBar와 PlayTime을 세팅하고 플레이를 한다.

        videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {

            @Override

            public void onPrepared(MediaPlayer mp) {

                videoView.start();

                long finalTime = videoView.getDuration();

                TextView tvTotalTime = (TextView) findViewById(R.id.tvTotalTime);

                tvTotalTime.setText(String.format("%d:%d",

                                TimeUnit.MILLISECONDS.toMinutes((long) finalTime),

                                TimeUnit.MILLISECONDS.toSeconds((long) finalTime) -

                                        TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) finalTime)))

                );

                seekBar.setMax((int) finalTime);

                seekBar.setProgress(0);

                updateHandler.postDelayed(updateVideoTime, 100);

                //Toast Box

                Toast.makeText(getApplicationContext(), "Playing Video", Toast.LENGTH_SHORT).show();

            }

        });

    }


하나씩 뜯어보면, 비디오 플레이는  viewView.SetVideoURI(URI)를 지정하면 비디오가 플레이가 시작되고, videoView.requestFocus()를 하면, 해당 view에 포커스를 한다.

다음으로 videoView.start()를 하면 플레이가 되는데, 중간에 몇가지 추가 코드가 들어가 있다.


먼저 이 예제는 로컬에 있는 파일을 플레이 하는 것이 아니라 Remote에 있는 MP4파일을 http로 다운 받으면서 플레이하는 Progressive download 형태의 플레이 방식이기 때문에 네트워크 상태에 따라서 로딩 시간이 걸릴 수 있다. 다음 videoView.setOnInfoListner는 VideoView의 상태가 변경되었을때 호출되는 콜백함수로, 버퍼링이 시작될때와 버퍼링이 끝날때의 이벤트를 잡아서 토스트 다이얼로그를 띄워주는 부분이다. 아래 구현에서는 토스트 다이얼로그를 사용했지만 Progress Dialog등을 이용하여 버퍼링 중임을 알려줄 수 도 있다.

버퍼링이 시작될때는 MediaPlayer.MEDIA_INFO_BUFFERING_START라는 이벤트를 보내고, 버퍼링이 끝날때는 MediaPlayer.MEDIA_INFO_BUFFERING_END라는 이벤트를 보내기 때문에, 이 시점에 맞춰서 버퍼링중임을 알리는 메세지를 보이거나 감추면 된다.


        // 토스트 다이얼로그를 이용하여 버퍼링중임을 알린다.

        videoView.setOnInfoListener(new MediaPlayer.OnInfoListener() {


            @Override

            public boolean onInfo(MediaPlayer mp, int what, int extra) {

                switch(what){

                    case MediaPlayer.MEDIA_INFO_BUFFERING_START:

                        // Progress Diaglog 출력

                        Toast.makeText(getApplicationContext(), "Buffering", Toast.LENGTH_LONG).show();

                        break;

                    case MediaPlayer.MEDIA_INFO_BUFFERING_END:

                        // Progress Dialog 삭제

                        Toast.makeText(getApplicationContext(), "Buffering finished.\nResume playing", Toast.LENGTH_LONG).show();

                        videoView.start();

                        break;

                }

                return false;

            }

        }


        );


다음 코드 부분은 버퍼링이 다 끝나고 처음 플레이 준비가 되었을 때 전체 플레이 시간을 세팅하고 SeekBar의 전체 길이등을 세팅하는 초기 작업을 하기 위한 콜백 함수이다. setOnPreparedListner에서, MediaPlayer.OnPreparedListner를 콜백함수로 다음과 같이 정의한다.


        // 플레이 준비가 되면, seekBar와 PlayTime을 세팅하고 플레이를 한다.

        videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {

            @Override

            public void onPrepared(MediaPlayer mp) {

                videoView.start();

                long finalTime = videoView.getDuration();

                TextView tvTotalTime = (TextView) findViewById(R.id.tvTotalTime);

                tvTotalTime.setText(String.format("%d:%d",

                                TimeUnit.MILLISECONDS.toMinutes((long) finalTime),

                                TimeUnit.MILLISECONDS.toSeconds((long) finalTime) -

                                        TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long) finalTime)))

                );

                seekBar.setMax((int) finalTime);

                seekBar.setProgress(0);

                updateHandler.postDelayed(updateVideoTime, 100);

                //Toast Box

                Toast.makeText(getApplicationContext(), "Playing Video", Toast.LENGTH_SHORT).show();

            }

        });


먼저 플레이 준비가 되면 videoView.start() 메서드로 동영상 플레이를 시작한다.

다음으로 vieoView.getDuration() 메서드를 이용하여 전체 동영상 플레이 시간을 받아서 분/초로 변환한후 출력하고, SeekBar의 Max값을 총 플레이 시간으로 저장한다. 그리고 100ms마다 seekBar의 플레이 상태를 이동시키기 위해서 updateHandler에 updateVideoTime 함수를 100ms 이후에 실행하도록 지정한다.

updateVideoTime 함수는 다음과 같다.


    // seekBar를 이동시키기 위한 쓰레드 객체

    // 100ms 마다 viewView의 플레이 상태를 체크하여, seekBar를 업데이트 한다.

    private Runnable updateVideoTime = new Runnable(){

        public void run(){

            long currentPosition = videoView.getCurrentPosition();

            seekBar.setProgress((int) currentPosition);

            updateHandler.postDelayed(this, 100);


        }

    };


만약에 안드로이드 에뮬레이터에서 실행을 하게 되면 일부 MP4 동영상은 Decoder를 찾을 수 없다는 에러가 나올 수 있는데, 이는 MP4 확장명을 가지더라도, 내부 인코딩 방식이 각기 틀리기 때문에, 안드로이드 에뮬레이터에서는 일부 디코더만 지원하기 때문에, 제대로 플레이가 되지 않을 수 있다.  실제 기기를 연결하면, 실 기기에는 대부분의 MP4 디코더가 탑재되어 있기 때문에, 문제 없이 동영상이 플레이 된다.

그렇지만 실제로 동영상을 서비스하는 플레이어를 만들려면, 동영상을 플레이어에 최적화된 형태로 다시 인코딩을 해서 플레이를 하는 것이 좋다. 화면 해상도에 따른 품질 차이나, 압축 방식 그리고 초당 프레임수 (FPS)등에 따라서 영향을 받기 때문에 최적의 인코딩 포맷을 찾은 후에, 그 포맷에 따라서 인코딩을 하는 것이 좋으며, 이런 인코딩 전문 플랫폼으로는 예전 엠군에서 제공하는 위켄디오나 해외의 zencoder등과 같인 인코딩 플랫폼을 사용하면 손쉽게 연동이 가능하다. 



참고 자료

http://www.techotopia.com/index.php/An_Android_Studio_VideoView_and_MediaController_Tutorial#Introducing_the_Android_MediaController_Class



SharedPreference를 이용한 데이타 저장 

및 애플리케이션간 데이타 공유


조대협 (http://bcho.tistory.com)



안드로이드에서는 데이타를 저장할때 여러가지 방법을 사용할 수 있다.

대표적인 방법으로는 일반적으로 파일을 생성해서 저장하는 방법, 그리고 안드로이드에 내장된 데이타 베이스인 SQLite 데이타 베이스를 활용하는 방법이외에 SharedPreference라는 클래스를 사용하여 데이타를 저장하는 방법이 있다.


SharedPreference


SharedPreference는 간단하게 Key/Value 형태로 데이타를 저장할 수 있는 데이타 구조로 내부적으로는 XML 파일로 저장이 된다.


사용법이 매우 간단해서 일반적인 설정값이나 세션 정보, 사용자 정보와 같은 값들을 저장하거나 주로 복잡하지 않고 경량의 데이타를 저장하는데 쉽게 사용할 수 있다. 단 해당 정보는 안드로이드 파일 시스템 내에 XML 파일로 접근이 되기 때문에, 보안적으로 안전하지 않을 수 있기 때문에, 노출등을 대비해서 중요한 정보는 저장하지 않거나 또는 최소한의 암호화를 해서 저장하는 게 어떨까 하는 생각이다. 


실제로 사용하는 방법은 Activity나 Service  클래스에서 Context를 가지고 온후, 해당 Context를 통하여 SharedPreference를 생성하여, 데이타를 저장하면 된다.


다음은 SharedPreference를 write 하는 예제이다



    public void writeSharedPreference(View view){


        EditText txtValue = (EditText) findViewById(R.id.txtValue);

        String value = txtValue.getText().toString();



        // 1. get Shared Preference

        SharedPreferences sharedPreference

                = this.getSharedPreferences("MYPREFRENCE", Context.MODE_MULTI_PROCESS | Context.MODE_WORLD_READABLE); 


        // 2. get Editor

        SharedPreferences.Editor editor = sharedPreference.edit();


        // 3. set Key values

        editor.putString("MYKEY",value);

        editor.putString("KEY2", "VALUE2");


        // 4. commit the values

        editor.commit();

    }


먼저 위의 1단계에서 getSharedPreference를 이용하여, 애플리케이션 컨텍스트로부터 SharedPreference를 가지고 온다. 이때 Preference 의 이름을 지정할 수 있는데, 여기서는 “MYPREFRENCE”라는 이름으로 Preference를 생성하였다. 그리고 다음으로는 access mode를 설정하였는데, 이 예제에서 설정한 access mode는 여러 프로세스간 공유가 가능하고, 읽을 수 있는 모드로 설정하였다. (뒤에서 다른 애플리케이션에서 이 Preference를 열기 위해서 권한을 부여하였다)


잠깐 access_mode에 대해서 짚고 넘어가면

보통은 MODE_PRIVATE을 사용하는데, 이 설정은 다른 애플리케이션은 접근이 불가하고, 오직 이 SharedPreference를 만든 애플리케이션만 접근이 가능하게 한다. MODE_WORLD_READABLE은 다른 애플리케이션도 읽기를 허용하는 권한이고, MODE_WORLD_WRITABLE은 다른 애플리케이션도 쓰기를 허용하는 권한이다.

조금더 자세한 권한에 대한 설명은 http://www.tutorialspoint.com/android/android_shared_preferences.htm 를 참고하기 바란다.



애플리케이션을 실행하면 텍스트 문자열을 입력 받아서 SharedPreference를 저장하게된다.

이때 바로 저장하는 것이 아니라 SharedPreference로 부터 Editor라는 객체를 받아온 후, setXXX메서드를 이용하여 데이타를 셋한후, 저장을 할때는 반드시 editor.commit()을 하여 저장한다.


주) 일종의 트렌젝션과 같은 기능을 제공하는데, 추측하건데 동시에 여러 애플리케이션이 접근하거나 여러 쓰레드가 접근할때 데이타가 틀어지는 것을 막기 위해서 단일 트렌젝션의 기능을 제공하는 걸로 이해 된다.




<그림 1. > 문자열을 입력받아 SharedPreference에 저장하는 앱


실제로 저장이 되었는지 확인을 하려면 Android Device Monitor 라는 도구를 이용하여, 파일 시스템을 살펴보면 생성된 SharedPreference 파일을 찾을 수 있다.


파일은 /data/data/PACKAGE_NAME/shared_prefs 에 저장된다.



<그림 2> Android Device Monitor


실제로 파일을 열어보면 다음과 같이 입력한 값이 XML 형태로 저장됨을 확인할 수 있다.



<그림 3> MYPREFRENCE.xml


SharedPreference를 통하여 다른 애플리케이션과 데이타를 공유하기


SharedPreference는 생성할때 다른 애플리케이션의 접근을 허용하여 생성할 경우, 다른 애플리케이션이 그 데이타를 액세스 할 수 있다. 즉 SharedPreference 를 이용해서 애플리케이션간에 데이타를 공유하는 것이 가능하다.


다음은, 타 애플리케이션의 SharedPreference로 부터 데이타를 읽어와서 EditText에 출력하는 예제의 실행예와 코드이다.




    public void readPreference(View view) throws Exception {



        // get Context of other application

        Context otherAppContext = null;

        try{

            otherAppContext =

                    createPackageContext("com.example.terry.sharedpreferencewriter",0);

        }catch(PackageManager.NameNotFoundException e){

            // log

            e.printStackTrace();

            throw e;

        }


        // getting Shared preference from other application

        SharedPreferences pref

                = otherAppContext.getSharedPreferences("MYPREFRENCE", Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS);

        String value = pref.getString("MYKEY", "NOTFOUND");


        EditText txtValue = (EditText)findViewById(R.id.txtValue);

        txtValue.setText(value);


    }


먼저 createPcakgeContext라는 메서드를 이용하여 다른 애플리케이션인 “com.example.terry.sharedpreferencewriter” 에 대한 애플리케이션 Context를 읽어온다.

다음으로는 얻어온 Context로 부터 getSharedPreferences 메서드를 읽어온후에, 해당 SharedPreference로 부터 getString 메서드를 이용하여 “MYKEY” 라는 키로 저장된 값을 읽어와서 출력한다.





간단한 웹뷰 컴포넌트의 이해와 최적화

조대협(http://bcho.tistory.com)


들어가기에 앞서서


이글은 안드로이드 프로그래밍 개발을 공부하면서 공부한 내용을 정리한 글입니다. 제가 아직 안드로이드에는 전문성이 없는 초보자 수준이기 때문에, 수준이 높지 않으니, 내용을 참고만 하시기를 바라며, 더 나은 프로그래밍 방법등이 있음을 미리 알려드립니다.


Webview(웹뷰) 컴포넌트에 대해서


웹뷰는 안드로이드 프레임웍에 내장된 웹 브라우져 컴포넌트로 뷰(View)형태로 앱에 임베딩이 가능하다. 비교해서 생각하자면, 안드로이드 앱안에, HTML iframe을 넣어놓은 것이라고나 할까?

단순하게, 웹 페이지를 보기위해도 사용하지만, 안드로이드 앱 안에서 HTML을 호출하여 앱을 구현하는 하이브리드 형태의 앱을 개발하는데에도 많이 사용된다. 이러한 하이브리드 앱은 안드로이드 네이티브 앱 개발에 비해서 개발이 비교적 쉽고, 특히나 기기간의 호환성을 해결하기가 상대적으로 편하다는 장점을 가지고 있으나, HTML 기반인 만큼 상대적으로 반응성이 약하고, 애니메이션등의 다양한 UI 효과를 넣기 어렵다는 단점을 가지고 있다.

그럼에도 불구하고 SNS나 기타 서비스 앱에서는 타 웹사이트 링크로 가는 기능등을 지원하기 위해서 많이 사용되고 있다.

웹 뷰에 사용되는 브라우져 컴포넌트는 우리가 일반적으로 생각하는 웹브라우져 수준으로 생각하면 곤란하다. 일반적인 웹 브라우져를 안드로이드 OS에 맞게 일부 기능들을 제외하고 작게 만든 웹브라우져라고 생각하면 되고 그렇기 때문에 HTML5 호환성등 기능의 제약을 많이 가지고 있다.



기본적인 웹뷰 사용방법


이해를 돕기 위해서 웹뷰를 이용한 간단한 애플리케이션을 만들어보자. 이 애플리케이션은 URL을 입력 받고 Go 버튼을 누르면 웹뷰에 해당 URL을 접속하여 보여주고, 해당 URL이 로딩되는데 까지 걸린 시간을 millisecond 로 표시해주는 애플리케이션이다.




먼저 안드로이드 스튜디오에서, MainActivity하나만 있는 심플한 앱 프로젝트를 생성한 후에, MainActivity의 레이아웃을 다음과 같이 디자인하자




위의 레이아웃의 XML 코드를 보면 다음과 같다.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context=".MainActivity"
android:weightSum="1">


<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<EditText
android:layout_width="252dp"
android:layout_height="wrap_content"
android:id="@+id/txtURL"
android:text="http://www.naver.com" />

<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Go"
android:id="@+id/btnGo"
android:onClick="goURL" />
</LinearLayout>

<WebView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/webView"
android:layout_weight="0.83" />

<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.13">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Elapsed time :"
android:id="@+id/tvElapsedTime" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="sec"
android:id="@+id/tvSec" />
</LinearLayout>

</LinearLayout>

레이아웃이 디자인 되었으면, [GO]버튼을 눌렀을때, 웹뷰에 URL을 표시하도록 goURL(View)라는 메소드를 다음과 같이 구현하자


public void goURL(View view){
TextView tvURL = (TextView)findViewById(R.id.txtURL);
String url = tvURL.getText().toString();
Log.i("URL","Opening URL :"+url);

WebView webView = (WebView)findViewById(R.id.webView);
webView.setWebViewClient(new WebViewClient()); // 이걸 안해주면 새창이 뜸
webView.loadUrl(url);

}


사용법은 의외로 간단한데, Webview객체를 얻어와서 loadURL로 url을 로딩하면 된다. 이때 주의할점은 setWebViewClient()  메서드를 사용해야 하는데, 안드로이드에서는 디폴트로, 다른 링크로 이동하고자 할때는 안드로이드의 디폴트 외부 웹 브라우져를 통해서 이동하도록 하고 있다. 그래서 만약 setWebViewClient로 내부 웹뷰 클라이언트를 지정해주지 않으면 별도의 브라우져가 뜨는 것을 볼 수 있다.

그리고 이 애플리케이션은 외부 인터넷을 억세스하기 때문에, AndroidManifest.xml에서INTERNET 퍼미션을 추가해줘야 한다.


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.terry.simplewebview" >
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:hardwareAccelerated="true"> // 하드웨어 가속 사용
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>


주목해서 봐야할점중 하나는 android:hardwareAccelerated  라는 필드인데, 웹 페이지를 렌더링 하는데 하드웨어 가속 기능을 사용하도록 하는 옵션으로, 이 옵션 사용 여부에 따라서 성능차이가 꽤 있기 때문에, 가급적이면 사용하도록 한다.


여기서 약간 개량을 해서, 해당 URL이 다 뜨고 나면 로딩하는데 걸리는 시간을 측정하는 로직을 추가해보자


public void goURL(View view){
TextView tvURL = (TextView)findViewById(R.id.txtURL);
String url = tvURL.getText().toString();
Log.i("URL", "Opening URL with WebView :" + url);

final long startTime = System.currentTimeMillis();
WebView webView = (WebView)findViewById(R.id.webView);

// 하드웨어 가속
// 캐쉬 끄기
//webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);

webView.setWebViewClient(new WebViewClient(){
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
long elapsedTime = System.currentTimeMillis()-startTime;
TextView tvSec = (TextView) findViewById(R.id.tvSec);
tvSec.setText(String.valueOf(elapsedTime));
}
});
webView.loadUrl(url);

}


setWebViewClient를 만들때, WebViewClient에 onPageFinished라는 메서드를 오버라이딩해서 종료 시간을 구할 수 있다. 이 메서드는 해당 URL이 모두 로딩되었을때 호출되는 메서드이다.


웹뷰의 개념 이해를 돕기 위해서 가장 간단한 예제를 통해서 설명하였는데, HTML 안에서 Javascript 를 사용하려면 별도의 옵션이 필요하고, 또한 javascript를 사용할때는 Javascript의 메서드를 실행하면, 안드로이드 애플리케이션내에 정의된 메서드를 수행할 수 있도록도 할 수 있다. 

기타 자세한 내용은 http://developer.android.com/guide/webapps/webview.html 를 참고하기 바란다.


웹뷰의 성능 향상과 확장


웹뷰에 대한 성능 관련 자료를 찾아보니, 성능이 안나오는 문제가 많아서 하이브리드 앱을 개발하는 쪽에는 다양한 솔루션이 제시되고 있다. 써드 파티 웹뷰 라이브러리를 이용하는 방법이나 또는 웹뷰를 커스터마이징 해서 WebSocket의 기능을 추가하거나 Canvas 성능을 개선하는 작업등이 있는데 (http://deview.kr/2013/detail.nhn?topicSeq=14) 성능 부분에 대해서는 조금 더 공부가 필요할듯 하고, 아래는 성능 관련해서 검색을 했을때 주로 많이 소개되는 내용을 정리하였다.


크롬뷰 사용


안드로이드 4.4 (API level 19)부터는 웹뷰에 대한 성능 개선의 일환으로 Chromium (http://www.chromium.org/Home

) 이라는 오픈소스( 브라우져가 사용되었는데, 별도의 설정없이도, 이 브라우져 기반의 웹뷰가 사용이 가능하다. Chromium 기반의 웹뷰는 성능 향상뿐만 아니라, HTML 호환성 개선도 같이 이루어졌다. 

https://www.google.co.kr/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=webview+performance+android&newwindow=1&start=10


일반적인 튜닝 옵션


일반적으로 웹뷰 성능 튜닝 관련 내용을 검색하면 단골로 나오는 3가지 튜닝 옵션들이 있는데 다음과 같다

  • 캐쉬 사용하지 않기 : webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);로 브라우져 캐쉬를 끄는 방법이 언급되는데, 사실 캐쉬를 쓰는데 왜 성능이 올라가는지는 잘 모르겠다. 이 부분은 조금 더 살펴봐야할것으로 보이고
  • 하드웨어 가속 기능 사용하기 :렌더링에 대해서 하드웨어 가속 기능을 사용할지 여부인데, 3.0 부터 지원이 되기 시작했고, 디폴트는 false이며, 4.0부터는 default가 true이다. 앞의 예제에서와 같이 AndroidManifest.xml에서, android:hardwareAccelrated를  true로 설정해주면 된다.
  • 렌더링 우선 순위 높이기 : 강제적으로 렌더링 기능에 대한 우선 순위를 높이는 방법이다. webView.getSettings().setRenderPriority(RenderPriority.HIGH)로 설정해주면 되는데, API Level 18부터는 deprecated 되었기 때문에 하위 버전에만 적용될 수 있도록 한다.

외부 라이브러리를 이용한 웹뷰의 성능 향상


다른 성능 향상 방법으로는 써드 파티 라이브러리를 찾아봤는데, 가장 많이 사용되는 라이브러리로 크로스워크(crosswalk)라는 오픈 소스 라이브러리가 있다. https://crosswalk-project.org/documentation/getting_started.html

성능 향상은 물론이거니와, HTML5에 대한 호환성 지원도 매우 높고, 안드로이드뿐만 아니라, iOS,Tizen등 다양한 OS를 지원하기 때문에 하이브리드 앱을 개발한다면 탑재를 고려해볼만하지만, 이러한 호환성이나 성능을 높이기 위해서 렌더링 관련 기능을 모두 자체 구현하였기 때문에 앱 사이즈가 대략 30~40MB정도 커지기 때문에, 앱 크기에 대한 부담과 dex 사이즈에 대한 부담 (안드로이드 실행 파일인 dex는 메서드수가 65535개로 제한이 되어 있다.)

http://monaca.mobi/en/blog/crosswalk-performance/ 는 HTML5에 대한 호환성을 Crosswalk와 기본 웹뷰 사이에 수치를 비교한것인데, 일반 웹뷰는 278, 크로스워크는 호환성 부분에서 493 포인트를 낸것을 확인할 수 있다.


게임이나 일반 애플리케이션의 경우는 네이티브 앱 개발이 아무래도 대세가 되겠지만, SNS나 미디어성 컨텐츠는 개발의 용이성과 호환성 그리고 다양한 컨텐츠 포맷을 지원하는 점에서 HTML5 기술이 지원되는 웹뷰는 아무래도 필수적인 기술이 되지 않을까 싶다.


 

REST API를 이용하여, 날씨를 조회하는 간단한 애플리케이션 만들기

 

조대협 (http://bcho.tistor


네트워크를 통한  REST API 호출 방법을 알아보기 위해서, 간단하게, 위도와 경도를 이용하여 온도를 조회해오는 간단한 애플리케이션을 만들어보자

이 애플리케이션은 경도와 위도를 EditText 뷰를 통해서 입력 받아서 GETWEATHER라는 버튼을 누르면 네트워크를 통하여 REST API를 호출 하여, 날씨 정보를 받아오고, 해당 위치의 온도(화씨) 출력해주는 애플리케이션이다.

 



 

날씨 API 는 http://www.openweathermap.org/ 에서 제공하는데, 사용법이 매우 간단하고, 별도의 API인증 절차가 필요하지 않기 때문에 쉽게 사용할 수 있다. 사용 방법은 다음과 같이 쿼리 스트링으로 위도와 경도를 넘겨주면 JSON  형태로 지정된 위치의 날씨 정보를 리턴해준다.

 

http://api.openweathermap.org/data/2.5/weather?lat=37&lon=127

 

아래는 리턴되는 JSON 샘플



이 리턴되는 값중에서 main.temp 에 해당하는 값을 얻어서 출력할 것이다.

 

날씨값을 저장하는 클래스 작성

 

package com.example.terry.simpleweather.client;

/**

 * Created by terry on 2015. 8. 27..

 */

public class Weather {
    int lat;
    int ion;
    int temprature;
    int cloudy;
    String city;

    public void setLat(int lat){ this.lat = lat;}
    public void setIon(int ion){ this.ion = ion;}
    public void setTemprature(int t){ this.temprature = t;}
    public void setCloudy(int cloudy){ this.cloudy = cloudy;}
    public void setCity(String city){ this.city = city;}

    public int getLat(){ return lat;}
    public int getIon() { return ion;}
    public int getTemprature() { return temprature;}
    public int getCloudy() { return cloudy; }
    public String getCity() { return city; }
}

 

다음으로 REST API를 호출하는 OpenWeatherAPIClient.java 코드를 다음과 같이 작성한다.

 

package com.example.terry.simpleweather.client;



import org.json.JSONException;

import org.json.JSONObject;


import java.io.BufferedInputStream;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.net.HttpURLConnection;

import java.net.MalformedURLException;

import java.net.URL;


/**

 * Created by terry on 2015. 8. 27..

 * 목표

 * 1. AsyncTask와 HTTPURLConnection을 이용한 간단한 HTTP 호출 만들기

 * 2. 리턴된 JSON을 파싱하는 방법을 통하여, JSON 객체 다루는 법 습득하기

 * 3. Phone Location (GPS) API 사용 방법 파악하기

 *

 * 참고 자료 : http://developer.android.com/training/basics/network-ops/connecting.html

 * */

public class OpenWeatherAPIClient {

    final static String openWeatherURL = "http://api.openweathermap.org/data/2.5/weather";

    public Weather getWeather(int lat,int lon){

        Weather w = new Weather();

        String urlString = openWeatherURL + "?lat="+lat+"&lon="+lon;


        try {

            // call API by using HTTPURLConnection

            URL url = new URL(urlString);

            HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();

//            urlConnection.setConnectTimeout(CONNECTION_TIMEOUT);

//            urlConnection.setReadTimeout(DATARETRIEVAL_TIMEOUT);


            InputStream in = new BufferedInputStream(urlConnection.getInputStream());

            JSONObject json = new JSONObject(getStringFromInputStream(in));


            // parse JSON

            w = parseJSON(json);

            w.setIon(lon);

            w.setLat(lat);


        }catch(MalformedURLException e){

            System.err.println("Malformed URL");

            e.printStackTrace();

            return null;


        }catch(JSONException e) {

            System.err.println("JSON parsing error");

            e.printStackTrace();

            return null;

        }catch(IOException e){

            System.err.println("URL Connection failed");

            e.printStackTrace();

            return null;

        }


        // set Weather Object


        return w;

    }


    private Weather parseJSON(JSONObject json) throws JSONException {

        Weather w = new Weather();

        w.setTemprature(json.getJSONObject("main").getInt("temp"));

        w.setCity(json.getString("name"));

        //w.setCloudy();


        return w;

    }


    private static String getStringFromInputStream(InputStream is) {


        BufferedReader br = null;

        StringBuilder sb = new StringBuilder();


        String line;

        try {


            br = new BufferedReader(new InputStreamReader(is));

            while ((line = br.readLine()) != null) {

                sb.append(line);

            }


        } catch (IOException e) {

            e.printStackTrace();

        } finally {

            if (br != null) {

                try {

                    br.close();

                } catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }


        return sb.toString();


    }

}

 

 

코드를 하나씩 뜯어보면

안드로이드에서 HTTP 호출을 하기 위해서는 HttpURLConnection이라는 클래스를 사용한다. URL이라는 클래스에 API를 호출하고자 하는 url주소를 지정하여 생성한후, url.openConnection()을 이용해서, HTTP Connection을 열고 호출한다.

URL url = new URL(urlString);

HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();

 

다음으로, 리턴되어 오는 문자열을 읽어서 JSON형태로 파싱을 해야 하는데,

InputStream in = new BufferedInputStream(urlConnection.getInputStream());

먼저 위와 같이 urlConnection으로 부터 getInputStream() 메서드를 통해서 InputStream을 리턴받고,

JSONObject json = new JSONObject(getStringFromInputStream(in));

getStringFromInput(InputStream xx)이라는 메서드를 이용하여 inputStream을 String으로 변환한후에 JSONObject로 변환한다. (여기서 getStringFromInput은 미리 정해진 메서드가 아니고 위의 소스코드 처럼 InputStream 을 String으로 변환하기 위해서 여기서 지정된 코드들이다.)

다음으로 해당 JSON을 파싱해서, main.temp 값을 읽은후 Weather class에 넣어서 리턴을 한다.

w = parseJSON(json);

w.setIon(lon);

w.setLat(lat);

에서 parseJSON이라는 메서드를 호출하는데, parseJSON은 다음과 같다.


private Weather parseJSON(JSONObject json) throws JSONException {

    Weather w = new Weather();

    w.setTemprature(json.getJSONObject("main").getInt("temp"));

    w.setCity(json.getString("name"));

    //w.setCloudy();


    return w;

}


위의 메서드에서는 json에서 먼저 getJSONObject(“main”)을 이용하여 “main” json 문서를 얻고, 그 다음 그 아래 있는 “temp”의 값을 읽어서 Weather 객체에 세팅한다.

 

여기까지 구현을 하면 REST API를 http를 이용하여 호출하고, 리턴으로 온 JSON 문자열을 파싱하여 Weather 객체에 세팅을해서 리턴하는 부분까지 완료가 되었다.

 

그다음 그러면 이 클래스의 메서드를 어떻게 호출하는가? 네트워크 통신은 IO작업으로 시간이 다소 걸리기 때문에, 안드로이드에서는 일반적으로 메서드를 호출하는 방식으로는 불가능하고, 반드시 비동기식으로 호출해야 하는데, 이를 위해서는 AsyncTask라는 클래스를 상속받아서 비동기 클래스를 구현한후, 그 안에서 호출한다.

다음은 OpenWeatherAPITask.java 클래스이다.

 

public class OpenWeatherAPITask extends AsyncTask<Integer, Void, Weather> {

    @Override

    public Weather doInBackground(Integer... params) {

        OpenWeatherAPIClient client = new OpenWeatherAPIClient();


        int lat = params[0];

        int lon = params[1];

        // API 호출

        Weather w = client.getWeather(lat,lon);


        //System.out.println("Weather : "+w.getTemprature());


        // 작업 후 리

        return w;

    }

}

 

위와 같이 AsyncTask를 상속받았으며, 호출은 doInBackground(..)메서드를 구현하여, 그 메서드 안에서 호출을 한다.

위의 코드에서 볼 수 있듯이, doInBackground에서 앞서 작성한 OpenWeatherAPIClient의 객체를 생성한후에, 인자로 넘어온 lat,lon값을 받아서, getWeahter를 통하여, 호출하였다.

 

이제 API를 호출할 준비가 모두 끝났다.

이제 UI를 만들어야 하는데, res/layout/activity_main.xml은 다음과 같다.

 

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"

    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">




    <TextView

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:textAppearance="?android:attr/textAppearanceLarge"

        android:text="Temperature"

        android:id="@+id/tem"

        android:layout_below="@+id/tvLongtitude"

        android:layout_alignParentStart="true"

        android:layout_marginTop="46dp" />


    <Button

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="getWeather"

        android:id="@+id/getWeatherBtn"

        android:onClick="getWeather"

        android:layout_alignBottom="@+id/tem"

        android:layout_alignParentEnd="true" />


    <TextView

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:textAppearance="?android:attr/textAppearanceMedium"

        android:text="latitude"

        android:id="@+id/tvLatitude"

        android:layout_marginTop="27dp"

        android:layout_alignParentTop="true"

        android:layout_alignParentStart="true" />


    <TextView

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:textAppearance="?android:attr/textAppearanceMedium"

        android:text="longtitude"

        android:id="@+id/tvLongtitude"

        android:layout_marginRight="62dp"

        android:layout_marginTop="30dp"

        android:layout_below="@+id/lat"

        android:layout_alignParentStart="true" />


    <EditText

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:id="@+id/lat"

        android:width="100dp"

        android:layout_marginRight="62dp"

        android:layout_alignBottom="@+id/tvLatitude"

        android:layout_toEndOf="@+id/tvLatitude"

        android:layout_marginStart="36dp" />


    <EditText

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:width="100dp"

        android:id="@+id/lon"

        android:layout_marginRight="62dp"

        android:layout_alignBottom="@+id/tvLongtitude"

        android:layout_alignStart="@+id/lat" />


</RelativeLayout>

 

화면 디자인이 끝났으면, 이제 MainActivity에서 버튼을 누르면 API를 호출하도록 구현해보자.

 

package com.example.terry.simpleweather;


import android.support.v7.app.ActionBarActivity;

import android.os.Bundle;

import android.view.Menu;

import android.view.MenuItem;

import android.view.View;

import android.widget.EditText;

import android.widget.TextView;


import com.example.terry.simpleweather.client.OpenWeatherAPITask;

import com.example.terry.simpleweather.client.Weather;


import java.util.concurrent.ExecutionException;



public class MainActivity extends ActionBarActivity {


    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

    }


    @Override

    public boolean onCreateOptionsMenu(Menu menu) {

        // Inflate the menu; this adds items to the action bar if it is present.

        getMenuInflater().inflate(R.menu.menu_main, menu);

        return true;

    }


    @Override

    public boolean onOptionsItemSelected(MenuItem item) {

        // Handle action bar item clicks here. The action bar will

        // automatically handle clicks on the Home/Up button, so long

        // as you specify a parent activity in AndroidManifest.xml.

        int id = item.getItemId();


        //noinspection SimplifiableIfStatement

        if (id == R.id.action_settings) {

            return true;

        }


        return super.onOptionsItemSelected(item);

    }


    // MapView 참고 http://seuny.tistory.com/14

    public void getWeather(View view)

    {

        EditText tvLon = (EditText) findViewById(R.id.lon);

        String strLon = tvLon.getText().toString();

        int lon = Integer.parseInt(strLon);


        EditText tvLat = (EditText) findViewById(R.id.lat);

        String strLat = tvLat.getText().toString();

        int lat = Integer.parseInt(strLat);



        // 날씨를 읽어오는 API 호출

        OpenWeatherAPITask t= new OpenWeatherAPITask();

        try {

            Weather w = t.execute(lon,lat).get();


            System.out.println("Temp :"+w.getTemprature());


            TextView tem = (TextView)findViewById(R.id.tem);

            String temperature = String.valueOf(w.getTemprature());


            tem.setText(temperature);

            //w.getTemprature());



        } catch (InterruptedException e) {

            e.printStackTrace();

        } catch (ExecutionException e) {

            e.printStackTrace();

        }

    }

}

 

 

위에서 getWeather(View )부분에서 호출을하게 되는데, 먼저 앞에서 AsyncTask를 이용해서 만들었던 OpenWeatherAPITask 객체 t를 생성한 후에, t.execute로 호출을하면 된다.

실행 결과는 t.execute()에 대해서 .get()을 호출함으로써 얻을 수 있다.

 

이때 주의할점이 안드로이드는 네트워크 호출과 같은 리소스에 대한 접근을 통제하고 있는데, 이러한 통제를 풀려면, AnrdoidManifest.xml에 다음과 같이 INTERNET 과 NETWORK 접근을 허용하는 내용을 추가해줘야 한다.

<uses-permission android:name="android.permission.INTERNET" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

 

다음은 위의 내용을 추가한 AndroidManifest.xml이다.

 

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="com.example.terry.simpleweather" >

    <uses-permission android:name="android.permission.INTERNET" />

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />


    <application

        android:allowBackup="true"

        android:icon="@mipmap/ic_launcher"

        android:label="@string/app_name"

        android:theme="@style/AppTheme" >

        <activity

            android:name=".MainActivity"

            android:label="@string/app_name" >

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />


                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>


    </application>


</manifest>

 

참고 : http://developer.android.com/training/basics/network-ops/connecting.html




9월 15일 추가 내용

  • t.execute().get 보다는  onPostExecute 를 통해서 리턴 값을 받는 것이 더 일반적인 패턴
  • AsyncTask를 사용하기 보다는 근래에는 Retrofit이라는 프레임웍을 사용하는게 더 효율적이며, 성능도 2배이상 빠름.
    관련 튜토리얼 : http://gun0912.tistory.com/30
    아래는 성능 비교 자료


 

안드로이드 주요 레이아웃

프로그래밍/안드로이드 | 2015.08.27 00:09 | Posted by 조대협

안드로이드 주요 레이아웃 (메모)



원본 : https://www.udacity.com/course/viewer#!/c-ud853/l-1395568821/m-1645248621


다른 종류의 레이아웃도 있으나 일반적으로 사용하는 레이아웃은 다음과 같다.

  • FrameLayout : 보려는 항목이 하나만 있을 경우. (ListView 하나만 넣는 경우와 같은 케이스)
  • LinearLayout : 수직 또는 수평으로 차례로 View를 출력할때
  • RelativeLayout : 다른 View에 비한 상대적 위치를 표현하는 반응형 UI에 적합


드로이드에 애플리케이션  컴파일,설치해서 실행하기 


(에뮬레이터 실행 포함)


조대협 (http://bcho.tistory.com)


다음은 개발된 애플리케이션을 단말이나 또는 에뮬레이터에서 실행하는 방법이다.

이 방법은 안드로이드 스튜디오 1.2.2 을 맥북에 설치한 기준으로 설명한다.

안드로이드 스튜디오 IDE가 좋아서, 필요는 없겠지만. 몰 하던간에 IDE 없이도 할 수 있는 방법이 있어야 하기에 테스트 해놓은 내용을 기록해놓는다.


1. 애플리케이션 컴파일 하기


디렉토리에서 ./gradlew assembleDebug

명령을 실행하면, 디버깅 모드로 컴파일이 되고 apk 파일이 생성된다.



위와 같은 에러가 발생할 수 있는데, 이 경우는 ANDROID_HOME이 설정되지 않은 경우이다.

~/bash_profile에

export ANDROID_HOME=~/Library/Android/sdk 으로 설정하면 된다.



2. 애플리케이션을 디바이스에 설치하기 


안드로이드 SDK가 설치되는 곳은 /User/{username}/Library/Android/sdk

안드로이드의 필수적인 adb는 /Users/{username}/Library/Android/sdk/platform-tools

에 있는데 Path가 걸려있지 않아서, 다른 디렉토리에서 실행이 되지 않는다.


~/.bash_profile 파일에서 PATH에 위의 platform-tools 디렉토리 경로를 추가해주면 된다.


adb를 사용할 수 있는 상태가 되면, apk 파일이 있는 위치에서 다음과 같이 adb install 명령으로 설치한다.


ChoByungwookui-MacBook-Pro:apk terry$ adb install app-debug.apk


8/17 추가. adb install 시 adb install -r 을 하면, 기존에 파일이 있으면 replace하게 된다.


3. 에뮬레이터 띄우기


  실제로 디바이스 없는 경우에는 소프트웨어 기반의 버추얼 디바이스를 사용하면된다. (에뮬레이터)

에뮬레이터는 

%/Users/{username}/Library/Android/sdk/android avd

로 실행하면 된다.



4. 애플리케이션 실행하기


에뮬레이터를 띄워서 adb를 이용해 애플리케이션을 설치했으면, 실행을 해야 하는데, 직접 에뮬레이터에 들어가서 실행하는 방법도 있지만 adb shell을 이용하면 조금 더 쉽게 커맨드라인에서 실행을 해볼 수 있다.


adb shell am start 패키지명/패키지명.액티버티클래스명

예) adb shell am start -n com.example.terry.basicintentsample/com.example.terry.basicintentsample.MyActivity



안드로이드 리소스 파일 개요


조대협 (http://bcho.tistory.com)


안드로이드 프로젝트에서 소스 코드 이외에 이미지나 디자인 레이아웃등의 기타 리소스등을 코드와 분리해서 취급한다.


이렇게 리소스를 코드와 분리해서 사용하는 이유는, 조금 더 유연하게 애플리케이션을 개발하게 하기 위함이다. string과 같은 문자열을 별도의 리소스 파일에서 관리함으로써, 애플리케이션 수정 없이 string 리소스 파일에만 문자열을 추가함으로 간단하게 다국어를 지원할 수 있다.


리소스는 크게 7가지 타입의 리소스가 있고, “/res” 디렉토리에 저장된다. 



Layout Resource (뷰 리소스)

애플리케이션 UI의 배치 에 대한 내용을 XML 형태로 정의하여 저장한다. res/layout 아래와 XML 형태로 저장되며, R.layout으로 접근이 가능하다.


Animation Resource (애니메이션 리소스)

안드로이드에서 애니메이션 기능을 구현하는데 사용되는 설정들이 저장된다. res/anim에 XML로 정의되며 R.anim 이라는 전역 변수를 통해서 접근이 가능하다.


Drawable Resource (그래픽 리소스)

res/drawable 디렉토리에 저장되는 gif,png와 같은 이미지나,  기타 화면에 그려지는 그래픽 관련 리소스들을 저장한다. 이미지 뿐만 아니라, 레이아웃 리스트등의 XML 등 여러 관련 리소스를 저장 하며 R.drawable 을 통해서 접근이 가능하다.


Color State List Resource (칼라 리소스)

res/color 디렉토리 안에 XML 형태로 저장되며, R.color 를 통해서 접근이 가능하다. 버튼이나 글자등의 색을 정의한다.


Menu Resource (메뉴 리소스)

res/menu 디렉토리 내에 XML 형태로 저장되며, Popup 이나 Option과 같은 메뉴를 정의하는 리소스들이 저장된다. R.menu를 이용하여 접근이 가능하다.


String Resource (문자열 리소스)

res/values에 XML 형태로 저장된다. 



위에서 간략하게 정리했지만, 각 리소스안에는 더 세부적인 설정들이 여러 파일로 저장된다. 조금 더 자세한 내용은 http://developer.android.com/guide/topics/resources/available-resources.html 를 참고하기 바란다.


안드로이드 인테트를 통해서 액티버티 생성하기


조대협(http://bcho.tistory.com)


요즘 일이 바쁘고 하는 역할도 바뀌어서 기술보다는 관리나 스크럼에 대한 글들을 읽고 공부하다보니, 반대적으로 기술에 대한 포스팅이 줄었다. 이래서는 안되겠다 싶어서, 어떤 부분을 봐야 할까 고민하다 보니 빅데이타와 앱 개발 기술쪽을 봐야겠다가 시작은 했는데, 막상 진도는 잘 안 나가고 있어서. 앱팀을 운영하고 있으니, 말이라도 통할려면 기본이라도 알아야 겠다 해서 안드로이드 앱쪽을 공부하고 있는데, 일단 해놓은 내용이라고 까먹지 않으려면 정리해놔야겠다는 생각에 간단하게 포스팅한다.


본 문서의 내용은 https://developer.android.com/training/index.html 를 기반으로 작성하였다. (문서에 있는데로 하면 생각보다 안되느 부분이 많다. 문서를 업데이트 하지 않아서 그런것 같은데). 

해당 프로그램은 화면에서 문자열을 입력 받아서, SEND라는 버튼을 누르면 다른 화면에서 입력 받은 문자열을 다른 화면에서 보여주는 예제이다 



MyActivity라는 액티비트에서 텍스트 박스를 통해서 문자열을 입력받고, SEND 버튼을 누르면, 텍스트 박스내의 문자열을 읽어서 Intent로 만들어서 DisplayActivity라는 액티비티로 전달하면 해당 문자열을 출력하는 내용이다.



먼저 인텐트의 개념에 대해서 이해할 필요가 있는데, 인텐트는 서로 다른 컴포넌트 간에 이벤트를 전달할 수 있는 개념이다. 위의 그림에서와 같이 MyActivity에서, DisplayActicity로 액티비티간에 출력할 문자열을 보내는것과 같은 이벤트이다. 유닉스의 IPC(Inter process call)과 같은 개념으로 생각을 하면된다. 

인텐트에 대한 자세한 내용은 https://developer.android.com/guide/components/intents-filters.html 를 참고하기 바라며, 나중에 다시 다루도록한다.


먼저, 문자열을 입력받을 화면을 작성한다. 안드로이드 스튜디오에서, 다음과 같은 레이아웃으로 작성을 한다. 



다음으로 버튼을 눌렀을때, 인텐트를 만들어서 보내기 위해서 버튼을 눌렀을때 실행되는 메서드를 정의하여 버튼 이벤트와 연결한다.

    public void sendMessage(View view){


        EditText editText = (EditText)findViewById(R.id.editText);

        String message = editText.getText().toString();


        Intent intent = new Intent(this,DisplayMessageActivity.class);

        intent.putExtra("com.example.terry.basicintentsample.MESSAGE",message);

        startActivity(intent);

    }


이 메서드는 새로운 인텐트를 생성하여 DisplayActivity라는 액티비티 클래스로 보내지는 인텐트가 된다.

인텐트를 통해서 보내지는 데이타는 “com.example.terry.basicintentsample.MESSAGE” 문자열을 키로 해서, EditText 텍스트 문자열 박스에서 읽어도 문자열을 값으로 해서 putExtra메서드를 이용해서 인텐트에 바인딩 된다. 

이 인텐트는 startActivity를 통해서 DisplayActivity 액티비티를 새롭게 시작하면서 같이 전달이 된다. 

위와 같이 코드를 작성하여,액티버티 뷰를 정의한 activity_my.xml에서 다음과 같이 버튼이 눌렀을때 작성한 “sendMessage”메서드가 호출되도록 정의 한다.

        <Button

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:text="SEND"

            android:id="@+id/button"

            android:layout_weight="1"

            android:onClick="sendMessage" /


인텐트를 만들어서 보내는 부분이 끝났으면, 이제 인텐트를 받아서 출력하는 부분을 구현해보도록 하자.

안드로이드 스튜디오에서,  새로운 액티비티를 생성한다.




생성된 액티비티에는 간단하게, “myMessage”라는 아이디로 TextView 하나만 배치 시킨다.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"

    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"

    android:paddingRight="@dimen/activity_horizontal_margin"

    android:paddingTop="@dimen/activity_vertical_margin"

    android:paddingBottom="@dimen/activity_vertical_margin"

    tools:context="com.example.terry.basicintentsample.DisplayActivity">


    <TextView android:text="@string/hello_world" android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:id="@+id/myMessage" />


</RelativeLayout>


다음으로 OnCreate 메세지에 다음과 같은 코드를 추가한다.

    @Override 

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);


        // Render View first

        setContentView(R.layout.activity_display);


        // Get the message from the intent

        Intent intent = getIntent();

        String message = intent.getStringExtra("com.example.terry.basicintentsample.MESSAGE");

        TextView textView = (TextView)findViewById(R.id.myMessage);

        textView.setText(message);


    }

위의 코드는 인텐트에서 문자열을 받아서 “myMessage”라는 id의 텍스트 뷰에 그 문자열을 출력하는 코드이다.

출력전에, setContentView를 먼저 호출해야 하는데, View를 렌더링한 후에 myMessage TextView가 생성된다. 

다음으로 getIntent() 메서드를 이용하면, 인텐트를 받을 수 있고, getStringExtra메서드를 이용해서 “com.example.terry.basicintentsample.MESSAGE” 키로 문자열을 받아서 TextView에 setText로 세팅한다.





Android View와 ViewGroup의 개념



View와 ViewGroup 객체를 이용해서 만듬.

View는 버튼이나 텍스트필드와 같은  UI Widget 이다. 

ViewGroup은 여러개의 View 또는 ViewGroup을 포함할 수 있는 컨테이너의 개념이며, ViewGroup을 상속하여 화면 배치 속성을 갖는 Layout을 가지고 있는 ViewGroup이 있다. ex)LinearLayout 등






<그림. ViewGroup과  View의 상하 관계>

출처 : https://developer.android.com/training/basics/firstapp/building-ui.html


이 ViewGroup은 res/layout/*.xml 에 XML 형태로 정의할 수 있다.

다음은 LearLayout 을 선언한 예이다.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="horizontal" >

</LinearLayout>

View의 경우에는 보통 다음과 같은 형태로 표현되는데, 

<TextView

    android:layout_width="wrap_content"

    android:layout_height="wrap_content"

    android:textAppearance="?android:attr/textAppearanceMedium"

    android:text="Medium Text"

    android:id="@+id/textView2" />

해당 뷰 컴포넌트를 식별하기 위한 id, 그리고 사이드를 정의하기 위한 layout_width,layout_height으로 정의한다. 위에서는 wrap_content로 정의했는데, 절대 크기가 아니라 화면에 나타난 뷰 크기만큼 맞추겠다는 정의이다. (안드로이드 디바이스는 해상도가 다양하기 때문에 절대 크기 보다는 위와 같은 상태 크기를 사용할 수 있도록 지원한다.)





안드로이드 Activity Lifecycle (액티버티 생명주기)와

, Back Stack & Task


조대협 (http://bcho.tistory.com)


Back Stack

Activity의 라이프 사이클 개념을 이해하기 전에, 먼저 BackStack이라는 개념을 이해해야 한다.
안드로이드 애플리케이션은 모바일 애플리케이션의 특성상 하나의 화면, 즉 Activity만 화면에 활성화가 되게 된다. 활성화 된 화면에서 작업하다가, Back 버튼등을 눌러서 이전화면으로 돌아갈 수 있는데, 이런 돌아가는 구조를 지원하기 위해서, 내부적으로 화면이 전환이 되더라도 기존의 화면을 없애는것이 아니라 메모리에 저장해놨다가, 돌아갈때 저장된 화면을 띄워 주는 구조가 된다
이런 구조를 구현하기 위해서 내부적으로 BackStack이라는 구조를 사용하는데, 각각의 화면이 Activity이기 때문에, 화면이 전환될때 마다, 그 Activity를  BackStack안에 저장한다. 그리고, Back 버튼등으로 돌아기기를 할 경우에는 Stack에서 하나씩 꺼내서 이전 화면으로 전환을 한다

아래 그림을 보자, 
  • 처음에 Activity 1 화면이 활성화 되었다.
  • 다음에 Activity 2가 실행되면, Activity 1이 BackStack에 저장된다.
  • 그 다음에 Activity 3이 실행되면, Activity 2가 BackStack에 추가적으로 저장되는 것을 볼 수 있다.
  • 이때 Activity 3가 Back 버튼등에 의해서 종료되면, Activity 3는 Destroy 되고, Back Stack에 있던 Activity2 가 활성화 된다.





Task

A task is a collection of activities that users interact with when performing a certain job. The activities are arranged in a stack (the back stack), in the order in which each activity is opened.

BackStack의 개념을 대략적으로 이해한후에, Task라는 개념을 이해하는게 좀 어려웠는데, Task하는 하나의 BackStack이라고 생각하면 된다. 그런데 왜 굳이 이 Task라는 개념을 사용하느냐? 아직은 잘 모르겠지만, 글들을 읽어보면 일단 키 포인트는 안드로이드 애플리케이션의 Activity는 서로 다른 application간에 공유가 가능하다.
즉, 내가 A라는 애플리케이션을 수행하다가, SNS 공유와 같은 기능을 사용할때, SNS 애플리케이션 B의 Activity들을 불러올 수 있다.  또는 이메일 클라이언트에서, 사진앱의 activity들을 불러서, 첨부 사진을 선택할 수 있다.

그렇다보니, 하나의 Activity는 동시에 여러개의 애플리케이션에 의해서 사용될 수 있기 때문에, 다른 개념을 사용하는데, 이를 Task 라고 한다. 현재 실행중인 애플리케이션의 Activity Stack은 이 Task라는 곳에 저장이 되게 된다.
(일종의 instance 개념과 유사한듯)


Activity의 라이프 사이클

Activity는 사용자와 Interaction이 있는 UI를 처리하는 컴포넌트로 다음과 같은 생명 주기를 갖는다. 각 주기마다 이벤트에 의해서 호출되는 함수들을 정리하면 다음과 같다.





  • OnCreate : 맨 처음 랩이 처음 실행되었을때, 실행되는 메서드로 UI 컴포넌트등을 로딩하고, Listner를 바인딩하고, 초기 데이타를 로딩하는 등의 초기화 작업을 수행한다.
  • OnStart : UI가 화면에 보이기 전에 바로 호출된다. UI가 로딩 된다고 해도, 사용자로 부터 Input을 받을 수 는 없다
  • OnResume : UI 로딩이 끝난후, 사용자 Input (Interaction)이 시작되기 전에 호출된다. 이 함수들이 다 호출되고 나면, 애플리케이션은 실행 가능 상태인 “Activity Running” 상태가 되며, UI도 모두 로딩되고, 사용자로 부터 입력을 받을 준비도 끝난다.
  • OnPause : 이 상태에서 만약에 다른 Activity 화면이 올 경우, OnPause가 호출된다.
    정확한 상태 정의는 “보이기는 하지만 사용자가 Interaction을 할 수 없는 상태” 정도로 정의할 수 있다. 이런 상태가 어떤 상태인가 하면, 다이얼로그등과 같은 다른 액티버티가 앞에 떠서 사용자 Interaction을 수행하는 상태이다 그러나 기존의 Activity는 그대로 뒤에 떠 있지만, 뒤에 떠 있는 activity 는 사용자 Interaction을 받지 못하는 상태이다.
 




이 때 사용중인 쓰레드 정리, 데이타 저장등을 수행하거나 또는 포커스를 잃은 화면이기 때문에 애니메이션등을 정지해야 한다.
  - 이때 다시 해당 Activity로 돌아가게 되면 OnResume으로 다시 돌아가서, 화면을 다시 호출하게 된다.
  - 화면이 보이지 않는 상태에서 만약에 메모리가 부족하게 되면 안드로이드 시스템에 의해서 이 단계에서 자동으로 정지(Killed) 될 수 있다. Killed 된 상태에서 다시 그 화면으로 돌아가게 되면 다시 onCreate로 돌아가서 앱을 처음부터 다시 시작하게 된다.

  • OnStop : 액티버티가 더이상 사용자에게 보여지지 않을때 호출된다. 
  • OnDestroy : 액티버티가 소멸될때 호출된다. 

Activities in the system are managed as an activity stack. When a new activity is started, it is placed on the top of the stack and becomes the running activity -- the previous activity always remains below it in the stack, and will not come to the foreground again until the new activity exits.

An activity has essentially four states:

  • If an activity in the foreground of the screen (at the top of the stack), it is active or running.
  • If an activity has lost focus but is still visible (that is, a new non-full-sized or transparent activity has focus on top of your activity), it ispaused. A paused activity is completely alive (it maintains all state and member information and remains attached to the window manager), but can be killed by the system in extreme low memory situations.
  • If an activity is completely obscured by another activity, it is stopped. It still retains all state and member information, however, it is no longer visible to the user so its window is hidden and it will often be killed by the system when memory is needed elsewhere.
  • If an activity is paused or stopped, the system can drop the activity from memory by either asking it to finish, or simply killing its process. When it is displayed again to the user, it must be completely restarted and restored to its previous state.

각 단계별로 실제로 돌아가는 걸 볼래면, 한번 만들어보고 이벤트 걸고 추적해봐야 겠다.





안드로이드 프로그래밍 구조의 기본


조대협 (http://bcho.tistory.com)


해당 포스팅은 https://class.coursera.org/androidpart1-005/lecture/13 의 내용을 바탕으로 정리하였습니다


안드로이드 애플리케이션의 주요 구성 요소

안드로이드 애플리케이션을 이루는 주요 구성 요소는 크게 다음 4가지이다.


  • Activity : 가장 메인이 되는 컴포넌트로, 모바일 앱의 특성상, 모바일앱은 하나의 UI가 떠서 사용자로 부터 입력을 받고, 출력을 담당한다. 즉 하나의 화면 인터페이스에 해당한다고 보면된다.
  • Service : 백그라운드에서 도는 컴포넌트로 UI가 없이 동작한다. 가장 쉬운 예로 음악 플레이 처럼 화면이 없는 상태에서 백그라운드로 도는 케이스가 가장 대표적인 예이다.
  • BroadCastReceiver : 이벤트를 처리하는 컴포넌트로, 안드로이드의 Intent를 받아서 처리한다. 이 Intent는 Pub/Sub형태로 바인딩되며, 특정 intent가 발생하면, 이를 subscribe하는 BroadCastReceiver가 이를 받아서 처린한다.
  • ContentsProvider : ContentsProvider는 일종의 Database를 추상화 해놓은 개념으로 , 단순히 데이타를 저장하는 것 뿐만 아니라, 저장된 데이타를 다른 앱간에 공유하는 기능도 지원한다.
안드로이드 빌드 & 배포 프로세스
안드로이드 프로젝트는 어떻게 빌드 및 패키징 되서, 단말에서 동작할까.
안드로이드 프로젝트를 빌드하게 되면 코드 컴파일 작업이 수행되고, 컴파일 후에는 컴파일된 코드 이외에 리소스를 포함하여 *.apk라는 파일로 패키징이 된다.
  • 패키징된 파일안에는 코드를 컴파일한 내용인 *.dex 파일이 생성되고
  • 컴파일된 리소스 파일이 *.arsc라는 파일로 저장된다.
  • 그리고 컴파일 되지 않은 리소스 (무엇일까?)가 같이 저장되고
  • 안드로이드 애플리케이션의 설정을 지정하는 AndoridManifest.xml 파일이 저장된다.

출처 : http://developer.android.com/tools/building/index.html


다음으로, 이  apk 파일을 싸이닝을 한다. 싸이닝(Signing)이란, 키를 이용하여 이 APK에 서명을 하는 작업으로, 이 APK 가 변조되었을 경우 서명 값이 틀려지게 되는데, 싸이닝은 apk가 타인에 의해서 위변조 되는 것을 방지하고, 내가 이 애플리케이션을 만들었다는 표시를 하는 과정이다. (나중에 애플리케이션이 업데이트가 되면 같은 키로 싸이닝을 해서, 내가 만든 애플리케이션임을 증명한다.)




안드로이드 플랫폼 기본 아키텍쳐


조대협 (http://bcho.tistory.com)


안드로이드 플랫폼의 기반 아키텍쳐를 살펴보면 다음 그림과 같다.




리눅스 커널
일단 가장 아랫단에, Linux 커널 이 올라가 있다. 일반적인 Linux 커널과 크게 다르지는 않지만, 모바일 디바이스에 최적화된 전력 관리 기능이나 안드로이드에 최적화된 Binder IPC (프로세스간 커뮤니케이션) 부분등이 포함되어 있다.

시스템 라이브러리
리눅스 커널위에는 C로 구현된 몇가지 네이티브 라이이브러리들이 올라가 있다. 3차원 그래픽을 위한, OPEN GL, 로컬 데이타 베이스를 제공하는 SQLLite 데이타 베이스, 웹 브라우징을 위한 WebKit, 멀티미디어 재생을 위한 Media Framework들이 올라가 있다. 
이러한 시스템 라이브러리들은 내부적으로 JNI 인터페이스를 통해서 자바 코드로부터 호출되게 된다. 

안드로이드 런타임
이러한 시스템 라이브러리 위에, 안드로이드 런타임이 올라가 있는데, 안드로이드 런타임은 JVM (Java Virtual Machine)이다. 단, 모바일 애플리케이션을 위해서 최적화된 JVM으로 안드로이드는 달빅(Dalvik)이라는 이름의 VM이 올라간다. 이 달빅 JVM이 실제로 안드로이드 애플리케이션을 시작하게 되낟.   그리고, 그위에 코어 자바라이브러리들이 올라가게 된다. (java.*, javax.* ,org.* ...등)


애플리케이션 프레임웍
안드로이드 런타임 까지 기본 JVM과 자바 라이브러리가 올라갔다면 애플리케이션 개발 프레임웍은 라이브러리이다. 마치 서버 개발에서 자바 위에, JEE 나 스프링,Hibernate와 같은 프레임웍이 있는 것 같이 애플리케이션 개발용 프레임웍이 올라가 있다. 
  • Package manager : 어떤 애플리케이션들이 설치되어 있는지를 관리한다. 
  • Windows manager : 윈도우 화면을 관리 (윈도우란, 영역으로 맨 윗부분의 네비게이션바, 다이얼로그 형식으로 나오는 윈도우등등 모든 윈도우 시스템을 관리하는 부분이다.)
  • View manager : 기본적인 그래픽 컴포넌트를 관리 한다. 라디오 버튼이나, 탭, 버튼등. 
  • Resource manager  : 컴파일이 되지 않는 리소스를 관리한다. 예를 들어 폰 애플리케이션에 같이 패키징된 string, 이미지 파일등을 관리한다. (안드로이드 프로젝트상 main/res 내에 있는 것을 관리하는 듯)
  • Activity manager : 안드로이드의 액티버티를 관리한다. 이 액티버티는 안드로이드 애플리케이션내의 하나의 화면에 해당(?)하는 것으로, 이 액티버터의 생성 및 소멸까지의 라이프 싸이클을 관리한다.
  • Contents provider: 데이타 저장소에 대한 추상화된 계층으로, 이 Contents Provider 계층을 통하여, 데이타를 저장할 수 있고, 이 저장소를 다른 애플리케이션에게 공유하여 애플리케이션 간에 데이타를 공유할 수 도 있다.  
  • Location manager : 위치 관련 서비스 기능을 제공한다.  
  • Notification manager : notification bar에 중요한 이벤트를 보여주는 기능을 제공한다. (푸쉬 시스템도 여기서 관리 하나?)


기본 애플리케이션

그위에, 기본적으로 폰에 프리로드 되어 설치되는 애플리케이션들이 존재한다. 연락처, 메신져, 브라우져, 카메라등의 기본적인 애플리케이션 등이 이에 해당한다. 


안드로이드 프로그래밍 시작하기 (둘러보기)


조대협 (http://bcho.tistory.com)


요즘 서버쪽 기술은 다소 시들해진것도 같고, 하는 일이 모바일 서비스인지라 (얼마전에도 그랬지만..) 알아야할것 같아서, 안드로이드 프로그래밍을 보기 시작했다. 
아무래도 서버 개발자 출신이다 보니 안드로이드 개발이라는 생소한 분야를 이 나이에 공부하면서 정리하는 내용이니 혹여나 내용이 허접하더라도.. 이해해주시기를.. (초보자 시각에서 정리한 내용입니다. )

구글에서 안드로이드 IDE인 안드로이드 스튜디오를 다운받아서 설치하면, 통합 개발 환경을 사용할 수 있다. 이 IDE는 보니, JetBrain의 IntelliJ를 기반으로 만들어진 환경이다. (구글이 이런 투자는 잘한듯..)
그러다 보니 이클립스를 쓰면서 불편했던 문제는 없이 제법 꽤 완성도가 높은 개발도구로 개발이 가능할 듯 하다.

스튜디오를 다운받아서 실행을 하고, 새로운 프로젝트를 만들면, 특이 한것이 다음과 같이, “Minimum SDK” 버전이라는 것을 고르는게 나온다. 즉, 어느 SDK버전으로 개발을 할것인지를 선택하는 것인데, 




이는 앱의 호환성과 기능 문제와 결부 된다. 너무 낮은 SDK버전으로 개발을 하게 되면 여러 앱을 커버할 수 있는 호환성은 높겠으나, 고급 기능을 사용할 수 없고, 높은 SDK 버전을 사용하게 되면 낮은 단말을 지원할 수 없어서 많은 사용자를 놓칠 수 있다.

위자드 상에서 가이드를 보면, 안드로이드 4.0.3 아이스크림 샌드위치 버전을 권장하는데, 이 버전을 사용하면 현존 단말이 90.4% 가 커버가 가능하다고 나온다.




약 10%가 커버가 안된다는 것인데, 말이 10%이지 100만 배포의 경우에는 10만대, 1000만 배포의 경우에는 100만대의 시장을 잃어버린다는 것이지 어마어마한 규모이다. (근대 2.X 대에서 요즘 앱이 잘돌아가나? 가벼운 앱이면 몰라도, 어느정도 기능이 있으면 어렵지 않을까??)

안드로이드 스튜디오에서 프로젝트를 생성하면 다음과 같은 파일 구조가 나온다. 몇몇 중요한 점만 짚고 넘어가보자




자바 소스 코드들은 main/java 디렉토리에 들어가 있고,
기타 리소스 파일들은 main/res 디렉토리에 들어가 있다. 일반적인 자바 프로젝트와 크게 다르지 않다
빌드 스크립트가 특이하게 gradle로 생성이된다. 예전에는 무엇으로 생성이 되었는지 모르겠지만, maven에 익숙한 나로써는 다소 챌린지이다.
그리고, proguard 관련 설정 파일이 생성되는 것으로 봐서, 기본으로 사용이 가능한거 같은데, proguard는 난독화 (소스 코드를 읽기 어렵게 만들어서 디컴파일시에 소스코드를 봐도 이해 하기가 어렵게 만드는 도구) 기능이 통합 되어 있는 듯하다.

몇몇 눈에 띄는 중요한 디렉토리와 파일들이 있어서 정리를 해보면 다음과 같다.

layout 파일 
모바일 프로그래밍은 GUI가 있는 만큼 역시나 안드로이드도 레이아웃 시스템을 사용한다. 예전 Java Swing이나, 현대 웹 프로그래밍의 BootStrap UI 프로임웍과 크게 다르지 않다. IDE에서 에디팅을 통해서 작성할 수 있기 때문에, 일반 컴포넌트를 사용하는 GUI 배치 구성은 layout이라는 파일에 작성하여 지정을 한다.

string.xml 파일
라벨등에 사용되는 기타 문자열들을 string.xml이라는 파일에 몰아서 저장해놓고 불러 쓸 수 있는데, 이는 Localization(지역화)를 위해서 디자인 되었다. 하나의 파일에 모든 텍스트를 지정해놓고 사용하고 다른 언어를 바꾸게 되면 이 string.xml 파일만 교체하면 되기 때문에, 지역화에 대응하기 좋은 구조이다.




그런데, 하나의 앱으로 여러 언어를 표현할 수 있는 Globalization(세계화)지원은 어떻게 하지?