DaleSchool

컬렉션 깊이 이해하기: Vec · HashMap · String

중급20분

학습 목표

  • Vec<T>의 주요 메서드를 활용할 수 있다
  • HashMap<K, V>를 생성하고 entry API를 사용할 수 있다
  • String의 UTF-8 특성 때문에 인덱싱이 제한되는 이유를 설명할 수 있다
  • 컬렉션에서 소유권과 빌림이 어떻게 동작하는지 이해한다

모듈 06에서 Vec을 맛봤었죠? 이번에는 Vec, HashMap, String 세 가지 주요 컬렉션을 깊이 파고들어볼 거예요.

동작하는 코드

use std::collections::HashMap;

fn main() {
    // Vec — 순서가 있는 목록
    let mut scores = vec![85, 92, 78];
    scores.push(95);
    println!("점수: {:?}", scores);

    // HashMap — 키-값 저장소
    let mut contacts = HashMap::new();
    contacts.insert("민수", "010-1234-5678");
    contacts.insert("영희", "010-9876-5432");
    println!("민수: {}", contacts["민수"]);

    // String — UTF-8 텍스트
    let mut greeting = String::from("안녕");
    greeting.push_str("하세요!");
    println!("{greeting}");
}

Vec<T> 심화

유용한 메서드들

fn main() {
    let mut v = vec![10, 20, 30, 40, 50];

    // insert — 특정 위치에 삽입
    v.insert(2, 25); // 인덱스 2에 25 삽입
    println!("insert 후: {:?}", v); // [10, 20, 25, 30, 40, 50]

    // remove — 특정 위치 제거 (제거된 값 반환)
    let removed = v.remove(0);
    println!("remove: {removed}, 나머지: {:?}", v); // 10, [20, 25, 30, 40, 50]

    // retain — 조건을 만족하는 요소만 남기기
    v.retain(|x| *x >= 30);
    println!("retain 후: {:?}", v); // [30, 40, 50]

    // split_off — 특정 위치에서 분할
    let mut a = vec![1, 2, 3, 4, 5];
    let b = a.split_off(3);
    println!("a: {:?}, b: {:?}", a, b); // a: [1, 2, 3], b: [4, 5]

    // contains — 포함 여부
    println!("3 포함? {}", a.contains(&3)); // true

    // sort, dedup — 정렬과 중복 제거
    let mut nums = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
    nums.sort();
    nums.dedup();
    println!("정렬 + 중복 제거: {:?}", nums); // [1, 2, 3, 4, 5, 6, 9]
}

안전한 접근: get() vs 인덱스

fn main() {
    let v = vec![10, 20, 30];

    // 인덱스 접근 — 범위 밖이면 panic!
    println!("{}", v[1]); // 20
    // println!("{}", v[10]); // 💥 panic!

    // get() — Option을 반환하여 안전
    match v.get(10) {
        Some(val) => println!("값: {val}"),
        None => println!("인덱스가 범위 밖이에요"),
    }
}

HashMap<K, V>

기본 사용법

use std::collections::HashMap;

fn main() {
    let mut scores: HashMap<String, i32> = HashMap::new();

    // 삽입
    scores.insert(String::from("수학"), 95);
    scores.insert(String::from("영어"), 87);
    scores.insert(String::from("과학"), 92);

    // 조회 — get()은 Option<&V>를 반환
    match scores.get("수학") {
        Some(score) => println!("수학: {score}"),
        None => println!("수학 점수가 없어요"),
    }

    // 순회
    for (subject, score) in &scores {
        println!("{subject}: {score}점");
    }

    // 삭제
    scores.remove("영어");
    println!("영어 삭제 후: {:?}", scores);
}

entry API — Rust다운 "없으면 넣기" 패턴

HashMap에서 "키가 없으면 기본값을 넣고, 있으면 기존 값을 사용"하는 패턴이 자주 필요해요. entry() API가 이걸 깔끔하게 해줘요.

use std::collections::HashMap;

fn main() {
    let text = "hello world hello rust hello";
    let mut word_count: HashMap<&str, i32> = HashMap::new();

    // ❌ 이렇게 하면 매번 조건 검사가 필요해요
    // for word in text.split_whitespace() {
    //     if word_count.contains_key(word) {
    //         let count = word_count.get_mut(word).unwrap();
    //         *count += 1;
    //     } else {
    //         word_count.insert(word, 1);
    //     }
    // }

    // ✅ entry API — 한 줄로 해결!
    for word in text.split_whitespace() {
        let count = word_count.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{:?}", word_count);
    // {"hello": 3, "world": 1, "rust": 1}
}

entry(key).or_insert(default) — 키가 없으면 기본값을 넣고, 있든 없든 그 값의 가변 참조를 반환해요.

from()으로 간편하게 만들기

use std::collections::HashMap;

fn main() {
    // 배열로부터 HashMap 만들기
    let scores: HashMap<&str, i32> = HashMap::from([
        ("수학", 95),
        ("영어", 87),
        ("과학", 92),
    ]);
    println!("{:?}", scores);

    // 이터레이터의 collect()로도 가능
    let names = vec!["민수", "영희", "철수"];
    let ages = vec![25, 30, 28];
    let people: HashMap<&str, i32> = names
        .iter()
        .zip(ages.iter())
        .map(|(n, a)| (*n, *a))
        .collect();
    println!("{:?}", people);
}

String과 UTF-8

String은 인덱싱이 안 돼요!

다른 언어에서 s[0]으로 첫 글자를 가져오는 게 당연하죠? Rust에서는 안 돼요.

fn main() {
    let hello = String::from("안녕하세요");
    // let first = hello[0]; // ❌ 컴파일 에러!
    println!("바이트 길이: {}", hello.len()); // 15 (5글자 × 3바이트)
}

"왜?" — UTF-8 때문이에요

String은 내부적으로 UTF-8 바이트의 시퀀스예요. 한글은 글자 하나가 3바이트예요.

fn main() {
    let s = String::from("가나다");

    // 바이트로 보기 — 9바이트!
    println!("바이트: {:?}", s.as_bytes());

    // 문자(char)로 보기 — 3글자
    for c in s.chars() {
        println!("문자: {c}");
    }

    // 첫 글자 가져오기
    let first = s.chars().next().unwrap();
    println!("첫 글자: {first}"); // 가

    // 슬라이싱은 바이트 경계에서만 가능
    let slice = &s[0..3]; // "가" (3바이트)
    println!("슬라이스: {slice}");
    // let bad = &s[0..2]; // 💥 panic! 글자 중간을 잘라서
}

| 표현 | "가나다" | "abc" | | ---------------------------- | ------------ | ----------- | | .len() (바이트) | 9 | 3 | | .chars().count() (글자 수) | 3 | 3 | | s[0] | ❌ 불가 | ❌ 불가 | | .chars().next() | Some('가') | Some('a') |

문자열 만들기와 합치기

fn main() {
    // 여러 방법으로 만들기
    let s1 = String::from("hello");
    let s2 = "hello".to_string();
    let s3 = format!("안녕, {}!", "세상");

    // 합치기
    let greeting = format!("{} {}", s1, s2);
    println!("{greeting}");

    // push_str로 추가
    let mut s = String::from("hello");
    s.push_str(", world");
    s.push('!'); // 한 글자는 push
    println!("{s}");
}

컬렉션과 소유권

컬렉션에서 소유권이 어떻게 동작하는지 주의해야 해요.

Vec에 넣으면 소유권이 이동해요

fn main() {
    let name = String::from("민수");
    let mut names = Vec::new();

    names.push(name);
    // println!("{name}"); // ❌ name은 Vec로 이동됨

    // 해결: 참조를 저장하거나, clone()을 사용
    let name2 = String::from("영희");
    names.push(name2.clone()); // clone으로 복사본 넣기
    println!("{name2}"); // ✅ 원본은 살아있어요
}

HashMap 키의 소유권

use std::collections::HashMap;

fn main() {
    let key = String::from("이름");
    let value = String::from("민수");

    let mut map = HashMap::new();
    map.insert(key, value);

    // println!("{key}");   // ❌ 소유권이 HashMap으로 이동
    // println!("{value}"); // ❌ 마찬가지

    // &str을 키로 쓰면 소유권 문제 없음
    let mut map2: HashMap<&str, &str> = HashMap::new();
    let k = "이름";
    map2.insert(k, "민수");
    println!("{k}"); // ✅ &str은 Copy 트레이트가 있어요
}

빌림 규칙 주의

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0]; // 불변 참조
    // v.push(6);       // ❌ 불변 참조가 있는데 수정하려 함!
    println!("첫 번째: {first}");

    // first 사용이 끝난 후에는 OK
    v.push(6); // ✅
    println!("{:?}", v);
}

모듈 14에서 배운 빌림 규칙이 그대로 적용돼요! Vecpush()로 메모리를 재할당하면 기존 참조가 무효가 될 수 있어서 Rust가 막아줘요.

연습 1. entry API를 사용해서, 문장의 각 글자가 몇 번 나오는지 세는 프로그램을 만들어보세요. (공백은 제외)

use std::collections::HashMap;

fn char_count(text: &str) -> HashMap<char, usize> {
    // 여기를 완성하세요
    // 힌트: text.chars()로 순회, 공백은 건너뛰기
}

fn main() {
    let counts = char_count("hello world");
    println!("{:?}", counts);
    // {'h': 1, 'e': 1, 'l': 3, 'o': 2, 'w': 1, 'r': 1, 'd': 1}
}

연습 2. 아래 함수를 완성하세요. 한국어 문자열의 첫 N글자를 반환해요.

fn take_chars(s: &str, n: usize) -> String {
    // 여기를 완성하세요
    // 힌트: s.chars().take(n).collect()
}

fn main() {
    let text = "안녕하세요, Rust!";
    println!("{}", take_chars(text, 3)); // "안녕하"
    println!("{}", take_chars(text, 10)); // "안녕하세요, Rus"
}

연습 3 (도전). 학생별 점수를 HashMap<String, Vec<i32>>로 관리하는 프로그램을 만들어보세요. add_score 함수와 average 함수를 완성하세요.

use std::collections::HashMap;

fn add_score(records: &mut HashMap<String, Vec<i32>>, name: &str, score: i32) {
    // 여기를 완성하세요
    // 힌트: entry API + or_insert_with(Vec::new)
}

fn average(records: &HashMap<String, Vec<i32>>, name: &str) -> Option<f64> {
    // 여기를 완성하세요
    // 점수가 없으면 None 반환
}

fn main() {
    let mut records = HashMap::new();
    add_score(&mut records, "민수", 90);
    add_score(&mut records, "민수", 85);
    add_score(&mut records, "영희", 95);

    println!("민수 평균: {:?}", average(&records, "민수"));   // Some(87.5)
    println!("영희 평균: {:?}", average(&records, "영희"));   // Some(95.0)
    println!("철수 평균: {:?}", average(&records, "철수"));   // None
}

Q1. Rust에서 String에 인덱스(s[0])로 접근할 수 없는 이유는?

  • A) String이 불변이라서
  • B) 성능이 느려서 금지한 것
  • C) UTF-8 인코딩에서 한 글자가 여러 바이트일 수 있어서
  • D) Rust에 문자 타입이 없어서

Q2. HashMapentry(key).or_insert(default)는 무엇을 하나요?

  • A) 항상 default를 덮어쓴다
  • B) 키가 있으면 에러를 발생시킨다
  • C) 키가 없으면 default를 넣고, 있든 없든 값의 가변 참조를 반환한다
  • D) 키가 있으면 삭제하고 다시 넣는다

Q3. 아래 코드에서 에러가 나는 이유는?

fn main() {
    let name = String::from("민수");
    let mut names = Vec::new();
    names.push(name);
    println!("{name}");
}
  • A) Vec에 String을 넣을 수 없다
  • B) names가 가변이 아니라서
  • C) push()로 name의 소유권이 Vec로 이동해서 더 이상 사용할 수 없다
  • D) println!에 String을 넣을 수 없다