let str: String = "hello, world~!"
print(str[0]) // 결과는?
swift
위 코드를 실행하면 어떤 결과가 출력될까? "h"? 🙅♀️ 정답은 바로 컴파일 에러다.
아래와 같은 문구가 나타나면서 컴파일 에러가 발생한다.
'subscript(_:)' is unavailable: cannot subscript String with an Int, use a String.Index instead.
→ Int를 사용하여 문자열의 특정 위치에 접근할 수 없다. Int 대신 String.Index를 사용해라.
왜 스위프트에서는 여타 다른 많은 언어와 달리, Int를 사용하여 문자열의 위치에 직접 접근할 수 없을까? String도 결국 Character 배열로 이루어져있으면서?
이 문제를 이해하려면 가장 먼저 '인코딩', 특히 문자열 인코딩에 대해 알아야 한다.
문자를 표현하는 부호 체계와 문자열 인코딩 방식
컴퓨터의 구조적 한계 때문에 우리는 모든 문자를 숫자로 표현해야 한다. 다행히도 모든 문자는 결국 유한한 범위를 가진 문자열의 집합으로 이루어져있다. 그래서 개발자들은 각각의 문자에 대응하는 숫자로 이루어진 테이블을 만들었다. 이렇게 탄생한 것이 오늘날 문자 인코딩의 근간이 된 아스키코드(ASCII(American Standard Code for Information Interchange, 미국 정보 교환 표준 부호))다.

아스키(ASCII)코드에 대해 간략히 설명하자면, 1바이트(=8비트)중 7개의 비트를 사용하여 총 128개의 부호를 표현하는 방식이다. (나머지 1개의 비트로는 오류 검출을 위한 용도로 사용된다.) 초기에는 영어를 비롯한 언어가 128개만으로 표현이 가능했기 때문에 문제가 없었다.
그러나, 한국어나 중국어, 일본어처럼 128개만으로 표현할 수 없는 언어가 생기면서 이야기가 달라진다.
(한글만 살펴봐도 자음 19개, 모음 21개, 받침 29개로 만들 수 있는 모든 글자의 경우의 수를 따지면 128개가 훌쩍 넘는다.)
그래서 128개가 아닌, 다양한 언어의 모든 문자와 부호를 표현하기 위해 나온 것이 바로 유니코드(Unicode)다. (참고로 유니코드는 문자뿐만 아니라 특수문자나 이모티콘까지도 표현할 수 있다!) 유니코드란 전 세계의 모든 문자를 컴퓨터에서 일관되게 표현하고 다룰 수 있도록 설계된 산업 표준이다. 유니코드는 버전이 계속 추가되고 있으며, 글을 작성한 시간 기준으로 14.0 버전이 최신 버전이다.
그렇게 유니코드라는 표준화된 형식 덕분에 (거의)모든 문자와 코드가 1:1로 매칭될 수 있게 되었다. 이게 끝이 아니다. 이 문자와 매칭시킨 코드는 곧바로 컴퓨터가 사용할 수 있는 것이 아니다. 컴퓨터가 이해할 수 있도록 인코딩 단계를 한 단계 더 거쳐야 한다.
예를 들어, '가나다라'를 각각 '0001', '0002', '0003', '0004'라는 코드로 매칭시켰을 때, 컴퓨터에 이것을
'0001 0002 0003 0004'
로 저장할 수도 있고,
'00010002 00030004'
로 저장할 수도 있다. 이건 어떤 인코딩 방법을 채택하느냐에 따라 달라지게 된다.
그리고 이 인코딩 방법에 우리가 흔히 아는 'UTF-8', 'UTF-16'같은 반가운 이름들이 등장하는 것이다!
Swift와 Unicode의 상관관계
공식 문서를 살펴보면 알 수 있듯이, Swift는 ASCII코드 대신 Unicode를 사용하고 있다. Swift의 String은 유니코드 스칼라 값으로 이루어져있다. 하나의 유니코드는 고유한 21비트 숫자로 구성되어 있다. 예를 들어, U+0061는 소문자a, U+1F425는 귀여운 병아리 이모티콘🐥를 나타낸다.
이야기가 길었다. 여기서부터가 본문이다😅
그렇다면 Swift의 문자열에서의 index 접근과 Unicode가 무슨 상관이 있을까?
그건 바로 Swift에서 문자를 나타낼 때, 단일 유니코드 뿐만 아니라 확장된 문자소 클러스터 (Extended Grapheme Clusters)를 사용하기 때문이다.
- 확장된 문자소 클러스터란?
간단히 말해서, 여러개의 유니코드를 합쳐 하나의 문자를 표현하는 것이다.
Ex. \u{D55C} = "한" 이지만, \u{1112}\u{1161}\u{11AB} = "ㅎ + ㅏ + ㄴ"으로 똑같이 "한"을 표현할 수 있다.
이렇게 Character가 확장된 문자소 클러스터를 사용하기 때문에, 서로 다른 문자를 표현하거나, 혹은 같은 문자를 다른 방식으로 표현하게 될 경우, 이 문자들을 저장하기 위해 항상 동일한 크기가 아니라, 가변적인 메모리를 필요로 한다는 것이다.
예를 들어, 똑같은 한 글자라도 '하'와 '한'을 저장하기 위한 메모리 공간이 다르다.
전자는 ' \u{1112}\u{1161}'지만, 후자인 '한'은 '\u{1112}\u{1161}\u{11AB}'이기 때문이다.
그렇기 때문에, 문자열 내에서 Character의 수를 계산하려면 전체 문자열 스칼라를 반복하며 확인하는 작업이 필요하다!
[공식 문서 원문]
Extended grapheme clusters can be composed of multiple Unicode scalars. This means that different characters—and different representations of the same character—can require different amounts of memory to store. Because of this, characters in Swift don’t each take up the same amount of memory within a string’s representation. As a result, the number of characters in a string can’t be calculated without iterating through the string to determine its extended grapheme cluster boundaries.
즉, Swift에서는 글자 하나가 단 하나의 유니코드가 아니라
여러개의 유니코드의 결합으로 이루어져있을 수 있기 때문에
그 길이가 가변적이어서 Int값을 통한 index로 특정 문자에 접근할 수 없다.
그래서 Swift에서는 Int 대신 StringString.Index를 통해 문자열의 Index에 접근할 수 있도록 만들었다.
다양한 문자를 표현할 수 있는 대신, 이런 제약사항이 따르게 됐다.
평소에 Swift에서 문자열 접근이 불편하다는 것만 알고 있었는데, 알고 나니 (여전히 싫지만)어느 정도 이해할 수 있을 것 같다.
'Swift' 카테고리의 다른 글
[Swift] Optional과 Optional Binding (0) | 2021.10.28 |
---|---|
[Swift] 옵셔널 바인딩(Optional Binding)과 암시적 추출 옵셔널(Implicitly Unwrapped Optionals) (0) | 2021.06.09 |