-
Swift 공식문서 톺아보기 (3) - Strings and CharactersSwift 2025. 9. 16. 19:19
안녕하세요 :) 🧀
이번 포스팅에서는 공식문서의 Strings and Characters 섹션을 정독하면서,
그동안 헷갈렸던 부분이나 새롭게 알게 된 내용을 정리해보려 합니다.
🎯 문자열 리터럴의 이스케이프 문자
문자열 안에서는 바로 입력하기 어려운 특수 문자들을 이스케이프 시퀀스로 표현할 수 있습니다.
Swift에서는 \ (백슬래시)로 시작하는 형태로 작성합니다.
- \0 → null 문자
- \\ → 백슬래시
- \t → 탭
- \n → 줄 바꿈(개행)
- \r → 캐리지 리턴
- \” → 큰따옴표
- \’ → 작은따옴표
let wiseWords = "\"Imagination is more important than knowledge\" - Einstein" // "Imagination is more important than knowledge" - Einstein🎯 확장된 문자열 구분자
문자열 리터럴에 "(큰 따옴표) 앞에 #을 감싸면 이스케이프 문자가 실행되지 않고 문자 그대로 인식됩니다.
즉, \n 같은 문자가 줄 바꿈이 아니라 "\n"처럼 텍스트 그대로 들어갑니다.
print(#"Line 1\nLine 2"#) // Line 1\nLine 2멀티라인 문자열에서도 마찬가지로 #"""..."""# 형태로 작성하면 특수문자를 그대로 포함합니다.
let threeMoreDoubleQuotationMarks = #""" Here are three more double quotes: """ """# // Here are three more double quotes: """🎯 문자열의 값 타입(Value Type) 특징
1. 값 타입
- String은 struct 기반이기 때문에 값 타입입니다.
- 함수나 메서드에 전달하거나 다른 변수, 상수에 할당할 때, 원본이 아닌 복사본이 전달됩니다.
2. Copy by Default
- 복사본을 사용하기 때문에 원본이 예상치 못하게 수정될 걱정이 없습니다.
- 즉, 전달받은 곳에서 데이터에 대한 온전한 소유권을 가집니다.
3. 성능 최적화
- 컴파일러가 필요할 때만 실제 복사를 수행합니다.
- 불필요한 성능 저하는 없고, 값 타입의 안정성과 참조 타입 수준의 효율성을 동시에 얻습니다.
🎯 문자(Character) 다루기
문자열은 문자의 컬렉션이므로, for-in 루프로 각 문자를 하나씩 꺼낼 수 있습니다.
for character in "Dog!🐶" { print(character) } /* D o g ! 🐶 */문자 하나만 저장하려면 Character 타입을 직접 지정해야 합니다.
let exclamationMark: Character = "!" let exclamationMark = "!" // 이렇게 지정하면 String으로 추론여러 문자를 배열로 묶어서 String 생성자로 문자열을 만들 수 있습니다.
let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"] let catString = String(catCharacters) // Cat!🐱🎯 문자열과 문자 연결하기
+ 연산자로 두 문자열을 연결하는 방법은 많이 사용했었는데, 문자열 += 문자열
즉, 기존 문자열 변수에 다른 문자열을 추가하는 연산자 방식은 생소했습니다.
let string2 = " there" var instruction = "look over" instruction += string2 // look over there🎯 유니코드(Unicode)
- 전 세계 다양한 문자 체계(글자, 기호, 이모지 등)를 표준화해 표현하는 국제 규격
- 어떤 언어든 동일한 방식으로 저장, 읽기, 처리할 수 있게 해 줍니다.
Swift의 String과 Character 타입은 완전히 유니코드 호환 되어 있습니다.
🎯 유니코드 스칼라 값(Scalar Value)
문자나 기호를 고유하게 나타내는 21비트 정수 값입니다.
예를 들어 아래와 같이 표현됩니다.
- “a”는 U+0061 (LATIN SMALL LETTER A)
- “🐥”는 U+1F425 (FRONT-FACING BABY CHICK)
특징으로는 21비트 정수 값에서 모든 값이 해당 문자를 표현하는 데 사용되지는 않습니다.
일부 값은 UTF-16과 같은 인코딩용으로 남겨져 있습니다.
또한 실제로 문자에 할당된 값들은 보통 이름(Unicode Name)도 함께 가집니다.

https://www.compart.com/en/unicode/U+0061 Swift의 String은 내부적으로 이 유니코드 스칼라 값들의 집합으로 구성됩니다.
따라서 다양한 언어와 이모지를 안정적으로 표현 가능합니다.
import Foundation let text = "a🐥♥" for character in text { print("Character: \(character)") for scalar in character.unicodeScalars { print(" Scalar: \(scalar) → U+\(String(format: "%04X", scalar.value))") } } /* Character: a Scalar: a → U+0061 Character: 🐥 Scalar: 🐥 → U+1F425 Character: ♥ Scalar: ♥ → U+2665 */+ 갑자기 보다가 생각난 내용인데, 가끔씩 듀오링고를 하다 보면 아래와 같은 버그를 볼 수 있는데,
아마도 유니코드 값을 제대로 디코딩하지 않는 버그인 듯!?

🎯 EGC(Extended Grapheme Cluster)
Swift의 Character 타입의 각 인스턴스는 하나의 확장된 문자소 클러스터(Extended Grapheme Cluster, EGC)를 표현합니다.
EGC란 여러 개의 유니코드 스칼라가 결합하여 사람이 인식할 수 있는 하나의 글자 단위를 이루는 것을 의미합니다.
예를 들어, 한국어의 “한”은 아래 두 가지 방식으로 표현할 수 있습니다.
- 정규화된 단일 스칼라, "\u{D55C}” → 한
- 분해된 조합형, "\u{1112}\u{1161}\u{11AB}” → ㅎ + ㅏ + ㄴ
Swift에서 두 경우 모두 동일하게 하나의 Character(EGC)로 인식됩니다.
let precomposed: Character = "\u{D55C}" // 한 let decomposed: Character = "\u{1112}\u{1161}\u{11AB}" // ᄒ + ᅡ + ᆫ print(precomposed) // 한 print(decomposed) // 한 print(precomposed == decomposed) // true출력 결과를 보면, 단일 스칼라로 이루어진 “한”과 세 개의 스칼라가 조합된 “한”이 동일하게 처리됩니다.
이는 Swift가 내부적으로 스칼라의 개수와 관계없이 사람이 읽는 글자 단위(문자소 클러스터)를
기준으로 Character를 다루기 때문입니다.
즉, Character는 단순히 하나의 유니코드 스칼라가 아닌 사람이 인식하는 문자 단위를 보장합니다.
🎯 String Indices(문자열 인덱스)
Swift에서 String은 고유한 인덱스 타입인 String.Index를 사용합니다.
이 인덱스는 문자열의 각 문자가 차지하는 위치를 나타냅니다.
왜 일반 배열처럼 Int 인덱스를 사용하지 않을까?
문자열의 각 글자는 내부적으로 유니코드 스칼라 개수가 다를 수 있습니다.
예를 들어, 어떤 문자는 하나의 스칼라로만 표현되지만, 어떤 문자는 여러 개의 스칼라가 합쳐져 하나의 문자처럼 보이기도 합니다.
따라서 Swift는 단순히 정수(Int)로 인덱싱 하지 않고, 문자 단위(EGC)를 기준으로 안전하게 다룰 수 있도록 String.Index를 제공합니다.
주요 속성
- startIndex: 문자열의 첫 번째 문자의 위치
- endIndex: 마지막 문자 뒤의 위치 (유요한 인덱스가 아님)
- 빈 문자열이라면 startIndex == endIndex
인덱스 이동
- index(before:): 현재 인덱스의 이전 위치
- index(after:): 현재 인덱스의 다음 위치
- index(_:offsetBy:): 기준 인덱스로부터 지정한 거리만큼 떨어진 위치
let greeting = "Guten Tag!" print(greeting[greeting.startIndex]) // G print(greeting[greeting.index(before: greeting.endIndex)]) // ! print(greeting[greeting.index(after: greeting.startIndex)]) // u let index = greeting.index(greeting.startIndex, offsetBy: 7) print(greeting[index]) // a참고로 잘못된 인덱스를 접근하면 런타임 에러가 발생합니다.
🎯 모든 문자 순회
indices 프로퍼티를 사용하면 문자열의 모든 인덱스를 순회할 수 있습니다.
for inex in greeting.indices { print("\(greeting[inex])", terminator: "") } // Guten Tag!🎯 문자열 삽입과 삭제
Swift에서 String안에 문자를 삽입하거나 삭제할 때, String.Index를 사용해야 합니다.
Insert
- insert(_:at:) → 한 개의 Character를 삽입
- insert(contentsOf:at:) → String을 삽입
var welcome = "hello" welcome.insert("!", at: welcome.endIndex) // hello! welcome.insert(contentsOf: " there", at: welcome.index(before: welcome.endIndex)) // hello there!Remove
- remove(at:) → 특정 인덱스의 Character를 삭제
- removeSubrange(_:) → 특정 범위에 해당하는 부분 문자열을 삭제
welcome.remove(at: welcome.index(before: welcome.endIndex)) // hello there let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex welcome.removeSubrange(range) // hello🎯 Substrings(부분 문자열)
Swift에서 문자열에서 일부를 잘라내면(prefix, 서브스크립트 등), 그 결과는 String을 반환하는 게 아닌 Substring 타입입니다.
let greeting = "Hello, World!" let index = greeting.firstIndex(of: ",") ?? greeting.endIndex let beginning = greeting[..<index] print(beginning) // Hello // beginning의 타입은 Substring // String처럼 다루기위해 장기 보관하기 위해서는 아래처럼 변환해야 함 let newString = String(beginning)
Substring인 이유는?
- Substring은 String과 거의 동일한 메서드를 지원합니다.
- 하지만 Substring은 성능 최적화를 위해 원본 문자열의 메모리를 공유합니다.
- 즉, 복사 비용 없이 빠르게 부분 문자열을 얻을 수 있습니다.
Substring을 오래 변수에 담아놓고 보관하면 원본 문자열 전체가 메모리에 남아 있게 됩니다.
최대한 일시적인 연산용으로만 사용하고, 장기 저장이 필요하다면 반드시
String 타입으로 변환해야 합니다.

메모리 관점에서 보면,
- greeting은 독립적인 String 이므로 자체 메모리를 가집니다.
- beginning은 Substring 이므로 greeting의 메모리를 재사용합니다.
- newString은 String으로 변환되었기 때문에 자체 메모리를 가집니다.
🎯 문자열 비교
문자열을 비교하는 데는 세 가지 방법이 있습니다.
- 문자열 및 문자 동등성 (Equality)
- 접두사 비교 (Prefix Equality)
- 접미사 비교 (Suffix 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// "한" (단일 스칼라) let precomposed = "\u{D55C}" // "ㅎ" + "ㅏ" + "ㄴ" (분해된 조합형) let decomposed = "\u{1112}\u{1161}\u{11AB}" if precomposed == decomposed { print("이 두 문자열은 동일하게 취급됩니다.") } // 이 두 문자열은 동일하게 취급됩니다.두 문자열 또는 두 문자가 확장된 문자소 클러스터(EGC) 기준으로 정규적으로 동등하다면 같은 값으로 간주됩니다.
즉, 겉보기와 언어적 의미가 동일하다면, 내부적으로 어떤 유니코드 스칼라로 이루어졌든 상관없이 동일한 문자로 취급됩니다.
접두사와 접미사 비교
특정 문자열로 시작하는지 또는 끝나는지를 확인할 수 있는 메서드를 제공합니다.
- hasPrefix(_:) → 문자열이 지정한 접두사로 시작하면 true 반환
- hasSuffix(_:) → 문자열이 지정한 접미사로 끝나면 true 반환
두 메서드 모두 EGC 기준으로 정규적 동등성을 비교합니다.
🎯 문자열의 유니코드 표현
문자열이 파일에 저장되거나 전송될 때, 내부의 유니코드 스칼라 들은 특정 인코딩 방식에 따라 코드 유닛 단위로 변환됩니다.
대표적인 인코딩 방식은 아래와 같습니다.
- UTF-8 → 8비트(1바이트) 단위 코드 유닛
- UTF-16 → 16비트(2바이트) 단위 코드 유닛
- UTF-32 → 32비트(4바이트) 단위 코드 유닛
Swift의 String은 기본적으로 EGC 단위로 다루지만, 필요하다면 다른 Unicode 표현으로도 접근할 수 있습니다.
- string.utf8 → UTF-8 코드 유닛 컬렉션
- string.utf16 → UTF-16 코드 유닛 컬렉션
- string.unicodeScalars → 유니코드 스칼라(UTF-32와 대응)

UTF-16 표현 예시
마치며
앞부분의 내용은 복습 차원에서 다시 정리해 봤지만, 뒷부분의 유니코드와 EGC 관련 내용은
이번에 문서를 보면서 새롭게 알게 된 부분이 꽤 있었습니다.
앞으로 문자열을 다룰 때는 더 깊이 이해할 수 있을 것 같습니다👀
참조
Documentation
docs.swift.org
'Swift' 카테고리의 다른 글
Swift 공식문서 톺아보기 (4) - Collection Types (1) 2025.11.03 SPM 라이브러리 생성부터 GitHub 배포까지 (2) 2025.07.30 Swift 공식문서 톺아보기 (2) - 둘러보기 2 (0) 2025.06.18 Swift 공식문서 톺아보기 (1) - 둘러보기 1 (1) 2025.04.18