문자열과 문자
문자열과 문자 (Strings and Characters)
Swift에서 텍스트를 다루는 일은 대부분 String 타입에서 시작해요. 이번 챕터에서는 문자열 리터럴을 만드는 문법부터 시작해서, 여러 줄 문자열, 문자 하나하나를 다루는 방법, 문자열을 비교하고 유니코드로 표현하는 방법까지 하나씩 살펴볼게요. 코드를 따라가며 실제로 어떤 값이 만들어지는지 확인하다 보면, Swift의 문자열이 얼마나 직관적이면서도 정교하게 설계됐는지 자연스럽게 느껴질 거예요.
본문
*문자열(string)*은 "hello, world"나 "albatross" 같은 일련의 문자들을 말해요. Swift에서 문자열은 String 타입으로 표현되고, 그 내용물은 여러 방식으로 접근할 수 있는데, 그중 하나가 Character 값의 모음으로 다루는 방법이에요.
Swift의 String과 Character 타입은 코드에서 텍스트를 빠르고 유니코드에 부합하는 방식으로 다루게 해 줘요. 문자열을 만들고 조작하는 문법은 C와 비슷한 문자열 리터럴 덕분에 가볍고 읽기 쉬워요. 두 문자열을 + 연산자로 이어 붙이는 것처럼 문자열 병합은 단순하고, 수정 가능 여부는 Swift의 다른 값들과 마찬가지로 상수냐 변수냐를 골라서 관리해요. 또 문자열 안에 상수, 변수, 리터럴, 표현식을 끼워 넣을 수도 있는데, 이 과정을 문자열 보간(string interpolation)이라고 불러요. 덕분에 화면에 보여주거나 저장하거나 출력할 문자열 값을 손쉽게 만들 수 있어요.
문법이 단순하다고 해서 String 타입이 단순한 건 아니에요. Swift의 String은 빠르고 현대적인 문자열 구현이에요. 모든 문자열은 인코딩과 무관한 유니코드 문자들로 구성되어 있고, 그 문자들을 여러 유니코드 표현 방식으로 접근할 수 있게 지원해요.
Note: Swift의
String타입은 Foundation의NSString클래스와 브리징(bridging)돼요. Foundation은 또String을 확장해서NSString이 정의한 메서드를 드러내 주기도 해요. 그래서 Foundation을 import 했다면 형 변환 없이도String에서 그NSString메서드들을 쓸 수 있어요. Foundation이나 Cocoa에서String을 쓰는 방법에 대한 자세한 내용은 Bridging Between String and NSString을 참고하세요.
문자열 리터럴 (String Literals)
코드 안에 미리 정의해 둔 String 값을 *문자열 리터럴(string literal)*로 넣을 수 있어요. 문자열 리터럴은 큰따옴표(")로 둘러싼 문자들의 연속이에요.
문자열 리터럴을 상수나 변수의 초깃값으로 사용해 볼게요.
let someString = "Some string literal value"
여기서 Swift는 someString 상수가 문자열 리터럴 값으로 초기화됐기 때문에 타입을 String으로 추론해요.
여러 줄 문자열 리터럴 (Multiline String Literals)
여러 줄에 걸친 문자열이 필요하다면 세 개의 큰따옴표로 둘러싼 여러 줄 문자열 리터럴(multiline string literal)을 사용해요.
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."
"""
여러 줄 문자열 리터럴은 여는 따옴표와 닫는 따옴표 사이에 있는 모든 줄을 포함해요. 문자열은 여는 따옴표(""") 다음의 첫 번째 줄에서 시작하고, 닫는 따옴표가 있는 줄의 바로 앞에서 끝나요. 그래서 아래 두 문자열은 모두 줄 바꿈으로 시작하지도 끝나지도 않아요.
let singleLineString = "These are the same."
let multilineString = """
These are the same.
"""
소스 코드의 여러 줄 문자열 리터럴 안에 줄 바꿈을 넣으면, 그 줄 바꿈이 문자열 값에도 그대로 나타나요. 줄 바꿈을 소스 코드를 읽기 쉽게 만드는 용도로만 쓰고 문자열 값에는 포함시키고 싶지 않다면, 그 줄들의 끝에 백슬래시(\)를 적어 주면 돼요.
let softWrappedQuotation = """
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."
"""
여러 줄 문자열 리터럴을 줄 바꿈으로 시작하거나 끝나게 하려면, 첫 줄이나 마지막 줄을 비워 두면 돼요. 예를 들어 볼게요.
let lineBreaks = """
This string starts with a line break.
It also ends with a line break.
"""
여러 줄 문자열은 주변 코드에 맞춰 들여쓸 수 있어요. 닫는 따옴표(""") 앞의 공백은 Swift에게 다른 모든 줄 앞에서 무시할 공백이 얼마인지 알려 줘요. 하지만 닫는 따옴표 앞 공백보다 여분으로 줄 앞에 들여쓰기를 적으면, 그 여분의 공백은 포함돼요.
위 예시에서 여러 줄 문자열 리터럴 전체가 들여쓰기 되어 있어도, 문자열의 첫 줄과 마지막 줄은 공백으로 시작하지 않아요. 가운데 줄은 닫는 따옴표보다 더 들여쓰기 되어 있어서, 그 여분의 네 칸 들여쓰기로 시작하게 돼요.
문자열 리터럴의 특수 문자 (Special Characters in String Literals)
문자열 리터럴은 다음과 같은 특수 문자를 포함할 수 있어요.
- 이스케이프된 특수 문자
\0(null 문자),\\(백슬래시),\t(가로 탭),\n(줄 바꿈),\r(캐리지 리턴),\"(큰따옴표),\'(작은따옴표) \u{n}로 적는 임의의 유니코드 스칼라 값. 여기서 n은 1~8자리 16진수예요. (유니코드는 아래 doc:StringsAndCharacters#Unicode에서 다뤄요)
아래 코드는 이런 특수 문자들이 네 가지 쓰인 예시를 보여 줘요. wiseWords 상수에는 두 개의 이스케이프된 큰따옴표가 들어 있고, dollarSign, blackHeart, sparklingHeart 상수는 유니코드 스칼라 형식을 보여 줘요.
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
여러 줄 문자열 리터럴은 큰따옴표 하나 대신 세 개를 쓰기 때문에, 큰따옴표(")를 이스케이프 없이 그대로 넣을 수 있어요. 여러 줄 문자열 안에 """라는 텍스트를 넣으려면 따옴표 하나 이상을 이스케이프해 주면 돼요. 예를 들어 볼게요.
let threeDoubleQuotationMarks = """
Escaping the first quotation mark \"""
Escaping all three quotation marks \"\"\"
"""
확장된 문자열 구분자 (Extended String Delimiters)
문자열 리터럴을 확장된 구분자(extended delimiters) 안에 넣으면, 특수 문자의 효과를 발동시키지 않고 문자열에 포함할 수 있어요. 문자열을 따옴표(") 안에 넣고, 그 주위를 숫자 기호(#)로 감싸면 돼요. 예를 들어 #"Line 1\nLine 2"#이라는 문자열 리터럴을 출력하면, 문자열을 두 줄로 나눠 출력하는 대신 줄 바꿈 이스케이프 시퀀스(\n)가 그대로 출력돼요.
문자열 리터럴 안에서 어떤 문자의 특수 효과가 필요하다면, 이스케이프 문자(\) 뒤에 문자열 안의 숫자 기호 개수와 맞춰서 적어 주면 돼요. 예를 들어 문자열이 #"Line 1\nLine 2"#일 때 줄을 끊고 싶다면 #"Line 1\#nLine 2"#을 쓰면 되고, ###"Line1\###nLine2"### 역시 줄을 끊어 줘요.
확장된 구분자로 만든 문자열 리터럴은 여러 줄 문자열 리터럴이 될 수도 있어요. 확장된 구분자로 여러 줄 문자열 안에 """ 텍스트를 넣으면, 리터럴을 끝내는 기본 동작을 덮어쓸 수 있어요. 예를 들어 볼게요.
let threeMoreDoubleQuotationMarks = #"""
Here are three more double quotes: """
"""#
빈 문자열 초기화 (Initializing an Empty String)
더 긴 문자열을 만들기 위한 출발점으로 빈 String 값을 만들려면, 빈 문자열 리터럴을 변수에 할당하거나 이니셜라이저 문법으로 새 String 인스턴스를 초기화하면 돼요.
var emptyString = "" // empty string literal
var anotherEmptyString = String() // initializer syntax
// these two strings are both empty, and are equivalent to each other
String 값이 비어 있는지는 Boolean 타입의 isEmpty 프로퍼티로 확인할 수 있어요.
if emptyString.isEmpty {
print("Nothing to see here")
}
// Prints "Nothing to see here".
문자열 가변성 (String Mutability)
특정 String을 수정할 수 있는지(즉 mutation될 수 있는지)는 변수에 할당했는지(그러면 수정 가능), 아니면 상수에 할당했는지(그러면 수정 불가)로 나타내요.
var variableString = "Horse"
variableString += " and carriage"
// variableString is now "Horse and carriage"
let constantString = "Highlander"
constantString += " and another Highlander"
// this reports a compile-time error - a constant string cannot be modified
Note: 이 방식은 문자열 수정 여부를 나타내기 위해 두 클래스(
NSString과NSMutableString) 중 하나를 골라야 했던 Objective-C와 Cocoa에서의 문자열 mutation과 달라요.
문자열은 값 타입 (Strings Are Value Types)
Swift의 String 타입은 *값 타입(value type)*이에요. 새 String 값을 만들면 그 값은 함수나 메서드로 전달될 때, 또는 상수나 변수에 할당될 때 *복사(copied)*돼요. 어떤 경우든 기존 String 값의 새 복사본이 만들어지고, 원본이 아니라 그 새 복사본이 전달되거나 할당돼요. 값 타입에 대한 자세한 내용은 doc:ClassesAndStructures#Structures-and-Enumerations-Are-Value-Types에서 다뤄요.
Swift의 기본 복사 동작 덕분에, 함수나 메서드가 String 값을 넘겨주면 그 값이 어디서 왔든 그 정확한 String 값을 내가 소유한다는 게 분명해져요. 넘겨받은 문자열은 내가 직접 수정하지 않는 한 수정되지 않는다고 확신할 수 있어요.
내부적으로 Swift 컴파일러는 문자열 사용을 최적화해서, 복사가 꼭 필요한 경우에만 실제 복사가 일어나게 해요. 그래서 문자열을 값 타입으로 다룰 때 항상 훌륭한 성능을 얻을 수 있어요.
문자 다루기 (Working with Characters)
String의 개별 Character 값은 for-in 반복문으로 문자열을 순회하면서 접근할 수 있어요.
for character in "Dog!🐶" {
print(character)
}
// D
// o
// g
// !
// 🐶
for-in 반복문은 doc:ControlFlow#For-In-Loops에서 다뤄요.
대신 단일 문자 문자열 리터럴에 Character 타입 어노테이션을 붙여 독립적인 Character 상수나 변수를 만들 수도 있어요.
let exclamationMark: Character = "!"
Character 값들의 배열을 이니셜라이저 인자로 전달하면 String 값을 만들 수 있어요.
let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"]
let catString = String(catCharacters)
print(catString)
// Prints "Cat!🐱".
문자열과 문자 병합 (Concatenating Strings and Characters)
String 값은 덧셈 연산자(+)로 더해서(즉 concatenate해서) 새 String 값을 만들 수 있어요.
let string1 = "hello"
let string2 = " there"
var welcome = string1 + string2
// welcome now equals "hello there"
덧셈 할당 연산자(+=)로 기존 String 변수에 String 값을 덧붙일 수도 있어요.
var instruction = "look over"
instruction += string2
// instruction now equals "look over there"
String 타입의 append() 메서드로 String 변수에 Character 값을 덧붙일 수 있어요.
let exclamationMark: Character = "!"
welcome.append(exclamationMark)
// welcome now equals "hello there!"
Note: 기존
Character변수에는String이나Character를 덧붙일 수 없어요.Character값은 반드시 단일 문자만 담아야 하기 때문이에요.
여러 줄 문자열 리터럴로 더 긴 문자열의 줄들을 만들고 있다면, 마지막 줄을 포함해 문자열의 모든 줄이 줄 바꿈으로 끝나기를 원할 거예요. 예를 들어 볼게요.
let badStart = """
one
two
"""
let end = """
three
"""
print(badStart + end)
// Prints two lines:
// one
// twothree
let goodStart = """
one
two
"""
print(goodStart + end)
// Prints three lines:
// one
// two
// three
위 코드에서 badStart와 end를 병합하면 두 줄짜리 문자열이 나오는데, 이건 원하는 결과가 아니에요. badStart의 마지막 줄이 줄 바꿈으로 끝나지 않아서, 그 줄이 end의 첫 번째 줄과 합쳐지기 때문이에요. 반면 goodStart의 두 줄 모두 줄 바꿈으로 끝나므로 end와 합치면 기대한 대로 세 줄짜리 결과가 나와요.
문자열 보간 (String Interpolation)
*문자열 보간(string interpolation)*은 문자열 리터럴 안에 상수, 변수, 리터럴, 표현식의 값들을 포함시켜 새 String 값을 만드는 방법이에요. 문자열 보간은 한 줄 문자열 리터럴과 여러 줄 문자열 리터럴 모두에서 쓸 수 있어요. 문자열 리터럴에 끼워 넣는 각 항목은 백슬래시(\)를 앞에 붙인 괄호 한 쌍으로 감싸요.
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"
위 예시에서 multiplier의 값은 \(multiplier)로 문자열 리터럴에 삽입돼요. 이 자리표시자는 문자열 보간이 평가되어 실제 문자열을 만들 때 multiplier의 실제 값으로 대체돼요.
multiplier의 값은 문자열 안의 더 큰 표현식에도 포함돼 있어요. 이 표현식은 Double(multiplier) * 2.5의 값을 계산해서 그 결과(7.5)를 문자열에 삽입해요. 이 경우 그 표현식은 문자열 리터럴 안에 \(Double(multiplier) * 2.5)로 적혀요.
확장된 문자열 구분자를 쓰면 문자열 보간으로 취급될 문자들을 그대로 담은 문자열을 만들 수 있어요. 예를 들어 볼게요.
print(#"Write an interpolated string in Swift using \(multiplier)."#)
// Prints "Write an interpolated string in Swift using \(multiplier)."
확장된 구분자를 쓰는 문자열 안에서 문자열 보간을 사용하려면, 백슬래시 뒤의 숫자 기호 개수를 문자열 시작과 끝의 숫자 기호 개수와 맞춰 주면 돼요. 예를 들어 볼게요.
print(#"6 times 7 is \#(6 * 7)."#)
// Prints "6 times 7 is 42."
Note: 보간 문자열 안의 괄호 안에 적는 표현식에는 이스케이프되지 않은 백슬래시(
\), 캐리지 리턴, 줄 바꿈이 들어갈 수 없어요. 단, 다른 문자열 리터럴은 넣을 수 있어요.
유니코드 (Unicode)
*유니코드(Unicode)*는 서로 다른 문자 체계의 텍스트를 인코딩하고 표현하고 처리하기 위한 국제 표준이에요. 유니코드를 쓰면 거의 모든 언어의 거의 모든 문자를 표준화된 형태로 표현할 수 있고, 텍스트 파일이나 웹 페이지 같은 외부 소스와 그 문자들을 주고받을 수 있어요. Swift의 String과 Character 타입은 이 절에서 설명하듯 완전히 유니코드를 준수해요.
유니코드 스칼라 값 (Unicode Scalar Values)
내부적으로 Swift의 네이티브 String 타입은 *유니코드 스칼라 값(Unicode scalar value)*들로 구성돼요. 유니코드 스칼라 값은 문자나 수식자에 대한 고유한 21비트 숫자로, LATIN SMALL LETTER A("a")를 뜻하는 U+0061이나 FRONT-FACING BABY CHICK("🐥")을 뜻하는 U+1F425 같은 값이에요.
모든 21비트 유니코드 스칼라 값이 문자에 배정된 건 아니라는 점을 기억할게요. 일부 스칼라는 미래 배정이나 UTF-16 인코딩에서의 사용을 위해 예약되어 있어요. 문자에 배정된 스칼라 값은 대개 이름까지 가지는데, 위 예시의 LATIN SMALL LETTER A와 FRONT-FACING BABY CHICK이 그런 경우예요.
확장된 자소 클러스터 (Extended Grapheme Clusters)
Swift Character 타입의 모든 인스턴스는 단일 *확장된 자소 클러스터(extended grapheme cluster)*를 나타내요. 확장된 자소 클러스터는 하나 이상의 유니코드 스칼라가 (결합되어) 사람이 읽을 수 있는 하나의 문자를 만들어 내는 연속이에요.
예시를 하나 볼게요. é라는 글자는 단일 유니코드 스칼라 é(LATIN SMALL LETTER E WITH ACUTE, 즉 U+00E9)로 표현할 수 있어요. 그런데 같은 글자를 스칼라 한 쌍으로 표현할 수도 있는데, 표준 글자 e(LATIN SMALL LETTER E, 즉 U+0065) 뒤에 COMBINING ACUTE ACCENT 스칼라(U+0301)가 따라오는 형태예요. COMBINING ACUTE ACCENT 스칼라는 앞에 오는 스칼라에 그래픽으로 적용되어, 유니코드를 아는 텍스트 렌더링 시스템이 그릴 때 e를 é로 만들어 줘요.
두 경우 모두 é라는 글자는 확장된 자소 클러스터를 나타내는 단일 Swift Character 값으로 표현돼요. 첫 번째 경우 클러스터는 스칼라 하나를, 두 번째 경우 스칼라 두 개를 담고 있어요.
let eAcute: Character = "\u{E9}" // é
let combinedEAcute: Character = "\u{65}\u{301}" // e followed by ́
// eAcute is é, combinedEAcute is é
확장된 자소 클러스터는 많은 복잡한 문자를 단일 Character 값으로 표현할 수 있는 유연한 방법이에요. 예를 들어 한글 음절은 미리 조합된(precomposed) 형태나 분해된(decomposed) 형태로 표현할 수 있는데, 둘 다 Swift에서는 단일 Character 값으로 인정돼요.
let precomposed: Character = "\u{D55C}" // 한
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}" // ᄒ, ᅡ, ᆫ
// precomposed is 한, decomposed is 한
확장된 자소 클러스터는 둘러싸는 기호(enclosing mark)(COMBINING ENCLOSING CIRCLE, 즉 U+20DD 같은)의 스칼라가 다른 유니코드 스칼라를 단일 Character 값의 일부로 둘러싸게 해 줘요.
let enclosedEAcute: Character = "\u{E9}\u{20DD}"
// enclosedEAcute is é⃝
지역 표시 기호(regional indicator symbol)용 유니코드 스칼라는 쌍으로 결합해 단일 Character 값을 만들 수 있어요. REGIONAL INDICATOR SYMBOL LETTER U(U+1F1FA)와 REGIONAL INDICATOR SYMBOL LETTER S(U+1F1F8)의 조합이 그런 예시예요.
let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}"
// regionalIndicatorForUS is 🇺🇸
문자 세기 (Counting Characters)
문자열 안의 Character 값 개수를 얻으려면 문자열의 count 프로퍼티를 사용해요.
let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
print("unusualMenagerie has \(unusualMenagerie.count) characters")
// Prints "unusualMenagerie has 40 characters".
Swift가 Character 값에 확장된 자소 클러스터를 사용한다는 점은, 문자열 병합이나 수정이 문자열의 문자 수에 영향을 주지 않을 수도 있다는 뜻이에요.
예를 들어 네 글자 단어 cafe로 새 문자열을 초기화한 뒤, 문자열 끝에 COMBINING ACUTE ACCENT(U+0301)를 덧붙여도 결과 문자열의 문자 수는 여전히 4이고, 네 번째 문자는 e가 아니라 é가 돼요.
var word = "cafe"
print("the number of characters in \(word) is \(word.count)")
// Prints "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)")
// Prints "the number of characters in café is 4".
Note: 확장된 자소 클러스터는 여러 유니코드 스칼라로 구성될 수 있어요. 이 말은 서로 다른 문자들, 그리고 같은 문자의 서로 다른 표현들이 저장에 서로 다른 양의 메모리를 요구할 수 있다는 뜻이에요. 그래서 Swift의 문자들은 문자열 표현 안에서 각각 같은 양의 메모리를 차지하지 않아요. 그 결과 문자열의 문자 수는 문자열을 순회하며 확장된 자소 클러스터 경계를 알아내기 전에는 계산할 수 없어요. 특히 긴 문자열 값을 다룬다면
count프로퍼티가 그 문자열의 문자를 알아내려고 문자열 전체의 유니코드 스칼라를 반복해야 한다는 점을 염두에 둘게요.count프로퍼티가 돌려주는 문자 수와 같은 문자들을 담은NSString의length프로퍼티가 항상 같지는 않아요.NSString의 길이는 문자열의 UTF-16 표현 안의 16비트 코드 유닛 수에 기반하고, 문자열 안의 유니코드 확장 자소 클러스터 수에는 기반하지 않아요.
문자열 접근과 수정 (Accessing and Modifying a String)
문자열에는 메서드와 프로퍼티, 또는 아래 첨자(subscript) 문법으로 접근하고 수정할 수 있어요.
문자열 인덱스 (String Indices)
각 String 값에는 문자열 안의 각 Character 위치에 해당하는 인덱스 타입(index type), String.Index가 연관되어 있어요.
아까 언급했듯 서로 다른 문자는 저장에 서로 다른 양의 메모리를 요구할 수 있어서, 특정 위치에 어떤 Character가 있는지 알아내려면 그 String의 시작이나 끝에서부터 각 유니코드 스칼라를 반복해야 해요. 이런 이유로 Swift 문자열은 정수 값으로 인덱싱할 수 없어요.
startIndex 프로퍼티로 String의 첫 Character의 위치에 접근하고, endIndex 프로퍼티는 String의 마지막 문자 다음 위치예요. 그래서 endIndex 프로퍼티는 문자열의 아래 첨자에 유효한 인자가 아니에요. String이 비어 있으면 startIndex와 endIndex는 같아요.
주어진 인덱스의 앞뒤 인덱스에는 String의 index(before:)와 index(after:) 메서드로 접근할 수 있어요. 주어진 인덱스에서 더 멀리 떨어진 인덱스에 접근하려면 이 메서드들을 여러 번 호출하는 대신 index(_:offsetBy:) 메서드를 쓰면 돼요.
아래 첨자 문법으로 특정 String 인덱스의 Character에 접근할 수 있어요.
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
문자열의 범위를 벗어난 인덱스, 또는 문자열의 범위를 벗어난 인덱스의 Character에 접근하려 하면 런타임 에러가 발생해요.
greeting[greeting.endIndex] // Error
greeting.index(after: greeting.endIndex) // Error
indices 프로퍼티로 문자열에 있는 개별 문자들의 모든 인덱스에 접근할 수 있어요.
for index in greeting.indices {
print("\(greeting[index]) ", terminator: "")
}
// Prints "G u t e n T a g ! ".
Note:
Collection프로토콜을 준수하는 모든 타입에서startIndex,endIndex프로퍼티와index(before:),index(after:),index(_:offsetBy:)메서드를 쓸 수 있어요. 여기서 보여 준String뿐 아니라Array,Dictionary,Set같은 컬렉션 타입도 포함돼요.
삽입과 제거 (Inserting and Removing)
지정된 인덱스에 단일 문자를 문자열에 삽입하려면 insert(_:at:) 메서드를, 지정된 인덱스에 다른 문자열의 내용을 삽입하려면 insert(contentsOf:at:) 메서드를 사용해요.
var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)
// welcome now equals "hello!"
welcome.insert(contentsOf: " there", at: welcome.index(before: welcome.endIndex))
// welcome now equals "hello there!"
지정된 인덱스에서 문자열에서 단일 문자를 제거하려면 remove(at:) 메서드를, 지정된 범위의 부분 문자열을 제거하려면 removeSubrange(_:) 메서드를 사용해요.
welcome.remove(at: welcome.index(before: welcome.endIndex))
// welcome now equals "hello there"
let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex
welcome.removeSubrange(range)
// welcome now equals "hello"
Note:
RangeReplaceableCollection프로토콜을 준수하는 모든 타입에서insert(_:at:),insert(contentsOf:at:),remove(at:),removeSubrange(_:)메서드를 쓸 수 있어요. 여기서 보여 준String뿐 아니라Array,Dictionary,Set같은 컬렉션 타입도 포함돼요.
부분 문자열 (Substrings)
문자열에서 부분 문자열(substring)을 얻을 때(예를 들어 아래 첨자나 prefix(_:) 같은 메서드를 쓸 때), 그 결과는 또 다른 문자열이 아니라 Substring 인스턴스예요. Swift의 부분 문자열은 문자열과 대부분 같은 메서드를 갖고 있어서, 문자열과 같은 방식으로 다룰 수 있어요. 하지만 문자열과 달리 부분 문자열은 문자열에 대해 작업하는 짧은 시간 동안만 써요. 결과를 더 오래 저장할 준비가 되면 부분 문자열을 String 인스턴스로 변환해요. 예를 들어 볼게요.
let greeting = "Hello, world!"
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex
let beginning = greeting[..<index]
// beginning is "Hello"
// Convert the result to a String for long-term storage.
let newString = String(beginning)
문자열과 마찬가지로 각 부분 문자열에는 부분 문자열을 구성하는 문자들이 저장되는 메모리 영역이 있어요. 문자열과 부분 문자열의 차이는, 성능 최적화로서 부분 문자열이 원본 문자열을 저장하는 데 쓰인 메모리의 일부, 또는 다른 부분 문자열을 저장하는 데 쓰인 메모리의 일부를 재사용할 수 있다는 점이에요. (문자열에도 비슷한 최적화가 있지만, 두 문자열이 메모리를 공유하면 서로 같아요.) 이런 성능 최적화 덕분에 문자열이나 부분 문자열을 수정하기 전까지는 메모리 복사 성능 비용을 지불하지 않아도 돼요. 앞서 말했듯 부분 문자열은 장기 저장에 적합하지 않아요. 원본 문자열의 저장을 재사용하기 때문에, 부분 문자열 중 하나라도 사용되는 동안에는 원본 문자열 전체가 메모리에 유지되어야 해요.
위 예시에서 greeting은 문자열이라서, 문자열을 구성하는 문자들이 저장되는 메모리 영역을 가져요. beginning은 greeting의 부분 문자열이므로 greeting이 쓰는 메모리를 재사용해요. 반면 newString은 문자열이라서, 부분 문자열에서 만들어질 때 자기만의 저장 공간을 가져요. 아래 그림이 이 관계들을 보여 줘요.
Note:
String과Substring모두StringProtocol프로토콜을 준수해요. 그래서 문자열 조작 함수가StringProtocol값을 받아들이면 편리한 경우가 많아요. 그런 함수는String이나Substring값 어느 쪽으로도 호출할 수 있어요.
문자열 비교 (Comparing Strings)
Swift는 텍스트 값을 비교하는 세 가지 방법을 제공해요. 문자열과 문자 동등성, 접두어 동등성, 접미어 동등성이에요.
문자열과 문자 동등성 (String and Character Equality)
문자열과 문자 동등성은 "equal to" 연산자(==)와 "not equal to" 연산자(!=)로 확인해요. doc:BasicOperators#Comparison-Operators에서 설명하는 것처럼요.
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")
}
// Prints "These two strings are considered equal".
두 String 값(또는 두 Character 값)은 그 확장된 자소 클러스터가 *정규적으로 동등(canonically equivalent)*하면 같다고 간주돼요. 확장된 자소 클러스터는 내부적으로 서로 다른 유니코드 스칼라로 구성됐어도 언어적 의미와 모양이 같으면 정규적으로 동등해요.
예를 들어 LATIN SMALL LETTER E WITH ACUTE(U+00E9)는 LATIN SMALL LETTER E(U+0065) 뒤에 COMBINING ACUTE ACCENT(U+0301)가 따라오는 것과 정규적으로 동등해요. 두 확장 자소 클러스터 모두 é라는 문자를 나타내는 유효한 방법이라 정규적으로 동등한 것으로 간주돼요.
// "Voulez-vous un café?" using LATIN SMALL LETTER E WITH ACUTE
let eAcuteQuestion = "Voulez-vous un caf\u{E9}?"
// "Voulez-vous un café?" using 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")
}
// Prints "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.")
}
// Prints "These two characters aren't equivalent."
Note: Swift의 문자열과 문자 비교는 로케일(locale)에 민감하지 않아요.
접두어와 접미어 동등성 (Prefix and Suffix Equality)
문자열이 특정 문자열 접두어나 접미어를 갖고 있는지 확인하려면 문자열의 hasPrefix(_:)와 hasSuffix(_:) 메서드를 호출하면 돼요. 두 메서드 모두 String 타입 인자 하나를 받아 Boolean 값을 돌려줘요.
아래 예시들은 셰익스피어의 로미오와 줄리엣 앞의 두 막에 나오는 장소들을 표현하는 문자열 배열을 다뤄요.
let romeoAndJuliet = [
"Act 1 Scene 1: Verona, A public place",
"Act 1 Scene 2: Capulet's mansion",
"Act 1 Scene 3: A room in Capulet's mansion",
"Act 1 Scene 4: A street outside Capulet's mansion",
"Act 1 Scene 5: The Great Hall in Capulet's mansion",
"Act 2 Scene 1: Outside Capulet's mansion",
"Act 2 Scene 2: Capulet's orchard",
"Act 2 Scene 3: Outside Friar Lawrence's cell",
"Act 2 Scene 4: A street in Verona",
"Act 2 Scene 5: Capulet's mansion",
"Act 2 Scene 6: Friar Lawrence's cell"
]
romeoAndJuliet 배열과 hasPrefix(_:) 메서드로 연극 1막의 장면 수를 세어 볼 수 있어요.
var act1SceneCount = 0
for scene in romeoAndJuliet {
if scene.hasPrefix("Act 1 ") {
act1SceneCount += 1
}
}
print("There are \(act1SceneCount) scenes in Act 1")
// Prints "There are 5 scenes in Act 1".
비슷하게 hasSuffix(_:) 메서드로 Capulet 저택과 Friar Lawrence의 방을 중심으로 또는 그 주변에서 일어나는 장면 수를 세어 볼게요.
var mansionCount = 0
var cellCount = 0
for scene in romeoAndJuliet {
if scene.hasSuffix("Capulet's mansion") {
mansionCount += 1
} else if scene.hasSuffix("Friar Lawrence's cell") {
cellCount += 1
}
}
print("\(mansionCount) mansion scenes; \(cellCount) cell scenes")
// Prints "6 mansion scenes; 2 cell scenes".
Note:
hasPrefix(_:)와hasSuffix(_:)메서드는 각 문자열의 확장된 자소 클러스터 사이에서 문자 단위로 정규적 동등성 비교를 수행해요. doc:StringsAndCharacters#String-and-Character-Equality에서 설명한 것처럼요.
문자열의 유니코드 표현 (Unicode Representations of Strings)
유니코드 문자열을 텍스트 파일이나 다른 저장소에 쓸 때, 그 문자열의 유니코드 스칼라는 유니코드가 정의한 여러 인코딩 형식(encoding form) 중 하나로 인코딩돼요. 각 형식은 문자열을 *코드 유닛(code unit)*이라 불리는 작은 조각으로 인코딩해요. 여기에는 문자열을 8비트 코드 유닛으로 인코딩하는 UTF-8 인코딩 형식, 16비트 코드 유닛으로 인코딩하는 UTF-16 인코딩 형식, 32비트 코드 유닛으로 인코딩하는 UTF-32 인코딩 형식이 포함돼요.
Swift는 문자열의 유니코드 표현에 접근하는 여러 다른 방법을 제공해요. for-in 문으로 문자열을 순회해 개별 Character 값을 유니코드 확장 자소 클러스터로 접근할 수 있어요. 이 과정은 doc:StringsAndCharacters#Working-with-Characters에서 다뤄요.
대신 String 값에 다음 세 가지 유니코드 준수 표현 중 하나로 접근할 수도 있어요.
- UTF-8 코드 유닛의 모음 (문자열의
utf8프로퍼티로 접근) - UTF-16 코드 유닛의 모음 (문자열의
utf16프로퍼티로 접근) - 문자열의 UTF-32 인코딩 형식과 동등한 21비트 유니코드 스칼라 값의 모음 (문자열의
unicodeScalars프로퍼티로 접근)
아래 각 예시는 D, o, g, ‼(DOUBLE EXCLAMATION MARK, 즉 유니코드 스칼라 U+203C), 그리고 🐶 문자(DOG FACE, 즉 유니코드 스칼라 U+1F436)로 구성된 다음 문자열의 서로 다른 표현을 보여 줘요.
let dogString = "Dog‼🐶"
UTF-8 표현 (UTF-8 Representation)
String의 UTF-8 표현은 그 utf8 프로퍼티를 순회하며 접근할 수 있어요. 이 프로퍼티는 String.UTF8View 타입으로, 문자열 UTF-8 표현의 각 바이트마다 하나씩 있는 부호 없는 8비트(UInt8) 값들의 모음이에요.
for codeUnit in dogString.utf8 {
print("\(codeUnit) ", terminator: "")
}
print("")
// Prints "68 111 103 226 128 188 240 159 144 182 ".
위 예시에서 처음 세 개의 십진 codeUnit 값(68, 111, 103)은 D, o, g 문자를 나타내는데, 이 문자들의 UTF-8 표현은 ASCII 표현과 같아요. 다음 세 개의 십진 codeUnit 값(226, 128, 188)은 DOUBLE EXCLAMATION MARK 문자의 3바이트 UTF-8 표현이에요. 마지막 네 개의 codeUnit 값(240, 159, 144, 182)은 DOG FACE 문자의 4바이트 UTF-8 표현이에요.
UTF-16 표현 (UTF-16 Representation)
String의 UTF-16 표현은 그 utf16 프로퍼티를 순회하며 접근할 수 있어요. 이 프로퍼티는 String.UTF16View 타입으로, 문자열 UTF-16 표현의 각 16비트 코드 유닛마다 하나씩 있는 부호 없는 16비트(UInt16) 값들의 모음이에요.
for codeUnit in dogString.utf16 {
print("\(codeUnit) ", terminator: "")
}
print("")
// Prints "68 111 103 8252 55357 56374 ".
다시 처음 세 개의 codeUnit 값(68, 111, 103)은 D, o, g 문자를 나타내는데, 이 문자들의 UTF-16 코드 유닛은 문자열 UTF-8 표현과 같은 값이에요. (이 유니코드 스칼라들이 ASCII 문자를 나타내기 때문이에요.)
네 번째 codeUnit 값(8252)은 16진수 값 203C의 십진값으로, DOUBLE EXCLAMATION MARK 문자용 유니코드 스칼라 U+203C를 나타내요. 이 문자는 UTF-16에서 단일 코드 유닛으로 표현될 수 있어요.
다섯 번째와 여섯 번째 codeUnit 값(55357, 56374)은 DOG FACE 문자의 UTF-16 서러게이트 쌍(surrogate pair) 표현이에요. 이 값들은 high-surrogate 값 U+D83D(십진값 55357)와 low-surrogate 값 U+DC36(십진값 56374)이에요.
유니코드 스칼라 표현 (Unicode Scalar Representation)
String 값의 유니코드 스칼라 표현은 그 unicodeScalars 프로퍼티를 순회하며 접근할 수 있어요. 이 프로퍼티는 UnicodeScalarView 타입으로, UnicodeScalar 타입 값들의 모음이에요.
각 UnicodeScalar는 그 스칼라의 21비트 값을 UInt32 값으로 돌려주는 value 프로퍼티를 가져요.
for scalar in dogString.unicodeScalars {
print("\(scalar.value) ", terminator: "")
}
print("")
// Prints "68 111 103 8252 128054 ".
처음 세 개의 UnicodeScalar 값의 value 프로퍼티(68, 111, 103)는 다시 한 번 D, o, g 문자를 나타내요.
네 번째 codeUnit 값(8252)은 역시 16진수 값 203C의 십진값으로, DOUBLE EXCLAMATION MARK 문자용 유니코드 스칼라 U+203C를 나타내요.
다섯 번째이자 마지막 UnicodeScalar의 value 프로퍼티인 128054는 16진수 값 1F436의 십진값으로, DOG FACE 문자용 유니코드 스칼라 U+1F436을 나타내요.
그 value 프로퍼티를 조회하는 대안으로, 각 UnicodeScalar 값을 문자열 보간처럼 사용해 새 String 값을 만드는 데 쓸 수도 있어요.
for scalar in dogString.unicodeScalars {
print("\(scalar) ")
}
// D
// o
// g
// ‼
// 🐶
더 알아보기
- 문자열 일반: 텍스트 값의 생성·조작 문법이 가볍고 읽기 쉬운 이유는 문자열 리터럴이 C와 비슷하기 때문이에요. 여러 줄 문자열, 특수 문자, 확장된 구분자를 갖춰 리터럴 표현력이 매우 넓어요.
- 값 타입 성격:
String은 값 타입이라 전달·할당 때 복사되는 게 기본이지만, 컴파일러 최적화 덕분에 실제 복사는 꼭 필요할 때만 일어나요. - 유니코드 처리: 문자는 확장된 자소 클러스터 단위로 취급돼요. 그래서
count가 자소 클러스터 수를 세고, 문자열 비교는 정규적 동등성에 기반하며, 정수 인덱스 대신String.Index를 써야 해요. - 부분 문자열:
Substring은 원본 문자열의 메모리를 재사용하므로 짧은 시간 동안만 쓰고, 오래 저장할 때는String으로 변환해요. - 여러 유니코드 표현:
utf8,utf16,unicodeScalars프로퍼티로 같은 문자열을 각각 UTF-8, UTF-16, 유니코드 스칼라 관점에서 순회할 수 있어요.
원문: The Swift Programming Language — Strings and Characters (Swift.org 공식 문서, Apache License 2.0)