JS, TS랑 비교하며 Rust 공부하기 (1)

하던 공부를 계속하다보면 질리고 지칠때가 있는데, 그럴땐 딴짓을 하면 좀 기분이 괜찮아진다.
그렇다고 딴짓으로 유희를 즐기면 뭔가 남는게 없는 듯해서 다른 언어를 공부하며 기분 전환을 해왔다.
지금까지 이것저것 다른 언어들을 슬며시 공부해왔지만 시간이 지나면 다까먹어서 왜 공부한거지? 라는 생각을 계속 해왔는데, 물론 공부를 위한 공부가 아닌 딴짓을 위한 공부라 시간 낭비라고 생각하진 않지만 이번에는 좀 제대로 정리하고 꾸준히 써보자는 결심을 했다. 그래서 이번에 선택한게 Rust!

이전 부터 Rust에 관심을 가지고 있었는데 그 이유를 나열해보자면,

  • C, C++ 보다 안전하다고 들었고
  • Deno를 Rust로 작성했고
  • WebAssembly를 Rust로 작성할 수 있고
  • 기존의 코드를 Rust로 작성해서 돌리면 몇배나 빠르다는 주워들은 이야기
  • 함수형 프로그래밍 관련 책에 등장
  • TypeScript랑 비슷한 부분이 있다.

이런 이유들이 있다.

그럼 한글로 변역된 문서를 보면서 정리한 글을 공유해보겠다!


설치

최신 stable 버전 러스트 설치 명령어는 아래와 같다.

$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

설치 확인

설치가 됐는지 확인할땐 버전을 통해 확인한는건 관습인건가..?!

$ rustc --version

rustc x.y.z (abcabcabc yyyy-mm-dd)

업데이트 및 삭제

업데이트 및 삭제할 일은 많이 없지만 알아두자.

$ rustup update
$ rustup self uninstall

Cargo

Cargo는 러스트 빌드 시스템 및 패키지 매니저이다.
npm을 생각하면 편하다.

프로젝트 생성

새로운 디렉터리를 생성하고 프로젝트를 생성하고 싶다면

cargo new 디렉터리명

원하는 디렉터리 내부에서 프로젝트를 생성하고 싶다면

cargo init

프로젝트 디렉터리 구조

├── src
│   └── 소스 코드
└── Cargo.toml

Cargo는 소스 파일이 src 디렉터리 내에 있다고 예상한다.
Cargo.tomlpackage.json과 비슷한 것이다.

빌드와 실행

JS와는 다르게 컴파일 언어는 실행 파일을 만들어서 실행해야 한다.
많이 다르지만 친숙하게 설명하면 TS에서 JS로 트랜스파일링 해준다고 생각하자.

빌드 진행

실행 파일은 ./target/debug 안에 실행 파일이 생성된다.

cargo build

릴리즈 빌드 진행

실행 파일은 ./target/release 안에 실행 파일이 생성된다.
컴파일이 오래 걸리는 대신 러스트 코드가 더 빠르게 작동한다.

cargo build --release

빌드와 실행을 한 번에 진행

매번 빌드하고 실행하면 귀찮으니 run 명령어를 쓰면 된다.

cargo run

빌드되는지 확인

실행 파일은 생성하지 않고 소스 코드가 문제없이 빌드되는지 확인할 수 있다.

cargo check

문법

변수

변수를 선언하면 기본적으로 불변이지만 mut 키워드를 사용해서 가변으로 바꿀 수 있다.
JS로 설명하면 const라고 생각하면 편하고 mut를 사용하면 let으로 선언했다고 생각하면 된다.

섀도잉이 가능하며 같은 변수명으로 다른 타입의 값을 저장할 수 있다.
var처럼 쓸 수 있는 부분.

가변인 상태에서 다른 타입을 할당하면 에러가 발생한다.
TS에서 타입을 선언하고 다른 타입으로 재할당하면 뜨는 에러를 생각해보자.

let a = 1;
let mut b = 2;

let a = "a";
a = 3; // error
b = 3;
b = "b"; // error

상수

항상 불변이고 타입을 반드시 명시해야 한다.
런타임에서만 계산될 수 있는 결과값은 할당할 수 없다.
JS의 const로 상수화해주는 느낌.

const NUMBER_ONE: u32 = 1;

구문과 표현식

  • 구문은 어떤 동작을 수행하고 값을 반환하지 않는 명령이다.
  • 표현식은 결과값을 평가한다.

함수를 호출하는 것도, 매크로를 호출하는 것도, 스코프 블록도 표현식이다.
표현식은 종결을 나타내는 세미콜론을 쓰지 않는다. 만약 표현식 끝에 세미콜론을 추가하면, 표현식은 구문으로 변경되고 값을 반환하지 않게 된다.
구문은 값을 평가하지 않기에 ()로 표현되는 유닛 타입으로 표현된다. 약간 함수의 반환값이 없으면 undefined으로 평가하는거랑 비슷한 듯하다.

let y = {
    let x = 3;
    x + 1 // return을 생략할 수 있다!
};

함수

러스트는 함수 위치를 고려하지 않으며, 호출하는 쪽에서 볼 수 있는 스코프 어딘가에 정의만 되어있으면 된다. JS의 함수 선언문과 비슷한 동작.
TS와는 다르게 함수 시그니처에서는 각 매개변수의 타입과 반환값의 타입은 반드시 선언되어야 한다.

fn add_five(num: i32) -> i32 {
    num + 5
}

if 표현식

러스트는 부울린 타입이 아닌 값을 부울린 타입으로 자동 변환하지 않기 때문에, 항상 명시적으로 부울린 타입의 조건식을 제공해야 한다.
JS와는 다르게 소괄호로 감싸지 않는다.

if number % 4 == 0 {
    println!("number is divisible by 4");
} else if number % 3 == 0 {
    println!("number is divisible by 3");
} else if number % 2 == 0 {
    println!("number is divisible by 2");
} else {
    println!("number is not divisible by 4, 3, or 2");
}

변수가 가질 수 있는 타입은 오직 하나이기 때문에, 다른 조건에 따라 다른 타입을 할당하면 에러가 발생한다.

let number = if condition { 5 } else { "six" }; // error

반복문

loop 키워드는 무한 반복을 수행한다. (루프 라벨도 있다.)

let mut counter = 0;

let result = loop {
    counter += 1;

    if counter == 10 {
        break counter * 2;
    }
};

while 반복문은 JS와 같다.

let mut number = 3;

while number != 0 {
    println!("{number}"); // 3 2 1

    number -= 1;
}

for 반복문을 사용하여 컬렉션의 각 아이템에 대해 코드를 수행한다.
JS의 for..of문으로 생각하면 된다.

let a = [10, 20, 30, 40, 50];

for element in a {
    println!("the value is: {element}");
}

Range 타입을 이용하면 특정 횟수만큼 반복문을 구현할 수 있다.
Range 는 어떤 숫자에서 시작하여 다른 숫자 종료 전까지의 모든 숫자를 차례로 생성해준다. JS에도 추가해주면 좋을텐데…

for number in (1..4).rev() {
    println!("{number}"); // 3 2 1
}

자리표시자

{}는 어떤 위치에 값을 출력하기 위해 사용한다.

let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);

match 표현식

주어진 값이 특정 패턴과 매칭되면 해당 패턴의 코드를 실행한다.
JS의 switch문과 비슷하지만, 러스트의 컴파일러는 모든 가능한 경우가 처리되는지 검사한다.

match value {
	패턴1 => 코드1,
	패턴2 => 코드2,
	패턴2 => 코드3,
}

포괄 패턴을 통해 나머지 모든 값에 대해 마지막 패턴이 매칭되게 할 수 있다.
_는 어떠한 값이라도 매칭되지만, 그 값을 바인딩하지는 않는 특별한 패턴이다.

match num {
		2 => two(),
		4 => four(),
		other => number(other)
}

match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => reroll(),
}

if let

if let 문법은 if와 let을 조합하여 하나의 패턴만 매칭시키고 나머지 경우는 무시하도록 값을 처리한다.
아래와 같은 match 문으로 작성된 코드의 문법 설탕이라고 생각하면 되며, match가 강제했던 철저한 검사를 안하게 되므로 잘 생각하고 써야 한다.

let config_max = Some(3u8);
match config_max {
    Some(max) => println!("The maximum is configured to be {}", max),
    _ => (),
}

else도 사용할 수 있으며, else 뒤에 나오는 코드 블록은 match 표현식에서 _ 케이스 뒤에 나오는 코드 블록과 동일하다.

let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is configured to be {}", max);
} else {
	println!("else");
}

데이터 타입

스칼라 타입

스칼라 타입은 하나의 값을 표현하며, 네 가지 스칼라 타입이 있다.

  • 정수
  • 부동 소수점 숫자
  • 부울린
  • 문자

정수 타입

길이부호 있음 (signed)부호 없음 (unsigned)
8-biti8u8
16-biti16u16
32-bit (기본)i32u32
64-biti64u64
128-biti128u128
arch (컴퓨터 환경)isizeusize
숫자 리터럴
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b’A’

부동 소수점 타입

  • f32
  • f64 (기본)
let x = 2.0; // f64
let y: f32 = 3.0; // f32

부울린 타입

  • bool
let t = true;
let f: bool = false;

문자 타입

  • char : 4바이트, 유니코드, 이모지 포함
let c = 'z';
let z: char = 'ℤ';
let heart_eyed_cat = '😻';

복합 타입

튜플 타입

고정된 길이를 가지며, 튜플 내의 타입들은 달라도 된다.
비어있는 튜플은 유닛으로 불리며 빈 값이나 비어있는 반환 타입을 나타낸다.
표현식이 어떠한 값도 반환하지 않는다면 암묵적으로 유닛 값을 반환한다.

let tup = (500, 6.4, 1);

let (x, y, z) = tup;

let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;

let six_point_four = x.1;

let one = x.2;

배열 타입

러스트의 배열은 고정된 길이를 가지며, 모든 요소는 같은 타입이어야 한다.
유효하지 않은 배열 요소에 접근하면 패닉을 일으키며 즉시 실행을 종료한다.
길이에는 상수를 넣어줘야 하며, 만약 길이가 동적으로 결정된다면 벡터를 사용해야 한다.

let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [3; 5]; // [3, 3, 3, 3, 3];

let first = a[0];

기본적인 내용을 확실히 숙지해야 그 다음으로 넘어가도 시간을 절약할 수 있다.
다음에 공유할 내용의 시작은 Rust의 특별한 개념인 소유권이다.