RUST · 심층 가이드
Rust Hello World 완전 정리
출력 한 줄에서 시작해 함수와 배열 순회를 거쳐 명령행 인자와 환경 변수까지, Rust 프로그램이 바깥세상과 맞닿는 지점을 10개 예제로 훑는 입문 코스입니다.
Rust의 첫 줄은 함수 호출처럼 보이지만 함수가 아닙니다. println!의 느낌표는 매크로 표시이고, 포맷 문자열은 컴파일 시점에 파싱되어 인자 개수와 타입이 맞는지 검사됩니다. 자리 표시자를 하나 빠뜨리면 실행이 아니라 빌드가 멈춥니다. 여기서 이미 Rust의 성격이 드러납니다. 런타임까지 미룰 수 있는 검사도 되도록 컴파일 시점으로 끌어올립니다. 출력 대상이 표준 출력이냐 표준 오류냐에 따라 print! 계열과 eprint! 계열이 갈리는 것도 첫날부터 알아둘 만합니다.
Hello World 출력과 변수 값을 같이 출력은 포맷 인자와 소유권이 처음 만나는 지점입니다. 이어지는 함수로 인사 만들기에서 &str를 받을지 String을 받을지 고르게 되고, 배열을 순회하며 인사에서 그 선택이 반복문 안의 빌림으로 확장됩니다. 명령행 인자 읽기와 환경 변수 읽기는 외부에서 들어오는 값이라 실패 가능성이 붙고, 자연스럽게 Option과 Result를 만나게 됩니다.
std::env::args()는 편해 보이지만 유효한 유니코드가 아닌 인자를 만나면 패닉을 냅니다. 파일 이름을 인자로 받는 도구라면 args_os()를 쓰는 편이 안전합니다. 그리고 반환되는 첫 원소는 프로그램 실행 경로이므로 사용자가 준 값은 인덱스 1부터라는 점, 그 경로마저 운영체제가 보장해 주지 않는다는 점을 기억해 두면 나중에 인자 파싱에서 한 칸씩 밀리는 실수를 피합니다.
01Hello World 출력
main 안에서 println! 하나로 Hello, World 1! 문자열을 표준 출력에 내보냅니다. 새 프로젝트가 실제로 빌드되고 실행되는지 확인하는 첫 단계로 쓰입니다. println!은 함수가 아니라 매크로여서 포맷 문자열과 인자 개수가 컴파일 시점에 검사되고, 줄바꿈은 자동으로 덧붙습니다.
Rust code
// Hello World 출력
fn main() {
println!("Hello, World 1!");
}println!은 매크로라 포맷 문자열과 인자 개수가 컴파일 시점에 검사되고, 어긋나면 실행하기 전에 오류로 잡힙니다.
포맷 문자열에 중괄호를 그대로 쓰려면 {{와 }}로 겹쳐 적어야 하며, 하나만 쓰면 자리 표시자로 해석되어 컴파일 오류가 납니다.
02변수 값을 같이 출력
env::var("APP_ENV")로 환경 변수를 조회하고, 없으면 local을 대신 써서 출력합니다. 실행 환경에 따라 동작을 나누는 프로그램의 가장 앞자리에 오는 코드입니다. 반환값이 Result라는 점이 핵심이며, unwrap_or_else는 실패했을 때만 클로저를 실행하므로 기본 문자열 할당이 성공 경로에서는 일어나지 않습니다.
Rust code
// 변수 값을 같이 출력
use std::env;
fn main() {
let env = env::var("APP_ENV").unwrap_or_else(|_| "local".to_string());
println!("{}", env);
}unwrap_or_else는 실패했을 때만 클로저를 실행하므로, 환경 변수가 있는 경우에는 기본 문자열을 만드는 할당이 아예 일어나지 않습니다.
환경 변수 이름을 오타로 적어도 오류가 아니라 기본값으로 조용히 넘어가므로, 설정이 반영되지 않았는데 정상 동작으로 보일 수 있습니다.
03함수로 인사 만들기
함수를 정의해 인사를 만들지는 않고, 리터럴 하나를 그대로 출력하는 코드입니다. 로직을 넣기 전에 실행 경로만 확인하고 싶을 때 이 정도면 됩니다. 큰따옴표로 적은 문자열은 String이 아니라 프로그램 바이너리에 박혀 있는 &'static str이라, 힙 할당이 전혀 발생하지 않습니다.
Rust code
// 함수로 인사 만들기
fn main() {
println!("Hello, World 3!");
}큰따옴표로 적은 문자열은 바이너리에 박혀 있는 &'static str이라, 출력만 할 때는 힙 할당이 전혀 생기지 않습니다.
인사말을 만드는 함수를 -> &str로 선언하면 어디서 빌린 값인지 알 수 없어 수명 오류가 나므로, String으로 돌려주어야 합니다.
04배열을 순회하며 인사
배열 순회는 없고, 환경 변수 조회 결과 또는 기본값 local을 한 줄 출력합니다. 배포 단계 이름을 로그 맨 앞에 남기는 용도로 자주 보이는 패턴입니다. env::var는 변수가 없을 때뿐 아니라 값이 올바른 UTF-8이 아닐 때도 오류를 내며, 두 경우는 VarError의 서로 다른 변형으로 구분됩니다.
Rust code
// 배열을 순회하며 인사
use std::env;
fn main() {
let env = env::var("APP_ENV").unwrap_or_else(|_| "local".to_string());
println!("{}", env);
}env::var는 변수가 없을 때뿐 아니라 값이 올바른 UTF-8이 아닐 때도 실패하며, 두 경우는 VarError의 서로 다른 변형으로 구분됩니다.
두 실패를 |_|로 뭉뚱그려 삼키면 값이 깨져 있는 상황과 아예 없는 상황이 같아 보여, 설정 문제를 찾는 데 시간이 오래 걸립니다.
05명령행 인자 읽기
명령행 인자를 읽는 코드는 들어 있지 않고 고정 문자열만 화면에 나옵니다. 인자 처리를 붙이기 전 단계의 뼈대라고 보면 됩니다. 실제로 인자를 받으려면 std::env::args()를 순회해야 하는데, 첫 항목은 사용자가 준 값이 아니라 실행 파일 경로라는 점을 놓치기 쉽습니다.
Rust code
// 명령행 인자 읽기
fn main() {
println!("Hello, World 5!");
}인자를 받으려면 std::env::args()를 순회하며, 첫 항목은 사용자가 준 값이 아니라 실행 파일 경로이므로 skip(1)로 건너뜁니다.
args()는 인자에 올바르지 않은 유니코드가 섞이면 패닉하므로, 임의의 바이트가 들어올 수 있는 환경에서는 args_os()를 써야 합니다.
06템플릿 문자열 인사
템플릿 문법 대신 환경 변수 값을 읽어 그대로 찍는 코드가 들어 있습니다. 설정 값이 제대로 주입됐는지 눈으로 확인할 때 이 한 줄이면 충분합니다. {} 자리에는 Display를 구현한 값만 들어갈 수 있고, 구조체처럼 Debug만 있는 타입은 {:?}로 적어야 합니다.
Rust code
// 템플릿 문자열 인사
use std::env;
fn main() {
let env = env::var("APP_ENV").unwrap_or_else(|_| "local".to_string());
println!("{}", env);
}{} 자리에는 Display를 구현한 값만 들어갈 수 있고, 구조체처럼 Debug만 있는 타입은 {:?}로 적어야 합니다.
중괄호 안에 이름을 적는 축약 표기는 지역 변수에만 통하므로, 필드 접근이나 메서드 호출을 넣으면 컴파일 오류가 납니다.
07유니코드 메시지 출력
유니코드를 다루는 부분 없이 ASCII 문자열 한 줄이 출력됩니다. 다국어 메시지를 넣더라도 코드 모양은 이것과 달라지지 않습니다. 러스트 소스 파일과 str 타입 모두 UTF-8로 고정되어 있어 한글이나 이모지를 그대로 적어도 되지만, len()은 글자 수가 아니라 바이트 수를 돌려줍니다.
Rust code
// 유니코드 메시지 출력
fn main() {
println!("Hello, World 7!");
}소스 파일과 str이 모두 UTF-8로 고정되어 있어 한글이나 이모지를 문자열에 그대로 적어도 별도 설정이 필요 없습니다.
len()은 글자 수가 아니라 바이트 수를 돌려주므로, 한글 문자열의 길이를 세려면 chars().count()를 써야 값이 맞습니다.
08환경 변수 읽기
제목 그대로 APP_ENV를 읽되 값이 없으면 local로 떨어지는 안전한 조회입니다. 컨테이너나 CI처럼 설정이 환경 변수로 주입되는 곳에서 그대로 쓰입니다. 환경 변수는 프로세스가 시작할 때 받은 복사본이라 나중에 셸에서 바꿔도 반영되지 않고, 실행 중 변경은 스레드 안전하지 않습니다.
Rust code
// 환경 변수 읽기
use std::env;
fn main() {
let env = env::var("APP_ENV").unwrap_or_else(|_| "local".to_string());
println!("{}", env);
}환경 변수는 프로세스가 시작할 때 받은 복사본이라, 실행 중에 셸에서 값을 바꿔도 이미 떠 있는 프로그램에는 반영되지 않습니다.
env::set_var로 실행 중에 값을 바꾸는 것은 스레드 안전하지 않아, 다른 스레드가 동시에 읽고 있으면 정의되지 않은 동작이 됩니다.
09컬렉션 요약 출력
컬렉션을 요약하지는 않고 짧은 문자열 한 줄을 출력하는 것이 전부입니다. 출력 자체가 잘 나오는지 확인하는 자리에 놓기 좋습니다. 표준 출력은 줄 단위로 비워지고 호출마다 잠금을 잡기 때문에, 반복문에서 대량으로 찍어야 한다면 stdout().lock()을 한 번 잡고 쓰는 편이 훨씬 낫습니다.
Rust code
// 컬렉션 요약 출력
fn main() {
println!("Hello, World 9!");
}표준 출력은 호출마다 잠금을 잡고 줄 단위로 비워지므로, 반복문에서 대량으로 찍을 때는 stdout().lock()을 한 번 잡고 쓰는 편이 훨씬 빠릅니다.
println!은 쓰기 실패를 조용히 넘기지 않고 패닉하므로, 출력을 파이프로 받던 상대가 먼저 끊기면 프로그램이 그 자리에서 멈춥니다.
10버전 정보 출력
버전 문자열이 아니라 환경 이름을 읽어 출력하는 코드가 실려 있습니다. 어떤 설정으로 떠 있는지 기동 로그에 남기는 용도에 맞습니다. 기본값을 unwrap_or("local".to_string())로 주면 성공했을 때도 문자열을 미리 만들어 버리므로, 여기처럼 unwrap_or_else를 쓰는 편이 낭비가 없습니다.
Rust code
// 버전 정보 출력
use std::env;
fn main() {
let env = env::var("APP_ENV").unwrap_or_else(|_| "local".to_string());
println!("{}", env);
}빌드 시점의 버전은 env!("CARGO_PKG_VERSION")으로 문자열 상수처럼 가져올 수 있어, 실행 환경 변수와 달리 값이 바뀔 여지가 없습니다.
기본값을 unwrap_or에 넘기면 성공한 경우에도 문자열을 미리 만들어 버리므로, 생성 비용이 있는 기본값에는 unwrap_or_else가 맞습니다.
정리하며
- println!은 매크로이므로 포맷 오류가 런타임이 아니라 빌드에서 잡힙니다
- 함수 인자는 우선 &str로 받고, 소유권이 정말 필요할 때만 String을 씁니다
- env::args()의 0번 원소는 실행 경로이니 사용자 인자는 1번부터 셉니다
- 비유니코드 인자를 다룰 가능성이 있으면 args_os()로 바꿔 둡니다
더 깊이 들어가고 싶다면 Rust 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.