All posts

Rust 초급 9편: 벡터·해시맵·문자열로 학습 기록 모으기

지금까지는 기록 하나를 주로 다뤘습니다. 이번에는 여러 번의 학습 시간을 모으고 주제별 합계를 계산합니다. 순서가 있는 목록에는 Vec, 이름으로 찾는 집계에는 HashMap을 사용해 보겠습니다.

상자와 서랍에 값을 정리하는 게로 벡터와 해시맵을 표현한 일러스트

지금까지는 기록 하나를 주로 다뤘습니다. 이번에는 여러 번의 학습 시간을 모으고 주제별 합계를 계산합니다. 순서가 있는 목록에는 Vec, 이름으로 찾는 집계에는 HashMap을 사용해 보겠습니다.

준비와 목표

이전 편의 for, String, Option을 사용합니다. 새 프로젝트를 만든 뒤 핵심 코드를 src/main.rs에 저장합니다.

cargo new study_collections
cd study_collections

목표는 값 추가·조회·순회를 직접 해 보고, 문자열의 바이트 길이와 문자 순회를 구분하는 것입니다.

핵심 실습: 주제별 학습 시간 합산하기

use std::collections::HashMap;

fn main() {
    let mut sessions: Vec<(&str, u32)> = vec![
        ("Rust", 25),
        ("Rust", 35),
        ("CLI", 20),
    ];
    sessions.push(("CLI", 10));

    let mut totals: HashMap<&str, u32> = HashMap::new();
    for (topic, minutes) in &sessions {
        let total = totals.entry(*topic).or_insert(0);
        *total += *minutes;
    }

    println!("기록 수: {}", sessions.len());
    for topic in ["Rust", "CLI"] {
        let minutes = totals.get(topic).copied().unwrap_or(0);
        println!("{topic}: {minutes}분");
    }

    match sessions.get(9) {
        Some((topic, minutes)) => println!("열 번째 기록: {topic} {minutes}분"),
        None => println!("열 번째 기록은 없습니다"),
    }
}
cargo run
기록 수: 4
Rust: 60분
CLI: 30분
열 번째 기록은 없습니다

Vec<(&str, u32)>는 문자열 참조와 정수의 튜플을 담는 벡터입니다. 튜플은 여러 값을 순서로 묶는 형식이며, 여기서는 주제와 시간을 한 기록으로 묶었습니다. 같은 벡터의 항목들은 같은 타입입니다. vec!로 초기값을 만들고 push로 끝에 추가했습니다.

빌려서 순회하고, 범위를 확인하며 조회하기

for ... in &sessions는 벡터를 읽기 전용으로 빌려 순회합니다. 벡터 자체를 소비하지 않으므로 반복 뒤에도 len과 get을 호출할 수 있습니다. *topic과 *minutes는 참조가 가리키는 값을 읽습니다. 이번 값들은 복사할 수 있는 타입입니다.

get(9)는 열 번째 항목을 요청합니다. 인덱스는 0부터 시작합니다. 없는 인덱스에는 None이 오므로 match로 처리합니다. sessions9]처럼 직접 인덱싱하면 범위 밖에서 panic이 납니다. 외부 입력으로 정한 위치는 get으로 확인하는 습관이 도움이 됩니다. [벡터 공식 설명

HashMap의 entry 패턴

HashMap은 키에서 값을 찾는 자료구조입니다. entry(*topic).or_insert(0)은 주제가 있으면 그 누적값에 대한 가변 참조를, 없으면 0을 넣은 뒤 그 값에 대한 가변 참조를 반환합니다. *total += *minutes로 실제 누적값을 바꿉니다.

totals.get(topic)은 Option<&u32>를 반환합니다. copied()로 Option<u32>를 만들고, 없을 때의 기본값을 unwrap_or(0)으로 정했습니다. 이미 배운 Option이 조회 결과에도 사용됩니다.

HashMap 자체의 순회 순서는 고정되어 있지 않습니다. 예상 출력 순서를 유지하려고 이 예제는 "Rust", "CLI"] 순서대로 키를 조회합니다. HashMap을 그대로 출력하고 순서가 달라졌다고 계산 오류로 생각하지 마세요. [해시맵 공식 설명

한글 문자열의 길이와 순회

다음은 src/main.rs 전체를 바꿔 실행할 수 있는 별도 예제입니다.

fn main() {
    let mut title = String::from("러스트");
    title.push_str(" 입문");
    println!("{title}");
    println!("바이트: {}", title.len());
    println!("스칼라 값 수: {}", title.chars().count());
    for ch in title.chars() {
        println!("{ch}");
    }
}

첫 세 줄은 다음과 같습니다. 이어서 문자별로 줄을 바꾸며, 공백도 한 줄로 출력합니다.

러스트 입문
바이트: 16
스칼라 값 수: 6

한글 다섯 글자가 각각 3바이트이고 공백은 1바이트여서 16입니다. chars는 Unicode 스칼라 값 단위입니다. 사용자 눈에 보이는 글자 단위와 항상 일치하지는 않습니다.

String에 title0]처럼 정수 인덱스를 사용할 수 없습니다. UTF-8에서 첫 바이트와 첫 문자는 같지 않을 수 있기 때문입니다. 바이트 범위 슬라이스도 유효한 문자 경계가 아니면 panic이 발생할 수 있습니다. 초급 단계에서는 chars와 lines 같은 순회 도구를 먼저 사용하세요. [문자열 공식 설명

오류를 확인해 보기

첫 예제에서 for ... in &sessions의 &를 지우면 벡터를 소비합니다. 뒤의 sessions.len()에서 이동한 값 사용 오류가 발생합니다. 반복 뒤에도 목록이 필요하다면 &로 빌립니다.

빈 HashMap을 만들고 아무 값도 넣지 않으면 컴파일러가 키와 값 타입을 추론하지 못할 수 있습니다. HashMap<&str, u32>처럼 의도를 적으면 해결됩니다. 타입 추론에 정보가 충분한지 확인하세요.

연습과 해설

sessions에 ("Rust", 15)를 하나 더 추가하고 결과를 확인하세요. 기록은 5개, Rust는 75분, CLI는 30분이어야 합니다.

해설: sessions.push(("Rust", 15));를 집계 반복문 전에 추가합니다. 집계 후에 추가하면 이미 계산한 totals에는 반영되지 않습니다. 데이터 변경과 계산 순서도 함께 확인하세요.

다음 10편에서는 커진 코드를 모듈로 나누고, 공통 동작을 trait으로 표현한 뒤 외부 크레이트로 JSON 출력을 만들어 봅니다.

실행 확인

이 글의 핵심 실습은 Rust 1.99.1, 2024 Edition의 Linux 환경에서 확인했습니다. 의도적으로 실패하는 예제는 설명한 오류가 발생하는지 구분해 검사했습니다. 운영체제별 오류 문구는 다를 수 있습니다.

Rust 초급 시리즈 전체 목차

  1. 설치와 Cargo로 첫 프로그램 만들기
  2. 변수·자료형·문자열로 학습 기록 만들기
  3. 함수·조건문·반복문으로 학습 기록 계산하기
  4. String으로 이해하는 소유권과 이동
  5. 참조와 빌림으로 문자열 읽고 수정하기
  6. 구조체와 메서드로 학습 할 일 만들기
  7. 열거형과 패턴 매칭으로 학습 상태 표현하기
  8. Option·Result로 빈 값과 오류 처리하기
  9. 벡터·해시맵·문자열로 학습 기록 모으기
  10. 모듈과 외부 크레이트로 코드 정리하기
  11. 테스트와 디버깅으로 목표 계산 검증하기
  12. 파일을 검색하는 CLI 프로젝트 완성하기

시리즈 모아보기

Discussion

No comments yet