[잡담] 웹앱은 무엇을 위한 대안인가

아이폰의 국내 도입이후에 비로소 모바일웹에 대한 폭발적인 관심이 일어났던 것은 이미 모두가 알고 있는 사실이니 긴 말하지 않아도 될 것 같다. 당시 웹 기술은 거의 브라우저에 대한 것들이 많았다. 해묵은 IE에 병적으로 집착하는 대다수 사용자들과 이른바 모던 브라우저라고 하는 새로운 브라우저간의 싸움만 지겹도록 반복될 뿐이었다. 이미 HTML4 규격은 10년이 넘어가는 시점이었고 이 때문에 웹표준을 제정하는 W3C에 대해서도 불만들이 많았다.

어쨌거나 이 모바일 웹에서 시작된 붐은 모바일이 되었든, 데스크톱이 되었든 그 플랫폼을 가리지 않고 웹 표준이라든지 최신 브라우저에 대한 관심을 새로운 국면으로 접어들게 하는 큰 한 방이 될 수 있었던 것이다. 웹 표준 준수나 비 IE 환경을 지원하는 것에 대해 인색했던 국내 포털들은 폭발적으로 증가하는 모바일 트래픽에 깜짝 놀라 부랴부랴 모바일 웹에 대한 지원을 서둘렀고 그에 따라 웹킷이나 게코 엔진에서 잘 동작하는 웹 페이지들이 하나 둘 늘어나기 시작했다.

그리고 덕분에 비 IE 사용자들은 데스크톱 버전의 페이지를 사용하기보다는 더 가볍고, 그들이 사용하는 브라우저에 더 잘 맞는 모바일용 웹 페이지를 데스크톱에서도 즐겨쓰게 되는 역전 현상도 많이 일었다. (…라고 썼지만 그리 붐은 일지 않았다.)

어쨌든 그 즈음에 우리나라나 외국이나 할 것 없이, 웹 플랫폼을 사용하여 앱을 만드는 일에 대해 포탈들은 관심을 갖기 시작한다. 그도 그럴 것이 스마트폰 사용자는 계속해서 엄청나게 증가하는데, 이들을 위해 포탈의 서비스를 앱으로 제공할 필요가 있었기 때문이다. 물론 이런 앱을 통한 유입보다는 모바일 웹 브라우저를 통한 유입이 더욱 의미가 있던 포탈에서는 “웹 앱”을 밀고 나서고자 하는 그런 움직임들이 있었고, 당시에 유행처럼 일어나던 모바일 트렌드 컨퍼런스에서는 주로 이런 포탈에서 일하는 분들이 웹 앱의 가능성에 대해 거의 약 파는 수준으로 설파하고 다니기도 했다.

때마침, W3C의 굼뜬 행보를 참다참다 참을 수 없었던 브라우저 제작사들은 HTML5에 대한 논의를 먼저 시작한다. 그러니가 대략의 아웃라인에 대해 브라우저 제작사들이 먼저 기술적으로 구현을 하고 그에 맞춰서 웹 표준을 정하든가 말든가하는 그런 움직임들이 일어나는 것이다. 웹 브라우저에서 충분히 풍부한 미디어나 상호작용을 다룰 수 있도록 하여, 웹 브라우저 자체가 앱이 돌아가는 플랫폼이 되도록 하고, 웹 서비스들은 더 이상 ‘문서’로 취급되지 않고 ‘앱’으로 발전해 나가는 움직임이 시작된 것이다.

이런 움직임은 예상보다 아주 빨리 가시적인 성과를 보이면서 이미 구글 크롬 웹 스토어는 널리 영업중에 있고, 모질라 재단에서도 파이어폭스를 위한 웹 스토어를 준비중에 있는 것으로 알고 있다.

그래서 모바일 웹 앱도 탄력을 받아서 엄청 발전을 했느냐? 글쎄 그 점이 문제라면 문제이다. 사실 HTML5 를 생각하지 않고서라도 (그러니까 웹 페이지에서 동영상을 재생하는 등의 고급 미디어 기능을 빼고) 이미지와 텍스트로 정보를 제공하면서 앱과 비슷한(?) 사용자 경험을 제공하는 일은 기존의 웹 기술로도 어느 정도 가능한 것은 사실이고, 이미 이렇게 상용화한 앱이 한 두 개가 아니다. 웹 앱을 밀고 있던 진영에서는 특히 안드로이드 계열 스마트폰의 폼팩터 단편화 (안드로이드 폰들의 제각각 스펙은 이미 초창기부터 서비스 제공자들에게는 골칫덩이였음에 분명하다) 를 극복하는 방법으로 웹 앱이 딱이라며 웹 앱으로 가야한다고 이야기했다. 이는 일견 맞는 말이기도 하다. 그런데 이들이 말하던 웹 앱의 단점은 “네이티브 앱에 비해 UI의 미려함이 떨어진다”는 것이었는데, 그게 과연 단지 “미려한 UI”에 국한될 문제일까하는 점에 의심을 품지 않을 수 없다.

문제는 상호작용이고 이는 사용자 만족도의 척도이다

모바일 플랫폼의 성격을 대표하는 말 중에 하나는 바로 ‘손안에 XX’라는 것이다. 폰 자체가 손바닥 보다 작기 (*최근 나온 일부 모델 제외) 때문에 손 안에서 모든 정보가 조작되는다는 의미도 있지만, 실제로 손가락이 닿아서 동작하는 앱들이기 때문에 이러한 감성적인 측면에서의 접근은 적지 않은 의미를 가진다고 본다. 즉, 사용자의 터치 제스처에 적절히 반응해야 한다는 점이 모바일 기기에서 돌아가는 서비스 (그것이 앱이든 웹이든)에서는 “사용자 경험에 대한 만족도”의 가장 핵심이 되는 부분이다. 따라서 단지 화려한 화면 전환 효과만이 웹 앱이 제공할 수 없는 ‘소소한’ 부분이라고 치부할 수 없다.

결국 웹 앱은 정확히는 스마트폰을 플랫폼으로 하는 것이 아니라 스마트폰 용 모바일 브라우저를 플랫폼으로 하는데, 그 때문에 치명적인 한계를 가지게 된다. 첫째로 사용자와 직접적으로 접점을 이루는 인터페이스는 모바일 브라우저가 제공하는 사용자 이벤트이다. 즉 마우스 클릭에 해당하는 탭, 그리고 “실질적인 상호작용”을 할 수 없는 쓸기와 벌리기가 모두 인 것이다. 아직까지 멀티터치 제스처를 제대로 지원하는 모바일 브라우저는 없는 실정이고, 그나마 유연하게 확대/축소나 관성 스크롤은 되고 있지만 이건 웹 앱이 지원하는 기능이 아니라 모바일 브라우저의 인터페이스일 뿐인 것이다.

덕분에 모바일 웹 앱들은 사용자의 손가락을 따라 정확하게 반응할 수 없고, 쓸기나 두 손가락으로 벌리기는 사용자가 손가락을 움직이는 동안에는 아무 일도 할 수 없으며, 제스쳐가 끝난 뒤에야 “아 사용자가 방금 손가락으로 쓸기를 했어”, “사용자가 방금 두 손가락을 벌렸어” 정도의 뒤늦은 이벤트만 알아차릴 수 있는 것이다. (이는 태블릿으로 다음 지도를 접속해서 두 손가락으로 확대/축소해보면 쉽게 느낄 수 있다. 그냥 내가 멍청이가 되는 그런 느낌이다.)

두 번째로 하드웨어에서의 부하가 너무 크다. 같은 기능, 같은 효과를 만들기 위해 이미 스마트폰은 웹 브라우저를 띄우고 있는 상태에서 시작해야 한다. 그리고 사용자의 요청을 웹 브라우저는 부지런히 웹 앱에 전달해야 하고, 웹 앱은 자바스크립트로 이에 반응해야 한다. 따라서 너무 무겁다. 똑같은 기능을 제공하는 웹 앱과 네이티브 앱이 있을 때, 이 버벅임은 품질에 너무나 큰 차이를 가져다 준다.

세 번째로 UI를 그리기 위해 필요한 자원 역시 매 페이지를 불러올 때 가져와야 한다. 페이지를 만드는 데 필요한 정보가 네이티브 앱보다 월등히 많기 때문에 웹 앱의 화면 전환은 필연적으로 느릴 수 밖에 없다. 그리고 사용자가 지루해 하는 틈을 노리는 꼼수를 부릴 수 있는 여지도 그만큼 적다.

네번째로 웹 앱을 지지하는 진영에서 말하는 “다양한 폼팩터에 구애 받지 않는 구현”도 그리 구애를 적게 받는 것은 아니라는 것이다. 물론 서비스 제공자 입장에서는 엄청나게 많은 버전의 똑같은 앱을 만들어 내는 것은 쉽지가 않을 것이다. 하지만 이를 웹앱으로 만드는 것 역시 그리 녹록치 않은 일임에는 틀림없다. 스마트폰의 화면 크기나 해상도는 역시 저마다 각각 다른데, 이를 ‘적절히 맞추어 주는’ 디자인은 가능은 하나 어렵고, 모든 요구를 다 맞추기 힘들기 때문에 어떤 면에서는 포기를 해야 한다. 그런 면에서 ‘품질’은 계속 떨어지기 마련이다.

트위터와 페이스북, 애플 그리고 Path

트위터와 페이스북은 최근 앱 업데이트를 단행하면서 한 가지 치명적인 실수를 저질렀다. 바로 웹 앱으로의 전환이다. 물론 100% 웹 앱은 아니고 네이티브 앱 내에서 웹뷰를 사용하여 화면 UI를 웹을 통해 그려주고 있다. (이는 페이스북 앱에서 담벼락 글을 지우기 위해 손가락으로 쓸어보면 알 수 있다. 통상적인 테이블 뷰와 달리 손가락을 떼고 나서야 버튼이 표시된다.) 덕분에 엄청 반응이 느리고, 사소한 동작이나 사용자의 쓸데 없는 터치 때문에 기껏 공들여(!) 불러온 화면이 하얗게 변하면서 리프레시 된다.

서비스의 UX에 있어서 최고점을 보여주는 애플도 이 측면에서는 그리 잘 해내지 못하고 있다.애플의 앱스토어 앱도 웹 뷰로 구현되어 있다. 덕분에, 역시나 엄청나게 느리다. 심지어 목록에서 앱 상세 정보로 들어간 다음 다시 밖으로 나오면 처음부터 다시 스크롤하고 목록을 추가적으로 불러와야 한다. 엄청나게 짜증나는 일이 아닐 수 없다. 비슷비슷한 앱이 많은 앱 스토어에서 쓸만한 앱을 찾기 위해 상세 화면을 들락여야 하는 상황이라면 상당한 인내심을 가져야 한다. (아 진짜 제발 이거 좀 개선해라 ㅠㅠ 앱 깔자고 맥 켜야 하는 상황이 유쾌하지는 않잖아.)

물론 앞으로 스마트폰의 메모리 사정이나 프로세서 성능등이 점차 좋아지고, (역시 국내에서는 요원하기만 하다만) 무선 네트워크 상황이 좀 더 좋아진다면 이러한 문제는 현재의 일시적인 것으로 여길 수도 있다. 하지만 Path가 보여준 네이티브 앱만이 제공할 수 있는 UX의 장점들을 볼 때 트위터나 페이스북의 공식 앱은 그저 “겨우 보여주기에 급급하다”라는 느낌을 지우기가 힘들다.

웹 표준화 진영에서 내걸던 “One Web”이라는 구호가 기묘하게 변경되어 모바일 앱에 적용되고 있는 현실이 못내 씁쓸하다. 여기저기서 아직도 “N-Screen”이라는 말을 남발하면서 왜 “같은 화면을 보여주는 것”을 자랑하나. 통합되어야 하는 건 화면에 보이는 디자인이 아니라 총체적인 사용자 경험이어야 한다.

아, 이딴 글이나 적고 앉아있자니 괜시리 아이폰5가 기다려지는 그런 날이다. 날씨도 괜히 꿀꿀하다.

 

iOS에서 SQLite 사용방법

자주 쓰이지는 않지만, 현재로서는 미리 만들어 놓은 데이터를 검색하여 사용하는 가장 단순한(?) 방법이다. (코어데이터는 데이터 셋을 미리 만들어 사용하기가 까다롭다) 대신, 애플은 영구저장소를 활용하는 방법으로 코어데이터를 밀고 있기 때문에 SQL과 관련한 내용을 애플 개발자 문서에서 친절하게 소개하고 있는 자료는 좀 드물다. 대신 SQL의 C인터페이스를 설명하는 글은 인터넷에서 많이 있으므로 적절하게 찾아보면 된다.

SQLite3를 사용하기

SQLite를 사용하기 위해서는 SQLite3 프레임워크를 프로젝트에 포함시켜 줘야 한다. 프레임워크 중에는 sqlite3 이 있고 sqlite3.0 이 있는데 sqlite3을 선택해야 한다.

데이터 베이스를 액세스해서 자료를 읽어오거나 혹은 새로운 값을 쓰는 부분은 “Model”에 해당하므로, 별도의 클래스에서 이 처리를 담당하도록 하는 것이 좋다. 즉 해당 클래스는 데이터베이스 파일에 대한 인터페이스로 기능하면서 MVC에서는 Model을 담당하게 하는 것이다.

예제 – DBInterface.m

DBInterface 클래스는 SQLite3 DB 저장소를 액세스하는 클래스의 템플릿으로, DB 액세스에 대한 내용을 설명하면서 하나 하나 구성해 가는 과정을 살펴보도록 하겠다. 먼저 내부 인터페이스에서는 sqlite3 객체하나와 DB 파일의 경로를 담을 문자열 프로퍼티를 하나 선언한다.

1
2
3
4
5
6
7
8
9
10
11
#import "DBInterface.h"
#import
#define aDB_FILENAME @"database.sqlite"
@interface DBInterface()
{
sqlite3 *myDB;
}
@property (readonly, nonatomic) NSString *dbFilePath
@end

DB 파일의 경로 구하기 (및 DB 파일 복사)

DB파일의 경로를 구하고자 할 때 DB파일의 위치를 구하면 된다. 만약 사전과 같이 DB에 있는 내용을 읽어오기만 하면 된다면 번들에 포함될 DB 파일을 샌드박스의 사용자 문서 폴더로 복사해줄 필요가 없다. 하지만 DB 파일에 내용을 추가로 쓰는 기능을 수행해야 한다면 이 파일을 사용자 문서 폴더로 복사한다.

물론 이 내용은 init 메소드에서 처리해주어도 상관없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@systhesize dbFilePath = _dbFilePath;
-(NSString *)dbFilePath
{
if(!_dbFilePath) {
NSString *databaseSourcePath = [[[NSBundle mainBundel] resourcePath] stringByAppendingPathComponent:aDB_FILENAME];
NSString *docPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:aDB_FILENAME];
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:docPath])
[fileManager copyItemAtPath:databaseSourcePath toPath:docPath error:nil];
_dbFilePath = docPath;
}
return _dbFilePath;
}

참고로 이 예제에서 사용할 테이블의 이름은 WORD 이고 이 테이블은 id, name, description 의 칼럼을 가지고 있다고 가정한다. id는 정수값, 나머지는 모두 텍스트 정보를 담는 테이블이다.

DB에서 검색하기

이 테이블에서 특정한 name을 검색하는 함수를 작성해보자. SELECT 구문을 사용하는 메소드가 될 것이다. 이를 위해서는 다음의 단계를 따른다.

  1. DB를 연다.
  2. 쿼리문을 작성한다.
  3. SQL 포인터를 준비한다.
  4. 검색 결과에 따라 각 ROW에 대해 루프를 돌린다.
  5. 루프 내에서 한 레코드에 대해 각 필드값을 추출해 와 이를 사전 객체에 담고, 반환을 위한 배열에 추가한다.
  6. SQL 포인터를 종료한다. (finalize)
  7. DB 연결을 닫는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-(NSArray *)serachWithKeyword:(NSString *)keyword
{
NSMutableArray *result = [NSMutableArray array];
sqlite3_stmt *stmt;
NSString *queryString = [NSString stringWithFormat:@"SELECT * FROM WORD WHERE NAME LIKE \"%@%%\"",keyword];
const char *dbPath = [self.dbFilePath UTF8String];
if (sqlite3_open(dbPath, &myDB)==SQLITE_OK) {
const char *sql = [queryString UTF8String];
if (sqlite3_prepare_v2(myDB, sql, -1, &stmt, NULL)==SQLITE_OK) {
while(sqlite3_step(stmt)==SQLITE_ROW) {
NSMutableDictionary *anItem = [NSMutableDictionary dictionary];
NSNumber *indexNumber = [NSNumber numberWithInt:sqlite3_column_int(stmt,0)];
NSString *name = [NSString stringWithUTF8String:(const char*)sqlite3_column_text(stmt,1)];
NSString *description = [NSString stringWithUTF8String:(const char *)sqlite3_column_text(stmt,2)];
[anItem setValue:indexNumber forKey:@"indexNumber"];
[anItem setValue:name forKey:@"name"];
[anItem setValue:description forKey:@"description];
[result addObject:anItem];
}
}
sqlite3_finalize(stmt);
}
sqlite_close(myDB)
return (NSArray *)result;
}

비록 코드는 더럽게 길었지만, 몇 가지 sqlite3의 C 인터페이스에 정의된 함수들과 위에서 설명한 절차만 기억한다면 크게 어렵지 않게 구현할 수 있는 내용이라 하겠다.

업데이트 하기

특정 id의 데이터를 업데이트하는 과정은 select와는 조금 다른데, 쿼리 문자열에 데이터값들을 바인드하는 과정이 추가된다. (물론 그냥 쿼리문 자체에 넣어버리는 방법도 있는데, 만약 원격 DB를 사용하는 경우라면 보안을 위해 바인드하는 것을 추천)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-(void)updateRecordWithName:(NSString *)name description:(NSString *)description atIndex:(int)indexNumber
{
sqlite3_stmt *stmt;
NSString *queryString = @"UPDATE WORD SET NAME=?, DESCRIPTION=? WHERE ID=?";
const char *dbPath = [self.dbFilePath UTF8String];
if(sqlite3_open(dbPath,&myDB)==SQLITE_OK) {
const char *sql = [queryString UTF8String];
if(sqlite3_prepare_v2(myDB, sql, -1, &stmt, NULL)==SQLITE_OK) {
sqlite3_bind_text(stmt,1,[name UTF8String],-1,NULL);
sqlite3_bind_text(stmt,2,[description UTF8String],-1,NULL];
sqlite3_bind_int(stmt,3,indexNumber);
if(!sqlite3_step(stmt)==SQLITE_DONE){
NSLog(@"fail to update");
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(myDB);
}

실제 레코드의 업데이트가 일어나는 부분은 sqlite3_step() 함수가 호출되는 시점이고, 이 때 성공 여부를 판별하는 값이 SQLITE_DONE 이라는 점만 감안한다면 충분히 insert / delete 의 경우에 대해서도 메소드를 작성하는 것이 그리 어렵지 않을 것이라 생각된다.

참고로 SQLite는 경량 데이터베이스이며 상당히 속도도 빠른 편이지만 DB 자체의 크기가 매우 커지는 경우 그 속도가 현저하게 떨어질 수 있다. 따라서 DB를 액세스 하는 경우에는 호출해주는 쪽에서  GCD 등을 사용해서 UI의 블락 현상을 방지해줄 필요가 있다.

 

[iOS/OSX] 특정 작업을 병렬로 처리하기

“동시에 진행되는 작업”을 처리하기 위해서는 iOS 및 OSX 환경에서는 크게 두 가지 방법을 (흔히) 사용한다. GCD (dispatch queue)와 Operation Queue가 그것이다. 오퍼레이션 큐는 GCD의 Objective-C 버전이라 할 만큼 비슷한데 (사실 좀 다르기는 다르다) 어쨌거나 이 두 가지 방법은 스레드의 생성과 관리를 시스템이 알아서 처리해주는 레벨로 가지고 내려가기 때문에 실제로 프로그래머가 신경써야 할 부분을 “동시에 진행되는 작업을 처리”하는 부분에만 집중하면 되도록 해준다.

예를 들면 네트워크를 통해 데이터를 로드해야 하는 경우나 그 반대로 네트워크를 통해 데이터를 저장해야 하는 경우에 응답이 느리다면 (이는 디스크 같은 영구 저장소를 액세스할 때도 일어날 수 있다. 아주 미묘한 수준이기는 하나 이런 작업은 앱에 blocking을 가져오고 UI에 대한 반응을 느리게 만든다) 이 작업의 처리를 기다리는 동안 앱은 사용자의 터치에 반응하지 못하고 계속 대기하게 될 것이다. 따라서 사용자 경험의 품질이 매우 나빠질 수 있다. 이런 경우에는 “동시작업 처리”를 하도록 해주는 것이 좋다. 동시작업 처리를 사용하면 멀티 코어 프로세서를 효율적으로 사용할 수 있고, 시스템을 보다 “바삐” 움직이게 할 수 있기 때문이다.

(*여기서 주목해야 할 부분은 “동시작업 처리”를 “멀티 스레드”로 기재하지 않은 것이다. GCD에서 동시작업을 처리하는 것은 “디스패치 큐”를 분리하여 동시에 2개 이상의 작업을 진행시키는 것인데, 놀라운 점은 GCD를 사용한 동시작업은 해당 작업에서 다시 스레드를 생성하지 않는 이상, 모두 메인 스레드에서 돌아간다. 따라서 멀티스레드가 아닌 경우가 있을 수 있다.)

NSOperationQueue를 통한 동시작업

먼저 NSOperationQueue를 사용하는 경우를 살펴보도록 하자.

만약 Block 객체를 사용하는 코딩 문법에 조금 익숙하다면 (특히 이는 애니메이션과 관련한 새로운 메소드에서 자주 등장한다. 우리가 익힌 바 있는 UIDocument 관련 글에서도 본 적이 있을 것이다.) 상당히 쉽게 익숙해질 수 있다. 즉 NSOperation은 코드 블럭과 같이 “일련의 작업을 지시하는 코드”를 객체로 만들어 이를 별도의 큐에서 실행하도록 하는 방식이다. 이 때 스레드의 생성과 관리는 큐가 알아서 하게 되므로 여전히 스레드 관리에 대한 크나큰 부담을 덜 수 있게 되는 것이다.

작업 객체 생성

NSOperation은 우리가 작업해야 하는 코드를 담는 객체인데, 이를 활용하는 방법에는 다음 세 가지가 있다.

  • NSInvocationOperation
  • NSBlockOperation
  • subclassing NSOperation
먼저 NSInvocation은 특정 객체의 메소드를 작업 객체로 만들어버리는 방법이다. 즉, 다른 스레드에서 동시 처리를 해야할 메소드를 가진 객체가 있다면, 그 객체의 메소드를 동시 처리 작업으로 만들 수 있다.
1
NSInvocationOperation *theOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doMyTask:) object:withData];

NSBlockOperation 객체는 코드 블럭을 사용해서 작업 객체를 만들 수 있다.

1
2
3
NSBlockOperation *theBlockOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"Block has started");
}];

1
2
3
4
// 아래와 같이 블럭을 계속 추가해 나갈 수 있음
[theBlockOp addBlock:^{
// do something...
}];

혹은 NSOperation 객체를 새로 생성할 수도 있다. (이에 대한 자세한 내용은 다른 글에서 다뤄볼까 한다.)

작업 객체의 실행

작업 객체는 물론 그대로도 실행이 가능하다. 하지만 특별히 멀티 스레드로 동작하도록 작업 객체를 커스터마이징 하지 않은 경우라면 이런 작업들은 메인 스레드에서 돌아가는 함수와 동일하다. (즉, 동시작업으로 처리되지 않는다.) 별도의 스레드에서 동시 작업으로 처리되도록 하려면 NSOperationQueue 객체를 생성하여, 이 곳에 앞서 말한 방법으로 생성된 작업객체를 추가해주면 된다.

1
2
NSOperationQueue *aQueue = [[NSOperationQueue alloc] init];
[aQueue addOperation:anOp];

작업이 추가되면 큐 객체는 자동으로 스레드를 만들고 먼저 큐에 추가된 순서대로 작업 객체에 start 메시지를 보내어 각각의 작업을 시작하게 된다.

단순한 예제를 만들 때 유의할 점은, 큐가 스레드를 새로 생성할 때는 약간의 시간이 걸리는 데, 그 사이에 메인 스레드가 종료되어 버린다면 큐에 담긴 작업이 아예 처리되지 못하고 프로그램이 종료될 수도 있다.

이런 예제와 같은 경우에는 큐를 처리하는 동안 큐를 생성했던 현재 스레드를 잠깐 멈추게 하여 큐가 처리된 이후에 그 다음 작업을 실행해주는 방법도 있다.

1
[aQueue waitUntilAllOperationsAreFinished];

하지만 이렇게 큐의 작업이 처리되는 것을 기다리는 것은 성능에도 좋지 않은 영향을 미치고 (왜냐면 그만큼 메인 스레드가 블럭킹을 당하고 잠기기 때문에) 되려 동시 작업성을 저해하는 결과를 가져오기 때문에 가능하면 쓰지 말 것을 권한다.

큐에서 작업을 시작할 때는 작업 객체에 start 메시지를 보낸다. 이와 같은 방법으로 NSInvocationOperation 객체나 NSBlockOperation 객체에 start 메시지를 보내 해당 작업을 실행시킬 수 있다. 하지만 메인 스레드에서 명시적으로 이런 작업을 실행하는 것은 그냥 코드 블럭을 실행하는 것과 아무런 차이가 없게된다.

Dispatch Queue 사용하기

Dispatch Queue도 큐에 코드 블럭을 밀어넣어 실행하는 것과 유사하게 디스패치 큐에 작업(코드 블럭)을 넣고 이를 동시에 실행시키는 방법이다. 동시 작업으로 진행될 작업은 메인 스레드에서 함께 돌아간다. 이것이 오퍼레이션 큐와의 가장 큰 차이점이라 하겠다. (실은 항상 메인 스레드에서 돌아가는지는 모르겠다. 동시에 처리되는 작업의 개수도 시스템이 코어의 개수나 시스템에 현재 걸려 있는 부하에 따라 자동으로 판별한다.

즉 동시 작업으로 병렬처리되는 일이 종료되었을 때 어떤 일이 이어서 일어나게 만들고자 할 때 (이때는 델리게이션이나 KVO를 써도 되지만) 이 방법을 사용하는 것도 굉장히 쉽고 간단하다. GCD를 이용해서 병렬작업을 처리하는 가장 간단한 방법은 글로벌 큐를 사용하는 것이다. 물론 글로벌 큐를 사용하지 않고 별도의 큐를 생성하여 작업을 처리할 수도 있다. 단 이렇게 생성되는 큐는 serial 큐로, 추가된 순서대로 작업이 수행된다. 대신 글로벌 큐는 들어간 순서대로 작업이 시작되나, 큐에 들어간 작업은 가능한 많은 수가 동시에 실행되므로 먼저 들어간 작업이 먼저 끝난다고는 특정할 수 없다.

큐에 작업을 추가하여 실행하기 위해서는 dispatch_async 함수를 사용한다. 이 함수에 수행할 작업을 코드 블럭으로 넘겨서 수행하도록 할 수 있다. 이 함수를 호출한 직후 프로그램의 흐름은 다음 라인으로 넘어가고, 디스패치 큐는 이와 동시에 넘겨진 작업을 즉시 처리하게 된다. 다음과 같이 글로벌 큐를 적용한 아주 간단한 코드를 사용해서 병렬 작업을 수행할 수 있다.

1
2
3
4
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
// 병렬적으로 진행할 코드
});

매우 큰 DB에서 값을 검색해 오거나, 인터넷을 통해 데이터를 다운로드 받아서 처리해야 하거나, 영구저장소에 저장된 파일을 액세스 하는 등 시간이 걸릴 수 있는 일을 처리하는 경우에는 메인스레드가 blocking 될 수 있으므로 이렇게 처리해주면 백그라운드에서 돌아가는 것처럼 처리되고 UI 반응은 멈추지 않고 계속 이루어질 수 있다.

만약 저렇게 큐에서 돌아가는 작업이 끝나거나 혹은 그 중간에 UI를 업데이트 하거나 해야 한다면, UI 갱신을 처리하는 부분은 메인 큐이므로 메인 큐에서 필요한 작업을 처리할 수 있다. 즉 메인 큐 ▶ 글로벌 큐에서 동시작업 ▶ 메인큐에서 작업 하는 식으로 중간에 메인 큐에 끼어들 수도 있다.

1
2
3
4
5
6
7
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
// 병렬적으로 진행할 코드
dispatch_async(dispatch_get_main_queue(), ^{
//메인큐에서 UI 업데이트 등을 실행
});
});

참고자료 : Concurrency Programming Guide

 

[iOS] 키보드의 크기 구하기

텍스트 뷰나 텍스트 필드가 화면의 아래쪽에 위치한다면, 이를 탭했을 때 키보드가 올라오면서 내용을 가려버리게 된다. 따라서 키보드가 올라올 때 텍스트 뷰의 크기나 위치를 변경할 필요가 있다.

키보드가 올라오는 부분은 텍스트 필드/ 텍스트 뷰의 경우에는 didBeginEditing 등의 메소드를 사용할 수도 있지만, 보다 확실하게는 키보드가 나타날 때 발송되는 notification 메시지를 받는 것이 일반적이라 하겠다.

먼저 해당 뷰 컨트롤러가 로드될 때, 키보드에 대한 알림을 받도록 한다.

1
2
3
4
5
6
7
8
9
-(void)viewDidLaod
{
  ...
  [[NSNotificationCenter defaultCenter]
addObserver:self  
selector:@selector(keyboardMoved:)
name:UIKeyboardHasShownNotification
object:nil];
}

UIKeyboardHadShownNotification 알림은 키보드가 나타났을 때 보내지게 된다. 이 알림을 수신하면 selector에서 지정한 메소드가 호출된다.

이 메소드드는 알림 객체를 받게 되고, 이 알림 객체 속에는 우리가 필요로 하는 정보들이 들어있다. userInfo 프로퍼티를 사용하여 키보드의 프레임을 구한다.

1
2
3
4
5
6
7
8
-(void)keyboardMoved:(NSNotification *)notification
{
if(notification.name = UIKeyboardHasShownNotification) {
CGRect keyboardFrame = [notification.userInfo valueForKey:UIKeyboardFrameBeginUserInfoKey];
// <# keyboardFrame 으로부터 키보드의 높이를 구해서 뷰의 크기를 조정 #>
....
}
}

이렇게 알림을 받도록 옵저버를 등록한 후에는 반드시 해제를 해줘야 한다.

1
2
3
4
5
6
-(void)viewDidUnload
{
...
[[NSNotificationCenter defaultCenter] removeObserver:self];
...
}