DaleSchool

문자열 깊이 이해하기: String과 &str

중급15분

학습 목표

  • String과 &str의 차이를 설명할 수 있다
  • String을 생성하고 조작할 수 있다 (String::from(), push_str(), format!)
  • 문자열 슬라이스(&str)가 무엇인지 이해한다
  • 함수에서 문자열을 효율적으로 전달하는 방법을 선택할 수 있다

동작하는 코드

지금까지 문자열을 두 가지 방식으로 사용했어요. 차이를 눈으로 확인해볼게요.

fn main() {
    // 방식 1: 큰따옴표만 쓰기
    let greeting = "안녕하세요";

    // 방식 2: String::from() 사용
    let mut name = String::from("민수");
    name.push_str("님");

    println!("{greeting}, {name}!");
}

"안녕하세요"는 그냥 쓸 수 있는데, 왜 어떤 때는 String::from()을 써야 할까요?

직접 수정하기

  1. greetingpush_str("!") 을 추가해보세요. 어떤 에러가 나나요?
  2. namegreeting처럼 let name = "민수";로 바꾸고, push_str을 시도해보세요. 무슨 일이 일어나나요?

"왜?" — 두 가지 문자열이 필요한 이유

소유권 관점에서 보면 명확해요.

| | String | &str | | --------- | ------------------- | ---------------------------------------- | | 비유 | 내가 주인인 문자열 | 빌린 문자열 | | 저장 위치 | 힙 (크기 변경 가능) | 읽기 전용 메모리 또는 다른 String의 일부 | | 수정 가능 | mut이면 가능 | 불가 | | 소유권 | 있음 | 없음 (참조일 뿐) |

"안녕하세요"처럼 코드에 직접 쓴 문자열은 &str 타입이에요. 프로그램 바이너리에 포함되어 있고, 수정할 수 없어요. 빌려 쓰는 거예요.

String::from("안녕하세요")는 그 내용을 힙에 복사해서 내 것으로 만든 거예요. 주인이 있으니까 수정도 가능해요.

String 만드는 방법들

fn main() {
    // 방법 1: String::from()
    let s1 = String::from("hello");

    // 방법 2: .to_string()
    let s2 = "hello".to_string();

    // 방법 3: format! 매크로
    let name = "민수";
    let age = 25;
    let s3 = format!("{name}님은 {age}살");

    println!("{s1}, {s2}, {s3}");
}

세 방법 모두 String 타입을 만들어요. format!은 여러 값을 조합할 때 편해요.

String 수정하기

fn main() {
    let mut s = String::from("안녕");

    s.push_str("하세요");     // 문자열 추가
    s.push('!');              // 문자 하나 추가

    println!("{s}");           // "안녕하세요!"
    println!("길이: {}", s.len()); // 바이트 수
}

push_str()&str을 받아서 뒤에 붙여요. push()char 하나를 추가해요.

문자열 슬라이스 — 일부분만 빌리기

&str은 문자열의 일부분을 가리킬 수도 있어요.

fn main() {
    let sentence = String::from("hello world");

    let hello = &sentence[0..5];   // "hello"
    let world = &sentence[6..11];  // "world"

    println!("{hello}, {world}");
}

&sentence[0..5]sentence의 0번째부터 5번째 바이트 직전까지를 빌리는 거예요. 새로운 String을 만드는 게 아니라, 원본의 일부를 참조하는 거예요.

함수에서 문자열 받기 — &str이 더 유연해요

함수 매개변수로 문자열을 받을 때는 &str이 좋아요.

fn greet(name: &str) {
    println!("안녕, {name}!");
}

fn main() {
    let owned = String::from("민수");
    let literal = "지수";

    greet(&owned);    // String을 &str로 전달 가능!
    greet(literal);   // &str도 당연히 가능!
}

&str을 매개변수로 쓰면 String이든 문자열 리터럴이든 둘 다 받을 수 있어요. &String을 매개변수로 쓰면 String만 받을 수 있고요. 그래서 &str이 더 유연한 선택이에요.

더 알아보기: String과 &str의 변환

String&str 사이를 오가는 방법이에요.

fn main() {
    // &str -> String
    let s: String = "hello".to_string();
    let s2: String = String::from("hello");

    // String -> &str
    let borrowed: &str = &s;      // 자동 변환
    let slice: &str = s.as_str(); // 명시적 변환

    println!("{borrowed}, {slice}");
}

String에서 &str로 바꾸는 건 그냥 빌리는 거라 비용이 없어요. 반대로 &str에서 String으로 바꾸는 건 힙에 복사하는 거라 비용이 들어요.

연습 1. 아래 함수를 완성하세요. 이름과 나이를 받아서 인사 문자열을 반환해요.

fn make_greeting(name: &str, age: i32) -> String {
    // "안녕, {name}님! {age}살이시군요!" 형태로 반환
    // 힌트: format! 매크로를 사용하세요
}

fn main() {
    let msg = make_greeting("민수", 25);
    println!("{msg}");
}

연습 2. 아래 코드의 에러를 고쳐보세요. 가능하면 clone()을 사용하지 않고 해결하세요.

fn print_length(s: String) {
    println!("길이: {}", s.len());
}

fn main() {
    let word = String::from("Rust");
    print_length(word);
    println!("단어: {word}"); // 에러!
}

힌트: 함수가 소유권을 가져가지 않으려면 어떻게 해야 할까요? 매개변수 타입을 바꿔보세요.

Q1. String&str의 가장 큰 차이는?

  • A) String은 숫자를 저장하고, &str은 문자를 저장한다
  • B) String은 소유권이 있고 수정 가능하며, &str은 빌린 참조다
  • C) String은 느리고, &str은 빠르다
  • D) 차이가 없다, 같은 타입이다

Q2. 함수 매개변수로 &str을 쓰면 좋은 이유는?

  • A) 속도가 더 빠르다
  • B) String&str 모두 받을 수 있어서 유연하다
  • C) 에러가 발생하지 않는다
  • D) 문자열을 수정할 수 있다

Q3. 아래 코드의 결과는?

fn main() {
    let s = String::from("hello world");
    let word = &s[0..5];
    println!("{word}");
}
  • A) "hello world"
  • B) "hello"
  • C) "h"
  • D) 컴파일 에러