
GPT에게 언어와 주제를 추천받았다.
웬만한 언어는 다 해봤지만 Rust는 해본적이 없으므로 Rust로 개발 하기로 결정
1. Rust 환경 설치
별다른 설치파일같은거 없이 그냥 명령어 실행만 하면 되는 것 같다.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
2. Rust 프로젝트 설계

확장자는 rs를 쓴다. 프로젝트를 위해 프로젝트 폴더와 rs파일을 세팅해 두었다.
2-1. 요구사항 분석
일단 GPT에서 요구한 사항은 다음과 같다.
1. 파일 입력을 받기
2. 단어 등장 횟수 집계
3. 상위 10개 단어 출력
3. 파일 작성
기본적인 구조는 c++와 유사한 듯 하다.
일단 파일 읽기 부터 작성해 볼것이다. 기본적으로는 디렉토리에 있는 txt파일 전체를 읽어서, 해당 파일마다 분석을 실행할 것이며, 옵션으로 파일명을 받고, 그 파일에 대해서만 분석을 돌리는 방법도 고려중이다.
3-1. 파일 읽기
일단 파일 읽기 부터.

위와 같이 일단 테스트 파일을 하나 만들었다.
use std::fs::File;
use std::io::Read;
fn main() {
let mut file = File::open("test.txt").expect("Unable to open file");
let mut data_buffer = String::new();
file.read_to_string(&mut data_buffer).expect("Unable to read file");
println!("{}", data_buffer);
}
변수 선언 하는 형식이 좀 특이해 보이기는 한다. mut에 대해 알아보니 다른 언어들의 var와 동일한 구조로 보인다.
let은 단수 변수 선언이고 그 뒤에 옵션으로mut과 const 정도로 옵션을 주는 듯 하다.
rustc textCounter.rs
rustc명령어로 러스트 파일을 컴파일 해준다.
컴파일에 성공하면 textCounter라는 파일이 생기는데
./textCounter
bash에서 이렇게 실행해 주면

이렇게 실행이 된다.
학부생때 vim으로 cpp파일을 작성하고 gpp로 컴파일 해서 실행했던... 어떤 괴로운 기억이 떠오르지만 아무튼 이렇게 사용한다.
3-2. 파일 텍스트 스플릿
fn main() {
~
textSplitter(&data_buffer);
~
}
fn textSplitter(data: &str) -> Vec<&str>{
let splited: Vec<&str> = data.split(" ").collect();
println!("{splited:?}");
return splited;
}
C#등에서 사용하는 List대신 여기선 Vector형을 쓴다. 공식 문서 설명상 array는 길이 불변이지만 vector는 가변 길이 array라고 했으니까 그냥 List형으로 생각하면 될것 같다.
여기서 함수로 String값을 넘기게 되는데, 최초 데이터를 버퍼에서 읽을때는 String으로 받았지만, 이후 data_buffer값을 써야 하는 경우, &data_buffer로 넘겨주고, 참조 데이터들도 문자열 리터럴이기에 다 &str로 써야한다.
참고로 여타 다른 언어들과 같이 문자열을 자를때는 split을 사용하지만, split을 사용한 경우 반환값은 split형이므로, collect를 이용해서&str로 변환해줘야한다.

문자열들이 스플릿되어서 잘 출력되는걸 볼 수 있다.하지만 보다시피 줄바꿈 문자도 포함이 되어있다.
귀찮게도 함수명들은 snake_case를 사용하라고 강요권장하고 있으므로 줄바꿈문자 스플릿을 수정하는 김에 같이 수정해준다.
let splited: Vec<&str> = data.split_whitespace().collect();
놀랍게도 탭, 스페이스, 줄바꿈 문자 기준으로 스플릿 해주는 유틸이 존재한다!
특정 문자 기준으로 스플릿 하는건 다른곳에 사용하도록 하자.

문제 없이 잘 분리되는 것을 볼 수 있다.
3-3. 파일 텍스트 카운트
use std::collections::HashMap;
fn text_counter<'a>(data: &Vec<&'a str>) -> HashMap<&'a str, u32>{
let mut counter: HashMap<&'a str, u32> = HashMap::new();
for word in data{
let count = counter.entry(word).or_insert(0);
*count += 1;
}
println!("{counter:?}");
return counter;
}
텍스트 카운트 함수는, 문자열 리터럴 벡터 값을 받고, HashMap<&str, u32>형태로 반환한다.
u32는 unsigned integer32고, HashMap은 여타함수의 dictionary와 유사한 형태이다.
let mut counter로 HashMap을 선언했으므로, 이제 이 HashMap 은 수정이 가능하다.
for문은 평범하게 쓰면 된다. &Vec<&str>인 data 를 순회한다.
count는 counter: HashMap<&str, u32>의 요소 중에 word가 포함이 되어 있다면, 그 값(u32)값을 반환하고, 아니라면 0을 삽입 후, 그 값(u32 여기서는 0)을 반환한다.
이후 count에다 값을 1 추가한다.
통상 let은 수정이 불가능하나, *로 역참조해서 접근하면 수정이 가능한 듯 하다...
여기서 'a가 중요하다. 'a는 라이프타임 파라미터로, 반환되는 HashMap에참조하는 문자열 리터럴이 split Vector의 라이프타임을 따른다는 뜻이다.
즉, 반환되는 HashMap은 split Vector가 살아 있는 동안만 안전하게 사용이 가능하다는 뜻이다.

이제 갯수도 문제없이 잘 표시하는 것을 볼 수 있다.
4. 다중 파일 지원
다중파일 지원을 추가할 것이다. 일단 그 전에 파일당 양식을 정해주는 것이 좋아 보인다.
4-1. 출력 양식 추가
조금 깔끔하게 보기 위해서 파일명, 탑10 문자열을 표기하는 형식을 취하기로 했다. 파일 내용도 보이면 좋지만, 간단한 테스트 파일에서나 유효해 보이고, 대규모 파일인 경우는 부적절 할 듯 해서 파일명가 탑10만 표기하도록 할것이다. 이왕이면 보일때 순위도 함께 보이면 좋을 것 같다.
일단은 제일 우선순위인 top10 데이터
fn text_printer<'a>(data: &HashMap<&'a str, u32>) -> (){
let mut items: Vec<(&str, u32)> = data.iter().map(|(&k, &v)| (k, v)).collect();
items.sort_by(|a, b| b.1.cmp(&a.1));
if items.len() > 10 {
items.truncate(10);
}
println!("========== Top 10 Keyword ==========");
for (i, item) in items.iter().enumerate() {
println!("{}위: {} : {}", i+1, item.0, item.1);
}
println!("====================================");
}
HashMap 데이터를 다시 vector로 변경해 준다. 사실 처음부터 HashMap을 Vector<Tuple()>로 빼면 되었었는데, Hashmap에는 순서랄게 없어서 정렬이 불가능했다. 그래서 HashMap을 Vector<Tuple>로 변경해서 내림차순 정렬을 했다. 그리고 Top10키워드만 뽑기로 했으므로 길이가 10 이상이면 잘라낸다. 그 이후 n위: keyword : count 형태로 출력되도록 했다.

정상적으로 잘 나온다.
fn main() {
file_reader("test.txt");
}
fn file_reader(file_path: &str) -> (){
let mut file = File::open(file_path).expect("Unable to open file");
let mut data_buffer = String::new();
file.read_to_string(&mut data_buffer).expect("Unable to read file");
let splited =text_splitter(&data_buffer);
let _counted = text_counter(&splited);
println!("파일 명 : {file_path:?}");
text_printer(&_counted);
}
이제 다중 파일 지원을 하기 전에, 파일 제목을 알아야 한다.그래서 메인함수에서 파일 명을 출력해 주는 부분을 따로 뺐다.

파일명도 잘 나온다.
4-2. 다중 파일 지원
이제 프로그램이 있는 디렉토리 전체를 훑어서, *.txt파일등을 모두 훑고, Vector로 가지고 있으며, 그 길이만큼 함수에 변수 전달을 해서 다회 실행할 수 있도록 한다.
fn file_picker() -> Vec<String>{
let mut txt_files = Vec::new();
if let Ok(entries) = fs::read_dir(".") {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "txt" {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
txt_files.push(name.to_string());
}
}
}
}
}
}
return txt_files;
}
현재 디렉토리에 존재하는 파일들 중 txt확장자인 파일들만 추려서 그 이름을 반환하는 함수이다.

정상 작동하는 것을 볼 수 있다.
5. 최종 코드
use std::fs::File;
use std::io::Read;
use std::collections::HashMap;
use std::fs;
fn main() {
let files = file_picker();
for file in files{
file_reader(&file);
}
}
fn file_picker() -> Vec<String>{
let mut txt_files = Vec::new();
if let Ok(entries) = fs::read_dir(".") {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "txt" {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
txt_files.push(name.to_string());
}
}
}
}
}
}
return txt_files;
}
fn file_reader(file_path: &str) -> (){
let mut file = File::open(file_path).expect("Unable to open file");
let mut data_buffer = String::new();
file.read_to_string(&mut data_buffer).expect("Unable to read file");
let splited =text_splitter(&data_buffer);
let _counted = text_counter(&splited);
println!("파일 명 : {file_path:?}");
text_printer(&_counted);
}
//TextSplitter
fn text_splitter(data: &str) -> Vec<&str>{
let splited: Vec<&str> = data.split_whitespace().collect();
return splited;
}
//TextCounter
fn text_counter<'a>(data: &Vec<&'a str>) -> HashMap<&'a str, u32>{
let mut counter: HashMap<&'a str, u32> = HashMap::new();
for word in data{
let count = counter.entry(word).or_insert(0);
*count += 1;
}
return counter;
}
//TextPrinter
fn text_printer<'a>(data: &HashMap<&'a str, u32>) -> (){
let mut items: Vec<(&str, u32)> = data.iter().map(|(&k, &v)| (k, v)).collect();
items.sort_by(|a, b| b.1.cmp(&a.1));
if items.len() > 10 {
items.truncate(10);
}
println!("========== Top 10 Keyword ==========");
for (i, item) in items.iter().enumerate() {
println!("{}위: {} : {}", i+1, item.0, item.1);
}
println!("====================================");
}
6. 후기
생각보다 C++이랑 비슷한게 많아서 그렇게 어렵지는 않았던 것 같다. 다만, c++ 도 놓은지 좀 돼서 기억이 잘안나, 이게 rust문법인지 원래 c++에도 있는 문법인지 헷갈리는게 좀 있었다.
간략한 프로젝트라서 별도로 추가적인 공동n 위 처리같은건 안했지만 그걸 했어도 괜찮았을 것 같다.
무엇보다도 엄청 빠르다. 언젠가 c++의 많은것들이 rust로 대체되면 좋겠다. 요새 막 신규언어라고 하면서 붐이 일긴 하지만, 금방 식고 사장되는 경향이 있어서 새로운 언어 배우는거에 대해 좀 회의적이었는데 어쩌면 이건 꽤 오래 살아남을 것 같다.
'1일 1앱 챌린지' 카테고리의 다른 글
| [1일 1앱 챌린지] 2일차Python 감정일기 분석기 (1) | 2025.09.03 |
|---|