본문 바로가기

Swift 문서 탐방

[Swift 문서 탐방] Swift 기본 타입(2) - String과 Character

String은 "hello, world" 또는 "albatross"와 같이 일련의 Character들로 이루어집니다.

 

String과 Character은 빠르며, 유니코드와 호환되는 방식으로 텍스트를 다룹니다. 여기에 String간의 연결은 단순히 + 연산자로 연결이 가능하며, 보간법(Interpolation)을 이용하여 더 긴 문자열을 표현해줄 수 있습니다.

 

특히, String은 Swift 언어의 조상격인 Objective-C의 NSString과 브리징(Bridging)되어 NSString에서 정의되어 있는 메서드들을 String에서 NSString에 형변환(Casting) 과정없이 사용가능합니다.

String Literals

String Literal은 큰 따옴표("")로 묶인 문자 시퀀스(Sequence)입니다.

let someString = "Some string literal value"

Multiline String Literals

여러 줄에 걸친 문자열이 필요한 경우 다중 줄 문자열 리터럴을 사용합니다. 다중 줄 문자열 리터럴은 세 개의 큰따옴표(""")로 둘러싸인 문자 시퀀스입니다.
let quotation = """
The White Rabbit put on his spectacles.  "Where shall I begin,
please your Majesty?" he asked.

"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""
다중 줄  문자열 리터럴을 사용할 때 주의할 점은 닫는 따옴표(""")을 기준으로 들여쓰기가 정해진다는 것입니다.

위 이미지를 보시면 닫는 따옴표를 기준으로 문자열이 시작되며 그 앞에 작성된 여백(Whitespace)들은 무시된다는 사실을 알 수 있습니다.

Special Characters in String Literals

문자열 리터럴은 아래와 같이 특별한 Character들을 가지고 있습니다.

  • 이스케이프 특수 문자(The escaped special characters)
    • \0 (널 문자), \\ (백슬래시), \t (수평 탭), \n (줄 바꿈), \r (캐리지 리턴), \" (큰따옴표) 및 \' (작은따옴표)
  • 임의의 유니코드 스칼라 값 - \u{n}으로 작성되며, 여기서 n은 1~8자리 16진수
let wiseWords = "\"Imagination is more important than knowledge\" - Einstein"
// "Imagination is more important than knowledge" - Einstein
let dollarSign = "\u{24}"        // $,  Unicode scalar U+0024
let blackHeart = "\u{2665}"      // ♥,  Unicode scalar U+2665
let sparklingHeart = "\u{1F496}" // 💖, Unicode scalar U+1F496

Extended String Delimiters

Swift에서 확장된 문자열 구분 기호는 줄 바꿈이나 탭과 같은 일반적인 효과를 트리거하지 않고 문자열에 특수 문자를 포함하는 데 사용됩니다. 

print(#"Line 1\nLine 2"#)

// Line 1\nLine 2

Initializing an Empty String

빈 문자열을 초기화(Initialization)하기 위해선 빈 문자열 리터럴을 할당하거나 새로운 String 인스턴스를 생성해주면 됩니다.

var emptyString = ""               // 빈 문자열 리터럴
var anotherEmptyString = String()  // 초기화 문법
// 이 두 문자열은 모두 비어 있으며 서로 동등합니다.

 

String Mutability

String에 대한 Mutability는 변수(variable)에 할당하거나 상수(constant)에 할당에 따라 달라집니다.

var variableString = "Horse"
variableString += " and carriage"
// variableString 은 "Horse and carriage" 값을 가지고 있습니다.

let constantString = "Highlander"
constantString += " and another Highlander"
// 이는 상수 문자열은 수정할 수 없다는 컴파일 타임 오류 발생시킵니다.

Strings Are Value Types

문자열을 기본적으로 값타입입니다. 따라서 함수나 매서드, 그리고 변수나 상수에 할당할 때 복사됩니다.(copied)

하지만 Swift 언어 자체적으로 cop-by-default 기술에 의해 String이 함수나 매서드에 문자열 값이 전달될 때, 이 문자열에 쓰기 작업이 일어나지 않은 한 메모리 상 원본 그대로의 String 값이 함수나 매서드에 전달됩니다.

Working with Characters

for-in 루프로 문자열을 반복하여 문자열의 개별 Character 값에 접근할 수 있습니다.

for character in "Dog!🐶" {
    print(character)
}
// D
// o
// g
// !
// 🐶

 

혹은 아래 코드와 같이 변수나 상수에 할당하고 Character 타입 Annotation을 사용하여 단일 Character를 선언해줄 수 있습니다.

let exclamationMark: Character = "!"

 

또한, Character 배열을 가지고 새로운 String을 생성해줄 수도 있습니다.

let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"]
let catString = String(catCharacters)
print(catString)
// "Cat!🐱" 출력

Concatenating Strings and Characters

문자열 값은 추가 연산자(+)를 사용하여 추가(또는 연결)되어 새로운 문자열 값을 생성할 수 있습니다.
let string1 = "hello"
let string2 = " there"
var welcome = string1 + string2
// 현재 welcome의 값은 "hello there"

 

추가 할당 연산자(+=)를 사용하여 기존 문자열 변수에 문자열 값을 추가할 수도 있습니다.

var instruction = "look over"
instruction += string2
// 현재 instruction의 값은 "look over there"

 

String 유형의 append() 메서드를 사용하여 문자 값을 문자열 변수에 추가할 수 있습니다.

let exclamationMark: Character = "!"
welcome.append(exclamationMark)
// 현재 welcome의 값은 "hello there!"
기존 Character 변수에 문자열이나 문자를 추가할 수 없습니다. 왜냐하면 Character 값은 단일 문자만 포함해야 하기 때문입니다.
긴 문자열의 줄을 구성하기 위해 여러 줄 문자열 리터럴을 사용하는 경우 마지막 줄을 포함하여 문자열의 모든 줄이 줄 바꿈으로 끝나야 합니다.
let badStart = """
    one
    two
    """
let end = """
    three
    """
print(badStart + end)
// 두줄이 출력:
// one
// twothree


let goodStart = """
    one
    two

    """
print(goodStart + end)
// 3줄이 출력:
// one
// two
// three

 

위의 코드에서 badStart를 end와 연결하면 두 줄의 문자열이 생성되는 것을 보실 수 있습니다. 왜냐하면 badStart의 마지막 줄이 줄 바꿈으로 끝나지 않아, 그 줄이 end의 첫번째 줄과 결합되었기 때문입니다. 반면 goodStart의 두 줄은 모두 줄 바꿈으로 끝나므로, end와 결합하면 예상대로 세 줄이 됩니다.

String Interpolation

String 보간법은 상수, 변수, 리터럴 및 표현식을 혼합하여 해당 값을 문자열 리터럴 내부에 포함하여 새 문자열 값을 구성하는 방법입니다.

이 보간법은 단일 줄과 다중 줄 문자열 리터럴 모두에서 사용할 수 있습니다.

let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message의 값은 "3 times 2.5 is 7.5"

 

이 보간법은 확장된 문자열 구분 기호(Extended String Delimiter)에도 사용됩니다.

이 보간법을  확장된 문자열 구분 기호(Extended String Delimiter)에서 사용하기 위해선 # 기호의 개수를 일치시켜주어야 합니다.

print(#"6 times 7 is \#(6 * 7)."#)
// "6 times 7 is 42." 출력

 

여기서 주의할 점은 보간된 문자열 내에서 괄호 안에 쓰는 표현식은 이스케이프되지 않은 백슬래시(\), 캐리지 리턴 또는 줄 바꿈을 포함할 수 없다는 점입니다.

Unicode

유니코드는 다양한 문자 체계에서 텍스트를 인코딩, 표현 및 처리하기 위한 국제 표준입니다. 이를 통해 거의 모든 언어의 모든 문자를 표준화된 형태로 표현하고, 텍스트 파일이나 웹 페이지와 같은 외부 소스에서 해당 문자를 읽고 쓸 수 있습니다. Swift의 문자열 및 문자 유형은 완전히 유니코드와 호환됩니다.

Unicode Scalar Values

Swift의 기본 String 타입은 유니코드 스칼라 값에서 빌드됩니다. 유니코드 스칼라 값은 문자 또는 수정자(Modifier)에 대한 고유한 21비트 숫자입니다. 예를 들어 LATIN SMALL LETTER ("a")의 경우 U+0061, FRONT-FACING BABY CHICK("🐥")의 경우 U+1F425입니다.

모든 21비트 유니코드 스칼라 값이 문자에 할당되는 것은 아니라는 점에 유의해야합니다. 일부 스칼라는 향후 할당을 위해 예약되거나 (reserved) UTF-16 인코딩에서 사용됩니다.

Extended Grapheme Clusters

Swift의 Character 타입의 모든 인스턴스는 단일 확장된 문자소 클러스터(Extended Grapheme Cluster)를 나타냅니다. 확장된 문자소 클러스터는 하나 이상의 유니코드 스칼라 시퀀스로, 결합 시 인간이 읽을 수 있는 문자를 생성합니다.

 

예를 들어, 문자 é는 단일 유니코드 스칼라 é(LATIN SMALL LETTER E WITH ACUTE 또는 U+00E9)로 표현할 수 있습니다. 그러나 동일한 문자(letter)는 스칼라의 쌍으로 표현할 수도 있습니다. 표준 문자 e(LATIN SMALL LETTER E 또는 U+0065) 뒤에 COMBINING ACUTE ACCENT 스칼라(U+0301)가 옵니다. COMBINING ACUTE ACCENT 스칼라는 앞에 오는 스칼라에 그래픽으로 적용되어 유니코드 인식 텍스트 렌더링 시스템에서 렌더링할 때 e가 é로 바뀝니다.

 

두 경우 모두 문자(letter) é는 확장된 문자소 클러스터를 나타내는 단일 Swift 문자 값으로 표현됩니다.

첫 번째 경우, 클러스터에는 단일 스칼라가 포함되어 있습니다. 두 번째 경우, 클러스터는 두 개의 스칼라 클러스터입니다.

let eAcute: Character = "\u{E9}"                         // é
let combinedEAcute: Character = "\u{65}\u{301}"          // e followed by ́
// eAcute는 é, combinedEAcute는 é

 

확장된 문자소 클러스터는 여러 복잡한 스크립트 문자를 단일 문자 값으로 표현하는 유연한 방법입니다. 예를 들어, 한국어 알파벳의 한글 음절은 사전 구성된 시퀀스 또는 분해된 시퀀스로 표현할 수 있습니다. 이러한 두 표현은 모두 Swift에서 단일 문자 값으로 적격합니다.

let precomposed: Character = "\u{D55C}"                  // 한
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}"   // ᄒ, ᅡ, ᆫ
// precomposed는 한, decomposed는 한

 

확장된 문자소 클러스터는 Enclosing Marks(예: COMBINING ENCLOSING CIRCLE 또는 U+20DD)에 대한 스칼라가 단일 문자 값의 일부로 다른 유니코드 스칼라를 묶을 수 있도록 합니다.

let enclosedEAcute: Character = "\u{E9}\u{20DD}"
// enclosedEAcute는 é⃝

 

지역 표시기 기호에 대한 유니코드 스칼라는 단일 문자 값을 만들기 위해 쌍으로 결합될 수 있습니다. 예를 들어 REGIONAL INDICATOR SYMBOL LETTER U (U+1F1FA)와 REGIONAL INDICATOR SYMBOL LETTER S (U+1F1F8)의 조합은 다음과 같습니다.

let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}"
// regionalIndicatorForUS는 🇺🇸

Counting Characters

Character로 이루어진 String은 보통 count 속성을 통해 계산됩니다.

let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
print("unusualMenageried은 \(unusualMenagerie.count) 문자이다.")
// "unusualMenagerie 40 문자이다." 출력

 

여기서 주목해야할 점은 Character 값은 확장된 문자소 클러스터(Extended Grapheme Clusters)을 사용한다 하더라도 전체적인 String 길이에는 변화가 없을 수 도 있다는 점입니다.

 

예를 들어, cafe라는 4자 단어로 새 문자열을 초기화한 다음 문자열 끝에 COMBINING ACUTE ACCENT(U+0301)를 추가하면 결과 문자열의 문자 수는 여전히 4개이고 네 번째 문자는 e가 아닌 é입니다.

var word = "cafe"
print("the number of characters in \(word) is \(word.count)")
// "the number of characters in cafe is 4" 출력


word += "\u{301}"    // COMBINING ACUTE ACCENT, U+0301


print("the number of characters in \(word) is \(word.count)")
// "the number of characters in café is 4" 출력

 

더구나 확장된 문자소 클러스터(Extended Grapheme Clusters)는 여러 유니코드 스칼라로 구성될 수 있습니다. 즉, 서로 다른 문자와 동일한 문자의 서로 다른 표현은 저장하는 데 서로 다른 양의 메모리가 필요할 수 있습니다. 이 때문에 Swift의 문자는 문자열 표현 내에서 각각 동일한 양의 메모리를 차지하지 않습니다. 결과적으로 문자열의 문자 수는 문자열을 반복하여 확장된 문자소 클러스터 경계를 확인하지 않고는 계산할 수 없습니다. 특히, 긴 문자열 값으로 작업하는 경우 count 속성는 해당 문자열의 문자를 확인하기 위해 전체 문자열의 유니코드 스칼라를 반복해야 한다는 점에 유의하여야 합니다.

String Indices

String에서 각 String 값에 접근할 때 사용되는 인덱스 타입은 String.Index입니다. 이는 문자열을 구성하는 각 Character들의 위치에 해당합니다.

 

위의 'Counting Characters' 에서 언급했듯이, 서로 다른 charater들을 저장할 때 각기 다른 메모리를 차지합니다.  따라서 Character가 어떤 특정 위치에 있는지 결정하기 위해선 문자열 처음부터 끝까지 유니코드 스칼라 값을 반복해야만 합니다. 이러한 이유로 Swift 문자열은 Intergar 값으로 인덱싱할 수 없습니다.

 

Swift 문자열에서 첫번째 Character에 접근하기 위해선 startIndext 속성을 사용하면 됩니다. 그리고 문자열 마지막 Character 이후의 위치에 접근하기 위해선 endIndex 속성을 사용하면 됩니다. 이 말은 endIndex 속성은 문자열 서브스크립트를 접근하기 위해선 유효하진 않다는 의미입니다. 그리고 만약 문자열이 비어있다면 startIndex 속성과 endIndex 속성은 동일합니다.

 

String의 index(before:) 및 index(after:) 메서드를 사용하여 주어진 인덱스의 앞뒤 인덱스에 액세스할 수 있습니다. 주어진 인덱스에서 더 멀리 떨어진 인덱스에 액세스하려면 이러한 메서드 중 하나를 여러 번 호출하는 대신 index(_:offsetBy:) 메서드를 사용할 수 있습니다.

let greeting = "Guten Tag!"
greeting[greeting.startIndex]
// G
greeting[greeting.index(before: greeting.endIndex)]
// !
greeting[greeting.index(after: greeting.startIndex)]
// u
let index = greeting.index(greeting.startIndex, offsetBy: 7)
greeting[index]
// a

 

문자열 범위를 벗어난 인덱스에 액세스하려고 하거나 문자열 범위를 벗어난 인덱스의 문자에 액세스하려고 하면 런타임 오류가 발생합니다.

greeting[greeting.endIndex] // Error
greeting.index(after: greeting.endIndex) // Error

 

indices 속성을 사용하면 문자열의 각의 character의 모든 인덱스에 접근이 가능합니다.

for index in greeting.indices {
    print("\(greeting[index]) ", terminator: "")
}
// Prints "G u t e n   T a g ! "

 

여기서 주목할 점은 startIndex 속성, endIndex 속성, index(before:), index(after:), index(_:offsetBy:) 메서드는 Collection 프로토코을 따라는 모든 타입에서 사용이 가능합니다. 이는 String, Array, Dictionary, Set 타입을 포함합니다.

Inserting and Removing

문자열의 지정된 인덱스에 단일 문자를 삽입하려면 insert(_:at:) 메서드를 사용하고, 다른 문자열의 내용을 지정된 인덱스에 삽입하려면 insert(contentsOf:at:) 메서드를 사용합니다.

var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)
// 현재 welcome의 값은 "hello!"

welcome.insert(contentsOf: " there", at: welcome.index(before: welcome.endIndex))
// 현재 welcome의 값은 "hello there!"

 

지정된 인덱스에서 문자열에서 단일 문자를 제거하려면 remove(at:) 메서드를 사용하고 지정된 범위에서 하위 문자열을 제거하려면 removeSubrange(_:) 메서드를 사용합니다.
welcome.remove(at: welcome.index(before: welcome.endIndex))
// 현재 welcome의 값은 "hello there"


let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex
welcome.removeSubrange(range)
// 현재 welcome의 값은 "hello"


여기서 주목할 점은 insert(_:at:), insert(contentsOf:at:), remove(at:), removeSubrange(_:) 매서드는 RangeReplaceableCollection 프로토콜을 따르는 모든 타입에서 사용 가능합니다. 이는 String을 포함하여 Array, Dictionary, Set에도 해당됩니다.

Substrings

문자열에서 하위 문자열을 가져오는 경우(예: 서브스크립트 또는 prefix(_:)와 같은 메서드 사용) 결과는 다른 문자열이 아니라 Substring의 인스턴스입니다. Swift의 substring들은 문자열과 거의 동일한 메서드를 가지고 있으므로 문자열을 다루는 것과 같은 방식으로 하위 문자열을 다룰 수 있습니다. 그러나 문자열과 달리 문자열에서 작업을 수행하는 동안 substring들을 짧은 시간 동안만 사용합니다. 결과를 더 오랫동안 저장할 땐 substring들을 String의 인스턴스로 변환합니다.

let greeting = "Hello, world!"
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex
let beginning = greeting[..<index]
// beginning은 "Hello"

// 장기간 저장을 위해 beginning을 String으로 변환
let newString = String(beginning)

 

문자열과 마찬가지로 각 substring들은 해당 substring들을 구성하는 character가 저장되는 메모리 영역에 있습니다. 문자열과 부분 문자열의 차이는 성능 최적화로서 부분 문자열이 원래 문자열을 저장하는 데 사용된 메모리의 일부나 다른 부분 문자열을 저장하는 데 사용된 메모리의 일부를 재사용할 수 있다는 것입니다. (문자열은 유사한 최적화를 가지고 있지만 만약 두 문자열이 같은 메모리를 공유한다면 이 두 문자열은 같습니다.) 이 성능 최적화는 문자열이나 부분 문자열이 수정될 때까지 메모리 복사에 대한 성능 비용을 지불할 필요가 없다는 말입니다. 위에서 언급했듯이, 부분 문자열은 장기 저장에는 적합하지 않습니다. 왜냐하면 부분 문자열들은 원본 문자열의 저장소를 재사용하기 때문입니다. 따라서 부분 문자열이 사용되는 동안에는 전체 원래 문자열을 메모리에 보관해야 합니다.

 

위의 예에서 greeting은 문자열입니다. 즉, 문자열을 구성하는 문자가 저장되는 메모리 영역이 있습니다. beginning은 greeting의 하위 문자열이므로 greeting이 사용하는 메모리를 재사용합니다. 반면 newString은 문자열입니다. 하위 문자열에서 생성되면 자체 저장소가 있습니다.

 

아래 그림은 위 코드의 각 변수들의 관계를 도식화한 것입니다.

 

주목할 점은  String과 Substring은 모두 StringProtocol 프로토콜을 따르므로 문자열 조작 함수가 StringProtocol 값을 허용하는 것이 편리한 경우가 많습니다. 이러한 함수는 String 또는 Substring 값으로 호출할 수 있습니다.

Comparing Strings

Swift는 텍스트 값을 비교하는 세 가지 방법을 제공합니다. 문자열과 문자 동일성, 접두사 동일성, 접미사 동일성입니다.

String and Character Equality

문자열과 문자의 동일성은 비교 연산자에서 설명한 대로 "같음" 연산자(==)와 "같지 않음" 연산자(!=)로 검사됩니다.

let quotation = "We're a lot alike, you and I."
let sameQuotation = "We're a lot alike, you and I."
if quotation == sameQuotation {
    print("These two strings are considered equal")
}
// "These two strings are considered equal" 출력

 

두 개의 String 값(또는 두 개의 Character 값)은 확장된 문자소 클러스터(Extended Grapheme Clusters)가 정규적으로 동등할 경우 동일한 것(canonically equivalent)으로 간주됩니다. 확장된 문자소 클러스터는 동일한 언어적 의미와 모양을 가질 경우 정규적으로 동등하며, 이는 백그라운드에서 서로 다른 유니코드 스칼라로 구성되었더라도 마찬가지입니다.

 

예를 들어,  LATIN SMALL LETTER E WITH ACUTE (U+00E9)(é) 은  LATIN SMALL LETTER E (U+0065)(e) 와 COMBINING ACUTE ACCENT (U+0301) (´)가 결합된 것은 정규적으로 동일합니다. 이 두 확장된 문자소 클러스터들은 모두 문자 é를 표현하는 유효한 방법이므로 표준적으로 동일한 것으로 간주됩니다.

// "Voulez-vous un café?"은 LATIN SMALL LETTER E WITH ACUTE 을 사용함
let eAcuteQuestion = "Voulez-vous un caf\u{E9}?"

// "Voulez-vous un café?"은 LATIN SMALL LETTER E and COMBINING ACUTE ACCENT 을 사용함
let combinedEAcuteQuestion = "Voulez-vous un caf\u{65}\u{301}?"

if eAcuteQuestion == combinedEAcuteQuestion {
    print("These two strings are considered equal")
}
// "These two strings are considered equal" 출력

 

반대로, 영어에서 사용되는 LATIN CAPITAL LETTER A (U+0041 또는 "A")는 러시아어에서 사용되는 CYRILLIC CAPITAL LETTER A (U+0410 또는 "А")와 동일하지 않습니다. 이 문자들은 시각적으로 유사하지만 언어적 의미는 동일하지 않습니다.

let latinCapitalLetterA: Character = "\u{41}"

let cyrillicCapitalLetterA: Character = "\u{0410}"

if latinCapitalLetterA != cyrillicCapitalLetterA {
    print("These two characters aren't equivalent.")
}
// "These two characters aren't equivalent." 출력

 

Swift에서 String과 Character 비교는 지역(Locale)에 따라 달라지지 않습니다.

Prefix and Suffix Equality

문자열에 특정 문자열 접두사나 접미사가 있는지 확인하려면 문자열의 hasPrefix(_:) 및 hasSuffix(_:) 메서드를 호출합니다. 둘 다 문자열 유형의 단일 인수를 사용하고 부울 값을 반환합니다.

 

단, hasPrefix(_:) 및 hasSuffix(_:) 메서드는 각 문자열의 확장된 문자소 클러스터 간의 문자별 정규화 동등성 비교를 수행합니다.

참고 사이트

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/stringsandcharacters (Swift 공식문서 - Strings and Characters)