Rust
webp converter cli를 만들면서 배운 것들
Rust 첫 프로젝트(WebP CLI)를 만들면서 배운 것들
최근 Rust를 공부하면서 WebP 이미지 변환 CLI를 직접 만들어 보았다.
처음에는 단순히 실무에서 사용할 “이미지를 WebP로 변환하는 프로그램” 정도로 시작했지만, 개발을 진행하면서 Rust의 핵심 문법부터 병렬 처리(Rayon)까지 정말 많은 것을 배울 수 있었다.
이번 글에서는 프로젝트를 진행하면서 배운 문법과 개념들을 정리해보려고 한다.
프로젝트 소개
이번 프로젝트는 다음과 같은 기능을 가진 CLI 프로그램이다.
- 이미지 → WebP 변환
- 폴더 재귀 탐색
- Progress Bar
- Summary 출력
- Overwrite 옵션
- Quality 조절
- Rayon을 이용한 병렬 처리 최종적으로는 아래와 같이 사용할 수 있다.
webp-con convert ./images -o ./output --overwrite -q 80
1. Rust 프로젝트 구조
처음에는 모든 코드를 main.rs에 작성했지만 프로젝트가 커질수록 관리하기 어려워졌다.
최종적으로는 아래와 같이 모듈을 분리하였다.
src/
├── main.rs
├── lib.rs
├── converter.rs
├── summary.rs
└── utils.rs
각 파일의 역할은 다음과 같다.
| **파일** | **역할** |
| main.rs | 프로그램 시작점 |
| lib.rs | 모듈 공개 |
| converter.rs | 이미지 변환 |
| summary.rs | 결과 집계 |
| utils.rs | 공통 함수 |
Rust에서는 lib.rs가 프로젝트의 라이브러리 진입점 역할을 한다.
pub mod converter;
pub mod summary;
pub mod utils;
2. Struct와 impl
Rust에서는 데이터를 struct로 표현하고 기능은 impl에서 정의한다.
예를 들어 변환 결과를 관리하기 위해 아래와 같은 구조체를 만들었다.
struct ConvertSummary {
converted: usize,
skipped: usize,
failed: usize,
}
메서드는 impl에서 작성한다.
impl ConvertSummary {
fn add_converted(&mut self) {
self.converted += 1;
}
}
여기서
Self→ 타입 자체self→ 현재 인스턴스 라는 차이도 처음 알게 되었다.
3. Ownership과 Borrow
Rust를 공부하면서 가장 많이 들었던 개념이다.
Rust에서는 하나의 값은 하나의 Owner만 가진다.
Move가 발생하는 타입
String
Vec
Struct
Copy 타입
bool
u32
i32
usize
char
함수에 데이터를 넘길 때는 대부분 Borrow를 사용했다.
fn convert(path: &Path)
읽기 전용
&T
수정 가능
&mut T
4. Option과 Result
Rust에는 null이 없다.
대신 Option을 사용한다.
Option<PathBuf>
값이 있으면
Some(value)
없으면
None
에러 처리는 Result를 사용한다.
Result<T, E>
성공
Ok(())
실패
Err(err)
그리고 가장 많이 사용한 문법이 바로 ? 연산자였다.
let img = image::open(path)?;
에러가 발생하면 즉시 호출자에게 전달해 준다.
5. Enum과 Match
이번 프로젝트에서 가장 많이 성장했다고 느낀 부분이다.
처음에는 함수 내부에서 Summary를 직접 수정했는데,
리팩터링하면서 함수는 결과만 반환하도록 변경하였다.
enum ConvertResult {
Converted,
Skipped,
}
호출하는 쪽에서
match result {
ConvertResult::Converted => {}
ConvertResult::Skipped => {}
}
이렇게 처리하도록 변경했다.
덕분에 함수의 책임이 훨씬 명확해졌다.
6. Iterator
Rust에서는 for문보다 Iterator를 정말 많이 사용한다.
WalkDir::new(input)
.into_iter()
.filter(...)
.map(...)
.collect()
이번 프로젝트를 진행하면서
- filter
- map
- collect 를 자연스럽게 익힐 수 있었다.
7. Vec
Vec는 Rust의 동적 배열이다.
JavaScript의 Array와 거의 같은 역할이다.
Vec<PathBuf>
특히 병렬 처리를 위해
let files: Vec<_> = ...
형태로 작업 목록을 만들어 두는 방식이 매우 인상적이었다.
8. Clap으로 CLI 만들기
CLI는 Clap 라이브러리를 사용했다.
#[derive(Parser)]
Subcommand도 매우 간단하게 만들 수 있었다.
#[derive(Subcommand)]
enum Commands {
Convert {
...
}
}
자동으로
--help
--version
까지 생성되는 점도 매우 편리했다.
9. Progress Bar
이미지가 많아질수록 사용자는 현재 진행 상황을 알 수 있어야 한다.
indicatif를 이용하여 Progress Bar를 추가하였다.
ProgressBar
ProgressStyle
덕분에 CLI 사용성이 훨씬 좋아졌다.
10. Rayon으로 병렬 처리
이번 프로젝트에서 가장 재미있었던 부분.
기존에는
for
으로 순차 처리하였다.
이를
files.par_iter()
로 변경하였다.
하지만 단순히 par_iter()만 사용하는 것이 아니라,
공유 상태(Summary)를 병렬에서 직접 수정하지 않고
결과만 반환한 뒤 마지막에 집계하는 방식으로 변경하였다.
ConvertOutcome
↓
Vec<ConvertOutcome>
↓
Summary
이 과정에서 병렬 프로그래밍에서 왜 공유 상태를 최소화해야 하는지도 이해할 수 있었다.
성능 비교
100장의 이미지를 기준으로 테스트하였다.
| **구분** | **실행 시간** |
| 순차 처리 | 1분 55.82초 |
| Rayon 병렬 처리 | 27.80초 |
약 4.2배 정도 빨라졌다.
CPU 사용률도
- 순차 처리 : 약 99%
- 병렬 처리 : 약 626% 로 여러 코어를 효율적으로 사용하는 것을 확인할 수 있었다.
이번 프로젝트를 통해 배운 것
단순히 WebP 변환기를 만든 것이 아니라 Rust의 핵심 문법을 대부분 경험할 수 있었다.
- Module
- Struct
- impl
- Ownership
- Borrow
- Option
- Result
- anyhow
- Enum
- Match
- Iterator
- Vec
- Closure
- Clap
- WalkDir
- Progress Bar
- Rayon
- 병렬 처리
- 책임 분리(SRP)
- 모듈 분리
- CLI 개발
마무리하며
처음에는 Rust 문법조차 익숙하지 않았지만,
프로젝트를 하나 끝내고 나니 문법을 외우는 것보다 직접 사용하면서 익히는 것이 훨씬 빠르다는 것을 느꼈다.
다음 프로젝트에서는 프론트엔드 개발 경험을 살려 프로젝트 초기 세팅 CLI, Swagger 기반 타입 생성기, 사내 문서 자동화 도구 등을 만들어 보려고 한다.
Rust를 처음 공부하는 분이라면 작은 CLI 프로젝트 하나를 직접 만들어 보는 것을 추천한다. 생각보다 훨씬 많은 것을 배울 수 있다.