Hello, World!

이 섹션에서는 Rust와 WebAssembly 프로그램을 처음 실행하는 과정을 다룹니다. "Hello, World!" 메세지를 보여주는 웹페이지를 만들어봅시다.

시작하기 전에 셋업 가이드를 읽고 잘 따라왔는지 한 번 더 확인해 주세요.

프로젝트 템플릿 클론하기

이 프로젝트 템플릿은 합리적인 기본 설정으로 구성돼 있습니다. 이 템플릿을 활용해서 웹으로 코드를 빠르게 개발, 통합 및 패키징할 수 있습니다.

이 명령어로 프로젝트를 클론해보세요:

cargo generate --git https://github.com/rustwasm/wasm-pack-template

새 프로젝트 이름을 어떻게 지을지 입력하게 됩니다. "wasm-game-of-life" 이라는 이름을 사용합시다.

wasm-game-of-life

어떻게 구성돼 있나요?

새로 생성한 wasm-game-of-life 프로젝트를 열어보도록 합시다.

cd wasm-game-of-life

그다음 안에 어떤 파일이 담겨있는지 확인해 봅시다:

wasm-game-of-life/
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
└── src
    ├── lib.rs
    └── utils.rs

몇 가지 파일을 더 자세히 살펴볼까요?

wasm-game-of-life/Cargo.toml

Cargo.toml 파일은 Rust의 패키지 매니저이자 빌드 툴인 cargo와 함께 사용되는데, 의존성과 메타데이터를 지정하는 역할을 합니다. 이 파일은 wasm-bindgen 의존성과 함께 wasm 라이브러리 생성에 사용될 수 있도록 올바르게 설정된 crate-type이 함께 사전에 미리 포함돼 있습니다. 몇 가지 필수가 아닌 의존성은 나중에 살펴보겠습니다.

wasm-game-of-life/src/lib.rs

src/lib.rs 파일은 WebAssembly로 컴파일하는 Rust 크레이트의 핵심 코드입니다. wasm-bindgen을 JavaScript를 조작하기 위해 사용하고, window.alert JavaScript 함수를 불러온 다음에 greet Rust 함수를 JavaScript로 보냅니다. 이렇게 "Hello World" alert 메세지를 표시시킬 수 있게 됩니다.

#![allow(unused)]
fn main() {
mod utils;

use wasm_bindgen::prelude::*;

// `wee_alloc` 기능이 활성화돼 있으면, `wee-alloc`를 전역 할당자로 사용합니다.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet() {
    alert("Hello, wasm-game-of-life!");
}
}

wasm-game-of-life/src/utils.rs

src/utils.rs 모듈은 자주 사용되는 유틸리티를 제공하는데, 이 유틸리티가 WebAssembly로 컴파일된 Rust 코드 작업을 더 쉽게 할 수 있도록 도와줍니다. wasm 코드 디버깅하기와 같은 튜토리얼 후반 섹션에서 이러한 유틸리티들을 더 자세히 살펴보겠습니다. 현재 시점에서는 이 파일을 무시해도 괜찮습니다.

프로젝트 빌드하기

wasm-pack을 사용하여 다음 빌드 과정을 자동화하게 됩니다:

  • Rust 1.30나 그 이후 버전을 사용하고 있는지, wasm32-unknown-unknown 타겟이 rustup을 통해 설치돼 있는지 확인합니다.
  • cargo를 사용하여 Rust 소스 코드를 WebAssembly .wasm 바이너리로 컴파일합니다.
  • wasm-bindgen을 사용하여 Rust로 생성한 WebAssembly를 사용할 수 있도록 JavaScript API를 생성합니다.

위 작업을 시작하려면, 프로젝트 경로에서 다음 명령어를 실행해 주세요:

wasm-pack build

빌드가 완료되면 결과물을 pkg 경로에서 확인해 보세요. 다음 내용물을 확인할 수 있습니다:

pkg/
├── package.json
├── README.md
├── wasm_game_of_life_bg.wasm
├── wasm_game_of_life.d.ts
└── wasm_game_of_life.js

README.md 파일이 메인 프로젝트에서 복사됐지만 나머지 파일들은 완전히 새로 생성된 부분을 확인할 수 있습니다.

wasm-game-of-life/pkg/wasm_game_of_life_bg.wasm

이러한 .wasm 파일은 Rust 컴파일러가 Rust 소스 코드로 생성한 바이너리 파일입니다. 이 파일은 wasm 형식으로 컴파일된 모든 Rust 함수들과 데이터로 구성돼 있습니다. 변환된 "greet" 함수가 예가 될 수 있습니다.

wasm-game-of-life/pkg/wasm_game_of_life.js

이러한 .js 파일은 wasm-bindgen에 의해 생성됩니다. DOM과 JavaScript 함수를 Rust에서 호출할 수 있게 해주고, JavaScript 환경에서도 WebAssembly 함수를 호출할 수 있도록 도와주는 유용한 API를 노출시켜줍니다. 예를 들어서, greet 이라는 JavaScript 함수를 통해 WebAssembly의 해당하는 함수를 호출할 수 있습니다. 현재 시점에서는 이러한 바인딩(bindings glue)들이 큰 역할을 하지는 않지만, 더 복잡한 값들을 wasm과 JavaScript 사이에서 주고받을 때 정말 도움이 많이 됩니다.

import * as wasm from './wasm_game_of_life_bg';

// ...

export function greet() {
    return wasm.greet();
}

wasm-game-of-life/pkg/wasm_game_of_life.d.ts

.d.ts 파일은 생성한 JavaScript 파일과 함께 사용할 수 있는 TypeScript 타입 정의를 포함합니다. TypeScript를 사용한다면 정적 타입 정의를 통해 IDE가 제공하는 자동 완성과 같은 기능을 사용해서 더 쉽게 WebAssembly 함수를 호출할 수 있게 됩니다. TypeScript를 사용하지 않는다면, 이 파일은 무시해도 괜찮습니다.

export function greet(): void;

wasm-game-of-life/pkg/package.json

package.json 파일은 생성된 JavaScript와 WebAssembly 패키지에 대한 메타데이터를 포함합니다. 이런 메타데이터는 npm과 JavaScript 번들러가 여러 패키지들의 종속성, 패키지 이름, 버전 등을 결정할 때 사용됩니다. JavaScript 툴링과 함께 사용하고 npm에 WebAssembly 패키지를 배포할 때 유용합니다.

{
  "name": "wasm-game-of-life",
  "collaborators": [
    "Your Name <your.email@example.com>"
  ],
  "description": null,
  "version": "0.1.0",
  "license": null,
  "repository": null,
  "files": [
    "wasm_game_of_life_bg.wasm",
    "wasm_game_of_life.d.ts"
  ],
  "main": "wasm_game_of_life.js",
  "types": "wasm_game_of_life.d.ts"
}

웹 페이지에 포함시키기

wasm-game-of-life 패키지를 웹페이지에 포함시키고 웹 환경에서 작동시키기 위해, create-wasm-app JavaScript 프로젝트 템플릿 을 사용해 봅시다.

다음 명령어를 wasm-game-of-life 경로 내부에서 실행해 주세요:

npm init wasm-app www

새롭게 생성된 내부 경로인 wasm-game-of-life/www 는 다음 파일들을 포함합니다:

wasm-game-of-life/www/
├── bootstrap.js
├── index.html
├── index.js
├── LICENSE-APACHE
├── LICENSE-MIT
├── package.json
├── README.md
└── webpack.config.js

한 번 더 생성된 파일들을 자세히 살펴봅시다.

wasm-game-of-life/www/package.json

package.json 파일은 webpackwebpack-dev-server 종속성들로 미리 셋업이 되어 있고, npm 에 배포돼 있는 wasm-pack-template의 초기 버전인 hello-wasm-pack 패키지 또한 종속성으로 포함하고 있습니다.

wasm-game-of-life/www/webpack.config.js

이 파일은 webpack과 로컬 개발 서버를 설정합니다. 미리 셋업 작업이 돼 있는데, webpack과 로컬 개발 서버를 사용할 때에도 전혀 따로 수정할 필요가 없습니다.

wasm-game-of-life/www/index.html

웹사이트에 사용되는 최상단 HTML 파일입니다. index.js 를 감싸주는 bootstrap.js를 부르는 것 외에는 특별한 동작을 하지 않습니다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello wasm-pack!</title>
  </head>
  <body>
    <script src="./bootstrap.js"></script>
  </body>
</html>

wasm-game-of-life/www/index.js

index.js는 작업을 하게 될 웹사이트 JavaScript의 진입점 (entry point) 입니다. JavaScript 코드와 wasm-pack-template의 컴파일 결과물인 WebAssembly가 포함돼 있는데, hello-wasm-pack npm 패키지를 로드하고 패키지 내부의 greet 함수를 호출해 줍니다.

import * as wasm from "hello-wasm-pack";

wasm.greet();

종속성 설치하기

우선, wasm-game-of-life/www 내부 경로에서 npm install 명령어를 실행하여 로컬 개발 서버와 종속성들이 설치돼 있는지 확인해 주세요:

npm install

이 커맨드는 한 번만 실행돼야 합니다. 실행하면 webpack JavaScript 번들러와 개발 서버를 설치할 수 있습니다.

단순히 간편하게 책을 진행하기 위해 이 번들러와 개발 서버를 사용할 예정이지만 webpack이 Rust와 WebAssembly 작업에 필수가 아니라는 점을 기억해 주세요. Parcel과 Rollup도 WebAssembly와 ECMAScript 모듈을 부르는 데 사용할 수 있습니다. 원한다면 Rust와 WebAssembly를 번들러 없이 사용할 수도 있습니다.

www 패키지 내부에서 로컬 wasm-game-of-life 패키지 사용하기

npm에서 다운로드한 hello-wasm-pack 대신에 로컬 환경에 있는 wasm-game-of-life를 사용해 봅시다. 이렇게 함으로써 Game of Life 프로그램을 더 점진적으로 개발할 수 있게 됩니다.

wasm-game-of-life/www/package.json 파일을 열고 devDependencies 다음에 dependencies 필드를 생성해 주세요. 그다음에, "wasm-game-of-life": "file:../pkg" 엔트리를 포함시켜주세요.

{
  // ...
  "dependencies": {                     // 이 3줄 길이의 블럭을 추가해주세요!
    "wasm-game-of-life": "file:../pkg"
  },
  "devDependencies": {
    //...
  }
}

다음으로, hello-wasm-pack 대신에 wasm-game-of-life를 부를 수 있도록 wasm-game-of-life/www/index.js 파일을 수정해 주세요:

import * as wasm from "wasm-game-of-life";

wasm.greet();

새 종속성을 만들었다면, 다음 명령어로 설치해야 합니다:

npm install

이제 웹사이트를 로컬 환경에서 구동할 수 있게 됐습니다!

로컬 환경에서 구동하기

이제 개발 서버를 구동할 새 터미널을 열어주세요. 새로 연 터미널에서 서버를 구동하면 백그라운드에서 계속 서버를 구동하면서 다른 명령어를 계속 입력할 수 있게 됩니다. 새 터미널에서 wasm-game-of-life/www 경로로 들어간 다음 이 명령어를 실행해 주세요:

npm run start

웹 브라우저를 열고 http://localhost:8080/ 를 열면 표시되는 "Hello World" alert 메세지를 확인할 수 있습니다:

"Hello, wasm-game-of-life!" 웹 페이지 alert 메세지 스크린샷

파일을 저장할 때마다 http://localhost:8080/ 페이지에 반영되도록 하고 싶다면, wasm-pack build 명령어를 wasm-game-of-life 경로에서 다시 실행해 주세요.

연습해 보기

  • wasm-game-of-life/src/lib.rs 경로에 있는 greet 함수를 수정해서 표시되는 메세지를 커스터마이징할 수 있도록 name: &str 매개변수를 추가해 보고, wasm-game-of-life/www/index.js 파일에서 greet 함수를 이름과 함께 호출해 보세요. wasm-pack build 명령어로 .wasm 바이너리를 다시 빌드하고, http://localhost:8080/ 페이지를 새로고침하면 브라우저에서 수정된 알림 메세지를 확인할 수 있습니다.

    정답

    wasm-game-of-life/src/lib.rs 파일에서 새롭게 수정된 greet 함수:

    #![allow(unused)]
    fn main() {
    #[wasm_bindgen]
    pub fn greet(name: &str) {
        alert(&format!("Hello, {}!", name));
    }
    }

    wasm-game-of-life/www/index.js 파일에서 수정된 greet 함수 호출하기:

    wasm.greet("Your Name");