JavaScript 개발자를 위한 Rust 입문: 첫 1000줄까지 완벽 가이드 (2026)
"Rust가 미래다"라는 말, 요즘 정말 많이 들리시죠? Stack Overflow에서 8년 연속 가장 사랑받는 언어로 선정됐고, Discord는 인프라를 Rust로 다시 만들었어요. Cloudflare는 엣지 서비스를 Rust로 돌리고 있고, Figma의 멀티플레이어 엔진도 Rust입니다. 심지어 Microsoft도 Windows 핵심 컴포넌트를 Rust로 다시 쓰고 있어요.
근데 문제가 있어요. JavaScript나 TypeScript 개발자 입장에서 보면, 대부분의 Rust 튜토리얼이 C++ 배경 지식을 전제로 쓰여 있다는 거예요. 소유권? 빌림? 라이프타임? 용어만 봐도 머리가 아파서 그냥 탭 닫고 편안한 npm install로 돌아가고 싶어지죠.
이 글은 다릅니다. JavaScript 개발자의 눈으로 Rust를 배워볼 거예요. 이 글을 끝까지 읽으면, 첫 1000줄짜리 Rust 코드를 작성하고 실제로 뭐가 어떻게 돌아가는지 이해하게 될 거예요.
근데 왜 웹 개발자가 Rust를?
코드 치기 전에 솔직한 얘기 먼저 하죠: 프론트엔드/백엔드 개발자가 왜 갑자기 시스템 언어를?
성능 현실 체크
JavaScript는 인터프리터(또는 JIT 컴파일) 언어예요. Rust는 네이티브 머신 코드로 컴파일됩니다. 그 차이는 미묘하지 않아요:
// JavaScript: JSON 파싱하고 최댓값 찾기 const data = JSON.parse(hugeJsonString); const max = Math.max(...data.numbers); // 실행 시간: 1000만 개 숫자에 ~450ms
// Rust: 같은 작업 let data: Data = serde_json::from_str(&huge_json_string)?; let max = data.numbers.iter().max(); // 실행 시간: 1000만 개 숫자에 ~12ms
오타 아닙니다—똑같은 로직인데 37배 빠릅니다. 이게 중요한 상황들이 있어요:
- 즉시 반응해야 하는 CLI 도구 만들 때
- 큰 파일 처리할 때 (빌드 도구, 린터 등)
- 콜드 스타트 시간이 곧 비용인 서버리스 함수
- 브라우저에서 무거운 연산을 처리하는 WebAssembly 모듈
WebAssembly 연결 고리
웹 개발자에게 흥미로운 부분이 여기예요. Rust는 다른 어떤 언어보다 WebAssembly(WASM)로 잘 컴파일됩니다:
// 이 Rust 코드가... #[wasm_bindgen] pub fn fibonacci(n: u32) -> u32 { match n { 0 => 0, 1 => 1, _ => fibonacci(n - 1) + fibonacci(n - 2) } }
...JavaScript에서 바로 import할 수 있는 .wasm 파일이 됩니다:
import init, { fibonacci } from './pkg/my_rust_lib.js'; await init(); console.log(fibonacci(40)); // 순수 JS보다 10-20배 빠름
Figma, Photoshop 웹 버전, Google Earth가 정확히 이 패턴을 성능이 중요한 코드에 사용하고 있어요.
첫 번째 Rust 프로그램: JavaScript와 비교하기
익숙한 것부터 시작해볼게요. 두 언어로 간단한 프로그램을 작성해봅시다:
JavaScript:
function greet(name) { const message = `Hello, ${name}!`; console.log(message); } greet("World");
Rust:
fn greet(name: &str) { let message = format!("Hello, {}!", name); println!("{}", message); } fn main() { greet("World"); }
벌써 몇 가지 차이점이 보이죠:
function대신fn- 타입이 명시적:
name: &str let은 비슷하게 작동하지만,const는 같은 방식으로 존재하지 않음format!과println!에!가 붙는 건 매크로라서- 모든 Rust 프로그램에는
main함수가 필요함
환경 설정하기
더 진행하기 전에, Rust를 설치합시다:
# Rust 설치 (macOS, Linux, WSL에서 작동) curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # 설치 확인 rustc --version cargo --version
cargo는 Rust의 패키지 매니저이자 빌드 도구예요—npm + webpack이 합쳐진 거라고 생각하면 됩니다.
# 새 프로젝트 생성 (npm init처럼) cargo new my-first-rust-app cd my-first-rust-app # 프로젝트 실행 cargo run
Rust의 3대 개념: 소유권, 빌림, 라이프타임
JS 개발자들이 여기서 멘붕이 와요. 근데 걱정 마세요, JS로 비유해서 설명해드릴게요.
문제: JavaScript의 숨겨진 메모리 관리
JavaScript에서는 메모리를 생각할 필요가 없죠:
function processData() { const data = [1, 2, 3, 4, 5]; // 메모리 할당됨 const doubled = data.map(x => x * 2); // 또 메모리 할당됨 return doubled; } // 메모리는... 언젠가 가비지 컬렉션됨 // 메모리가 언제 해제되는지 제어할 수 없음 // 성능이 중요한 코드에서 예기치 않은 GC 일시 정지 발생 가능
JavaScript는 가비지 컬렉션을 사용해요. 편리하지만 예측 불가능하죠. Rust는 수동 메모리 관리의 위험 없이 제어권을 줍니다.
소유권: 항상 한 명의 소유자
Rust에서 모든 값은 정확히 한 명의 소유자를 가집니다:
fn main() { let s1 = String::from("hello"); // s1이 문자열을 소유함 let s2 = s1; // 소유권이 s2로 이동함 // println!("{}", s1); // 에러! s1은 더 이상 아무것도 소유하지 않음 println!("{}", s2); // 잘 됨 }
JavaScript 용어로 표현하면, 이런 일이 일어난다고 상상해보세요:
// JavaScript에 "소유권"이 있다면 let s1 = "hello"; let s2 = s1; // Rust에서는 이게 s1을 무효화함 console.log(s1); // Rust에서는 이게 에러!
왜 Rust가 이렇게 할까요? s2가 스코프를 벗어날 때, Rust는 정확히 언제 메모리를 해제해야 하는지 알기 때문이에요. 가비지 컬렉터가 필요 없습니다.
빌림: 소유권 없이 참조하기
잠깐—소유권을 가져가지 않고 값을 사용만 하고 싶으면 어떻게 할까요? 그게 바로 빌림이에요:
fn calculate_length(s: &String) -> usize { s.len() } // s가 스코프를 벗어나지만, String을 소유하지 않으니 아무 일도 안 일어남 fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // &로 s1을 빌림 println!("'{}'의 길이는 {}입니다.", s1, len); // s1 여전히 유효! }
&는 "그냥 보기만 할게, 가져가진 않을게"라고 말하는 거예요.
JavaScript 비교:
// JavaScript는 어차피 객체를 참조로 전달함 function calculateLength(s) { return s.length; } const s1 = "hello"; const len = calculateLength(s1); console.log(`'${s1}'의 길이는 ${len}입니다.`); // 똑같이 작동
차이점이요? Rust에서는 컴파일러가 calculate_length가 s1을 수정하거나 유지할 수 없다는 것을 보장해요. JavaScript에서는 그냥 함수를 믿어야 하죠.
가변 빌림: 단일 작성자 규칙
Rust에서는 둘 중 하나만 가질 수 있어요:
- 여러 개의 불변 참조 (
&T) - 또는 하나의 가변 참조 (
&mut T)
동시에 둘 다는 안 됩니다.
fn main() { let mut s = String::from("hello"); let r1 = &s; // OK: 불변 빌림 let r2 = &s; // OK: 또 다른 불변 빌림 // let r3 = &mut s; // 에러! 불변으로 빌린 동안 가변으로 빌릴 수 없음 println!("{} and {}", r1, r2); // r1과 r2는 이 시점 이후로 더 이상 사용되지 않음 let r3 = &mut s; // 이제 OK! r3.push_str(", world"); }
이게 왜 중요할까요: 이 규칙은 컴파일 시점에 데이터 레이스를 방지해요. JavaScript에서 코드의 한 부분이 객체를 수정하는 동안 다른 부분이 읽어서 생긴 버그를 경험해본 적 있으시죠? Rust는 이걸 불가능하게 만듭니다.
배열과 반복
JavaScript:
const numbers = [1, 2, 3, 4, 5]; // Map const doubled = numbers.map(x => x * 2); // Filter const evens = numbers.filter(x => x % 2 === 0); // Reduce const sum = numbers.reduce((acc, x) => acc + x, 0); // Find const firstEven = numbers.find(x => x % 2 === 0);
Rust:
let numbers = vec![1, 2, 3, 4, 5]; // Map let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect(); // Filter let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect(); // Reduce (Rust에서는 fold라고 함) let sum: i32 = numbers.iter().fold(0, |acc, x| acc + x); // 또는 간단하게: let sum: i32 = numbers.iter().sum(); // Find let first_even = numbers.iter().find(|x| *x % 2 == 0);
핵심 차이점:
|x|는 Rust의 클로저 문법 (화살표 함수처럼).iter()는 이터레이터를 생성함.collect()는 이터레이터를 다시 컬렉션으로 변환함- 타입을 지정하거나 Rust가 추론하게 해야 함
옵셔널 값 (null 처리)
JavaScript:
function findUser(id) { const user = database.get(id); if (user === null || user === undefined) { return "Anonymous"; } return user.name; } // 또는 옵셔널 체이닝으로 const name = user?.name ?? "Anonymous";
Rust:
fn find_user(id: u32) -> String { let user = database.get(id); match user { Some(u) => u.name.clone(), None => String::from("Anonymous"), } } // 더 간결하게 let name = user.map(|u| u.name.clone()).unwrap_or(String::from("Anonymous")); // 또는 if let으로 더 간단하게 if let Some(user) = database.get(id) { println!("찾음: {}", user.name); }
Rust에는 null이 없어요. 대신 Option<T>를 사용합니다:
Some(value)= 값이 있음None= 값이 없음
컴파일러가 두 경우 모두 처리하도록 강제해요. 더 이상 "undefined is not an object" 에러 없음!
에러 처리
JavaScript:
async function fetchData(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return await response.json(); } catch (error) { console.error("Fetch 실패:", error); throw error; } }
Rust:
use reqwest; async fn fetch_data(url: &str) -> Result<serde_json::Value, reqwest::Error> { let response = reqwest::get(url).await?; let data = response.json().await?; Ok(data) } // 사용하기 match fetch_data("https://api.example.com/data").await { Ok(data) => println!("데이터 받음: {:?}", data), Err(e) => eprintln!("Fetch 실패: {}", e), }
? 연산자는 Rust의 "이게 실패하면 에러를 즉시 반환"과 같아요. 자동 try-catch 전파 같은 거죠.
진짜 뭔가 만들어보자: JSON CLI 도구
JS 개발자가 실제로 쓸 법한 CLI 도구를 만들어볼게요—JSON 포맷터:
use std::env; use std::fs; use serde_json::{Value, to_string_pretty}; fn main() { // 커맨드 라인 인자 가져오기 (process.argv처럼) let args: Vec<String> = env::args().collect(); if args.len() != 2 { eprintln!("Usage: {} <file.json>", args[0]); std::process::exit(1); } let filename = &args[1]; // 파일 읽기 (fs.readFileSync처럼) let contents = match fs::read_to_string(filename) { Ok(c) => c, Err(e) => { eprintln!("파일 읽기 에러: {}", e); std::process::exit(1); } }; // JSON 파싱 let parsed: Value = match serde_json::from_str(&contents) { Ok(v) => v, Err(e) => { eprintln!("잘못된 JSON: {}", e); std::process::exit(1); } }; // 예쁘게 출력 match to_string_pretty(&parsed) { Ok(pretty) => println!("{}", pretty), Err(e) => eprintln!("포맷팅 에러: {}", e), }; }
실행하려면:
# Cargo.toml에 의존성 추가 # [dependencies] # serde_json = "1.0" cargo build --release ./target/release/json-formatter messy.json
컴파일된 바이너리는 ~1MB이고 밀리초 단위로 실행됩니다—CLI 도구와 함께 Node.js를 배포하는 것과 비교해보세요.
비동기 Rust: 그렇게 다르지 않아요
현대 JavaScript는 async/await가 전부죠. Rust도 있어요:
JavaScript:
async function fetchMultiple(urls) { const promises = urls.map(url => fetch(url).then(r => r.json())); const results = await Promise.all(promises); return results; }
Rust (tokio 런타임 사용):
use futures::future::join_all; async fn fetch_multiple(urls: Vec<&str>) -> Vec<Result<String, reqwest::Error>> { let futures = urls.iter().map(|url| async move { let response = reqwest::get(*url).await?; response.text().await }); join_all(futures).await } #[tokio::main] async fn main() { let urls = vec![ "https://api.example.com/1", "https://api.example.com/2", ]; let results = fetch_multiple(urls).await; for result in results { match result { Ok(body) => println!("받음: {:.100}...", body), Err(e) => eprintln!("에러: {}", e), } } }
구조가 놀랍도록 비슷하죠! 주요 차이점은 Rust는 명시적인 async 런타임이 필요하다는 거예요 (tokio가 가장 인기 있음).
웹 개발자를 위한 Rust 생태계
가장 많이 쓸 크레이트들(Rust의 npm 패키지)이에요:
웹 프레임워크
- Axum - 새로운 표준, Tokio 팀이 만듦
- Actix Web - 실전 검증됨, 극도로 빠름
- Rocket - 개발자 친화적, 훌륭한 인체공학
직렬화
- Serde - JSON, YAML, TOML 등의 사실상 표준
- serde_json - JSON 전용
HTTP 클라이언트
- Reqwest - Rust의 axios
CLI 도구
- Clap - 인자 파싱 (commander.js처럼)
- Indicatif - 프로그레스 바
- Colored - 터미널 색상
WebAssembly
- wasm-bindgen - JS/Rust 상호운용
- wasm-pack - WASM 패키지 빌드 및 배포
성능 비교: 실제 숫자
현실적인 워크로드를 비교해볼게요—100MB JSON 파일 처리:
| 작업 | Node.js | Rust | 속도 향상 |
|---|---|---|---|
| JSON 파싱 | 2.3s | 0.18s | 12.7배 |
| 이메일 찾기 (정규식) | 4.1s | 0.31s | 13.2배 |
| 변환 & 직렬화 | 3.8s | 0.24s | 15.8배 |
| 메모리 사용량 | 890MB | 210MB | 4.2배 적음 |
이 숫자가 중요한 경우들:
- 빌드 도구 만들 때 (esbuild가 비슷한 이유로 Go로 작성됨)
- 로그나 큰 데이터셋 처리할 때
- 메모리가 제한된 환경에서 실행할 때 (서버리스, 엣지)
JS 개발자가 삽질하기 쉬운 부분들
1. 문자열이 복잡함
let s1 = "hello"; // &str - 문자열 슬라이스 (빌린 것) let s2 = String::from("hello"); // String - 소유한 문자열 // 이건 안 됨: // let s3: String = "hello"; // 에러! // 변환해야 함: let s3: String = "hello".to_string(); let s4: String = String::from("hello");
경험칙: 함수 파라미터에는 &str, 데이터를 소유해야 할 때는 String 사용.
2. 예외 없음, Result만 있음
// 이건 컴파일 안 됨 - Result를 처리해야 함 let file = File::open("data.txt"); // Result<File, Error> 반환 // 처리해야 함 let file = File::open("data.txt")?; // 에러 전파 // 또는 let file = File::open("data.txt").unwrap(); // 에러면 패닉 // 또는 let file = File::open("data.txt").expect("파일 열기 실패"); // 메시지와 함께 패닉
3. 불변이 기본값
let x = 5; // x = 6; // 에러! 변수는 기본적으로 불변 let mut y = 5; y = 6; // 됨!
이건 JavaScript의 let(가변) vs const(불변)의 반대예요.
4. 암시적 타입 변환 없음
let x: i32 = 5; let y: i64 = 10; // let z = x + y; // 에러! i32와 i64를 더할 수 없음 let z = x as i64 + y; // 명시적으로 변환해야 함
이제 뭘 공부하면 될까요?
JS 개발자를 위한 현실적인 학습 로드맵이에요:
1-2주차: 기초
- "The Rust Book" 처음 8장 완료 (무료 온라인)
- 작은 프로그램 작성: FizzBuzz, 파일 읽기, 간단한 CLI
3-4주차: 소유권 심화
- 소유권 챕터 다시 읽기
- Rustlings 연습 완료 (인터랙티브 연습)
- 파일 저장 기능이 있는 TODO CLI 앱 만들기
2개월차: 웹 개발
- Axum으로 REST API 만들기
- 데이터베이스 연결 (SQLx 또는 Diesel)
- 클라우드 플랫폼에 배포
3개월차: WebAssembly
- WASM 모듈 만들기
- React/Vue/Svelte 앱에 통합
- 순수 JavaScript와 성능 비교
결론: 그래서 Rust, 배울 만한 거야?
JavaScript 개발자에게 Rust는 대체재가 아니라 보완재예요. 웹 앱은 여전히 TypeScript로 작성할 거예요. 하지만 이런 게 필요할 때:
- 연산 집약적인 작업에 최대 성능
- GC 일시 정지 없는 예측 가능한 지연 시간
- CLI 도구나 서버리스를 위한 작은 바이너리
- 브라우저 성능을 위한 WebAssembly
...Rust가 답입니다.
학습 곡선은 진짜예요. 소유권과 빌림이 처음에는 혼란스러울 거예요. 컴파일러가 계속 코드를 거부할 거예요 (하지만 에러 메시지가 정말 도움됨).
하지만 한 번 감이 오면—그리고 반드시 옵니다—대부분의 JavaScript 개발자가 갖지 못한 슈퍼파워를 갖게 될 거예요. 메모리가 실제로 어떻게 작동하는지 이해하게 될 거예요. 어떤 언어에서든 더 안전한 코드를 작성하게 될 거예요. 그리고 JavaScript로는 그냥 못 푸는 문제를 해결할 수 있는 도구를 갖게 될 거예요.
작은 것부터 시작하세요. JSON 포맷터. 파일 이름 변경기. 간단한 CLI 도구. 컴파일러가 가르쳐주게 하세요. 그러면 어느새 JavaScript보다 20배 빠른 Rust를 작성하고 있을 거예요.
Rust에 오신 걸 환영합니다. 컴파일러는 엄격하지만, 여러분 편이에요.
관련 도구 둘러보기
Pockit의 무료 개발자 도구를 사용해 보세요