10. String 과 &str 깊게 — UTF-8·인덱싱
Rust 의 문자열은 UTF-8 인코딩으로 저장됩니다. String 은 힙에 사는 가변 소유 문자열, &str 은 그 일부에 대한 참조형 뷰. 문자열 인덱싱이 왜 정수로는 안 되고 .chars() 이터레이터를 거쳐야 하는지, 그리고 UTF-8 의 가변 길이 인코딩 특성을 직관적으로 짚습니다.
이 강의에서 배우는 것
- 1String 과 &str 의 메모리 표현 차이를 설명한다
- 2String 을 만들고 push_str·push·+ 로 연장한다
- 3UTF-8 가변 길이 때문에 정수 인덱싱이 막힌 이유를 안다
- 4.chars() / .bytes() / .char_indices() 의 차이를 안다
- 5format! 매크로로 문자열을 조합한다
소개
다른 언어에서 `s[0]` 으로 첫 글자를 가져오는 게 익숙하다면, Rust 는 처음에 답답하게 느껴질 수 있습니다 — 그게 안 됩니다. 이유는 단순합니다: UTF-8 은 한 글자가 1~4바이트 가변 길이라, 정수 인덱스가 "바이트" 인지 "글자" 인지 본질적으로 모호합니다. Rust 는 그 모호함을 회피하기 위해 의도적으로 인덱싱을 막아두고 .chars() 같은 명시적 도구를 줍니다.
핵심 개념
1) String 의 메모리 표현
String 은 내부적으로 `Vec<u8>` 입니다 — 힙에 사는 가변 크기 바이트 벡터. UTF-8 으로 인코딩된 바이트열이 들어있고, 메서드를 통해서만 안전하게 접근합니다.
2) &str 의 메모리 표현
(데이터 포인터, 바이트 길이) 의 fat pointer. 어디든 가리킬 수 있음 — String 의 일부, 'static 리터럴, 다른 사람의 메모리.
3) UTF-8 가변 길이
| 글자 | 바이트 수 | 예 |
|---|---|---|
| ASCII | 1 | 'a' = 0x61 |
| 한글·한자 | 3 | '한' = 0xED 0x95 0x9C |
| 일부 이모지 | 4 | '😀' = 0xF0 0x9F 0x98 0x80 |
그래서 `s[0]` 은 "첫 바이트" 인지 "첫 글자" 인지 모호 → 컴파일 에러로 막아둠.
4) 순회 방법
- **.chars()** — 글자(char) 단위
- **.bytes()** — 바이트(u8) 단위
- **.char_indices()** — (바이트 인덱스, char) 쌍
핵심 예제
String 만들기·붙이기:
fn main() {
let mut s = String::new();
s.push_str("hello");
s.push(' ');
s.push_str("world");
println!("{}", s); // hello world
let a = String::from("안녕, ");
let b = String::from("세상!");
let c = a + &b; // a 는 move, b 는 빌림
println!("{}", c);
}.chars() 로 글자 단위 처리:
fn main() {
let s = "안녕 hi😀";
println!("바이트 길이: {}", s.len()); // 13
println!("글자 수: {}", s.chars().count()); // 6
for c in s.chars() { print!("[{}]", c); }
println!();
}format! — 새 String 을 만드는 편리한 매크로:
fn main() {
let name = "Rust";
let n = 22;
let msg = format!("{} 강의 {}편", name, n);
println!("{}", msg);
}자주 하는 실수
Q. s[0] 으로 첫 글자를 못 가져오나요?
A. 못 합니다. `.chars().next().unwrap()` 또는 `.chars().nth(0).unwrap()`. ASCII 만 다룬다고 확신하면 `.as_bytes()[0]` 으로 첫 바이트도 가능.
Q. + 로 두 String 을 더했더니 첫 변수가 사라졌어요
A. `+` 연산은 좌변의 소유권을 가져갑니다(move). 양쪽을 다 살리려면 `format!("{}{}", a, b)` 또는 `.clone()` 사용.
Q. 한글 문자열의 길이가 이상해요
A. `.len()` 은 **바이트 길이** 입니다. 글자 수는 `.chars().count()`. 한국어 처리에서 이걸 혼동하면 인덱스 panic 의 원인이 됩니다.
정리
- String 은 힙의 가변 Vec<u8>, &str 은 fat pointer 뷰
- UTF-8 가변 길이라 정수 인덱싱은 의도적으로 막힘
- .chars() / .bytes() / .char_indices() 로 명시적 순회
- format! 매크로가 String 조합의 idiomatic 방법
과제
- 한글 한 문장을 받아 글자 수와 바이트 수를 비교해 출력
- 역순 문자열을 만드는 fn reverse(s: &str) -> String 작성 (.chars().rev())
- 공백으로 문자열을 분리해 단어별로 첫 글자만 대문자로 만드는 함수 작성