본문으로 건너뛰기

안동민 개발노트

본문 시작

Hello, World!

첫 Rust 소스 파일을 작성해 rustc로 컴파일·실행하고 main 함수와 println 매크로의 기본 구조를 이해합니다.

설치도 마쳤으니, 러스트 프로그램을 만들 시간입니다.

새 언어를 배울 때면 늘 그렇듯, 만들어 볼 프로그램은 화면에 Hello, world! 문자를 출력하는 간단한 프로그램입니다.

main.rs를 저장한 뒤 target용 표준 라이브러리와 linker를 사용해 rustc main.rs로 실행 파일을 만들고, 대상 운영체제와 ABI 및 동적 runtime dependency가 맞는 환경에서 실행해 Hello world 출력을 확인하며 파일 위치, 빌드 실패, 실행 실패를 구분하는 과정

Rust · single file · compile process

작성·컴파일·실행은 서로 다른 완료 조건이다

rustc main.rs가 성공해야 현재 대상용 실행 파일이 생기고, 그 파일을 별도로 실행해야 출력이 나타납니다. 막힌 단계를 나누면 파일 위치, 컴파일 진단, 실행 명령을 뒤섞지 않고 확인할 수 있습니다.

단일 Rust 파일 컴파일과 실행 과정 main.rs 소스를 저장하고 target용 표준 라이브러리와 linker를 사용해 rustc main.rs로 컴파일하면 Unix 계열에서는 main, Windows에서는 main.exe 실행 파일이 생성됩니다. 대상 운영체제와 ABI 및 동적 runtime dependency가 맞는 환경에서 실행하면 Hello world 출력이 나타납니다. 1 · 작성 소스 저장 main.rs 2 · 컴파일 컴파일러 호출 rustc main.rs 3 · 산출물 현재 대상용 실행 파일 Unix: main Windows: main.exe 4 · 실행 프로그램 출력 ./main | .\main.exe Hello, world!
  1. main.rs 저장

    터미널의 현재 디렉터리와 파일이 저장된 디렉터리가 같은지 먼저 확인합니다.

  2. rustc main.rs

    구문·타입 검사와 코드 생성·링크가 성공해야 실행 파일이 생깁니다. 이때 선택 target용 std 산출물과 linker·native link 입력은 빌드 전제입니다.

  3. 대상별 실행 파일 확인

    기본 호스트 빌드에서는 Unix 계열의 main, Windows의 main.exe처럼 이름과 형식이 플랫폼에 따라 달라집니다.

  4. 실행하고 출력 확인

    ./main 또는 .\main.exe를 호출합니다. 대상 OS·architecture·ABI와 동적 native library·system runtime이 맞아야 출력이 나타납니다.

file location

파일을 찾지 못함

main.rs가 저장된 위치와 터미널의 현재 디렉터리를 맞춥니다. 이 문제는 Rust 코드 진단과 별개입니다.

compile

컴파일 진단이 발생함

코드 진단뿐 아니라 선택 target용 std, linker와 native link 입력을 확인한 뒤 rustc를 다시 실행합니다. 성공 전에는 새 실행 파일을 기대하지 않습니다.

run

실행 명령이 실패함

파일 이름·권한과 대상 OS·architecture·ABI를 확인합니다. 동적으로 링크한 native library나 system runtime이 빠져도 컴파일 뒤 실행 단계에서 실패합니다.

빌드 전제: 선택 target용 std 산출물, linker, native link 입력. 실행 전제: 대상 OS·architecture·ABI, 동적 native library와 system runtime. rustc는 기본적으로 host target을 빌드하며, 두 경계의 실패를 따로 진단해야 합니다.

Note. 이 책은 커맨드 라인 위주로 설명하고 있습니다. 하지만 러스트에는 코드 작성 및 개발 도구 사용환경에 따로 정해진 규정이 없으므로 커맨드 라인 대신 IDE(통합 개발 환경)를 사용하실 분은 애용하는 IDE를 사용하셔도 좋습니다. (요즘은 IDE 대부분이 러스트를 어느 정도 지원하니 세부 사항은 각 IDE 문서를 참고 바랍니다) 러스트 팀은 rust-analyzer를 통하여 IDE 지원 수준을 높이는 데 집중하고 있습니다. 더 자세한 사항은 부록 D를 참고하세요.


프로젝트 디렉터리 생성하기

작성할 러스트 코드를 저장해 둘 디렉터리가 필요하겠죠.

러스트 코드 자체는 어디에 저장하건 실행하는 데 문제는 없습니다만, 이 책을 보며 연습하시는 분들은 편의를 위해 홈 디렉터리 내 projects 디렉터리를 생성해 각종 프로젝트를 보관하는 것을 권장해 드립니다.

터미널을 열고 다음 명령어를 입력해 projects 디렉터리를 생성한 후, projects 내에 ‘Hello, world!’ 프로젝트용 디렉터리를 만들어 봅시다.

Linux, macOS, Windows PowerShell에서는 다음 명령어를 입력해 주세요.

$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world

Windows CMD 사용자는 다음 명령어를 입력해 주세요.

> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world

러스트 프로그램 작성하고 실행하기

다음으로 main.rs 소스 파일을 만들어 봅시다.

러스트 파일은 항상 .rs 확장자로 끝납니다.

파일명을 지을 때는 두 단어 이상으로 이루어질 경우에는 helloworld.rs 와 같이 붙여서 쓰지 않고 hello_world.rs처럼 단어 사이에 밑줄(_)을 넣는 것이 관례입니다.

main.rs 파일에 예제 1-1 코드를 입력합시다.

예제 1-1: Hello, world!를 출력하는 프로그램
main.rs
fn main() {
    println!("Hello, world!");
}

파일을 저장하고 터미널 창으로 돌아가 ~/projects/hello_world 디렉터리로 갑니다.

Linux, macOS 사용자는 다음 명령어를 입력하여 컴파일하고 실행할 수 있습니다.

$ rustc main.rs
$ ./main
Hello, world!

Windows에서는 ./main.\main.exe로 바꿔주시면 됩니다.

> rustc main.rs
> .\main.exe
Hello, world!

사용하시는 운영체제와 상관없이 터미널에 Hello, world!가 출력되면 정상입니다.

출력되지 않으면 앞의 다이어그램처럼 파일 위치, 컴파일, 실행 단계를 나누어 확인하고 ‘트러블 슈팅’ 내용을 참고해 도움을 얻을 방법을 찾아보세요.

문제없이 Hello, world!가 출력됐다면 첫 러스트 프로그램을 정상적으로 작성한 것입니다.


러스트 프로그램 뜯어보기

방금 만든 ‘Hello, world!’ 프로그램을 자세히 살펴봅시다.

우선 첫 부분은 다음과 같습니다.

fn main() {

}

이 라인은 러스트에서 main이라는 이름의 함수를 정의합니다.

main 함수는 특별한 함수로, 러스트 실행 프로그램에서 항상 가장 먼저 실행되는 함수입니다.

여기서는 매개변수를 받지 않고 아무것도 반환하지 않는 main이라는 함수를 선언합니다.

함수에 매개변수가 있을 때는 () 안쪽에 이를 작성해야 합니다.

함수 본문은 {}로 감싸집니다.

러스트에서는 모든 함수에 대해 본문을 감싸는 중괄호({})가 필수입니다.

중괄호는 함수 정의와 같은 줄에 작성하고 그 사이에 공백을 한 칸 넣으면 보기 좋으니 참고하세요.

Note: 여러분이 러스트 프로젝트의 코드를 표준 스타일로 통일시키고 싶다면, 코드를 특정 스타일로 포맷팅해주는 rustfmt라는 이름의 자동 포맷팅 도구를 사용할 수 있습니다(더 자세한 사항은 부록 D에 있습니다.) 러스트 팀은 이 도구를 rustc처럼 기본 러스트 배포에 포함시켰으므로, 이미 여러분의 컴퓨터에 설치되어 있습니다!

main 함수 내 코드를 살펴봅시다.

    println!("Hello, world!");

화면에 텍스트를 출력하는 코드로, 이 한 라인이 이 자그마한 프로그램의 전부입니다.

하지만 이 단순한 코드에도 눈여겨볼 것이 네 가지 들어있습니다.

첫 번째로, 러스트에서는 탭 대신 스페이스 4칸을 사용합니다.

두 번째로, println!는 러스트의 매크로 호출 코드입니다.

이 코드가 함수 호출 코드였다면 ! 없이 println이라고 되어 있었을 것입니다.

매크로는 19장에서 자세히 다루며, 지금은 !가 붙으면 함수가 아니라 매크로 호출 코드이고, 매크로는 함수와 항상 같은 규칙을 따르지는 않는다는 것만 알아두시면 됩니다.

세 번째는 println!의 인수로 넘겨준 "Hello, world!" 문자열이 그대로 화면에 나타난 점입니다.

마지막으로, 이 라인은 세미콜론(;)으로 끝납니다.

이 표현식이 끝났으며 다음 표현식이 시작될 준비가 됐다는 표시지요.

러스트 코드의 거의 모든 라인이 세미콜론으로 끝납니다.


컴파일과 실행은 별개의 과정입니다

앞서 새 프로그램을 만들고 실행한 과정을 세세한 단계로 나누어 검토해봅시다.

러스트 프로그램을 실행하기 전에, 아래와 같이 rustc 명령어에 소스 파일명을 넘겨주어 컴파일해야 하는 과정이 있었습니다.

$ rustc main.rs

C나 C++ 을 다뤄보셨다면 gccclang 사용 방법과 비슷하다는 걸 눈치채셨을지도 모르겠네요.

러스트는 소스 파일 컴파일에 성공하면 실행 가능한 바이너리를 만들어 냅니다.

Linux, macOS, Windows PowerShell 상에서는 ls 명령어로 실행 파일을 확인할 수 있습니다.

$ ls
main  main.rs

Linux와 macOS에서는 두 개의 파일이 보일 것이고, PowerShell의 경우에는 CMD와 같이 세 개의 파일이 보일 것입니다.

Windows CMD 는 다음 명령어를 입력해야 합니다.

> dir /B %= `/B`는  파일명만 출력하는 옵션입니다 =%
main.exe
main.pdb
main.rs

.rs 확장자를 갖는 소스 파일과 실행 파일(타 플랫폼에서는 main, Windows에서는 main.exe입니다)을 확인할 수 있습니다.

Windows에서는 디버깅 정보가 포함된 pdb 확장자 파일도 볼 수 있네요.

여기서 main 이나 main.exe 를 실행하는 방법은 다음과 같습니다.

$ ./main # Windows에서는 .\main.exe

main.rs가 여러분의 ‘Hello, world!’ 프로그램이라면 터미널에 Hello, world!가 출력될 겁니다.

Ruby, Python, JavaScript 등 명령어 한 줄로 프로그램을 컴파일하고 실행할 수 있는 동적 프로그래밍 언어에 익숙한 분들은 컴파일과 실행이 별개의 과정으로 진행되는 게 낯설 겁니다.

하지만 이 언어들은 .rb, .py, .js 파일을 다른 곳에서 실행하려면 해당 언어의 구현체를 설치해야만 합니다.

반면 러스트는 AOT(ahead-of-time-compiled) 언어로, 컴파일과 실행이 별개인 대신 컴파일한 실행 파일을 러스트 toolchain이 설치되지 않은 환경에서도 실행할 수 있습니다.

빌드 단계에서는 이 예제처럼 std를 쓰는 프로그램이라면 선택한 target용 표준 라이브러리 산출물과 그 target에 맞는 linker·native link 입력이 필요합니다. rustc가 target triple을 안다는 사실만으로 system linker나 SDK까지 준비되는 것은 아닙니다.

실행 단계에서는 만들어진 파일이 대상 운영체제·아키텍처·ABI와 맞아야 하며, 동적으로 링크한 native library나 system runtime도 대상 환경에 호환되는 버전으로 있어야 합니다. 빌드에 쓴 Rust toolchain 자체는 일반적으로 실행 환경에 필요하지 않지만, 빌드 전제와 실행 시 dependency는 서로 구분해서 확인해야 합니다.

저마다 장단점이 있는 법이죠.

간단한 프로그램에는 rustc를 사용하는 것도 좋습니다.

다만 프로젝트가 커질수록 관리할 옵션이 많아지고, 코드 배포도 점점 번거로워지겠죠.

다음 내용에서 소개할 Cargo는 rustc를 대체하는 compiler가 아닙니다.

Cargo는 package manifest와 의존성 그래프를 읽고 필요한 rustc 호출, build profile, cache와 반복 작업을 프로젝트 단위로 조율합니다.

직접 rustc를 부르는 과정은 Cargo가 묶어 주는 compiler 호출과 주변 작업을 이해하기 위한 작은 모형입니다.

아래 비교는 두 방식의 입력 단위와 책임이 어디서 달라지는지 보여줍니다.

단일 파일을 rustc에 직접 넘기는 방식과 Cargo.toml이 있는 프로젝트를 Cargo로 관리하는 방식의 입력 단위, 명령, 의존성, 반복 작업과 실제 컴파일 책임을 비교한 표와 모바일 카드

Rust · rustc vs Cargo · responsibility matrix

Cargo는 컴파일러를 바꾸지 않고 프로젝트 작업을 조율한다

직접 rustc를 부르면 한 크레이트 루트와 옵션을 개발자가 조립합니다. Cargo는 manifest와 의존성 그래프를 읽어 필요한 rustc 호출, 프로필, 캐시와 실행 순서를 반복 가능하게 묶습니다.

직접 단일 파일 컴파일과 Cargo 프로젝트의 책임 경계
비교 기준직접 rustcCargo 프로젝트
입력 단위main.rs처럼 명령에 지정한 단일 크레이트 루트와 compiler 옵션Cargo.toml이 선언한 package·target 소스, feature와 의존성 그래프
기본 명령rustc main.rs 뒤 생성된 바이너리를 별도로 실행cargo build로 빌드하거나 cargo run으로 빌드 뒤 실행
의존성 책임필요한 crate artifact, --extern, 검색 경로와 관련 옵션을 호출자가 준비manifest와 lockfile을 바탕으로 버전을 해석·다운로드하고 올바른 순서로 의존성을 빌드
반복 작업대상, 최적화, 출력 경로와 실행 명령을 개발자나 별도 스크립트가 재현target·profile·feature·build script·테스트와 target/ 캐시를 프로젝트 규칙으로 재현
실제 Rust 컴파일rustc가 소스를 분석하고 코드 생성·링크Cargo가 적절한 rustc 호출을 조율하며, Rust 코드를 컴파일하는 주체는 여전히 rustc

직접 rustc · 단일 크레이트 루트

  • 입력: main.rs와 직접 지정한 compiler 옵션
  • 명령: rustc main.rs 뒤 바이너리를 별도로 실행
  • 의존성: artifact와 --extern·검색 경로를 호출자가 준비
  • 반복: 대상·최적화·출력·실행 명령을 직접 재현
  • 컴파일: rustc가 분석·코드 생성·링크

Cargo · manifest가 있는 프로젝트

  • 입력: package·target·feature·의존성을 선언한 Cargo.toml
  • 명령: cargo build 또는 cargo run
  • 의존성: manifest와 lockfile로 해석·다운로드·빌드 순서를 관리
  • 반복: profile·target·build script·테스트와 캐시를 규칙화
  • 컴파일: Cargo가 호출을 조율하고 실제 Rust 컴파일은 rustc가 수행

직접 호출이 잘 맞는 때

한 파일의 컴파일·실행 경계를 배우거나 외부 의존성 없는 작은 실험을 빠르게 확인할 때입니다.

Cargo로 넘어갈 기준

여러 target, 외부 crate, feature, 테스트, build script나 팀이 반복할 명령이 생기면 package 단위 조율이 필요합니다.

Cargo는 rustc를 대체하는 또 다른 compiler가 아닙니다. 개발자가 직접 구성하던 여러 compiler 호출과 주변 작업을 manifest 중심으로 조직하는 build system이자 package manager입니다.