문자열을 문자 단위로 다루기 | Swift

스위프트(Swift) 의 문자열(String) 타입은 다년 간의 다듬질(?)을 거쳐 완성될 대로 완성된 말 그대로 완성체의 문자열을 표현하기 위한 방법을 이미 제공합니다. 그런데 C 나 Python 등 다른 언어와 비교할 때는 좀 까다로운 녀석이라고 느낄 때도 있습니다.

예를 들자면, 문자열의 첫 글자를 얻기 위해 아래와 같은 코드를 생각해 볼 수 있습니다.
let string = "abc123"
let first = string[0]   // ERROR!
물론 두 번째 라인에서 우리는 '이런건 쓸 수 없다' 는 찰진 에러를 구경 할 수 있습니다.

문자열의 특정 인덱스 위치를 탐색할 경우 우리는 어떤 데이터를 얻어야 정상 일까요? C 식의 바이트 단위일까요? 아니면 유니코드 문자 단위여야 할까요? UTF-8 기준인가요 UTF-16 기준인가요?

여러 면에서 명확하지 않은 점을 해소하기 위해 스위프트 개발자들은 좀 고심을 했나 봅니다.

그래서 스위프트 에서는 문자열 에서 인덱스를 표현하기 위한 String.Index 라는 특수한 타입을 제공합니다.

String.Index 의 정체

String.Index 라는 타입은 스위프트 문자열의 인덱스(Index)를 표현하기 위한 타입입니다. 문자열에서 인덱스란 해당 문자열에서 특정 문자의 위치를 의미합니다. 번역이 문제인데, 한국에서 인덱스는 보통 색인이라는 의미로 번역 하겠지만 문자열에서는 적절한 번역을 모르겠네요. 그냥 계속 인덱스 라고 칭하겠습니다.

자 그럼 위 예제의 올바른 해답을 우선 알아봅시다. 0번째 인덱스라는 말은 첫 글자를 의미합니다. 이 경우 아래와 같은 식으로 코드를 작성 할 수 있습니다.
let first = string[string.startIndex]  // "a"
스위프트의 문자열 타입에서 친절하게도 시작인덱스(startIndex)라는 값을 제공합니다. 이 startIndex 는 문자열의 첫 번째 위치를 나타내는 String.Index 값입니다.

방법도 좀 다르고 C 나 Python 처럼 편하게(?) 숫자 자체를 쓰지는 못 하지만 어쨌든 불가능한 것은 아니다 라는 것을 알 수 있습니다.

명확히 이야기 하자면 String 에는 인자로 정수(Int)를 받는 subscript 메소드가 없습니다. 대신 String.Index 값을 인자로 받는 subscript 가 구현되어 있어서 위 처럼 코드를 작성 할 수 있습니다.

String.Index 생성하기

String.Index 를 숫자를 이용해 직접 생성해 봅시다.
String.Index(encodedOffset: 0)
이 방법은 UTF-16 encoded offset 으로 인덱스를 생성하는 예제입니다. 의미는 0번째 인덱스 즉 첫 번째 문자를 가리키는 인덱스 입니다.

위 처럼 String.Index 를 직접 생성해서 써도 되겠지만, 개인적으로는 문자열(String) 자체에서 제공되는 프로퍼티나 메소드를 이용하는 것을 추천합니다.

예를 들자면 이미 봤지만 아래와 같은 두 녀석이 있습니다.
string.startIndex
string.endIndex
이름만 봐도 알 수 있겠지만 문자열 제일 처음을 가리키는 인덱스(startIndex)와 끝을 가리키는 인덱스(endIndex)가 있습니다. 따라서 이 둘은 굳이 별도로 생성할 필요는 없겠지요.

참고로 endIndex 의 경우는 마지막 문자(위의 경우 "3")가 아니라 그 다음의 정말 아무것도 없는 끝 부분을 가리킵니다. 마치 C String 의 문자열 마지막을 장식하는 '\0' 의 위치를 의미한다고 보면 됩니다. 따라서 아래 코드는 프로그램을 죽여버립니다.
string[string.endIndex]  // SIGABRT
endIndex 의 의미를 오해하지 맙시다.

하여간 이 두 프로퍼티를 이용해 좀 더 유동적인 인덱스를 만들 수 있습니다. 예를 들어 index(after:) 메소드를 이용해 두 번째 문자를 가리키는 인덱스를 아래 처럼 만들 수 있습니다.
let secondIndex = string.index(after: string.startIndex)
let second = string[secondIndex]  // "b"
위의 코드 결과에서 second 에는 string 문자열의 두 번째 문자인 "b" 가 들어가게 됩니다.

앞서 이야기 했지만 endIndex 의 경우 끝 문자가 아니라 그 다음의 '문자열의 끝 자체' 를 가리킵니다. 그렇다면 문자열의 실제 마지막 문자를 구하고 싶다면 위의 방법이 힌트가 될 것입니다. 다만 이번에는 after 가 아닌 before 겠지요.
let endIndex = string.index(before: string.endIndex)
let last = string[endIndex]  // "3"
이렇게 하면 last 에는 3 이 들어가게 됩니다.

자 그럼 처음부터 3번째 문자를 구하려면 어떻게 해야 할까요?

String.index(after:)를 호출한 인덱스에 해당 메소드를 한 번 더 호출하면 3번째 문자를 가리키는 인덱스를 얻을 수 있겠지만 저는 이렇게 하기는 싫습니다. 더 좋은 방법이 있겠지요?

다행히도(?) 아래 예제 처럼 역시나 관련 메소드가 제공됩니다.
let thirdIndex = string.index(string.startIndex, offsetBy: 2)
인덱스(index)의 시작 값은 1이 아니라 0 이라는 것을 잊지 맙시다. 어쨌든 이 메소드 덕분에 이제 자유자제로 인덱스를 생성 할 수 있게 되었습니다. 아 참고로 offsetBy 파라미터는 음수가 될 수도 있습니다.

물론 이 외에도 아래와 같이 문자를 검색해서 인덱스를 찾는 방법도 있습니다.
"abc123".index(of: "c")
위 코드는 "abc123" 문자열에서 가장 최초로 발견되는 "c" 문자의 위치를 의미하는 String.Index 값을 돌려줍니다.

부분문자열(Substring)

앞서 문자열의 subscript 가 String.Index 를 받는다고 했으니 결국 문자열 내에서 일부를 잘라내는 이른바 부분문자열(substring)을 수동으로 구하는 방법은 구현 할 수 있을 것입니다.

뭐 그렇다고 굳이 구현할 필요는 없겠죠. 이미 제공 되니깐요. String 에는 String.Index 타입의 범위(Range)를 인자로 받는 subscript 가 구현되어 있습니다.

예를 들어 첫 글자와 마지막 글자를 제외한 문자열을 구해봅시다.
let start = string.index(after: string.startIndex)
let end = string.index(string.endIndex, offsetBy: -2)
let substring = string[start...end]  // "bc12"
String.Index 를 이용해 범위(range)를 만들고 이것을 문자열의 subscript 를 이용하면 부분문자열(substring)을 구할 수 있습니다. 위의 경우 마지막 substring 상수의 값은 "bc12" 를 가리킵니다. 왜 가리킨다고 표현했냐면... 일단 계속 읽어보세요.

위 코드는 범위 표현 방식에 따라 아래 처럼 할 수도 있겠지요.
let start = string.index(after: string.startIndex)
let end = string.index(before: string.endIndex)
let substring = string[start..<end]  // "bc12"
어쨌든 "bc12" 문자열을 얻기 위한 같은 동작을 하는 코드입니다.

그런데 약간 특이한 점이 있습니다. 앞서 subscript 를 통해 구한 값은 "bc12"를 가리킨다고 했습니다. 왜 이렇게 적었냐하면, 이 값의 타입이 Substring 이라는 특수한 타입이기 때문입니다.

이 Substring 타입은 이름 처럼 특정 문자열의 부분 문자열을 담기 위한 특수한 타입입니다. 하지만 문자열이 아니기 때문에 String 타입이 필요한 곳에 그냥 넘겨 줄 수가 없습니다.

물론 문자열로 쉽게 바꿀 수 있으니 별로 걱정할 필요는 없습니다.
let realSubstring = String(substring)
타입의 용도가 다르기 때문에 형변환(type casting)이 안되는 점은 주의합시다.

부분문자열 타입은 이 외에도 많은 기능이 제공되지만 여기서는 인덱스와 관련된 부분을 설명하기 위해 거론했으니 여기까지만 정리 하겠습니다.

마무리

스위프트에는 확장(extension) 이라는 훌륭한 기능이 제공됩니다. 이 글은 문자열의 subscript 가 정수형을 받지 않기 때문에 첫 글자를 구하는 단순한 코드를 쓰지 못 하는 문제로 시작 했었는데, 이제는 방법을 알게 되었으니 extension 을 이용하면 쉽게 구현 할 수 있을 것입니다.

개인적으로는 이런 코드를 만들어서 라이브러리화 해 놓고 있습니다. 궁금하시면 제 github 에 구현된 코드를 구경하실 수 있습니다.

https://github.com/seorenn/SRChoco/blob/master/Source/StringExtensions.swift

이 외에도 아래의 관련 글들도 참고해 보세요.

댓글

이 블로그의 인기 게시물

버전(Version)을 제대로 이해하기

소수점 제거 함수 삼총사 ceil(), floor(), round()