iOS에서 초성으로 연락처 검색하기

Lee young-jun
11 min readAug 25, 2024

--

This post will be posted as English later.

2010년에 Objective-C로 iPhone 개발을 익혔지만 실제 앱스토어에 올라가는 프로젝트에 참여하지는 못했었습니다.

6년 후 iPad 용 Presentation 앱을 만들면서 공식 문서들이 Swift로 되어가는 것 같아서 Swift를 배우기로 결심했습니다.

회사에 iOS 개발자가 더 이상 없던 때라, 2010년에 다른 개발자가 만들었었던 앱을 부활시켰습니다.

원래 이 앱은 Objective-C로 만들어져 있었는데요, 이미 말했듯이 Swift를 사용하기로 결심해서 재개발했습니다.

주요 기능은 전화 수신화면에서 누가 전화했는지 식별하기 위함이었습니다, 특히 동명이인이 있는 경우에 말이죠.

그리고 숨겨진 기능이 하나가 있는데 바로 연락처 앱에서 초성으로 검색을 가능하게 하는 것이었습니다.

초성은 한글 문자의 첫번째 자음입니다. ‘가나다’ 라는 문장이 있으면 초성은 ‘ㄱㄴㄷ’ 가 됩니다.

사람들은 또한 ‘애플’을 검색할 때 ‘애프’로 검색하고 싶어할 수도 있습니다.

사실 애플의 기본 연락처 앱은 아직 이런 기능을 제공하지 않고 있습니다.

그래서 편법을 사용했습니다.

한글 완성형

한글은 초성(자음), 중성(모음), 종성(받침)이 하나로 결합되어 글자 하나를 만들 수 있습니다. 위에서 말한 것 처럼 검색하려면 어떻게 해야할까요? 우선 애플을 ‘ㅇㅐㅍㅡㄹ'로 쪼갭니다. 그리고 애프도 쪼개면 ‘ㅇㅐㅍㅡ’가 됩니다. 공통 부분이 생겼죠? ㄹ을 제외한 앞부분은 동일하기 때문에 검색이 가능해집니다.

Swift로 한글을 분해하려면 어떻게 해야 할까요? 아래 코드로 유니코드 값을 얻을 수 있습니다.

var letter = "가"
var chars = letter.unicodeScalars
var codes = chars.map{ $0.value }
var hexs = codes.map{ String.init(format: "%x", $0) }

그러나 hexs의 값은 [초성, 중성, 종성]이 아닌 한개의 값인 0xAC00로 나타납니다. 즉, 우리가 직접 3개의 부분으로 쪼개야 한다는 것이죠.

그러기 위해서는 먼저 한글이 어떤 유니코드로 구성되는지 알아야합니다. 완성된 한글 한글자는 아래와 같은 코드 값들을 가집니다.

한글 완성형 유니코드

한글문자의 첫번째인 ‘가’를 예를 들면 AC00로 나타납니다.

ㄱ으로 시작하는 글자는 ‘가’로 시작 ‘가’로 완성될 수 있는 글자는 부터 ‘가’ 부터 ‘갛’까지 될 수 있습니다.

초성 추출하기

그럼 이제 이 글자를 어떻게 분해할 수 있을 까요?

초성 ‘ㄱ’, 중성 ‘ㅏ’의 조합으로 만들어지는 모든 글자는

‘가’, ‘각’ … ‘갛’가 될 수 있습니다. 마찬가지로 중성이 ‘ㅓ’인 경우는 ‘거’ …’ 겋’가 될 것 입니다. 이것을 공식으로 나타내면 아래와 같습니다.

ㄱ의 모든 조합 = ㄱ + 중성 x 종성

어떤 글자의 초성을 알아내려면 먼저 0xAC00(가)로 부터 몇 번째 글자 인지를 알아야합니다. 이를 위해 Character에 확장 메소드를 추가했습니다.

extension Character {
static let koreanBegin = "가".unicodeScalars.first!.value;

func getKoreanIndex() -> UInt32{
var value : UInt32?;

let uniCode = self.scalars.first!.value;

return uniCode - Self.koreanBegin;
}
}

이렇게 알아낸 순서를 (중성 개수 x 종성 개수) 나누면 몇 번째 초성인지 알 수 있습니다.

초성 Index = (글자 - ‘가’) / (중성 개수 x 종성 개수)

public static let koreanJungSeongs = ["ㅏ", ... , "ㅣ"];
public static let koreanJongSeongs = [" ", ... , "ㅎ"];

let korIndex = self.getKoreanIndex()

let choIndex = korIndex / UInt32(Self.koreanJungSeongs.count * Self.koreanJongSeongs.count);

그리고 전체 초성 목록(ㄱ, 0x3131부터)에서 초성 Index번째를 가져옵니다.

return Self.koreanChoSeongs[Int(choIndex)];
한글 자음/모음/받침

중성

초성 검색만 하는 것이 아니라면 중성도 추출해야 합니다. 초성을 제외한 나머지의 Index를 추출하고 다시 종성으로 나누면 중성의 순서를 알 수 있습니다.

중성 = (글자 - ‘가’) % (중성 개수 x 종성 개수) / 종성 개수

public func getKoreanJungSeong() -> String{
let korIndex = self.getKoreanIndex()!;
let jungIndex = (korIndex % UInt32(Self.koreanJungSeongs.count * Self.koreanJongSeongs.count)) / (UInt32(Self.koreanJongSeongs.count));

return Self.koreanJungSeongs[Int(idx_jung)]
}

종성

중성까지 얻었으니 종성도 궁금할 것 입니다. 이미 얻은 중성으로 나눈 나머지를 얻으면 끝입니다.

종성 = (글자 - ‘가’) % 중성 개수

public func getKoreanJongSeong() -> String{
let korIndex = self.getKoreanIndex()!;
let jongIndex = korIndex % UInt32(Self.koreanJungSeongs.count);

return Self.koreanJongSeongs[Int(idx_jong)];
}

이렇게 한 글자를 분해하는 함수도 만들었습니다.

public func getKoreanParts() -> String{
var value : String = "";
let korIndex : UInt32 = self.getKoreanIndex() else{

let choIndex = Int(korIndex / UInt32(Self.koreanJungSeongs.count * Self.koreanJongSeongs.count));
let jungIndex = Int((korIndex % UInt32(Self.koreanJungSeongs.count * Self.koreanJongSeongs.count)) / (UInt32(Self.koreanJongSeongs.count)));
let jongIndex = Int(korIndex % UInt32(Self.koreanJongSeongs.count));

let cho = Self.koreanChoSeongs[choIndex]
let jung = Self.koreanJungSeongs.count > jungIndex ? Self.koreanJungSeongs[Int(jungIndex)] : "";
let jong = Self.koreanJongSeongs.count > jongIndex ? Self.koreanJongSeongs[jongIndex] : "";

value = cho + jung + jong;

return value;
}

쌍자음

초성으로 검색할 때 쌍자음을 입력해야 하면 귀찮을 것 입니다. ‘ㄱ’를 입력했을 때 ‘ㄲ’도 검색 될 것이라고 기대할 수 있죠. 왜냐하면 ‘ㄲ’는 ‘ㄱ’가 두개 결합된 것이기 때문입니다.

이 문제를 해결하기 위해 문자열에서 초성을 추출할 때 모든 쌍자음을 단자음으로 변환했습니다.

if double2One && cho != nil{
let doubleCho = lastCho.getMergeKoreanChoseong(cho!);
if !doubleCho.isEmpty{
//remove cho seong before this cho seong
//value.replaceSubrange((value.index(value.endIndex, offsetBy: -1) ..< value.endIndex), with: "");
value.remove(at: value.index(before: value.endIndex));
cho = doubleCho;
}
}

‘까마귀’를 ‘ㄱㅁㄱ’로 검색할 수 있을 것 입니다. 대신 이 방법은 ‘ㄲㅁㄱ’로 검색할 수 없기 때문에 주의해야 합니다. 아니면 변환하지 않은 버전을 추가로 보관할 수도 있을 것 입니다.

메모 수정하기

이렇게 한글을 분해하고 초성을 추출했는데 어떻게 연락처에서 검색할 수 있다는 거죠? 이름 자체에 추가할 수도 있겠죠. 하지만 우리는 전화 수신 화면에 뜨는 것을 원하지 않았고 대체할 수 있는 필드를 찾았습니다.

여러분은 ‘메모’ 필드로 연락처를 검색할 수 있다는 것을 알고 계시나요? 연락처에 다른 필드도 많지만 사용 가능성이 적다고 판단되는 메모를 선택했습니다.

그러나 이 메모를 아무나 접근할 수 있는 것은 아닙니다. (물론 2016년에 개발할 때는 제약이 없었지만..)

메모에 접근하려면 entitlement가 필요합니다. 발급 받으려면 왜 그 필드를 사용하는지 애플에 소명해야 하죠.

연락처 앱에는 메모 라고 나타나지만 실제 필드 이름은 note 입니다.

아래 가이드를 참고해서 com.apple.developer.contacts.notes entitlement 발급

연락처를 조회할 때 어떤 속성을 얻고 싶은지 Key를 지정할 수 있습니다.

이 때 Note를 지정했으나 해당 entitlement가 없으면 unauthorizedKeys 오류가 발생합니다.

연락처는 아래와 같이 CNContactStore를 통해 조회하거나 저장할 수 있습니다.

Contacts API에 대해 더 알고 싶으면 이 글을 참고하세요.

추출한 초성을 메모에 설정해서

연락처를 초성으로 검색할 수 있게 되었습니다.

복원할 때 삭제하기 위해서 <WhoCallMe> 태그를 사용했습니다.

마치며

어쩌면 Swift로 단순 변환만 할 수도 있었겠지만, 당시의 저는 분해되는 구조가 궁금했습니다. 그 때 학습해 둔 덕분에 많은 다른 앱들에서도 초성검색을 활용할 수 있었죠. 초성 추출 및 한글 분해 기능은 Extension으로 만들어두었으니, 필요하시면 가져다 쓰세요.

이 글은 Seoul iOS Meetup에서 발표를 최종 목표로 작성되었습니다. 발표 자료를 작성하는 과정에서 내용이 변경될 수 있습니다. 먼저 한국어 발표를 유튜브에서 먼저 공개하고 영문 버전을 작성한 후 Meetup에서 발표 예정입니다.(언제할 수 있을지 모르겠지만..)

내용이 유용했다면, 박수 👏 를 주세요 🙏. 제가 올린 더 많은 iOS 관련 글들을 여기서 확인해보세요.

저의 생각과 제가 올리는 글들에 대해 궁금하시거나 의견이 있으시면 제 LinkedIn을 방문해주세요. 읽어주셔서 감사합니다!

--

--

No responses yet