← Rust 강의 목록으로
🦀
소유권
소유권 · 선수: 08강 · 소유권 · 선수: 09강

09–10. 라이프타임 입문 · String 과 &str 깊게 — UTF-8·인덱싱

라이프타임은 참조가 유효한 기간을 표현하는 컴파일 타임 마커입니다. 'a 같은 표기로 명시하지만, 대부분의 경우 컴파일러가 라이프타임 생략(elision) 규칙으로 자동 추론해 줍니다. 이 강의에서 라이프타임이 왜 필요한지, 언제 명시해야 하는지, 그리고 'static 의 의미까지 정리합니다. Rust 의 문자열은 UTF-8 인코딩으로 저장됩니다. String 은 힙에 사는 가변 소유 문자열, &str 은 그 일부에 대한 참조형 뷰. 문자열 인덱싱이 왜 정수로는 안 되고 .chars() 이터레이터를 거쳐야 하는지, 그리고 UTF-8 의 가변 길이 인코딩 특성을 직관적으로 짚습니다.

Rustlifetime'aelisionstatic참조String&strUTF-8유니코드chars
소요 시간
⏱ 약 1.5시간 + 약 1.5시간
난이도
📊 중급
선수 조건
🎯 08강
결과물
라이프타임은 참조가 유효한 기간을 표현하는 컴파일 타임 마커입니다. 'a 같은 표기로 명시하지만, 대부분의 경우 컴파일러가 라이프타임 생략(elision) 규칙으로 자동 추론해 줍니다. 이 강의에서 라이프타임이 왜 필요한지, 언제 명시해야 하는지, 그리고 'static 의 의미까지 정리합니다. / Rust 의 문자열은 UTF-8 인코딩으로 저장됩니다. String 은 힙에 사는 가변 소유 문자열, &str 은 그 일부에 대한 참조형 뷰. 문자열 인덱싱이 왜 정수로는 안 되고 .chars() 이터레이터를 거쳐야 하는지, 그리고 UTF-8 의 가변 길이 인코딩 특성을 직관적으로 짚습니다.

이 강의에서 배우는 것

  • 1왜 라이프타임이 필요한지 dangling reference 예시로 안다
  • 2'a 표기를 함수 시그니처와 struct 에 붙인다
  • 3라이프타임 생략 3규칙을 안다
  • 4'static 의 의미를 구분한다
  • 5라이프타임 에러 메시지를 읽고 해결한다
  • 6String 과 &str 의 메모리 표현 차이를 설명한다
  • 7String 을 만들고 push_str·push·+ 로 연장한다
  • 8UTF-8 가변 길이 때문에 정수 인덱싱이 막힌 이유를 안다
  • 9.chars() / .bytes() / .char_indices() 의 차이를 안다
  • 10format! 매크로로 문자열을 조합한다

09. 라이프타임 입문 ('a 명시·생략 규칙)

라이프타임은 참조가 유효한 기간을 표현하는 컴파일 타임 마커입니다. 'a 같은 표기로 명시하지만, 대부분의 경우 컴파일러가 라이프타임 생략(elision) 규칙으로 자동 추론해 줍니다. 이 강의에서 라이프타임이 왜 필요한지, 언제 명시해야 하는지, 그리고 'static 의 의미까지 정리합니다.

소개

참조가 가리키는 데이터가 사라지면 참조는 더 이상 유효하지 않습니다 — dangling reference. Rust 는 이걸 컴파일 타임에 막기 위해 모든 참조에 라이프타임을 추적합니다. 대부분 자동이라 평소엔 의식할 일이 없지만, 함수가 참조를 인자로 받아 참조를 반환할 때 등에서 명시가 필요해집니다.

핵심 개념

1) 라이프타임이 푸는 문제

rust
fn main() {
    let r;
    {
        let x = 5;
        r = &x;   // x 가 곧 사라질 텐데?
    } // x drop
    println!("{}", r); // error: `x` does not live long enough
}

2) 라이프타임 명시 'a

함수가 참조를 받아 참조를 반환할 때 컴파일러가 둘의 관계를 알아야 합니다. `'a` 는 임의의 이름(보통 a, b, c)으로 같은 라이프타임을 가짐을 의미.

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

3) 라이프타임 생략 3규칙

  1. 각 참조 인자는 자기만의 라이프타임을 부여받음
  2. 인자가 하나뿐이면 그 라이프타임이 반환값에도 부여됨
  3. 메서드에서 첫 인자가 `&self` 또는 `&mut self` 면 self 의 라이프타임이 반환값에 부여됨

이 세 규칙으로 추론이 끝나면 명시 생략 가능 — 그래서 평소 코드에서 'a 가 거의 안 보입니다.

4) 'static — 프로그램 전체 수명

`&'static str` 은 프로그램이 살아있는 동안 유효한 참조. 문자열 리터럴이 그 예.

핵심 예제

라이프타임 명시가 필요한 경우 — 두 참조 중 하나를 반환:

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("short");
        result = longest(s1.as_str(), s2.as_str());
        println!("longest = {}", result);
    } // s2 drop — result 는 여기서만 유효
}

struct 가 참조 필드를 가지면 라이프타임 필수:

rust
struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let e = Excerpt { part: first_sentence };
    println!("{}", e.part);
}

라이프타임 생략 — 컴파일러가 자동으로 채워주는 경우:

rust
// 생략 가능 — 인자 하나, 규칙 2 적용
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &b) in bytes.iter().enumerate() {
        if b == b' ' { return &s[0..i]; }
    }
    s
}
// 풀어쓰면: fn first_word<'a>(s: &'a str) -> &'a str { ... }

자주 하는 실수

Q. 라이프타임을 어디까지 명시해야 하나요?

A. 컴파일러가 시킬 때만요. 생략 규칙으로 추론 안 되는 경우에만 에러가 나고, 그때 명시하면 됩니다. 처음부터 'a 를 남발할 필요 없습니다.

Q. 'static 을 붙이면 만능 아닌가요?

A. 아닙니다. 'static 은 "프로그램 전체 수명" 이라는 강한 제약이라 짧은 수명의 참조는 통과 못 합니다. 일반 함수 시그니처에 'static 을 강제하면 호출자 자유가 크게 줄어듭니다.

Q. 라이프타임이 다른 두 참조를 어떻게 반환하나요?

A. `<'a, 'b>` 로 둘 다 적고 `'b: 'a` 같은 outlives 제약을 걸거나, 보통은 둘 중 짧은 쪽으로 통일합니다. 라이프타임 산수가 복잡해지면 대개 설계가 잘못된 신호.

정리

  • 라이프타임은 참조의 유효 기간을 컴파일러에 알려주는 마커
  • 대부분 생략 3규칙으로 자동 추론됨
  • 함수가 참조 ↔ 참조를 매핑할 때 명시 필요
  • 'static 은 프로그램 전체 수명 — 만능 카드 아님

과제

  1. longest 함수를 두 개의 다른 라이프타임 'a, 'b 로 시도해보고 어떤 에러가 나는지 확인
  2. struct Book<'a> { title: &'a str, author: &'a str } 를 만들어 사용
  3. dangling reference 가 발생할 코드를 일부러 작성하고 컴파일러 메시지를 옮겨 적기

10. String 과 &str 깊게 — UTF-8·인덱싱

Rust 의 문자열은 UTF-8 인코딩으로 저장됩니다. String 은 힙에 사는 가변 소유 문자열, &str 은 그 일부에 대한 참조형 뷰. 문자열 인덱싱이 왜 정수로는 안 되고 .chars() 이터레이터를 거쳐야 하는지, 그리고 UTF-8 의 가변 길이 인코딩 특성을 직관적으로 짚습니다.

소개

다른 언어에서 `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 가변 길이

글자바이트 수예
ASCII1'a' = 0x61
한글·한자3'한' = 0xED 0x95 0x9C
일부 이모지4'😀' = 0xF0 0x9F 0x98 0x80

그래서 `s[0]` 은 "첫 바이트" 인지 "첫 글자" 인지 모호 → 컴파일 에러로 막아둠.

4) 순회 방법

  • **.chars()** — 글자(char) 단위
  • **.bytes()** — 바이트(u8) 단위
  • **.char_indices()** — (바이트 인덱스, char) 쌍

핵심 예제

String 만들기·붙이기:

rust
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() 로 글자 단위 처리:

rust
fn main() {
    let s = "안녕 hi😀";
    println!("바이트 길이: {}", s.len());          // 13
    println!("글자 수: {}", s.chars().count());     // 6
    for c in s.chars() { print!("[{}]", c); }
    println!();
}

format! — 새 String 을 만드는 편리한 매크로:

rust
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 방법

과제

  1. 한글 한 문장을 받아 글자 수와 바이트 수를 비교해 출력
  2. 역순 문자열을 만드는 fn reverse(s: &str) -> String 작성 (.chars().rev())
  3. 공백으로 문자열을 분리해 단어별로 첫 글자만 대문자로 만드는 함수 작성
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.

댓글은 회원만 쓸 수 있습니다.

광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침