하이브리드 앱 강좌 #3. 웹브라우저형 앱

하이브리드 앱의 유형

하이브리드 앱에 대한 개념이 모호한 이유 중 하나는 하이브리드 앱이라 부를 수 있는 앱의 종류가 워낙 다양하기 때문입니다.
웹 앱을 웹뷰를 통해 네이티브 앱 속에 포장한다는 개념은 공통적이지만, 포장할 웹 앱을 가져오는 방법에 따라서 하이브리드 앱을 크게 3가지로 나누어 볼 수 있습니다.
(아래에 쓰인 용어들는 편의상 제가 자의적으로 붙인 이름입니다.)

  1. 웹브라우저형 앱
  2. 전자책형 앱
  3. 하이브리드형 앱

웹브라우저형 앱

웹브라우저형 앱은 웹브라우저처럼 매번 특정 URL에 접속해서 웹 앱(웹페이지)을 불러옵니다.
웹 앱은 자신이 개발한 웹사이트를 참조할 수도 있고, 타 웹사이트를 참조할 수도 있습니다.
네이버 모바일 페이지를 참조하는 네이버 앱이 전자에 해당합니다.

네이버 모바일

DC인사이드를 참조하는 여러 브라우저 앱들은 후자에 해당합니다(예: 하이브리드 DC).

하이브리드 DC

이런 종류의 앱은 오프라인 상태에서는 아무런 기능을 할 수 없습니다.

오프라인

심지어 제대로 코딩되지 않은 경우에는 모바일 앱이 접속하려는 웹 앱의 URL이 그대로 노출되기도 합니다.

URL 노출

애플 앱스토어 검수에서는 웹브라우저형 앱이 단지 모바일 페이지를 불러오는 것에 그칠 경우, 앱으로 만들지 말고 웹사이트의 북마크를 추가하라고 합니다.
따라서 이런 앱은 네이티브 앱에서만 사용 가능한 기능들을(푸쉬 알림, 카메라 등) 추가해야 합니다.

전자책형 앱

전자책형 앱은 앱 설치 파일 내부에 이미 htm 파일들이 포함되어 있습니다.
웹뷰를 통해 로컬 htm 파일을 불러오므로 웹 서버에 접속할 필요가 없습니다.
따라서 서버의 트래픽을 소모하지 않고, 오프라인 상태에서도 작동하고, 각 페이지를 로딩하는 시간이 짧다는 장점이 있습니다.
컨텐츠 제공을 주된 목적으로 하는 몇몇 앱들이 해당됩니다(예: jQuery Reference)

jQuery Reference

단점으로는 하나의 페이지를 업데이트 하더라도 어플을 새로이 마켓에 업로드해야 한다는 점입니다.
안드로이드 앱의 경우에는 수 시간 안에 업데이트되지만, iOS 앱의 경우에는 또 다시 검수 과정을 거쳐야 하므로 하이브리드 앱의 장점 중 하나를 잃게 됩니다.

애플 앱스토어 검수에서는 이런 앱이 단지 정해진 컨텐츠를 불러오는 것에 그칠 경우, 앱으로 만들지 말고 전자책을 만들라고 합니다.
따라서 이런 앱은 사용자와 상호작용하는 기능을 추가해야 합니다.
인터넷을 사용하지 않는 순수한 전자책형 앱은 찾기 어렵고, 대개는 상호작용 기능을 추가하기 위해 웹뷰를 통해 게시판을 보여주는 방식으로 제작됩니다.

하이브리드형 앱

하이브리드형 앱은 웹브라우저형 앱과 전자책형 앱의 장점을 결합하고 단점은 보완한 것입니다.
일부 기능들은 전자책형 앱으로 구현하고 나머지 기능들은 웹브라우저형 앱으로 구현하는 경우가 대표적입니다.
공통 자바스크립트, 이미지 파일 등은 앱 내부에서 불러오고, 나머지 컨텐츠는 웹을 통해서 가져오기도 합니다.
웹 앱에 처음 접속할 때에는 인터넷을 사용하지만, 다음부터는 캐시된 페이지를 보여주기도 합니다.

개요

이번 강좌에서는 하이브리드 앱의 유형 중 가장 단순한 웹브라우저형 앱을 제작하겠습니다.
앱의 이름은 `DC 도우미`이며, 이름대로 타 웹사이트(DC인사이드)를 참조하므로 따로 HTML, javascript 코딩은 하지 않습니다.
안드로이드 웹뷰 활용 강좌로 보아도 됩니다.

준비

앞 강좌에서 다룬 대로 이클립스에서 새 프로젝트를 생성합니다.
LinearLayout, WebView, Button을 배치하고 텍스트를 지정합니다.

레이아웃 배치

버튼 클릭 이벤트를 처리하기 위해 WebView와 Button에 id를 부여합니다.

id지정

기본 작업을 위한 소스는 다음과 같습니다.

public class MainActivity extends Activity {

	WebView webViewMain;
	Button buttonTitle, button1, button2, button3;
	OnClickListener cListener;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		webViewMain = (WebView) findViewById(R.id.webViewMain);
		buttonTitle = (Button) findViewById(R.id.buttonTitle);
		button1 = (Button) findViewById(R.id.button1);
		button2 = (Button) findViewById(R.id.button2);
		button3 = (Button) findViewById(R.id.button3);
		cListener = new OnClickListener() {

			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				switch (v.getId()) {
				case R.id.buttonTitle:
					buttonTitleClick();
					break;
				case R.id.button1:
					button1Click();
					break;
				case R.id.button2:
					button2Click();
					break;
				case R.id.button3:
					button3Click();
					break;
				default:
					break;
				}
			}
		};
		buttonTitle.setOnClickListener(cListener);
		button1.setOnClickListener(cListener);
		button2.setOnClickListener(cListener);
		button3.setOnClickListener(cListener);
		
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) { ... }

	void buttonTitleClick() { }

	void button1Click() { }

	void button2Click() { }

	void button3Click() { }
}

인터넷 퍼미션 추가

웹에 접속해야 하므로 `인터넷` 퍼미션을 사용해야 합니다.
이를 추가하기 위해서는 Project Explorer에서 AndroidManifest.xml을 더블클릭합니다.

퍼미션

Manifest 창 아래에서 Permission 탭을 선택합니다.

퍼미션

Add 버튼을 클릭합니다.

퍼미션

이 중에서 Uses Permission을 선택하고 OK를 클릭합니다.

퍼미션

Name 옆의 셀렉트 박스에서 android.permission.INTERNET 을 선택합니다.
Ctrl+S를 눌러 저장하면 반영된 것을 볼 수 있습니다.

이제 이 어플을 설치하려 하면 `네트워크 통신 – 완전한 네트워크 액세스`라는 권한이 필요하다는 메시지를 볼 수 있게 됩니다.
이러한 단계를 거치지 않은 경우, 인터넷이 되는 상황에서도 웹뷰에서는 웹페이지를 표시할 수 없다는 메시지만 나옵니다.

URL 불러오기

지금 상태에서 어플을 실행시킬 경우 웹뷰에서는 빈 화면만 볼 수 있습니다.
어플 실행이 마친 뒤에는 웹뷰에 홈페이지를 불러와야 합니다.
onCreate 메서드에 다음과 같은 코드를 추가하기만 하면 됩니다.

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		...
		
		webViewMain.loadUrl("http://http://m.dcinside.com/list.php?id=programming");
	}

WebSettings

첫실행

이제 어플을 실행하여 작동해 보면 몇 가지 이상한 점들이 눈에 뜨입니다.
우선 자바스크립트가 작동하지 않습니다.
또한 페이지 줌인/줌아웃이 작동하지 않습니다.
이러한 문제점들을 해결하기 위해서는 WebSettings를 이용해야 합니다.

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		...
		
		WebSettings webSettings = webViewMain.getSettings();
		webSettings.setJavaScriptEnabled(true);
		webSettings.setBuiltInZoomControls(true);
		webViewMain.loadUrl("http://http://m.dcinside.com/list.php?id=programming");
	}

setJavaScriptEnabled 메서드를 통해 javascript 실행을 허용할 수 있습니다.
setBuiltInZoomControls 메서드를 통해 화면 확대/축소를 허용할 수 있습니다.
그 이외에도 확대 축소를 할 때에 웹뷰 오른쪽 아래에 나타나는 돋보기 아이콘을 없애기 위해서는 setDisplayZoomControls(false)를 추가하면 됩니다.

WebViewClient 지정

현재 상태의 또 다른 문제점 중 하나는 링크를 클릭하면 웹뷰 안에서 열리지 않고 `작업을 수행할 때 사용하는 애플리케이션`을 선택하라고 하는 것입니다.

링크클릭

이러한 문제점은 WebView에 WebViewClient가 지정되어 있지 않기 때문입니다.
WebViewClient는 페이지 로딩 시작과 끝, 키입력 등의 여러 이벤트를 처리할 수 있도록 도와줍니다.
따라서 다음과 같은 코드를 추가하여 WebViewClient를 지정하면 WebView가 링크 클릭을 처리할 수 있게 됩니다.

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		...
		
		WebSettings webSettings = webViewMain.getSettings();
		webSettings.setJavaScriptEnabled(true);
		webSettings.setBuiltInZoomControls(true);
		
		webViewMain.setWebViewClient(new WebViewClient() {
			
		});
		
		webViewMain.loadUrl("http://m.dcinside.com/list.php?id=programming");
	}

WebChromeClient 지정

또 다른 문제점은 My 갤러리 메뉴를 클릭하면 `로그인 후 이용가능합니다. 로그인하시겠습니까?` 라는 대화상자가 표시되어야 하지만 아무런 반응이 없다는 것입니다.
이러한 문제점은 WebView에 WebChromeClient가 지정되어 있지 않기 때문입니다.
WebChromeClient는 dialog, favicon, title, progress를 다루도록 도와줍니다.
따라서 다음과 같은 코드를 추가하여 WebChromeClient를 지정하면 WebView가 대화상자를 출력할 수 있게 됩니다.

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		...
		
		WebSettings webSettings = webViewMain.getSettings();
		webSettings.setJavaScriptEnabled(true);
		webSettings.setBuiltInZoomControls(true);
		
		webViewMain.setWebViewClient(new WebViewClient() {
			
		});
		
		webViewMain.setWebChromeClient(new WebChromeClient() {
			
		});
		
		webViewMain.loadUrl("http://m.dcinside.com/list.php?id=programming");
	}

Back 버튼 눌러서 뒤로가기

또 다른 문제점은 Back 버튼을 누르면 곧바로 어플이 종료된다는 점입니다.
onBackPressed 메서드를 재정의하여 webViewMain에서 뒤로가기가 가능할 경우 어플을 종료하지 않고 웹페이지 뒤로가기 기능을 실행하도록 하겠습니다.
메서드를 재정의할 때에는 직접 타이핑하는 것이 아니라 적절한 위치에 커서를 위치하고 Alt+Shift+S를 누르면 다음과 같은 컨텍스트 메뉴가 나옵니다.

자동완성 메뉴

여기에서 Override/Implement Methods 메뉴를 선택하면 다음과 같은 창이 나옵니다.

메서드 재정의 또는 구현

목록에서 onBackPressed를 선택하고 OK 버튼을 클릭하면 커서가 있던 위치에 다음과 같은 소스가 자동삽입됩니다.

public class MainActivity extends Activity {
	...

	@Override
	public void onBackPressed() {
		// TODO Auto-generated method stub
		super.onBackPressed();
	}

	...
}

앞에서 말한 기능을 구현하기 위해서는 onBackPressed 메서드 안에 다음과 같이 코드를 수정/추가하면 됩니다.

public class MainActivity extends Activity {
	...

	@Override
	public void onBackPressed() {
		// TODO Auto-generated method stub
		//super.onBackPressed();
		if (webViewMain.canGoBack()) {
			webViewMain.goBack();
		} else {
			finish();
		}
	}

	...
}

액션바 숨기기

또 다른 불편한 점은 불필요한 액션바가 큰 공간을 차지하고 있다는 것입니다.
이것은 어플의 테마를 수정하여 해결할 수 있습니다.
AndroidManifest.xml 파일을 연 뒤, Application 탭을 선택합니다.

테마 설정

Theme 옆의 Browse 버튼을 클릭합니다.

테마 설정

Resource Chooser 창에서 System Resources를 선택하고, 텍스트칸에 theme.light.not 정도를 입력하면 `Theme.Light.NoTitleBar`가 보입니다.
선택하고 OK 버튼을 누른 뒤 Ctrl+S를 눌러 저장하고 실행하면 다음과 같은 결과를 얻을 수 있습니다.

테마 설정

로딩 관련 처리

링크를 클릭하여 웹뷰에서 로딩이 시작되면 상단 버튼의 텍스트를 `로딩 중…`으로 바꾸고, 로딩이 완료되면 해당 웹페이지의 타이틀을 상단 버튼의 텍스트로 지정합니다.
이를 위해서는 앞서 웹뷰에 지정한 WebViewClient의 소스를 수정해야 합니다.
onCreate 메서드에서 다음과 같이 되어 있었던 원래의 소스에서,

		webViewMain.setWebViewClient(new WebViewClient() {

		});

자동완성 기능을 이용하여 onPageStarted, onPageFinished 메서드를 추가해서 다음과 같이 수정합니다.

		webViewMain.setWebViewClient(new WebViewClient() {

			@Override
			public void onPageStarted(WebView view, String url, Bitmap favicon) {
				// TODO Auto-generated method stub
				super.onPageStarted(view, url, favicon);
			}
			
			@Override
			public void onPageFinished(WebView view, String url) {
				// TODO Auto-generated method stub
				super.onPageFinished(view, url);
			}
			
		});

onPageStarted 메서드에서는 상단 버튼의 텍스트를 `로딩 중…`으로 변경합니다.

			@Override
			public void onPageStarted(WebView view, String url, Bitmap favicon) {
				// TODO Auto-generated method stub
				super.onPageStarted(view, url, favicon);
				buttonTitle.setText("로딩 중...");
			}

onPageFinished 메서드에서는 상단 버튼의 텍스트를 웹뷰에 로딩된 페이지의 타이틀로 변경합니다.

			@Override
			public void onPageFinished(WebView view, String url) {
				// TODO Auto-generated method stub
				super.onPageFinished(view, url);
				buttonTitle.setText(webViewMain.getTitle());
			}

버튼 클릭 이벤트 처리

글쓰기 버튼은 button2이고, 앞에서 이를 클릭하면 button2Click 메서드가 실행되도록 코딩했습니다.
이 버튼을 클릭하면 글쓰기 링크로 연결되도록 합니다.
방법은 onCreate에서 홈페이지를 불러올 때와 동일합니다.

	void button2Click() {
		webViewMain.loadUrl("http://m.dcinside.com/write.php?id=programming&mode=write");
	}

새로고침 버튼은 button3이고, 앞에서 이를 클릭하면 button3Click 메서드가 실행되도록 코딩했습니다.
이 버튼을 클릭하면 자바스크립트 함수를 호출해야 합니다.
loadUrl 메서드를 사용한다는 점은 동일하지만, 앞에 `javascript:`를 붙여야 한다는 점이 다릅니다.

	void button3Click() {
		webViewMain.loadUrl("javascript:location.reload();");
	}

이를 응용하면 메시지 창을 띄우거나, 특정 element의 스타일을 변경하는 작업이 가능합니다.
예를 들어 button1Click 메서드를 다음과 같이 코딩하고 실행한 뒤 button1을 클릭하면 다음과 같이 특정 div를 감출 수 있습니다.

	void button1Click() {
		webViewMain.loadUrl("javascript:document.getElementsByTagName('div')[2].style.display='none';");
	}

실행 후 버튼을 클릭한 결과는 다음과 같습니다.

javascript 실행

이런 방법은 무궁무진하게 활용될 수 있지만, 이를 자세히 다루는 것은 본 강좌의 범위를 벗어납니다.
보다 자세한 내용을 알기 위해서는 HTML5, CSS, jQuery 등에 관한 좋은 강좌들을 참고하세요.

이런 과정으로 작성된 어플은 다음 링크에서 설치할 수 있습니다(구글 플레이 스토어).

DC 도우미

이상으로 웹브라우저형 하이브리드 앱 개발에 대해 예제를 통해 간단히 다루어 보았습니다.
다음 강좌에서는 전자책형 하이브리드 앱 개발 방법을 다루겠습니다.

관련 포스트