[iOS/OSX] 앱 로컬라이징 하기

nib 파일 로컬라이징

nib 파일을 선택하고 인터페이습 빌더에서 언어를 추가하면 해당 언어로 표시할 nib 파일이 복사된다. 이에 따라 버튼 레이블이나 뷰에 올라가는 이미지와 텍스트를 변경하면 기기에서 설정한 언어에 따라 자동으로 해당 언어의 nib 파일이 선택되어 로컬라이징된 UI를 보여주게 된다.

문자열 테이블

예를 들어 경고창이나 검색 결과 문구에 대해서는 문자열 테이블을 사용하여 로컬라이징한다. 문자열테이블은 .strings 라는 확장자를 가지고 있다. 예를 들어 코코아 앱에서 찾기 패널을 만들었다고 한다면 Find.strings 파일을 각 언어별로 만들어 둘 수 있다. 문자열 테이블은 키-값 쌍의 모음이다. 각각의 키와 값은 겹따옴표로 둘러진다.

이를 코드에서 사용하려면 NSBundle 을 사용해야 한다.

NSBundle *main = [NSBundle mainBundle];
NSString *aString = [main localizedStringForKey:@"Key1" value:@"DefaultValue1" table:@"Find"];

이 코드는 Find.strings 파일에서 Key1 의 키를 찾는다. 만약 값이 발견되지 않았다면 디폴트로 DefaultValue1 이라는 문자열이 된다.

문자열 테이블 만드는 법

Xcode에서 빈 파일을 하나 만든다음, 이름을 Localizable.strings 라고 하고 English.lproj 디렉토리에 저장한다. 파일의 내용은 앞서 설명한 바와 같이 키와 값 쌍으로 입력하며, 각각의 쌍은 세미콜론으로 끝나야 한다.

"DELETE" = "Delete";
"SURE_DELETE" = "Do you really want to delete %d people?";
"CANCEL" = "Cancel";

이제 만들어진 문자열 테이블을 Xcode에서 선택하고, 인스펙터 패널에서 다른 언어 버전을 추가한다. 신규로 추가한 언어를 선택한 다음, 내용을 편집한다.

"DELETE" = "삭제";
"SURE_DELETE" = "%d명의 인원을 삭제하겠습니까?";
"CANCEL" = "취소";

알파벳이 아닌 문자가 있는 경우에는 유니코드로 변환할 것인지 묻는 경우가 있는데 변환을 해야 한다.

이렇게 저장된 문자열은 다음과 같이 사용한다. (문자열 테이블이 하나 밖에 없는 경우) 문자열 테이블이 더 있는 경우에는 테이블 이름도 명시해야 한다.

NSString *deleteString;
deleteString = [[NSBundle mainBundle] localizedStringForKey:@"DELETE" value:@"Delete?" table:nil];

이렇게 쓰는게 상당히 번거롭기 때문에 매크로로 처리할 수도 있다.

#define NSLocalizedString(key, comment) [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]

이 때, comment 부분은 그냥 무시된다. 하지만 genstrings 툴을 사용할 때, 코멘트를 보고 활용할 수 있다.

ibtool 을 사용하여 nib 생성하기

ibtool 명령은 터미널을 통해 다국어 적용 문자열을 덤프할 수 있는 명령이다. 아래 예는 MyDocument.nib 파일로부터 Doc.strings 파일을 생성해 내는 명령이다.

$ cd SomeProject/English.lproj
$ ibtool --generate-stringsfile Doc.strings MyDocument.nib

이렇게 생성된 string 파일에서 만약 스페인어 버전을 만들겠다면, 스트링 파일의 값들을 수정해준다. 그런 다음,

$ mkdir ../Spanish.lproj
$ ibtool --strings-file Doc.strings --write ../Spanish.lproj/MyDocument.nib MyDocument.nib

즉, 현재 디렉터리의 MyDoument.nib 파일을 Doc.strings 파일을 참고하여 일부 레이블을 번역한 버전을 자동으로 생성하는 것이다.

문자열 포맷의 토큰 순서 변경하기

예를 들어 페이지 번호를 표시한다고 할 때,

NSString *theFormat = NSLocalizedString(@"PAGE", "%d of %d ");
x = [NSString stringWithFormat:theFormat, currentPage, totalPage];

와 같이 포맷팅할 수 있다. 하지만 언어별로는 순서가 달라질 수 있는데, 이는 다음과 같이 문자열 테이블 작성시에 토큰의 순서를 달러 표시와 함께 써서 변경할 수 있다.

"PAGE" = "%2$d 중 %1$d";

 

[iOS/OSX] Timer 사용하기

특정한 시간 이후에 작업을 시행하는 타이머를 사용하는 방법에는 몇 가지 방법이 있을 수 있는데, 코코아에서 타이머는 NSTimer 객체를 통해 구현된다.

타이머와 런루프

타이머는 런루프(Run Loop)와 밀접한 관련을 맺고 있다. 타이머는 스스로 동작기한을 가지고 있고, 그 상태로 런루프에 등록된다. 런루프는 그 기한이 지나는 시점에 타이머를 지켜보고 있다가, 타이머가 실행하기로 한 액션을 지정된 객체로 보내게 된다.

런루프는 (역시나 런루프와 스레드를 혼동하는 불편한 진실 때문에 이에 대한 별도의 포스팅도 필요할 듯) 사용자의 입력 등에 적절히 반응하기 위해서 계속해서 “기다리는” 일종의 루프이다. 예를 들면 키보드는 풀링이라는 방식으로 일종의 런루프를 가지고 있다. 즉 사용자가 키보드를 누르지 않는 상황에서 키보드는 ‘최선을, 총력을 다해서’ 사용자 입력을 반복해서 기다린다. 런루프는 이와 유사하게 스레드 내에서 사용자의 UI 입력을 최선을 다해 기다린다.

타이머가 지정한 액션은 런루프에 의해서 확인된다. 타이머를 사용할 때 가장 유의해야 하는 점은 NSTimer는 실시간 타이머가 아니라는 점이다. 만약 반복되는 주기를 가진 타이머의 특정 작업이 있다고 할 때, 런루프는 단지 그 타이머만을 감시하는 것이 아니라 아주 많은 종류의 입력을 처리해야 하기 때문에 이를 놓치는 경우가 있을 수 있다. 따라서 NSTimer를 통해 이뤄지는 지연된 작업이나, 반복작업은 실제 시간과 다소 차이가 있을 수 있으며, 경우에 따라서는 “상당히 큰” 차이가 발생할 수 있다.

타이머 사용하기

스케줄된 타이머 사용하기

타이머를 사용하는 가장 일반적인 방법은 NSTImer의 scheduledTimerWithTimeInterval:targer:selector:userInfo:repeats: 를 사용하는 방법이다. 이는 현재 메인 런루프에 타이머를 추가한다.

-(void)startOneOffTimer {
  [NSTimer scheduledTimerWithTimeInterval:1.0
                                   target:self
                                 selector:@selector(timerFired:)
                                 userInfo:[self userInfo]
                                   repeat:NO];
}
-(void)startRepeatingTimer {
  NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.5
        target:self selector:@selector(timerFired:)
      userInfo:[self userInfo] repeat:YES];
  self.repeatedTimer = timer;
}

타이머의 반복이 없는 경우에는 타이머가 종료된 후 해당 액션을 호출해주고 난 후 타이머는 자체적으로 해제된다. 그렇지 않은 경우에는 추후에 반복되는 타이머를 해제해주기 위해서 따로 참조점을 갖고 있어야 한다.

스케줄링 된 타이머는 생성 즉시 런루프에 추가되어 카운트다운을 시작하게 된다. 이와 별도로 특정 시점부터 동작을 시작하는 타이머는 다음과 같이 만들 수 있다.

-(void)startFireDateTimer {
  NSDate *fireDate = [NSDate dateWithTimeIntervalSinceNow:3600];
  NSTimer *timer = [[NSTimer alloc] 
                        initWithFireDate:fireDate
                                interval:0.5
                                  target:self
                                selector:@selector(timerFiredMethod:) 
                                userInfo:[self uesrInfo] 
                                  repeat:YES];
  NSRunLoop *theRunLoop = [NSRunLoop currentRunLoop];
  [theRunLoop addTimer:timer forMode:NSDefaultRunLoopMode];
}

시작된 타이머를 멈추기

위와 같이 만들어지고 시작된 타이머는 반복되지 않는다면 종료시 호출해야 하는 액션을 호출하고, 정지된다. 하지만 아직 정해진 시간을 다 채우지 않았거나, 반복해서 실행되는 타이머는 멈출 필요도 있다. 타이머를 멈추기 위해서는 해당 타이머에 invalidate 메시지를 보내면 된다.

-(void)stopRepatedTImer
{
  [self.repeatedTimer invalidate];
  self.repeatedTimer = nil;
}

타이머를 사용하지 않은 예약된 작업 호출

타이머를 사용하지 않고도 특정한 시간 지연 후에 간단히 어떤 기능을 실행할 수 있다. 이 기능은 NSObject에 이미 정의된 메소드이다. (역시 코코아의 루트 클래스가 NSObject라는 사실은 매우 다행스럽다!) 이는 -performSelector: withObject : afterDelay: 이다. 이는 다음과 같이 사용할 수 있다.

[self.tableView performSelector:@selector(reloadData)
                     withObject:nil
                     afterDelay:10.0];

이미 예약된 작업 취소하기

NSObject의 클래스 메소드인 +cancelPreviousPerformRequestsWithTarget:selector:object :+cancelPreviousPerformRequestsWithTarget: 을 사용한다. 이는 지연 후에 일을 하게되는 객체가 이 메시지를 받고 예약된 작업을 실행하지 않게 된다.

[스터디] 후위식을 사용하지 않는 공학용 계산기 로직

공학용 계산기 엔진 만들기

예전에 미진하게 만들다가 말았던 계산기 만들기와 관련해서. 지난 번 글은 사실 스탠포드의 iOS 앱 개발 강의 내용을 바탕으로 만들었다. 물론 일부 내용을 좀 수정하기는 했는데. 그 강의에서는 스택에 숫자와 연산자를 넣는 형태로 동작했다. 예를 들면 3 + 5 를 계산하기 위해서는 3, enter, 5, enter, plus 의 순서로 값을 먼저 입력하고 연산자를 뒤에 입력하는 형태의 약간 이상한 방식으로 동작했다. 이는 사실 그 계산기 프로젝트를 계속 발전시켜 공학용 계산기로 만들기 위해서이다.

보통의 공학용 계산기는 ‘후위식’을 이용한 계산 방법을 사용한다. 우리가 흔히 사용하는 1 + 2와 같은 수식의 형태를 연산자가 가운데에 들어간다고 해서 중위식이라고 하는데 이를 12+ 라 쓰고 뒤에서부터 계산하는 방식을 후위식이라 한다. 중위식을 후위식으로 변환할 때 연산의 우선 순위를 고려하게 되므로, 곱하기/나누기가 더하기/빼기와 섞여있는 식이나 괄호가 들어있는 식도 후위식으로 전환하고 나면 뒤쪽에서부터 차근차근 계산해서 한 번에 계산할 수 있는 장점이 있다. 물론 후위식 자체는 사람이 계산하기에는 무척 불편한 형태의 수식 표현이지만 컴퓨터는 스택에서 연산자와 피연산자를 꺼내서 쉽게 처리할 수 있다.

문제는… 이 후위식으로 변환하는 과정이 꽤나 골치아프다는 게 문제. (연산 우선순위를 정의한 기준과 별도의 스택이 추가로 필요) 그래서 생각한게 사람이 계산하는 방식과 마찬가지로 (단순 무식하게) 각 항별로 버퍼에서 우선 계산하고, 다음 항을 계산해야 할 때 버퍼에 있는 값을 결과값에 더해 버리면 되지 않을까? 하는 것이었다.

즉 이를 정리하면 다음과 같다.

  1. 버퍼의 값은 1로 초기화한다. 버퍼에는 숫자값과 현재 항이 음수인지, 양수인지를 구분하는 플래그, 그리고 새로 버퍼에 넣는 값을 곱할 것인지, 나눌 것인지를 결정할 플래그. 이렇게 3개의 값이 필요하다.
  2. 곱하기 연산자가 수식에서 나오면 다음에 나오는 숫자는 버퍼에 들어가 버퍼값과 곱해진다. 버퍼의 계산방식을 곱하기로 변경한다.
  3. 나누기 연산자가 수식에서 나오면 다음에 나오는 숫자는 버퍼에 들어가면서 기존 버퍼값을 나눈다. 버퍼의 계산 방식을 나누기로 변경한다.
  4. 더하기 연산자가 수식에서 나오면 이는 하나의 항에 대한 처리가 끝나고 다음 항이 시작되기 때문에 버퍼에 계산된 값을 꺼내 결과 값에 더해준다. 다음 버퍼는 “더해지는” 항이므로 버퍼의 부호를 양수로 설정해놓고, 값을 초기화한다.
  5. 빼기 연산자가 수식에서 나오면 이 역시 하나의 항에 대한 처리가 끝났다. 더하기와 같이 처리하고 다음 항의 부호는 음수이므로 버퍼의 부호를 음수로 설정한다.
  6. 숫자값이 나올 때는 이 값을 버퍼에 집어 넣는다. 버퍼의 곱하기/나누기 플래그에 따라 이값이 버퍼에 들어갈 때 기존의 버퍼의 값에 곱하거나 나눈다.

이 규칙에 따라 곱하기나 나누기로 연결된 식은 하나의 항으로 취급되어 버퍼에서 계산되고, 더하기나 빼기가 나타날 때 결과 값에 더해진다. 이 과정을 수식의 전체에 대해 진행하면 자동으로 식이 계산된다.

이 방식을 사용하면 괄호처리 역시 어렵지 않게 할 수 있다. 여는 괄호가 나타나면 그 괄호를 닫을 때까지 (따라서 열린 괄호의 수를 카운트하며 추적해야 한다) 수식에 들어있는 식을 별도의 수식으로 만든다. 그런 다음 이 수식을 수식 처리 함수 (즉 그 함수 자신)에게 넘겨 재귀적으로 처리하고 그 결과값을 버퍼에 집어넣으면 된다.

기타 파이, 로그, 제곱, 제곱근 등의 처리도 버퍼에서 별도의 필터를 통해 기능을 추가해 나갈 수 있을 것이다.

계산기엔진 소스코드 : http://www.box.com/s/2neu29qfmblaeoiaj5a8

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

아이폰의 국내 도입이후에 비로소 모바일웹에 대한 폭발적인 관심이 일어났던 것은 이미 모두가 알고 있는 사실이니 긴 말하지 않아도 될 것 같다. 당시 웹 기술은 거의 브라우저에 대한 것들이 많았다. 해묵은 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 파일의 경로를 담을 문자열 프로퍼티를 하나 선언한다.

#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 메소드에서 처리해주어도 상관없다.

@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 연결을 닫는다.
-(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를 사용하는 경우라면 보안을 위해 바인드하는 것을 추천)

-(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의 블락 현상을 방지해줄 필요가 있다.