본문 바로가기

Swift/Deep dive!!

[Swift] String과 Character 자료형 개념 뿌수기(unicode scalar, grapheme cluster). Let's d

728x90

문자열 알고리즘을 공부하기 전에 한번 복습하면 좋을 것 같아서 String, Character 개념을 정리합니다!

String structure

String. 문자열 입니다. 정말 많이 사용합니다. Swift에서 제공되는 기본 자료형에 속합니다. Swift's Standard Library에 속합니다. 그래서 import Foundation 없이도 사용할 수 있습니다.

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

String 자료형인 문자열은 Character들로 구성된 컬랙션입니다.

유니코드..?

유니코드란 전 세계 모든 문자를 '컴퓨터'에서 일관되게 표시하고 다룰 수 있도록 특정한 키(숫자 값)로 1대 1 매핑한 표준코드입니다. 영어 한 문자는 1byte입니다. 모든 언어가 1byte면 좋을 텐데 한글은 2byte로 표기합니다. 그렇기 때문에 전 세계 언어를 모두 표시할 수 있도록 정한 표준 코드입니다. 이렇게 정한 규칙을 컴퓨터에 저장할 때는 '인코딩'을 통해 저장해야 합니다(컴퓨터가 알아들을 수 있게 유니코드 -> 컴퓨터). 그 예로 UTF-8이 있습니다.

이렇게 사람들이 사용하는 문자 언어를 표준화, 코드표(집)으로 기준을 세웁니다. 그것을 컴퓨터가 사용할 수 있는 신호로 만드는 것이 인코딩 이라고 합니다. 컴퓨터가 이해할 수 있는 신호는 당연히 0, 1 바이너리ㄷㄷ.... ASCII 코드표 또한 미국에서 'a','b','c'등 영어에 대해 표준적으로 정한 코드집입니다. "Character Set"라고 불립니다. 이 코드집을 컴퓨터가 이해할 수 있도록 인코딩을 거쳐야 합니다. 예를들어 'A'라는 문자는 컴퓨터가 65 숫자로 저장합니다. 이 과정을 인코딩이라고 합니다... 사실 여기서 65를 binary로 만들어야 합니다. 이때 'A' -> 65. 문자 : 숫자 일대일 매핑 되는 표가 ASCII, Unicode 입니다. 왜냐?! 미국 표준 위원회가 지정했기 때문에,, 반박불가입니다.

한줄 결론: ASCII, Unicode는 국제 표준 문자표이다. 컴퓨터는 이 Character Set를 이용해 '인코딩'을 해서 문자를 다룬다.

다시 돌아와서.. Swift의 String 또한 완벽하게 Unicode를 이용, 지원합니다.

String 초기화

원하는 문장을 적고 더블 쿼우팅 "" 으로 감싸거나 트리플 쿼우팅 """으로 감싸면 됩니다.
또는 init메서드를 사용합니다.
이때 String을 구성하는 Character 자료형을 String으로 바꿀 때는 init(_ c: Character)를 이용하면 됩니다. init(_ c: Character)..

let text1 = "aaaa" //String으로 타입 추론! 
var text2 = "bbbb"
var text3 = String(repeating: "c", count: 4)

var temp = "dd"
var text4 = "dd" + temp
var text5: String = "eeee"

text1의 경우 let 키워드를 사용했기에 더이상 값을 바꿀 수 없습니다. 나머지 변수들은 값을 바꿀 수 있습니다.

String's method

이런 함수들이 static으로 정의되어 있기 때문에 text4에서 "dd" + temp 등의 입력이 가능합니다.
append()함수를 통해 문자열 뒤에 String, Character 추가가 가능합니다.
insert()함수를 통해 문자열 앞에 문자를 삽입할 수도 있습니다. 특정 문자 구간을 교체 할 수도, 삭제 할 수도 있습니다!!

String 특징

for in 구문을 통해 문자열의 한 문자씩 사용할 수 있습니다. 여기서 포인트는 String타입인 text1은 for in 구문의 원소로 사용할 땐 Character를 반환합니다. 사실 Swift 의 String은 Character타입 의 collection입니다. 한개의 문자열은 Character collection으로 구성되어 있습니다.

let texiCharacters: [Character] = ["T","e","x","i"]
let texiStr = String(textCharacters)

물론 Character타입의 배열도 한개의 문자열로 init할 수 있습니다.

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

Character의 설명입니다. 한개의 Character 타입을 표현하기 위해 한 개의 character는 한 개 또는 여러 개의 Unicode scalar값으로 만들어 집니다. 즉 Character로 구성된 collection 덕분에 String은 자연스러운 문자열 크기를 반환받을 수 있습니다.

당연한 말 아닌가요?

한개의 문자열에 대해 여러 개의 Character 타입의 Characters로 구성된 문자열의 길이는 Character 개수를 반환하면 되기 때문입니다. 하지만 Character는 하나 또는 여러 개의 Unicode scalar 값으로 표현됩니다. 여러개의 Unicode scalar는 한개의 grapheme cluster가 될 수 있습니다. 여러개의 Unicode scalar와 한개의 Unicode scalar는 같은 Character를 구성할 수 있습니다. 그렇기 때문에 Character의 컬랙션이 주 구성 요소인 String은 자연스러운 문자열 크기를 반환 받습니다.( ???: 반대로 생각하자면 Character가 아니라 Unicode scalar 로 collection이 이루어져있다면 String의 길이는 우리가 보는 문자 길이와 다르게 값을 반환할 수있습니다...)

하지만 문자열을 배열의 index처럼 활용할 수는 없습니다... 위 for in 구문을 통해 text1 문자열은 Character로 반환됬는데 말이죠 !?
그 이유는 String은 grapheme clusters인 collection으로 분류되고 이는 array처럼 인덱싱을 할 수 없기 때문입니다.

왜 그럴까요?

extended grapheme cluster는 Swift's single Character 타입을.. Unicode scalar values를 뜻합니다.

grapheme Clusters.
grapheme 클러스터 여러 개 또는 한 개가 Character를 구성합니다. A character 여러 개가 collection으로 구성되면 String 한 문자열이 됩니다. 한 문자(grapheme cluster)는 여러 개 또는 한 개의 Unicode scalar로 표현될 수 있습니다.

사진1

요 녀석은 악센트 acute tone mark입니다.

사진 출처: 위키피디아

영어 단어 책을 펴면 자주 볼 수 있습니다 !!
그리고 acute tone mark의 유니코드는

입니다.

Unicode의 상징을 뜻하는 U+가 붙고 연이어 숫자(Unicode code point 숫자는 code point로 표현)가 붙습니다.(LG U+아닙니다) 즉 U+0301은 유니코드라는 것을 알 수 있고 combining acute accent를 표현합니다.

Swift에선 유니코드를 사용할 때 " \u{} " 이렇게 표현합니다. ex) u{0301}

let é = "é"
let eCombiningAcuteToneMark = "e\u{0301}"

이 둘은

이렇게 출력됩니다. 같은 문자일까요?

print(é == eCombiningAcuteToneMark)

같다고 출력됩니다.

이 둘은 String으로 타입 추론 됩니다. 위에서 말했지만 String은 Characters collection으로 구성됩니다. collection을 구성하는 각각의 character는 one Unicode scalar or Unicode scalar values.. 한개 또는 여러개의 유니코드 스칼라값으로 이루어집니다. 하지만 한개 또는 여러개의 Unicode scalar value로 이루어진 것이 Character입니다. 그래서 Character의 비교시에는 같다고 출력됩니다.

Character가 아니라 Character를 구성하는 각각의 스칼라 값을 확인해 보면,,

print("DEBUG: é")
é.unicodeScalars.map{print($0.value)}
print("DEBUG: e with U+0301")
eCombiningAcuteToneMark.unicodeScalars.map{print($0.value)}

전자의 경우 Unicode scalar 값 233.
e에 악센트 유니코드를 결합 한 것은 101 769 두 개를 결합해 한 개의 Character를 구성했습니다.

Unicode scalar로 봤을 때 한개의 Character를 구성하기 위해 필요로 한 Unicode scalar value는 다르지만,Character타입인 String의 비교를 할 때는 같게 출력됩니다. 여기서 알 수 있는 점은 같은 Character라도 unicode scalar는 서로 다른 개수로 구성 될 수 있다는 점입니다.


반대로

한개의 Character를 구성하는 single grapheme cluster는 여러 개의 Unicode scalar로 이루어 질 수 있습니다.

let u = "\u{1F1FA}"
let s = "\u{1F1F8}"
print(u)
print(s)

regional indicator symbol을 나타내는 두 유니코드 U+1F1FA(== 🇺) , U+1F1F8(==🇸)가 있습니다.

출력

그리고

let us = u+s
print(us)

이 두 String을 합치면

이렇게 한개의 문자열이 출력됩니다.

두개의 Unicode scalar가 모여 single grapheme cluster(집단)을 이룰 수 있고 single grapheme cluster는 Character로 표현됩니다.

왜 Character type's collection인 String은 index를 사용해 문자 반환을 할 수 없는가?

String을 배열처럼 인덱싱을 사용해 특정 문자를 추출할 수 없는 이유는??

String을 구성하는 Character가 우리의 눈으로 봤을 때는 정말 똑같은 문자입니다(위의 악센트). 하지만 Character는 한 개 또는 Unicode code point의 조합으로 이루어집니다(요것이 원인입니다). Character를 뜻하는 single grapheme cluster는 Unicode scalar value가 한 개 또는 여러 개의 Unicode code point로 구성될 수 있습니다.

Unicode를 사용하기 위해서는 인코딩(0 or 1) 해야합니다. Character가 저장되기 위해서는 가변적인 크기가 필요합니다. 위에서는 단순히 악센트 e를 만들기 위해 두 개의 유니코드를 이용 했지만 어떤 이모지의 경우 여러 개의 code point가 사용됩니다. utf-8과 같은 Unicode 인코딩은 가변적인 Character에 대해 일정 시간내에 접근 할 수 없다고 합니다. 그래서 Character type의 컬랙션인 String에 인덱스 기능을 지원하지 않습니다.

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

그 대신!
String.Index를 이용하라고 권장합니다.
문자열에서 특정 index를 추출해서 Range를 이뤄 해당 문자열의 substring으로 사용하면 됩니다.

범위를 통해 얻은 abcde[1]과 같은 표현 입니다.

음..?

손이 많이 갑니다.

그리고 index(_:offsetBy:)의 시간복잡도는 O(N)입니다.

var arr = abcde.map{String($0)}
print(arr[1]) //1
print("abcde".enumerated().filter{$0.offset==1}.map{String($0.element)}.reduce("",+)) //2
print(abcde.enumerated().first{$0.offset==1}!.element) //3

아직까지 Range<String.Index> 사용은 많이 안 다뤄봐서,,,
1번 map을 통해서 각각을 배열로 바꾸는 방법도 O(N)에 속합니다.

2, 3번은 enumerated()를 사용하는 방법인데
enumerated()는 시간복잡도 O(1)에 속합니다.
2번의 경우 사용 안하는게 좋겠네요,,

3번의 first의 경우 시간복잡도 O(N)에 속합니다.

앞으로도 더 좋은 방법을 찾아갈 것인데 그래도 우선은 map 쓰거나 enumerated를 써야겠습니다..

String's comparable

기존에 정의한 문자열이 let 키워드가 아니라면 다른 문자열을 자유롭게 추가하거나 제거, 삽입이 가능합니다. 이 뿐 아니라 다른 문자열과 비교를 지원합니다.


Swift's String은 위에서도 설명했지만 Unicode code point에 따라서 Character를 구성합니다. Unicode 정식 표현을 사용해서 equatable ==는 물론 comparable 또한 가능합니다. 대소문자 또한 comparable을 수행합니다.

이 경우 ascii 값에 의하면
대문자 B의 경우 66
소문자 b의 경우 98
의 asciiValue를 갖고 있습니다.

따라서 "B"는 "b"보다 작습니다. 그렇기에 "B"와 "b"는 같은지 비교를 하면 false 처리됩니다.

다음 포스트는 Substring과 Range<String.Index>에 관해 작성할 것입니다.

틀린 내용이 발견시 댓글로 알려주신다면 정말 감사합니다 : ]

참고 자료
https://softwareengineering.stackexchange.com/questions/362103/why-doesnt-swift-allow-int-string-subscripting-and-integer-ranges-directly
https://medium.com/@banghua.zhao/why-cannot-subscript-string-with-an-int-in-swift-281cfc83402
https://docs.swift.org/swift-book/LanguageGuide/StringsAndCharacters.html

728x90