Swift/개념 & 응용

[Swift] Unicode Scalar와 String의 Random Access

유정주 2022. 9. 16. 20:19
반응형

서론

오늘 iOS Developers KR 카톡방에서 재밌는 사실을 알았습니다.

(사실 공식 문서에 나와 있는 내용이라 이전에도 알고 있긴 했음 ㅎ;;;)

👨‍👩‍👧‍👧는 여러 개의 유니코드가 합체하여 이루어진 이모지라는 것인데요.

그래서

var emoji = "👨‍👩‍👧‍👧"
print(emoji.count) //1
print(emoji.unicodeScalars.count) //7

 count를 출력하면 1이지만 unicodeScalars를 출력하면 7이 됩니다.

 

오늘은 unicodeScalar에 대해 알아보고

String에서 Random Access를 지원하지 않는 이유도 엮어서 알아보겠습니다.

 

Unicode

유니코드는 현존하는 전 세계의 모든 문자를 시스템에서 표현하고 다룰 수 있도록 고안된 표준입니다.

유니코드는 유니코드 컨소시엄(Unicode Consortirum)에서 표준을 제정하고, 문자 집합, 문자열 인코딩, 문자열 처리 방식, 문자 정보 데이터베이스 등을 제공합니다.

 

유니코드는 다국어 환경과 호환되지 않는 기존 문자 인코딩 체계를 대체하기 위해 개발되었고 유지되고 있습니다.

단일 인코딩으로 다국어 환경에서 호환되어 문자열 깨짐을 해결할 수 있습니다.

쉽게 말해 영어, 한글, 이모지를 포함해서 모든 글자 깨짐 문제를 해결하는 것입니다.

 

메모장이 깨질 때 utf 인코딩 방식을 바꿔보세요. 라는 해결법이 나온 것이 이 유니코드에서 비롯된 것입니다.

 

Swift의 Unicode는 String 안에 열거형으로 구현되어 있습니다.

유니코드를 다루기 위해 구성된 열거형인데요.

직접 안으로 들어가보면,

익숙한 이름인 ASCII와 UTF16, UTF32 등이 열거형으로 정의되어 있습니다.

 

공식 문서와 같이 살펴보았는데 인상 깊은건

typealiase는 Deprecated가 되었는데

enum은 그대로 남아있다는 점입니다?..?

저는 처음에 새로 구현이 된줄 알았는데

typealiase를 살펴보니 

그냥 enum에 접근하는 typealias였습니다.

굳이 왜 Deprecated 시켰을까 생각해봤는데 코드의 일관성을 위해서가 아닐까... 싶네요.

 

Swift에서의 유니코드

유니코드가 갑자기 왜 나와? 하실 수 있는데요.

Swift에서는 유니코드를 호환해 String과 Character에서 텍스트 처리를 빠르게 했습니다.

Swift의 모든 문자열은 인코딩에 독립적인 유니코드 문자로 구성되어 있고 다양한 유니코드 표현의 문자에 접근할 수 있도록 지원합니다.

(라고 Swift 레퍼런스에 써있음)

 

Unicode Scalar

이제 Unicode Scalar에 대해 알아보겠습니다.

유니코드 스칼라는 유니크한 21bit 숫자 또는 수식어로, 

Swift의 String 타입은 유니코드 스칼라 값으로부터 생성됩니다.

예를 들어 "a"는 U+0061이고 귀여운 🐥는 U+1F425입니다.

이 값은 UnicodeScalar를 이용해 확인할 수 있는데요.

0x1F425 값을 UnicodeScalar로 변환하고 String으로 출력하면 귀여운 병아리 이모티콘이 출력됩니다.

 

반대로 "a"를 UnicodeScalar로 변경하면 97이 나오게 됩니다. (0x0061은 97입니다 ㅎㅎ)

 

맛있는 고기는 127830 값이 나오고,

 

맨 처음에 본 👨‍👩‍👧‍👧 는

nil이 나옵니다.

지금까지 코드가 잘 나오다가 왜 갑자기 nil이 나올까요?

 

👨‍👩‍👧‍👧는 1개의 유니코드가 아니기 때문입니다.

var emoji = "👨‍👩‍👧‍👧"
print(emoji.unicodeScalars.count) //7

위에서 본 것처럼 👨‍👩‍👧‍👧의 unicodeScalar count는 7이 나옵니다.

 

그럼 🐥나 🍖는 몇이 나올까요? 한 번 추측해 보세요.

var emoji = "🍖"
print(emoji.unicodeScalars.count)

 바로 1이 출력됩니다.

즉, 👨‍👩‍👧‍👧는 단일 유니코드 스칼라 값이 아니기 때문에 UnicodeScalar가 nil로 나온 것이죠.

 

👨‍👩‍👧‍👧를 구성하고 있는 7가지 유니코드 값을 확인해 볼까요?

var emoji = "👨‍👩‍👧‍👧"
for unicodeScalar in emoji.unicodeScalars {
    let scalar = UnicodeScalar(unicodeScalar)
    print("scalar: \(scalar) / \(scalar.value)")
}

이모지 4개와 Join을 위한 값 3개로 구성되어 있네요.

WWDC에서 또다른 예시를 살펴볼 수 있어요.

여자가 이마를 짚고 있는 이모지는 5개의 Unicode Scalar Value가 합쳐진 이모지이죠.

 

이걸 조금 응용하면 이런 것도 가능합니다.

var emoji = "👨‍👩‍👧‍👧"
print(emoji.contains("👨")) //true
print(emoji.contains("👍")) //false

👨‍👩‍👧‍👧 구성원에 👨는 있지만 👍는 없다는 것을 체크할 수 있습니다 ㅎㅎ

 

String의 Random Access

String은 Character의 집합이고 Character는 Unicode Scalar의 집합이라는 것을 위 예시로부터 알 수 있습니다.

그럼 위에서 다룬 내용과 String의 Random Access는 무슨 연관일까요?

 

Random Access는 O(1)로 배열의 원소에 접근할 수 있는 방법입니다.

let arr: [Int] = [1, 2, 3]
print(arr[2]) //3

익숙하죠?

 

이 Random Access를 하기 위해서는 RandomAccessCollection 프로토콜을 이용해야 합니다.

이와 반대(?)의 위치에 있는 프로토콜이 BidirectionalCollection인데요.

BidirectionalCollection는 후방, 전방 순회를 지원하는 Collection 프로토콜로 O(n)의 시간복잡도를 가집니다.

 

Swift의 String은 BidirectionalCollection을 채택하고 있습니다.

그래서 String은 무조건 순차적인 접근만 허용하죠.

String이 BidirectionalCollection 프로토콜을 채택한 이유가 바로 Character마다 Unicode Scalar의 개수가 다르기 때문입니다.

순차적으로 접근하면서 카운트를 하는 것이죠.

 

여러 개의 Unicode Scalar로 이루어진 이모지라도 count를 출력하면 1로 출력됩니다.

하지만 개발자가 배열에 직접 접근하는 듯한 오해를 살 수도 있기 때문에 공식적으로 지원하지 않는 것(이라는 추측)입니다.

 

공식적으로 지원은 안 하지만 아예 방법이 없는 것은 아닙니다.

String을 extension하여 Array처럼 사용이 가능합니다.

extension String {
    subscript(i: Int) -> String {
        return String(self[index(startIndex, offsetBy: i)])
    }
}

let string = "ABC"
print(string[1]) //B

 

String의 count는 O(n)

String은 BidirectionalCollection이기 때문에 count를 구하는 작업이 O(n)의 시간복잡도를 가집니다.

String과 [Character]의 count 시간을 측정해보면,

var string = ""
(0..<1_000_000).forEach { _ in string += "ABCDEF" }
let charArray: [Character] = Array(string)

let start1 = CFAbsoluteTimeGetCurrent()
print(string.count) //6000000
let diff1 = CFAbsoluteTimeGetCurrent() - start1
print("\(diff1) sec") //0.032127976417541504 sec

let start2 = CFAbsoluteTimeGetCurrent()
print(charArray.count) //6000000
let diff2 = CFAbsoluteTimeGetCurrent() - start2
print("\(diff2) sec") //0.000011086463928222656 sec

[Character] 타입이 2000배 이상 빠른 것을 확인할 수 있습니다.

 

"어? 그럼 무조건 [Character]로 변환하는 게 이득인가?" 할 수 있지만,

let start0 = CFAbsoluteTimeGetCurrent()
let charArray: [Character] = Array(string)
let diff0 = CFAbsoluteTimeGetCurrent() - start0
print("\(diff0) sec") //1.732192039489746 sec

String을 [Character]로 변환하는 시간도 매우 크기 때문에

시간 단축이 필요하다면 String의 count를 최소한으로 사용하는 것이 베스트이겠습니다.

 

예를 들면,

let string = "ABCDE"
if string.count > 5 {
    print("ㅇㅇ: \(string.count)")
} else {
    print("ㄴㄴ: \(string.count)")
}

보다는

let string = "ABCDE"
let length: Int = string.count
if length > 5 {
    print("ㅇㅇ: \(length)")
} else {
    print("ㄴㄴ: \(length)")
}

이 코드가 더 빠르겠죠.

 

 

이상 Unicode Scalar와 String에 대해 알아보았습니다.

감사합니다!

 

참고

https://developer.apple.com/documentation/swift/unicode

https://developer.apple.com/documentation/swift/string/unicodescalarview

https://docs.swift.org/swift-book/LanguageGuide/StringsAndCharacters.html

https://jeong9216.tistory.com/177#유니코드-unicode

https://developer.apple.com/documentation/swift/string

 


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

swift, iOS, 스위프트, 개발, 코딩

반응형