Syntax Cram Sheet

바이브코딩만 해봤어도, 이 정도면 읽고 쓸 수 있습니다

직접 타이핑해본 적이 별로 없어도 괜찮습니다. 기본 문법 14개만 Rust와 TypeScript를 나란히 놓고 봅니다 — 복잡한 응용은 다 빼고, "이 코드가 뭘 하는 코드인지 알아보는" 수준까지만. 각 항목 아래 짧은 메모는 두 언어의 사고방식이 어디서 갈리는지를 짚어줍니다.

Rust — oxicode·oxios·oxibrain·oxibrowser TypeScript — 프론트엔드·Node 비교 기준
00

딱 5개만 먼저 외우고 시작

시간 없으면 이 카드 5개만이라도. 아래 14개 항목은 결국 이 5가지의 구체적인 예시입니다.

01

mut 기본값이 뒤바뀜

Rust는 기본 불변, mut를 붙여야 가변. TS는 반대로 let이 가변 기본값이고 const가 불변.

02

match는 다 다뤄야 함

match는 모든 경우를 처리 안 하면 컴파일 에러. switch/삼항연산자는 그런 강제가 없음.

03

Option·Result vs null·예외

Rust는 "없을 수도/실패할 수도"를 타입으로 강제. TS는 undefined 체크나 catch를 깜빡해도 컴파일이 통과.

04

소유권 vs GC

Rust는 "누가 이 값을 갖고 있는지"를 컴파일 타임에 추적. TS는 가비지 컬렉터가 알아서 치움.

05

세미콜론 없는 마지막 줄 = 반환값

Rust 함수 마지막 식에서 세미콜론을 빼면 그게 반환값. TS는 항상 return이 필요.

01

변수 & 타입

이름은 비슷한데 역할이 정반대인 게 있으니 여기서 먼저 걸러둡니다.

Rust
let x = 5;                    // 불변(기본값) — 재할당하면 에러
let mut y = 10;                // mut 없으면 가변 불가
y = 20;

let name: &str = "won";       // 타입은 보통 추론, 명시도 가능
const MAX: u32 = 100;          // const는 타입 명시 필수
TypeScript
let x = 5;                     // 재할당 가능 (기본)
const y = 10;                  // 재할당 불가 — 실무 기본값
let name: string = "won";
const MAX: number = 100;
헷갈리는 지점 Rust는 기본이 불변(let)이고 mut를 붙여야 가변입니다. TS는 정반대로 let이 가변 기본값이고 const가 불변이에요 — 글자는 비슷한데 역할이 뒤바뀐 대표 사례입니다.
02

함수

시그니처 쓰는 법과 반환값 처리 방식만 보면 됩니다.

Rust
fn add(a: i32, b: i32) -> i32 {
    a + b   // 세미콜론 없으면 이 줄이 곧 반환값
}

fn greet(name: &str) {
    println!("hello, {}", name);
}
TypeScript
function add(a: number, b: number): number {
    return a + b;   // return 필수
}

const greet = (name: string): void => {
    console.log(`hello, ${name}`);
};
핵심 Rust는 함수(혹은 블록)의 마지막 식에서 세미콜론을 빼면 그게 그대로 반환값이 됩니다. return은 중간에 조기 리턴할 때만 씁니다. TS/JS는 항상 return을 명시해야 해요.
03

구조체 vs 인터페이스 + 클래스

데이터 모양을 정의하고 그 위에 동작을 붙이는 방법입니다.

Rust
struct User {
    name: String,
    age: u32,
}

impl User {
    fn greet(&self) -> String {
        format!("hi, {}", self.name)
    }
}

let u = User { name: "won".to_string(), age: 20 };
println!("{}", u.greet());
TypeScript
interface User {
    name: string;
    age: number;
}

function greet(u: User): string {
    return `hi, ${u.name}`;
}

const u: User = { name: "won", age: 20 };
console.log(greet(u));
핵심 Rust는 데이터(struct)와 동작(impl)이 분리돼 있습니다. TS interface는 모양(shape)만 정의할 뿐이라, 메서드를 진짜로 붙이려면 class를 써야 해요.
04

enum

Rust enum은 단순 상수 나열이 아니라 각 값마다 데이터를 실을 수 있는 컨테이너입니다.

Rust
enum Status {
    Loading,
    Success(String),        // variant마다 다른 데이터를 담음
    Error { code: u32 },
}

let s = Status::Success("done".to_string());

match s {
    Status::Loading => println!("loading"),
    Status::Success(msg) => println!("ok: {}", msg),
    Status::Error { code } => println!("err {}", code),
}
TypeScript
type Status =
    | { kind: "loading" }
    | { kind: "success"; msg: string }
    | { kind: "error"; code: number };

const s: Status = { kind: "success", msg: "done" };

switch (s.kind) {
    case "loading": console.log("loading"); break;
    case "success": console.log(`ok: ${s.msg}`); break;
    case "error": console.log(`err ${s.code}`); break;
}
핵심 Rust enum은 variant마다 자기만의 데이터를 갖는 "태그드 유니온"입니다. TS도 enum 키워드가 있지만, 실무에서는 이렇게 kind 필드로 구분하는 union 패턴이 더 흔합니다.
05

조건 & 매칭

if는 거의 똑같고, match/switch에서 차이가 갈립니다.

Rust
let n = 7;

if n % 2 == 0 {
    println!("even");
} else {
    println!("odd");
}

let label = match n {
    0 => "zero",
    1..=9 => "single digit",   // 범위 매칭
    _ => "big",                  // _ 는 default
};
TypeScript
const n = 7;

if (n % 2 === 0) {
    console.log("even");
} else {
    console.log("odd");
}

const label =
    n === 0 ? "zero" :
    n < 10 ? "single digit" :
    "big";
면접에서 자주 물어보는 차이 match는 모든 경우를 다뤄야 컴파일이 통과합니다(exhaustive) — 하나라도 빠뜨리면 에러예요. switch나 삼항연산자는 그런 강제가 없어서, 케이스를 실수로 빠뜨려도 조용히 통과합니다.
06

반복문

range와 컬렉션 순회를 같은 for 문 하나로 처리한다는 점이 TS의 for...of와 닮았습니다.

Rust
for i in 0..5 {                // 0,1,2,3,4 (5는 미포함)
    println!("{}", i);
}

let v = vec![1, 2, 3];
for x in &v {                    // 참조로 순회 — v는 여전히 살아있음
    println!("{}", x);
}

let mut i = 0;
while i < 3 {
    i += 1;
}
TypeScript
for (let i = 0; i < 5; i++) {
    console.log(i);
}

const v = [1, 2, 3];
for (const x of v) {
    console.log(x);
}

let i = 0;
while (i < 3) {
    i++;
}
07

컬렉션

map/filter는 이름과 쓰임이 거의 똑같아서 제일 편한 지점입니다.

Rust
let mut v: Vec<i32> = vec![1, 2, 3];
v.push(4);

let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
let evens: Vec<&i32> = v.iter().filter(|x| *x % 2 == 0).collect();

use std::collections::HashMap;
let mut m: HashMap<String, i32> = HashMap::new();
m.insert("a".to_string(), 1);
TypeScript
const v: number[] = [1, 2, 3];
v.push(4);

const doubled: number[] = v.map((x) => x * 2);
const evens: number[] = v.filter((x) => x % 2 === 0);

const m = new Map<string, number>();
m.set("a", 1);
핵심 Rust는 .iter()로 순회 시작을 명시하고 .collect()로 마지막에 결과 타입을 정해줘야 컴파일됩니다. TS는 배열 메서드가 바로 새 배열을 돌려줘서 한 단계 짧게 느껴져요.
08

Option · Result vs null · try-catch

두 언어의 사고방식이 가장 크게 갈리는 지점입니다.

Rust
fn find(v: &[i32], target: i32) -> Option<usize> {
    for (i, x) in v.iter().enumerate() {
        if *x == target { return Some(i); }
    }
    None
}

match find(&[1, 2, 3], 2) {
    Some(i) => println!("found at {}", i),
    None => println!("not found"),
}

fn parse(s: &str) -> Result<i32, String> {
    s.parse::<i32>().map_err(|_| "bad number".to_string())
}

let n = parse("42")?;   // ? 는 에러면 즉시 함수 리턴
TypeScript
function find(v: number[], target: number): number | undefined {
    const i = v.indexOf(target);
    return i === -1 ? undefined : i;
}

const i = find([1, 2, 3], 2);
if (i !== undefined) console.log(`found at ${i}`);

function parse(s: string): number {
    const n = Number(s);
    if (Number.isNaN(n)) throw new Error("bad number");
    return n;
}

try {
    const n = parse("42");
} catch (e) {
    console.error(e);
}
이게 사실상 제일 중요한 차이 Rust는 "값이 없을 수도"(Option), "실패할 수도"(Result) 있다는 걸 타입으로 강제해서 컴파일러가 처리 누락을 잡아줍니다. TS/JS는 undefined 체크를 깜빡하거나 catch를 안 붙여도 컴파일이 그냥 통과해요. ? 연산자는 "Result가 에러면 그 자리에서 함수를 즉시 리턴"을 한 글자로 압축한 문법입니다.
09

클로저

화살표 함수와 개념적으로 거의 1:1 대응입니다.

Rust
let add_one = |x: i32| x + 1;
println!("{}", add_one(4));

let factor = 3;
let scale = |x: i32| x * factor;   // 바깥 변수를 자동으로 캡처
TypeScript
const addOne = (x: number) => x + 1;
console.log(addOne(4));

const factor = 3;
const scale = (x: number) => x * factor;  // 마찬가지로 자동 캡처
핵심 차이는 Rust가 캡처한 변수의 소유권/참조를 컴파일러가 추적해서, 나중에 잘못 쓰면 컴파일 에러로 막는다는 것 — TS는 그런 제약이 없습니다.
10

트레이트 vs 인터페이스 구현

다형성을 표현하는 역할이 거의 같습니다.

Rust
trait Greet {
    fn greet(&self) -> String;
}

struct Dog;

impl Greet for Dog {
    fn greet(&self) -> String {
        "woof".to_string()
    }
}

fn say_hi(g: &impl Greet) {   // 트레이트를 만족하는 아무 타입이나
    println!("{}", g.greet());
}
TypeScript
interface Greet {
    greet(): string;
}

class Dog implements Greet {
    greet(): string {
        return "woof";
    }
}

function sayHi(g: Greet): void {
    console.log(g.greet());
}
핵심 차이는 Rust가 이미 존재하는 타입(심지어 외부 crate 타입)에도 나중에 impl로 트레이트를 붙일 수 있다는 것 — TS는 클래스를 선언하는 시점에 implements를 못 박아야 합니다.
11

제네릭

<T> 문법 자체는 거의 동일합니다.

Rust
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut max = &list[0];
    for item in list {
        if item > max { max = item; }
    }
    max
}

let nums = vec![3, 7, 2];
println!("{}", largest(&nums));
TypeScript
function largest<T>(list: T[]): T {
    let max = list[0];
    for (const item of list) {
        if (item > max) max = item;
    }
    return max;
}

const nums = [3, 7, 2];
console.log(largest(nums));
핵심 차이는 Rust가 T: PartialOrd처럼 "이 타입은 비교 가능해야 함"을 트레이트 경계(bound)로 명시해야 컴파일된다는 것 — TS는 그런 제약을 안 걸면 일단 넘어가고 런타임에서야 문제가 드러날 수 있습니다.
12

소유권 — Rust에만 있는 개념

면접에서 "Rust를 왜 골랐냐"는 질문의 답이 여기 있습니다.

Rust
// String은 힙에 저장되는 소유 타입 — 정수 같은 Copy 타입은 이 규칙에서 열외
let s1 = String::from("hello");
let s2 = s1;                     // s1의 소유권이 s2로 "이동"(move) — s1은 이제 못 씀
// println!("{}", s1);      // 컴파일 에러!

fn print_len(s: &String) {        // &로 "빌려오기"(borrow) — 소유권은 호출자 것 그대로
    println!("{}", s.len());
}
print_len(&s2);                     // s2는 여전히 살아있음

fn add_world(s: &mut String) {     // &mut 로 가변 빌림 — 값을 바꿀 수 있음
    s.push_str(" world");
}
TS/JS엔 대응 개념이 없음 TS/JS는 가비지 컬렉터가 알아서 안 쓰는 메모리를 치워주기 때문에, "이 값을 누가 갖고 있는지"를 언어가 검사하지 않습니다. Rust는 GC 없이도 메모리를 안전하게 관리하려고, "이 값을 실제로 갖고 있는 사람이 항상 하나(혹은 읽기 전용으로 여럿)"임을 컴파일 타임에 강제합니다 — 그 대가로 move/borrow 규칙을 지켜야 컴파일이 통과해요. 성능(GC 없음)과 메모리 안전성을 동시에 잡는다는 것, 이게 면접에서 나올 답입니다.
13

async / await

키워드와 흐름은 거의 동일하고, 런타임이 필요한지 여부만 다릅니다.

Rust
async fn fetch_name() -> String {
    "won".to_string()
}

#[tokio::main]
async fn main() {
    let name = fetch_name().await;
    println!("{}", name);
}
TypeScript
async function fetchName(): Promise<string> {
    return "won";
}

async function main() {
    const name = await fetchName();
    console.log(name);
}

main();
핵심 Rust는 async fn을 실제로 돌려줄 런타임(tokio 같은)이 따로 필요합니다. TS는 Node나 브라우저에 런타임이 이미 내장돼 있어서 별도 설정이 없습니다.
14

출력 / 문자열 포맷

자리표시자 문법만 다르고 하는 일은 같습니다.

Rust
let name = "won";
let age = 20;
println!("{} is {} years old", name, age);
println!("{name} is {age} years old"); // 이름 있는 캡처(최신 문법)
let s = format!("{}-{}", name, age);       // 문자열로 "만들기"만, 출력은 안 함
TypeScript
const name = "won";
const age = 20;
console.log(`${name} is ${age} years old`);
const s = `${name}-${age}`;
핵심 println!/format!{} 자리에 인자를 순서대로 채우거나, {name}처럼 변수 이름을 직접 넣을 수 있습니다. TS 템플릿 리터럴의 ${}랑 하는 일은 똑같은데 문법만 다릅니다.
이 페이지는 문법 감을 빠르게 잡기 위한 최소 예제입니다 — 실제 인터뷰 코드베이스 맥락은 oxi 생태계 해부 · SDK ↔ oxios 관계 페이지를 참고하세요.