์๋ฌธ: http://www.sheshbabu.com/posts/rust-wasm-yew-single-page-application/
WebAssembly(wasm)์ ์ฌ์ฉํ๋ฉด ์๋ฐ์คํฌ๋ฆฝํธ ์ธ์ ์ธ์ด๋ก ์์ฑ๋ ์ฝ๋๋ฅผ ๋ธ๋ผ์ฐ์ ์์ ์คํํ ์ ์๋ค. ๋๋ถ๋ถ์ ์ฃผ์ ๋ธ๋ผ์ฐ์ ์์ wasm์ ์ง์ํ๊ณ ์ ์ธ๊ณ์ ์ผ๋ก 90% ์ด์์ ์ฌ์ฉ์๊ฐ wasm์ ๋์์ํค๋ ๋ธ๋ผ์ฐ์ ๋ฅผ ์ฌ์ฉํ๋ค.
Rust๊ฐ wasm์ผ๋ก ์ปดํ์ผ์ด ๋๋, ์๋ฐ์คํฌ๋ฆฝํธ ์ฝ๋๋ฅผ ์์ฑํ์ง ์๊ณ ์์ํ๊ฒ Rust๋ก SPAs(Single Page Applications)๋ฅผ ๋ง๋ค ์ ์๋๊ฐ? ๋ผ๊ณ ๋ฌป๋๋ค๋ฉด ๋๋ต์ ๊ทธ๋ ๋ค!์ด๋ค. ๊ตฌํ ๊ฒฐ๊ณผ๊ฐ ๊ถ๊ธํ๋ค๋ฉด ๋ฐ๋ชจ ์ฌ์ดํธ์ ๋ฐฉ๋ฌธํ๊ฑฐ๋ ๋ ๊ธ์ ์์ธํ ์ฝ์ด๋ณด๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค.
์ฌ๊ธฐ์๋ ๋ ํ์ด์ง๋ก ๊ตฌ์ฑ๋ "RustMart"๋ผ๋ ๊ฐ๋จํ ์ด์ปค๋จธ์ค ์ฌ์ดํธ๋ฅผ ๋ง๋ค์ด ๋ณผ ๊ฒ์ด๋ค.
๋ชจ๋ SPA ํ์ด์ง๋ฅผ ๊ตฌ์ฑํ๋ ๋ฐ ํ์ํ ์ต์ํ์ ๊ธฐ๋ฅ๋ง ์ด ์์ ๋ฅผ ๊ตฌ์ฑํ๋๋ฐ ์ฌ์ฉํ๋ค.
Rust๊ฐ ์ค์น๋์ด ์์ง ์๋ค๋ฉด ์ด ๋งํฌ์์ ์ค์นํ์.
๋ค์ Rust ๋๊ตฌ๋ค ๋ํ ์ค์นํ์.
$ cargo install wasm-pack # Rust๋ฅผ ์ปดํ์ผ ํด Wasm๊ณผ JS Interop ์ฝ๋๋ฅผ ์์ฑ
$ cargo install cargo-make # ํ์คํฌ ๋ฌ๋
$ cargo install simple-http-server # assets์ ์คํํ๋ Simple Server
๊ทธ๋ฆฌ๊ณ ์ ํ๋ก์ ํธ๋ฅผ ์์ฑํ๋ค.
$ cargo new --lib rustmart && cd rustmart
์ด ํ๋ก์ ํธ์์๋ UI์ปดํผ๋ํธ๋ฅผ ๋น๋ํ๊ธฐ ์ํด Yew
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ค. wasm ๋ํ๋์๋ฅผ Cargo.toml
์ ์ถ๊ฐํด๋ณด์.
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
yew = "0.17"
wasm-bindgen = "0.2"
Makefile.toml
๋ ์์ฑํด ๋ค์ ๋ด์ฉ์ ์ถ๊ฐํ๋ค.
[tasks.build]
command = "wasm-pack"
args = ["build", "--dev", "--target", "web", "--out-name", "wasm", "--out-dir", "./static"]
watch = { ignore_pattern = "static/*" }
[tasks.serve]
command = "simple-http-server"
args = ["-i", "./static/", "-p", "3000", "--nocache", "--try-file", "./static/index.html"]
๋น๋๋ฅผ ์์ํด๋ณด์
$ cargo make build
Rust๊ฐ ์ฒ์์ธ ์ฌ๋๋ค์ ์ํด ๋ช ๊ฐ์ง ๊ฐ์ด๋๋ฅผ ์์ฑํ์ผ๋ ๋์์ด ๋๊ธธ ๋ฐ๋๋ค.
"Hello world"์์ ๋ฅผ ๋ง๋ค์ด๋ณด์.
static/index.html
์ ๋ง๋ค๊ณ ๋ค์ ์ฝ๋๋ฅผ ์ถ๊ฐํด๋ณด์.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>RustMart</title>
<script type="module">
import init from "/wasm.js";
init();
</script>
<link rel="shortcut icon" href="#" />
</head>
<body></body>
</html>
๊ทธ๋ฆฌ๊ณ ๋ค์ ์ฝ๋ ๋ํ src/lib.rs
์ ์ถ๊ฐํด๋ณด์
// src/lib.rs
use wasm_bindgen::prelude::*;
use yew::prelude::*;
struct Hello {}
impl Component for Hello {
type Message = ();
type Properties = ();
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
Self {}
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
true
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
true
}
fn view(&self) -> Html {
html! { <span>{"Hello World!"}</span> }
}
}
#[wasm_bindgen(start)]
pub fn run_app() {
App::<Hello>::new().mount_to_body();
}
๋ง์ ์ผ์ด ์งํ๋์ง๋ง "Hello"๋ผ๋ ์๋ก์ด ์ปดํผ๋ํธ๋ฅผ ์์ฑํด DOM์ <span>Hello World!</span>
์ ์์ฑํ๋ ์์
์ ํ๋ค๋ ๊ฒ์ ์ ์ ์๋ค. Yew ์ปดํฌ๋ํธ์ ๋ํด์๋ ์ดํ์ ์ดํด๋ณด๋๋ก ํ๊ฒ ๋ค.
ํฐ๋ฏธ๋์์ ํ์คํฌ๋ฅผ ์คํํ ๋ค ๋ธ๋ผ์ฐ์ ์์ http://localhost:3000
์ ์ ์ํด ํ์ธํ ์ ์๋ค.
$ cargo make serve
์๋ํ๋ค! ๋จ์ง "Hello world"์ผ ๋ฟ์ด์ง๋ง ๋ชจ๋ ์ฝ๋๊ฐ Rust๋ก ์์ฑ๋๋ค.
๋ ์งํํ๊ธฐ ์ ์ ๋จผ์ ๋ค๋ฅธ SPA์ ์ปจ์ ๊ณผ ์ปดํฌ๋ํธ์ ๋ํด ์ดํด๋ณด๋๋ก ํ์
์ปดํฌ๋ํธ๋ฅผ ํฉ์ฑํ๊ณ ๋ฐ์ดํฐ๋ฅผ ๋จ๋ฐฉํฅ์ผ๋ก ์ ์กํ๋ฉฐ UI๋ฅผ ๋ง๋๋ ๊ฒ์ผ๋ก ํ๋ก ํธ์๋ ์ธ๊ณ์ ํฐ ํจ๋ฌ๋ค์ ๋ณํ๊ฐ ์์๋ค. UI์ ๋ํด ์ถ๋ก ํ๋ ๋ฐฉ์์ด ํฌ๊ฒ ๋ฐ์ ๋์์ผ๋ฉฐ ์ด๋ฐ ๋ฐฉ์์ ์ต์ํด์ง๋ฉด ๋ช ๋ นํ(imperative) DOM ์กฐ์์ ๋์๊ฐ๊ธฐ ๋งค์ฐ ์ด๋ ต๊ฒ ๋๋ค.
React๋ Vue, Yew, Flutter ๋ฑ ์ฌ๋ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ Component
๋ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ๊ฐ๋๋ค.
Props
- ํด๋น ์ปดํฌ๋ํธ์์ ์์ ์ปดํฌ๋ํธ๋ก ๋ฐ์ดํฐ๋ ์ฝ๋ฐฑ์ ์ ๋ฌState
- ์ปดํฌ๋ํธ์ ๋ก์ปฌ ์ํ๋ฅผ ์กฐ์AppState
- ์ ์ญ ์ํ๋ฅผ ์กฐ์๋ํ, ์ปดํฌ๋ํธ๋ ๋ค์๊ณผ ๊ฐ์ ์ํฉ์ ์ ๋ฐ์ดํธ(๋ฆฌ๋ ๋๋ง) ๋๋ค.
Props
์ ๋ณ๊ฒฝState
์ ๋ณ๊ฒฝAppState
์ ๋ณ๊ฒฝ์ ๋ฆฌํด๋ณด๋ฉด, ๋ช ์์ (imperatively)์ผ๋ก UI๋ฅผ ์ ๋ฐ์ดํธํ๋ ๋์ ์ฌ์ฉ์์ ์ํธ ์์ฉ์ด๋ ๋คํธ์ํฌ ์์ฒญ ๋ฑ์ด ๋ฐ์ํ ๋ ๋ฐ์ดํฐ(Props, State, AppState)๋ฅผ ์ ๋ฐ์ดํธํ๊ณ ์ด ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก UI๋ ์ ๋ฐ์ดํธ๋๋ค. ์ด ๋ง์ ๋๊ตฐ๊ฐ๊ฐ "UI๋ ์ํ์ ํจ์๋ค(UI is a function of state)"๋ผ๊ณ ํ๋ ๊ฒ์ ์๋ฏธํ๋ค๊ณ ํ ์ ์๋ค.
์ ํํ ์ธ๋ถ ์ฌํญ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ง๋ค ๋ค๋ฅด์ง๋ง, ์ผ๋ฐ์ ์ผ๋ก ์ฌ์ฉ๋๊ณ ์๋ ์์ด๋์ด์ด๋ค. ๋ง์ฝ ์ด๋ฐ ์์ด๋์ด๊ฐ ์ฒ์์ด๋ผ๋ฉด ์ต์ํด์ง๋๋ฐ ์๊ฐ์ด ์กฐ๊ธ ๊ฑธ๋ฆด ์ ์๋ค.
ํํ์ด์ง๋ฅผ ๋จผ์ ๋ง๋ค์ด๋ณด์. ์ฐ์ ๋จ์ผ ์ปดํฌ๋ํธ๋ก ๋ง๋ ๋ค ์ดํ์ ๋ถํดํ๋ฉฐ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ๋ก ๋ง๋ค ๊ฒ์ด๋ค
์๋ ํ์ผ์ ๋ง๋ค์ด๋ณด์.
// src/pages/home.rs
use yew::prelude::*;
pub struct Home {}
impl Component for Home {
type Message = ();
type Properties = ();
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
Self {}
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
true
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
true
}
fn view(&self) -> Html {
html! { <span>{"Home Sweet Home!"}</span> }
}
}
// src/pages/mod.rs
mod home;
pub use home::Home;
src/lib.rs
๋ฅผ ๋ค์์ฒ๋ผ ๋ฐ๊ฟ ์์ฑํ ๋ค HomePage ์ปดํฌ๋ํธ๋ฅผ import ํด๋ณด์
// src/lib.rs
+ mod pages;
+ use pages::Home;
use wasm_bindgen::prelude::*;
use yew::prelude::*;
- struct Hello {}
- impl Component for Hello {
- type Message = ();
- type Properties = ();
- fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
- Self {}
- }
- fn update(&mut self, _: Self::Message) -> ShouldRender {
- true
- }
- fn change(&mut self, _: Self::Properties) -> ShouldRender {
- true
- }
- fn view(&self) -> Html {
- html! { <span>{"Hello World!"}</span> }
- }
- }
#[wasm_bindgen(start)]
pub fn run_app() {
- App::<Hello>::new().mount_to_body();
+ App::<Home>::new().mount_to_body();
}
์ด๋ ๊ฒ ์์ฑํ๋ฉด "Hello World!" ๋์ "Home Sweet Home!"์ด ๋ธ๋ผ์ฐ์ ์ ๋ ๋๋ง ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ด์ ์ด ์ปดํฌ๋ํธ์ State
๋ฅผ ๊ตฌ์ฑํด๋ณด์.
๋จ์ํ๊ฒ Product
์ ๋ณด๋ฅผ ์ ์ฅํ ๊ตฌ์กฐ์ฒด๋ฅผ ๋ง๋ค์๋ค.
struct Product {
name: String,
description: String,
image: String,
price: f64,
}
๋ค์์ผ๋ก ์๋ฒ๋ก ๋ถํฐ ๋ถ๋ฌ์จ ์ํ์ ์ ๋ณด๋ฅผ ๋ด๋ ์ ํ๋ products
๋ฅผ ๋ง๋ค์ด State
์ ์ ์ธํด์ค๋ค.
struct State {
products: Vec<Product>,
}
๋ฐ๋ HomePage ์ปดํฌ๋ํธ๋ฅผ ์ดํด๋ณด์
use yew::prelude::*;
+ struct Product {
+ id: i32,
+ name: String,
+ description: String,
+ image: String,
+ price: f64,
+ }
+ struct State {
+ products: Vec<Product>,
+ }
- pub struct Home {}
+ pub struct Home {
+ state: State,
+ }
impl Component for Home {
type Message = ();
type Properties = ();
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
+ let products: Vec<Product> = vec![
+ Product {
+ id: 1,
+ name: "Apple".to_string(),
+ description: "An apple a day keeps the doctor away".to_string(),
+ image: "/products/apple.png".to_string(),
+ price: 3.65,
+ },
+ Product {
+ id: 2,
+ name: "Banana".to_string(),
+ description: "An old banana leaf was once young and green".to_string(),
+ image: "/products/banana.png".to_string(),
+ price: 7.99,
+ },
+ ];
- Self {}
+ Self {
+ state: State {
+ products,
+ },
+ }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
true
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
true
}
fn view(&self) -> Html {
+ let products: Vec<Html> = self
+ .state
+ .products
+ .iter()
+ .map(|product: &Product| {
+ html! {
+ <div>
+ <img src={&product.image}/>
+ <div>{&product.name}</div>
+ <div>{"$"}{&product.price}</div>
+ </div>
+ }
+ })
+ .collect();
+
+ html! { <span>{products}</span> }
- html! { <span>{"Home!"}</span> }
}
}
create
์๋ช
์ฃผ๊ธฐ ๋ฉ์๋๋ ์ปดํฌ๋ํธ๊ฐ ์์ฑ๋ ๋ ํธ์ถ๋๋ฉฐ ์ฌ๊ธฐ์ ์ด๊ธฐ ์ํ๋ฅผ ์ค์ ํ๋ค. ์ ์ ๋์ ์ํ ๋ชฉ๋ก์ ๋ชจํนํด์ ์ด๊ธฐ ์ํ๊ฐ์ผ๋ก ์ง์ ํ๋ค. ์ดํ์ ์ด ์ํ ๋ฆฌ์คํธ๋ ์์ฒญ์ ํตํด ๊ฐ์ ๊ฐ์ ธ์ฌ ๊ฒ์ด๋ค.
view
์๋ช
์ฃผ๊ธฐ ๋ฉ์๋๋ ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ ๋ค ๋ฐ์ํ๋ค. ์ ์ฝ๋์์๋ ์ํ ์นด๋๋ฅผ ์์ฑํ๊ธฐ ์ํด products
๋ฅผ ๋ฐ๋ณตํ๋ค. ๋ง์ฝ React์ ์ต์ํ๋ค๋ฉด, view
๋ render
์ ๋์ผํ๋ฉฐ html!
๋งคํฌ๋ก๋ JSX
์ ๋น์ทํ๋ค.
์์์ ์ด๋ฏธ์ง๋ฅผ static/products/apple.png
์ static/products/apple.png
๋ก ์ ์ฅํ๋ฉด ์๋์ ๊ฐ์ UI๋ฅผ ์ป์ ์ ์๋ค.
์ด์ "์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐ" ๊ธฐ๋ฅ์ ๋ง๋ค์ด๋ณด์.
cart_products
๋ผ๋ ์๋ก์ด state ํ๋์ ์ ์ฅํ๋ค.cart_products
๋ฅผ ์
๋ฐ์ดํธ ํ๋ ๋ก์ง์ ๋ง๋ ๋ค. use yew::prelude::*;
+ #[derive(Clone)]
struct Product {
id: i32,
name: String,
description: String,
image: String,
price: f64,
}
+ struct CartProduct {
+ product: Product,
+ quantity: i32,
+ }
struct State {
products: Vec<Product>,
+ cart_products: Vec<CartProduct>,
}
pub struct Home {
state: State,
+ link: ComponentLink<Self>,
}
+ pub enum Msg {
+ AddToCart(i32),
+ }
impl Component for Home {
- type Message = ();
+ type Message = Msg;
type Properties = ();
- fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
+ fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
let products: Vec<Product> = vec![
Product {
id: 1,
name: "Apple".to_string(),
description: "An apple a day keeps the doctor away".to_string(),
image: "/products/apple.png".to_string(),
price: 3.65,
},
Product {
id: 2,
name: "Banana".to_string(),
description: "An old banana leaf was once young and green".to_string(),
image: "/products/banana.png".to_string(),
price: 7.99,
},
];
+ let cart_products = vec![];
Self {
state: State {
products,
+ cart_products,
},
+ link,
}
}
- fn update(&mut self, _: Self::Message) -> ShouldRender {
+ fn update(&mut self, message: Self::Message) -> ShouldRender {
+ match message {
+ Msg::AddToCart(product_id) => {
+ let product = self
+ .state
+ .products
+ .iter()
+ .find(|p: &&Product| p.id == product_id)
+ .unwrap();
+ let cart_product = self
+ .state
+ .cart_products
+ .iter_mut()
+ .find(|cp: &&mut CartProduct| cp.product.id == product_id);
+
+ if let Some(cp) = cart_product {
+ cp.quantity += 1;
+ } else {
+ self.state.cart_products.push(CartProduct {
+ product: product.clone(),
+ quantity: 1,
+ })
+ }
+ true
+ }
+ }
- true
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
true
}
fn view(&self) -> Html {
let products: Vec<Html> = self
.state
.products
.iter()
.map(|product: &Product| {
+ let product_id = product.id;
html! {
<div>
<img src={&product.image}/>
<div>{&product.name}</div>
<div>{"$"}{&product.price}</div>
+ <button onclick=self.link.callback(move |_| Msg::AddToCart(product_id))>{"Add To Cart"}</button>
</div>
}
})
.collect();
+ let cart_value = self
+ .state
+ .cart_products
+ .iter()
+ .fold(0.0, |acc, cp| acc + (cp.quantity as f64 * cp.product.price));
- html! { <span>{products}</span> }
+ html! {
+ <div>
+ <span>{format!("Cart Value: {:.2}", cart_value)}</span>
+ <span>{products}</span>
+ </div>
+ }
}
}
clone
- Product
๊ตฌ์กฐ์ฒด์์ Clone
ํธ๋ ์(traits)์ ํ์(derive)์์ผ์ค ์ ์ ๊ฐ ์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐ ํ ๋๋ง๋ค ๋ณต์ ๋ Product
๋ฅผ CartProduct
์ ์ ์ฅํ๋๋ก ํ ์ ์๊ฒ ํ๋ค.update
- ์ด ๋ฉ์๋๋ ์ปดํฌ๋ํธ์ State
์
๋ฐ์ดํธ ๋ก์ง์ด ์กด์ฌํ๊ฑฐ๋ ์ฌ์ด๋ ์ดํํธ(๋คํธ์ํฌ ์์ฒญ ๊ฐ์)์์
์ด ์์นํ๋ ๊ณณ์ด๋ค. ์ปดํฌ๋ํธ๊ฐ ์ง์ํ๋ ๋ชจ๋ ์ก์
์ ํฌํจํ๋ Message
์ด๊ฑฐํ์ ์ฌ์ฉํด ๋ฐ์์ํจ๋ค. ์ด ๋ฉ์๋์์ true
๋ฅผ ๋ฐํํ ๋ ์ปดํฌ๋ํธ๋ ๋ฆฌ๋ ๋๋ง ํ๋ค. ์ ์ฝ๋์์ "์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐ" ๋ฒํผ์ ๋๋ฅด๋ฉด Msg::AddToCart
๋ฉ์ธ์ง๋ฅผ update
๋ก ๋ณด๋ธ๋ค. update
์์๋ ์ ํ์ด cart_product
์ ์์ผ๋ฉด ์ถ๊ฐํ๋ฉฐ, ์กด์ฌํ ๊ฒฝ์ฐ ์๋์ ์ฆ๊ฐ์ํจ๋ค.link
- update
์๋ช
์ฃผ๊ธฐ ํจ์๋ฅผ ๋ฐ์์ํฌ ์ ์๋ ์ฝ๋ฐฑ ํจ์๋ฅผ ๋ฑ๋ก์ํจ๋ค.Redux๋ฅผ ์ด์ ์ ์ฌ์ฉํด ๋ณธ ์ ์ด ์๋ค๋ฉด, update
๋ ๋ฆฌ๋์
(state ์
๋ฐ์ดํธ)์ ์ก์
์์ฑ์
(์ฌ์ด๋ ์ดํํธ)์ ์ ์ฌํ๋ฉฐ, Message
๋ ์ก์
, link
๋ Dispatch
์ ์ ์ฌํ๋ค.
UI๋ ์์ฒ๋ผ ๋ณํ๋ค. "Add to Cart" ๋ฒํผ์ ๋๋ฌ "Cart Value"๊ฐ ์ด๋ป๊ฒ ๋ณ๊ฒฝ๋๋์ง ์ดํด๋ด๋ผ.
์ํ ๋ฐ์ดํฐ๋ฅผ create
ํจ์์์ static/products/products.json
๋ก ์ฎ๊ธฐ๊ณ , fetch
api๋ฅผ ์ฌ์ฉํด ์ฟผ๋ฆฌํ ๊ฒ์ด๋ค.
[
{
"id": 1,
"name": "Apple",
"description": "An apple a day keeps the doctor away",
"image": "/products/apple.png",
"price": 3.65
},
{
"id": 2,
"name": "Banana",
"description": "An old banana leaf was once young and green",
"image": "/products/banana.png",
"price": 7.99
}
]
Yew๋ "services"๋ฅผ ํตํด ์ผ๋ฐ ๋ธ๋ผ์ฐ์ ์์ ์ ๊ณต๋๋ fetch๋ ๋ก์ปฌ ์คํ ๋ฆฌ์ง ๋ฑ์ ์ด์ฉํ ์ ์๊ฒ ํ๋ค. FetchService
๋ฅผ ์ฌ์ฉํด ๋คํธ์ํฌ ์์ฒญ์ ์์ฑํ๋ค. ์ฌ์ฉ์ ์ํด์๋ anyhow
์ serde
๊ฐ ํ์ํ๋ฐ, ํ๋ฒ ์ค์นํด๋ณด์.
[package]
name = "rustmart"
version = "0.1.0"
authors = ["sheshbabu <sheshbabu@gmail.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
yew = "0.17"
wasm-bindgen = "0.2"
+ anyhow = "1.0.32"
+ serde = { version = "1.0", features = ["derive"] }
Product
์ CartProduct
๋ฅผ src/types.rs
๋ก ์ถ์ถํด ์ฌ๋ฌ ํ์ผ์์ ๊ณต์ ํด ์ฌ์ฉํ ์ ์๋๋ก ํ์.
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Product {
pub id: i32,
pub name: String,
pub description: String,
pub image: String,
pub price: f64,
}
#[derive(Clone, Debug)]
pub struct CartProduct {
pub product: Product,
pub quantity: i32,
}
๋ ๊ตฌ์กฐ์ฒด์ ํ๋ ๋ชจ๋ public์ผ๋ก ๋ง๋ค์ด ์ค ๋ค, Deserialize
์ Serialize
ํธ๋ ์์ ํ์์์ผ์คฌ๋ค.
๋ํ, API ๋ชจ๋ ํจํด์ fetch ๋ก์ง์ ์ ์งํ๊ธฐ ์ํด src/api.rs
์ ๋ถ๋ฆฌ๋ ๋ชจ๋์ ๋ง๋ค์ด ์ฌ์ฉํ ๊ฒ์ด๋ค.
// src/api.rs
use crate::types::Product;
use anyhow::Error;
use yew::callback::Callback;
use yew::format::{Json, Nothing};
use yew::services::fetch::{FetchService, FetchTask, Request, Response};
pub type FetchResponse<T> = Response<Json<Result<T, Error>>>;
type FetchCallback<T> = Callback<FetchResponse<T>>;
pub fn get_products(callback: FetchCallback<Vec<Product>>) -> FetchTask {
let req = Request::get("/products/products.json")
.body(Nothing)
.unwrap();
FetchService::fetch(req, callback).unwrap()
}
FetchSerive
api๋ ์ฝ๊ฐ ์ด์ํ ๋ถ๋ถ์ด ์กด์ฌํ๋ค. ์์ฒญ ๊ฐ์ฒด์ ์ฝ๋ฐฑ์ ์ธ์๋ก ๋ฐ์ "FetchTask"๋ฅผ ๋ฐํํ๋ค๋ ๊ฒ์ด๋ค. ์ฌ๊ธฐ์ ๋ฐ์ํ๋ ํ ๊ฐ์ง ๋๋ผ์ด ๋ฌธ์ ๋ "FetchTask"๊ฐ ์ฌ๋ผ์ง๋ฉด ์ด ๋คํธ์ํฌ ์์ฒญ์ด ์ค๋จ๋๋ค๋ ๊ฒ์ด๋ค. ๊ทธ๋์ ์ฐ๋ฆฌ๋ ์ด FetchTask๋ฅผ ๋ฐํ์์ผ ์ปดํฌ๋ํธ์ ์ ์ฅ์ํฌ ๊ฒ์ด๋ค.
lib.rs
๋ฅผ ์์ ํด ์๋ก์ด ๋ชจ๋์ ๋ชจ๋ ํธ๋ฆฌ
์ ์ถ๊ฐ์ํค์.
// src/lib.rs
+ mod api;
+ mod types;
mod pages;
use pages::Home;
use wasm_bindgen::prelude::*;
use yew::prelude::*;
#[wasm_bindgen(start)]
pub fn run_app() {
App::<Home>::new().mount_to_body();
}
๋ง์ง๋ง์ผ๋ก HomePage ์ปดํฌ๋ํธ๋ฅผ ์ ๋ฐ์ดํธ ํ์.
+ use crate::api;
+ use crate::types::{CartProduct, Product};
+ use anyhow::Error;
+ use yew::format::Json;
+ use yew::services::fetch::FetchTask;
use yew::prelude::*;
- #[derive(Clone)]
- struct Product {
- id: i32,
- name: String,
- description: String,
- image: String,
- price: f64,
- }
- struct CartProduct {
- product: Product,
- quantity: i32,
- }
struct State {
products: Vec<Product>,
cart_products: Vec<CartProduct>,
+ get_products_error: Option<Error>,
+ get_products_loaded: bool,
}
pub struct Home {
state: State,
link: ComponentLink<Self>,
+ task: Option<FetchTask>,
}
pub enum Msg {
AddToCart(i32),
+ GetProducts,
+ GetProductsSuccess(Vec<Product>),
+ GetProductsError(Error),
}
impl Component for Home {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
- let products: Vec<Product> = vec![
- Product {
- id: 1,
- name: "Apple".to_string(),
- description: "An apple a day keeps the doctor away".to_string(),
- image: "/products/apple.png".to_string(),
- price: 3.65,
- },
- Product {
- id: 2,
- name: "Banana".to_string(),
- description: "An old banana leaf was once young and green".to_string(),
- image: "/products/banana.png".to_string(),
- price: 7.99,
- },
- ];
+ let products = vec![];
let cart_products = vec![];
+ link.send_message(Msg::GetProducts);
Self {
state: State {
products,
cart_products,
+ get_products_error: None,
+ get_products_loaded: false,
},
link,
+ task: None,
}
}
fn update(&mut self, message: Self::Message) -> ShouldRender {
match message {
+ Msg::GetProducts => {
+ self.state.get_products_loaded = false;
+ let handler =
+ self.link
+ .callback(move |response: api::FetchResponse<Vec<Product>>| {
+ let (_, Json(data)) = response.into_parts();
+ match data {
+ Ok(products) => Msg::GetProductsSuccess(products),
+ Err(err) => Msg::GetProductsError(err),
+ }
+ });
+ self.task = Some(api::get_products(handler));
+ true
+ }
+ Msg::GetProductsSuccess(products) => {
+ self.state.products = products;
+ self.state.get_products_loaded = true;
+ true
+ }
+ Msg::GetProductsError(error) => {
+ self.state.get_products_error = Some(error);
+ self.state.get_products_loaded = true;
+ true
+ }
Msg::AddToCart(product_id) => {
let product = self
.state
.products
.iter()
.find(|p: &&Product| p.id == product_id)
.unwrap();
let cart_product = self
.state
.cart_products
.iter_mut()
.find(|cp: &&mut CartProduct| cp.product.id == product_id);
if let Some(cp) = cart_product {
cp.quantity += 1;
} else {
self.state.cart_products.push(CartProduct {
product: product.clone(),
quantity: 1,
})
}
true
}
}
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
true
}
fn view(&self) -> Html {
let products: Vec<Html> = self
.state
.products
.iter()
.map(|product: &Product| {
let product_id = product.id;
html! {
<div>
<img src={&product.image}/>
<div>{&product.name}</div>
<div>{"$"}{&product.price}</div>
<button onclick=self.link.callback(move |_| Msg::AddToCart(product_id))>{"Add To Cart"}</button>
</div>
}
})
.collect();
let cart_value = self
.state
.cart_products
.iter()
.fold(0.0, |acc, cp| acc + (cp.quantity as f64 * cp.product.price));
+ if !self.state.get_products_loaded {
+ html! {
+ <div>{"Loading ..."}</div>
+ }
+ } else if let Some(_) = self.state.get_products_error {
+ html! {
+ <div>
+ <span>{"Error loading products! :("}</span>
+ </div>
+ }
+ } else {
html! {
<div>
<span>{format!("Cart Value: {:.2}", cart_value)}</span>
<span>{products}</span>
</div>
}
+ }
}
}
๊ฝค ๋ง์ ๋ณ๊ฒฝ์ด ์์์ง๋ง, ๋๋ถ๋ถ์ ์ดํด๋ฅผ ํ๊ณ ๋์ด๊ฐ์ผ ํ๋ค.
create
๋ฉ์๋์์ ๋น ๋ฐฐ์ด๋ก ํ๋์ฝ๋ฉ ๋์ด์์๋ ์ํ ๋ฆฌ์คํธ๊ฐ ๋์ฒด ๋์๋ค. Msg::GetProducts
๋ฉ์ธ์ง๋ฅผ ๋ณด๋ด update
์์ api
๋ชจ๋์ ์์นํ get_products
๋ฉ์๋๋ฅผ ํธ์ถ์์ผฐ๋ค. ๋ฐํ๋ FetchTask
๋ task
์ ์ ์ฅ๋๋ค.Msg::GetProductsSuccess
๋ฉ์ธ์ง๊ฐ ์ํ ๋ฆฌ์คํธ์ ํจ๊ป ํธ์ถ๋๋ฉฐ, ์คํจํ ๊ฒฝ์ฐ ์๋ฌ์ ํจ๊ป Msg::getProductsError
๊ฐ ํธ์ถ๋๋ค.products
์ get_products_error
ํ๋๋ฅผ ๊ฐ๊ฐ ์ค์ ํ๋ค. ๋ํ ์์ฒญ์ด ์๋ฃ๋ ํ get_products_loaded
์ํ ๋ํ true๋ก ๋ณ๊ฒฝํ๋ค.view
๋ฉ์๋์์๋ ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ ํตํด ์ปดํฌ๋ํธ์ ์ํ์ ๋ฐ๋ผ ๋ก๋ฉ, ์๋ฌ, ์ํ ๋ฆฌ์คํธ๋ฅผ ๋ณด์ฌ์ค๋ค."product cart" ์ปดํฌ๋ํธ๋ฅผ ๋ชจ๋ํ ์์ผ ๋ค๋ฅธ ํ์ด์ง์์ ์ฌ์ฌ์ฉํ ์ ์๋๋ก ๋ถ๋ฆฌํด๋ณด์.
// src/components/product_card.rs
use crate::types::Product;
use yew::prelude::*;
pub struct ProductCard {
props: Props,
}
#[derive(Properties, Clone)]
pub struct Props {
pub product: Product,
pub on_add_to_cart: Callback<()>,
}
impl Component for ProductCard {
type Message = ();
type Properties = Props;
fn create(props: Self::Properties, _link: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
true
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
true
}
fn view(&self) -> Html {
let onclick = self.props.on_add_to_cart.reform(|_| ());
html! {
<div>
<img src={&self.props.product.image}/>
<div>{&self.props.product.name}</div>
<div>{"$"}{&self.props.product.price}</div>
<button onclick=onclick>{"Add To Cart"}</button>
</div>
}
}
}
// src/components/mod.rs
mod product_card;
pub use product_card::ProductCard;
// src/lib.rs
mod api;
+ mod components;
mod pages;
mod types;
// No changes
// src/pages/home.rs
use crate::api;
+ use crate::components::ProductCard;
use crate::types::{CartProduct, Product};
use anyhow::Error;
use yew::format::Json;
use yew::prelude::*;
use yew::services::fetch::FetchTask;
// No changes
impl Component for Home {
// No changes
fn view(&self) -> Html {
let products: Vec<Html> = self
.state
.products
.iter()
.map(|product: &Product| {
let product_id = product.id;
html! {
- <div>
- <img src={&product.image}/>
- <div>{&product.name}</div>
- <div>{"$"}{&product.price}</div>
- <button onclick=self.link.callback(move |_| Msg::AddToCart(product_id))>{"Add To Cart"}</button>
- </div>
+ <ProductCard product={product} on_add_to_cart=self.link.callback(move |_| Msg::AddToCart(product_id))/>
}
})
.collect();
// No changes
}
}
properties
, Callback
๊ทธ๋ฆฌ๊ณ reform
์ ์ ์ธํ๊ณ ๋ ๊ฝค ๋ช
๋ฃํ๋ค.
properties
- ๊ธ ์์ ๋ถ๋ถ์์ ์ธ๊ธํ๋ฏ, "Properties"๋ "Props"๋ชจ๋ ์ปดํฌ๋ํธ์ ์
๋ ฅ๋๋ค. ๋ง์ฝ ์ปดํฌ๋ํธ๋ฅผ ํจ์๋ก ์๊ฐํ๊ณ ์๋ค๋ฉด, Props๋ ํจ์์ ์ธ์์ผ ๊ฒ์ด๋ค.ProductCard
์ปดํฌ๋ํธ์ ๊ฒฝ์ฐ, Product
๊ตฌ์กฐ์ฒด์ on_add_to_cart
์ฝ๋ฐฑ์ ๋๊ฒจ์ค๋ค. ์ด ์ปดํฌ๋ํธ๋ ์ํ๊ฐ ์์ผ๋ฏ๋ก, "์ฅ๋ฐ๊ตฌ๋๋ก ์ถ๊ฐ" ๋ฒํผ์ ๋๋ฅผ ๊ฒฝ์ฐ ๋ถ๋ชจ ์ปดํฌ๋ํธ์ cart_products
์ํ๋ฅผ ์
๋ฐ์ดํธํ๋ค. ์ด ์ฝ๋ฐฑ์ Callback<T>
ํ์
์ผ๋ก ์ฌ์ฉ๋๋ฉฐ ์์ ์ปดํฌ๋ํธ์์ ํธ์ถํ๋ ค๋ฉด ์ฝ๋ฐฑ์์ emit
๋๋ reform
๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ค.์ด UI๋ ์คํ์ผ์ ์ถ๊ฐํ์ง ์์๊ธฐ ๋๋ฌธ์ ๋ผ๋๋ง ์กด์ฌํ๋ค.
Yew๋ฅผ ์ฌ์ฉํด class ์์์ ์ธ๋ผ์ธ ์คํ์ผ ๋ชจ๋ ์ฌ์ฉํ ์ ์๋ค. ๋ช๊ฐ์ง ์คํ์ผ์ ์ถ๊ฐํด UI๋ฅผ ๋ณด๊ธฐ ์ข๊ฒ ๋ฐ๊ฟ๋ณด์.
CSS ํ์ผ์ static/styles.css
๋ก ๋ง๋ค๊ณ , static/index.html
์ ์ถ๊ฐํ ๋ค ์ปดํฌ๋ํธ์ ํด๋์ค๋ฅผ ์ถ๊ฐํด๋ณด์.
// src/pages/home.rs
html! {
<div>
- <span>{format!("Cart Value: {:.2}", cart_value)}</span>
- <span>{products}</span>
+ <div class="navbar">
+ <div class="navbar_title">{"RustMart"}</div>
+ <div class="navbar_cart_value">{format!("${:.2}", cart_value)}</div>
+ </div>
+ <div class="product_card_list">{products}</div>
</div>
}
html! {
- <div>
- <img src={&self.props.product.image}/>
- <div>{&self.props.product.name}</div>
- <div>{"$"}{&self.props.product.price}</div>
- <button onclick=onclick>{"Add To Cart"}</button>
- </div>
+ <div class="product_card_container">
+ <img class="product_card_image" src={&self.props.product.image}/>
+ <div class="product_card_name">{&self.props.product.name}</div>
+ <div class="product_card_price">{"$"}{&self.props.product.price}</div>
+ <button class="product_atc_button" onclick=onclick>{"Add To Cart"}</button>
+ </div>
}
๋ช ๊ฐ์ง ์คํ์ผ๊ณผ ์ํ๋ค์ ์ถ๊ฐํ ๋ค, UI๋ฅผ ์ดํด๋ณด์.
CSS ๋ณํ๋ ์ด ๊ธ์์ ๋ค๋ฃจ์ง ์์๋ค. Github ์ ์ฅ์๋ฅผ ์ฐธ๊ณ ํด๋ฌ๋ผ.
์๋ฒ ๋ ๋๋ง ํ์ด์ง(Jinja, ERB, JSP emdemd)์์๋ ์ฌ์ฉ์๊ฐ ๋ณด๋ ๊ฐ ํ์ด์ง๋ ๋ค๋ฅธ ํ ํ๋ฆฐ ํ์ผ๋ค์ ๋งค์นญ๋๋ค. ์๋ฅผ ๋ค์ด ์ฌ์ฉ์๊ฐ "/login"์ผ๋ก ์ด๋ํ ๋, ์๋ฒ๋ "login.html"๋ฅผ ์ด์ฉํด ๋ ๋๋งํ๋ฉฐ "/settings"๋ก ์ด๋ํ ๊ฒฝ์ฐ, "settings.html" ํ์ผ์ ์ด์ฉํด ๋ ๋๋งํ๋ค. ๊ฐ๊ฐ ๋ค๋ฅธ ํ๋ฉด์ ํ์ด์ง๋ฅผ ๋ ๋๋ง ํ ๋ ๊ณ ์ ํ URL์ ์ฌ์ฉํ๋ ๊ฒ์ ๋ถ๋งํฌ๋ฅผ ์ง์ ํ๊ฑฐ๋ ๊ณต์ ํ ๋๋ ์ ์ฉํ๋ค.
SPA๋ ํ๋์ html ํ์ด์ง(SPA์ "Single Page"๋ฅผ ์๋ฏธํ๋)๋ง ๊ฐ์ง๋ฉฐ, ์์์ ๋งํ ๋์์ ์ํํ ์ ์์ด์ผ ํ๋ค. ์ด ์์
์ Router
๋ฅผ ์ฌ์ฉํด ์ํํ๋ค. Router๋ ๋ค๋ฅธ URL ๊ฒฝ๋ก(์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์๋ fragment๋ฑ)๋ฅผ ๋ค๋ฅธ ํ์ด์ง ์ปดํฌ๋ํธ์ ๋งคํํ๊ณ ์๋ก ๊ณ ์นจ ์์ด ์ฌ๋ฌ ํ์ด์ง๋ฅผ ์ฎ๊ฒจ ๋ค๋ ์ ์๋ ์ญํ ์ ๋๋๋ค.
์ง๊ธ ๋ง๋๋ ์ฑ์์๋, ๋ค์์ฒ๋ผ ๋งคํํ ๊ฒ์ด๋ค.
/ => HomePage
/product/:id => ProductDetailPage
yew-router
๋ฅผ ์ค์นํด๋ณด์
[package]
name = "rustmart"
version = "0.1.0"
authors = ["sheshbabu <sheshbabu@gmail.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
yew = "0.17"
+ yew-router = "0.14.0"
wasm-bindgen = "0.2"
log = "0.4.6"
wasm-logger = "0.2.0"
anyhow = "1.0.32"
serde = { version = "1.0", features = ["derive"] }
๋ณด๊ธฐ ์ฝ๋๋ก ๋ชจ๋ ๊ฒฝ๋ก๋ฅผ ํ ํ์ผ์ ์ถ๊ฐํด๋ณด์.
// src/route.rs
use yew_router::prelude::*;
#[derive(Switch, Debug, Clone)]
pub enum Route {
#[to = "/"]
HomePage,
}
์ ์ ๋์ ํ๋์ ๊ฒฝ๋ก๋ง ์ถ๊ฐํ๋ค. ์ดํ์ ์ถ๊ฐํ ๊ฒ์ด๋ค.
HomePage
๋ฅผ ๋์ฒดํ ์๋ก์ด ๋ฃจํธ ์ปดํฌ๋ํธ๋ฅผ src/app.rs
์ ์์ฑํ์.
use yew::prelude::*;
use yew_router::prelude::*;
use crate::pages::Home;
use crate::route::Route;
pub struct App {}
impl Component for App {
type Message = ();
type Properties = ();
fn create(_: Self::Properties, _link: ComponentLink<Self>) -> Self {
Self {}
}
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
true
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
let render = Router::render(|switch: Route| match switch {
Route::HomePage => html! {<Home/>},
});
html! {
<Router<Route, ()> render=render/>
}
}
}
๊ทธ์ ๋ฐ๋ผ lib.rs
๋ํ ์์ ํ๋ค.
mod api;
+ mod app;
mod components;
mod pages;
+ mod route;
mod types;
- use pages::Home;
use wasm_bindgen::prelude::*;
use yew::prelude::*;
#[wasm_bindgen(start)]
pub fn run_app() {
wasm_logger::init(wasm_logger::Config::default());
- App::<Home>::new().mount_to_body();
+ App::<app::App>::new().mount_to_body();
}
์๋ ๊ทธ๋ฆผ์ ์ฑ์ ์ปดํฌ๋ํธ ๊ณ์ธต์ด ์ด๋ป๊ฒ ๋์๋์ง ์ดํด ๋ณผ ์ ์๋ค.
๋ผ์ฐํฐ๊ฐ ์ค๋น๋์์ผ๋, ์ด๋ฅผ ์ด์ฉํด ๋ค๋ฅธ ํ์ด์ง๋ก ์ด๋ํ๋๋ก ๋ง๋ค์ด๋ณด์. ์ด ์ฑ์ SPA์ด๊ธฐ ๋๋ฌธ์ ์ด๋ ์ค ํ์ด์ง๋ฅผ ์๋ก๊ณ ์นจ ํ๋ ๊ฒ์ ํผํด์ผ ํ๋ค.
/product/:id
๊ฒฝ๋ก๋ก ProductDetailPage์ ๋ํ ๊ฒฝ๋ก๋ฅผ ์ถ๊ฐํ์. ProductCard
๋ฅผ ํด๋ฆญํ๋ฉด, id
๋ฅผ Prop์ผ๋ก ๊ฒฝ๋ก์ ์ ๋ฌํด ์ธ๋ถ ํ์ด์ง๋ก ์ด๋ํ ๊ฒ์ด๋ค.
// src/route.rs
use yew_router::prelude::*;
#[derive(Switch, Debug, Clone)]
pub enum Route {
+ #[to = "/product/{id}"]
+ ProductDetail(i32),
#[to = "/"]
HomePage,
}
์ ๊ฒฝ๋ก์ ์์์ ๋ฐ๋ผ ๋จผ์ ๋ ๋๋ง ๋๋ ํ์ด์ง๊ฐ ๊ฒฐ์ ๋๋ค. ์๋ฅผ ๋ค์ด, /product/2
๋ /product/{id}
์ /
๋ ๋ค ๋งค์นญ๋์ง๋ง /product/{id}
๋ฅผ ๋จผ์ ์์ฑํด์คฌ์ผ๋ฏ๋ก, Home
๋์ ProductDetail
ํ์ด์ง๊ฐ ๋ ๋๋ง ๋ ๊ฒ์ด๋ค.
app.rs
๋ก ๊ฒฝ๋ก๋ฅผ ์ถ๊ฐํ์.
use yew::prelude::*;
use yew_router::prelude::*;
- use crate::pages::{Home};
+ use crate::pages::{Home, ProductDetail};
use crate::route::Route;
pub struct App {}
impl Component for App {
// No changes
fn view(&self) -> Html {
let render = Router::render(|switch: Route| match switch {
+ Route::ProductDetail(id) => html! {<ProductDetail id=id/>},
Route::HomePage => html! {<Home/>},
});
html! {
<Router<Route, ()> render=render/>
}
}
}
ProductCard
๋ฅผ ์์ ํด ์ํ ์ด๋ฏธ์ง๋ ์ด๋ฆ, ๊ฐ๊ฒฉ์ ํด๋ฆญํ๋ฉด ์๋ก์ด ํ์ด์ง๋ก ์ด๋ํ๋๋ก ๋ง๋ค์ด๋ณด์.
// src/components/product_card.rs
+ use crate::route::Route;
use crate::types::Product;
use yew::prelude::*;
+ use yew_router::components::RouterAnchor;
// No changes
impl Component for ProductCard {
// No changes
fn view(&self) -> Html {
+ type Anchor = RouterAnchor<Route>;
let onclick = self.props.on_add_to_cart.reform(|_| ());
html! {
<div class="product_card_container">
+ <Anchor route=Route::ProductDetail(self.props.product.id) classes="product_card_anchor">
<img class="product_card_image" src={&self.props.product.image}/>
<div class="product_card_name">{&self.props.product.name}</div>
<div class="product_card_price">{"$"}{&self.props.product.price}</div>
+ </Anchor>
<button class="product_atc_button" onclick=onclick>{"Add To Cart"}</button>
</div>
}
}
}
Anchor
์ class
๋์ classes
๋ฅผ ์ฌ์ฉํ๋ ๋ถ๋ถ๋ ์ฃผ์ํด์ ์ดํด๋ณด์.
์ด์ ๋ชจํน๋ ๋ฐ์ดํฐ๊ฐ ์๋ ํ์ผ๋ค์ static/products/1.json
, static/products/2.json
๋ฑ๋ฑ ์ผ๋ก ๋ง๋ค์ด๋ณด์.
{
"id": 1,
"name": "Apple",
"description": "An apple a day keeps the doctor away",
"image": "/products/apple.png",
"price": 3.65
}
๊ทธ๋ฆฌ๊ณ , api.rs
๋ชจ๋์ ์์ ํด ์๋ก์ด ๊ฒฝ๋ก๋ฅผ ์ถ๊ฐํ๋ค.
use crate::types::Product;
use anyhow::Error;
use yew::callback::Callback;
use yew::format::{Json, Nothing};
use yew::services::fetch::{FetchService, FetchTask, Request, Response};
pub type FetchResponse<T> = Response<Json<Result<T, Error>>>;
type FetchCallback<T> = Callback<FetchResponse<T>>;
pub fn get_products(callback: FetchCallback<Vec<Product>>) -> FetchTask {
let req = Request::get("/products/products.json")
.body(Nothing)
.unwrap();
FetchService::fetch(req, callback).unwrap()
}
+ pub fn get_product(id: i32, callback: FetchCallback<Product>) -> FetchTask {
+ let req = Request::get(format!("/products/{}.json", id))
+ .body(Nothing)
+ .unwrap();
+
+ FetchService::fetch(req, callback).unwrap()
+ }
๊ทธ๋ผ, ์ต์ข
ProductDetail
ํ์ด์ง๋ฅผ ์ดํด๋ณด์.
// src/pages/product_detail.rs
use crate::api;
use crate::types::Product;
use anyhow::Error;
use yew::format::Json;
use yew::prelude::*;
use yew::services::fetch::FetchTask;
struct State {
product: Option<Product>,
get_product_error: Option<Error>,
get_product_loaded: bool,
}
pub struct ProductDetail {
props: Props,
state: State,
link: ComponentLink<Self>,
task: Option<FetchTask>,
}
#[derive(Properties, Clone)]
pub struct Props {
pub id: i32,
}
pub enum Msg {
GetProduct,
GetProductSuccess(Product),
GetProductError(Error),
}
impl Component for ProductDetail {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
link.send_message(Msg::GetProduct);
Self {
props,
state: State {
product: None,
get_product_error: None,
get_product_loaded: false,
},
link,
task: None,
}
}
fn update(&mut self, message: Self::Message) -> ShouldRender {
match message {
Msg::GetProduct => {
let handler = self
.link
.callback(move |response: api::FetchResponse<Product>| {
let (_, Json(data)) = response.into_parts();
match data {
Ok(product) => Msg::GetProductSuccess(product),
Err(err) => Msg::GetProductError(err),
}
});
self.task = Some(api::get_product(self.props.id, handler));
true
}
Msg::GetProductSuccess(product) => {
self.state.product = Some(product);
self.state.get_product_loaded = true;
true
}
Msg::GetProductError(error) => {
self.state.get_product_error = Some(error);
self.state.get_product_loaded = true;
true
}
}
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
if let Some(ref product) = self.state.product {
html! {
<div class="product_detail_container">
<img class="product_detail_image" src={&product.image}/>
<div class="product_card_name">{&product.name}</div>
<div style="margin: 10px 0; line-height: 24px;">{&product.description}</div>
<div class="product_card_price">{"$"}{&product.price}</div>
<button class="product_atc_button">{"Add To Cart"}</button>
</div>
}
} else if !self.state.get_product_loaded {
html! {
<div class="loading_spinner_container">
<div class="loading_spinner"></div>
<div class="loading_spinner_text">{"Loading ..."}</div>
</div>
}
} else {
html! {
<div>
<span>{"Error loading product! :("}</span>
</div>
}
}
}
}
HomePage ์ปดํฌ๋ํธ์ ๋งค์ฐ ์ ์ฌํ๋ค. ๊ทธ๋ผ ์ด ํ์ผ ๋ํ module tree์ ์ถ๊ฐํ์.
// src/pages/mod.rs
mod home;
+ mod product_detail;
pub use home::Home;
+ pub use product_detail::ProductDetail;
ํ๋ฉด์ ์ดํด๋ณด์.
์ด์ ์๋ก๊ณ ์นจ ์์ด ์ฌ๋ฌ ํ์ด์ง๋ฅผ ์ด๋ํ ์ ์๊ฒ ๋์๋ค!
ProductDetail
ํ์ด์ง์์ ๋์น์ฑ๋์ง ๋ชจ๋ฅด๊ฒ ์ง๋ง, "์ฅ๋ฐ๊ตฌ๋๋ก ์ถ๊ฐ" ๋ฒํผ์ ๋๋ฅด๋ฉด ํ์ฌ๋ ์๋ํ์ง ์๋๋ค. cart_products
๊ฐ ํ์ฌ Home
ํ์ด์ง ์ปดํฌ๋ํธ์ ์กด์ฌํ๊ธฐ ๋๋ฌธ์ด๋ค.
๋ ์ปดํฌ๋ํธ์์ ์ํ๋ฅผ ๊ณต์ ํ๊ธฐ ์ํด์๋ ๋ ๊ฐ์ง๋ฅผ ํ ์ ์๋๋ฐ,
App
์ปดํฌ๋ํธ๋ ProductDetail
๊ณผ Home
์ ๊ณตํต ์กฐ์ ์ปดํฌ๋ํธ์ด๋ค. ๋ฐ๋ผ์, cart_products
์ํ๋ฅผ ์ฎ๊ธฐ๋ฉด ์ด ์ํ๋ฅผ prop์ผ๋ก ProductDetail
๊ณผ Home
์ ์ ๋ฌํด ์ค ์ ์๊ฒ ๋๋ค.
์ด ๋ฐฉ๋ฒ์ ์ปดํฌ๋ํธ๋ค์ด ์์ ๊ณ์ธต์ ๊ฐ์ง ๊ฒฝ์ฐ ์ ๋์ํ์ง๋ง, ๊ณ์ธต ๊ตฌ์กฐ๊ฐ ๊น์ ๊ฒฝ์ฐ(๋ ํฐ ๊ท๋ชจ์ SPA์ ๊ฒฝ์ฐ) ์ํ๋ ์ปดํฌ๋ํธ์ ๋๋ฌํ๊ธฐ ์ํด์๋ ์ฌ๋ฌ ๊ณ์ธต์ ์ปดํฌ๋ํธ(์ด prop์ ์ฌ์ฉํ์ง ์์ ์๋ ์๋)๋ฅผ ํตํด ์ด ์ํ๋ฅผ ์ ๋ฌํด์ผ ํ๋ค. ์ด๊ฒ์ "Prop Drilling"์ด๋ผ ํ๋ค.
ํ์ฌ ๊ตฌ์ฑ์์๋ cart_products
๊ฐ ์ํ์ ์ฌ์ฉ๋์ง ์๋๋ผ๋ App
์์ AppToCart
์ปดํฌ๋ํธ๋ก ์ ๋ฌ ๋๊ธฐ ์ํด ProductDetail
๊ณผ Home
์ปดํฌ๋ํธ๋ฅผ ๊ฑฐ์น๋ ๊ฒ์ ๋ณผ ์ ์๋ค. ๊ฐ์ ์๋๋ฆฌ์ค๊ฐ ๊น์ ๊ณ์ธต์ ๊ฐ๋ ์ปดํฌ๋ํธ์์ ์ผ์ด๋๋ค๊ณ ์๊ฐํด๋ณด๋ผ.
์ด๋ฐ ๋ฌธ์ ๋ฅผ ์ ์ญ ์ํ๋ฅผ ํตํด ํด๊ฒฐํ ์ ์๋ค. ์ฌ๊ธฐ ์ด๋ป๊ฒ ๋์ํ๋์ง ํจ๊ป ์ดํด๋ณด์.
๊ฐ ์ปดํฌ๋ํธ์ ์ ์ญ ์ํ๊ฐ ์ด๋ป๊ฒ ์ง์ ์ฐ๊ฒฐ์ด ๋์ด ์๋์ง๋ฅผ ์ฃผ์ ๊น๊ฒ ๋ณด์.
๋ถํํ๊ฒ๋, Yew๋ ์ข์ ํด๊ฒฐ์ฑ
์ด ์์ง ์กด์ฌํ์ง ์๋ ๊ฒ์ฒ๋ผ ๋ณด์ธ๋ค. ๊ถ์ฅ๋๋ ํด๊ฒฐ์ฑ
์ pubsub์ ํตํด ์ํ ๋ณ๊ฒฝ์ ๋ธ๋ก๋ ์บ์คํ
ํ๊ธฐ ์ํด Agents
๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค. ์ด๊ฒ์ ๋น ๋ฅด๊ฒ ์ฑ์ ๋๋ฝํ ์ ์์ด ์ฌ์ฉ์ ํ์ง ์๊ณ ์๋ค. ๋ฏธ๋์๋ React์ Context๋ Redux, Mobx ๊ฐ์ ๊ฒ์ ๋ณผ ์ ์๊ธธ ๊ธฐ๋ํ๋ค.
๊ทธ๋ผ ์ํ๋ฅผ ํธ์ด์คํ ํด์ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด๋ณด์.
cart_products
์ํ๋ฅผ App
์ผ๋ก ์ด๋์ํค๊ณ NavBar
๋ฐ AtcButton
๋ฅผ ๋ณ๋ ์ปดํฌ๋ํธ๋ก ๊ตฌ์ฑํ๋ ๋ฐฉ์์ผ๋ก ๋ฆฌํฉํ ๋ง ํด๋ณด์
// src/components/navbar.rs
use crate::types::CartProduct;
use yew::prelude::*;
pub struct Navbar {
props: Props,
}
#[derive(Properties, Clone)]
pub struct Props {
pub cart_products: Vec<CartProduct>,
}
impl Component for Navbar {
type Message = ();
type Properties = Props;
fn create(props: Self::Properties, _link: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
true
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.props = props;
true
}
fn view(&self) -> Html {
let cart_value = self
.props
.cart_products
.iter()
.fold(0.0, |acc, cp| acc + (cp.quantity as f64 * cp.product.price));
html! {
<div class="navbar">
<div class="navbar_title">{"RustMart"}</div>
<div class="navbar_cart_value">{format!("${:.2}", cart_value)}</div>
</div>
}
}
}
Navbar
์ปดํฌ๋ํธ์์ change
์๋ช
์ฃผ๊ธฐ ๋ฉ์๋๋ฅผ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง๋ฅผ ์ฃผ๋ชฉํด๋ณด์. ๋ถ๋ชจ์์ ๋ณด๋ธ props๊ฐ ๋ณ๊ฒฝ๋๋ค๋ฉด, UI๊ฐ ๋ค์ ๋ฆฌ๋ ๋๋ง ๋๋๋ก ์ปดํฌ๋ํธ ๋ด๋ถ์ props๋ฅผ ์
๋ฐ์ดํธ ํด์ผ ํ๋ค.
// src/components/atc_button.rs
use crate::types::Product;
use yew::prelude::*;
pub struct AtcButton {
props: Props,
link: ComponentLink<Self>,
}
#[derive(Properties, Clone)]
pub struct Props {
pub product: Product,
pub on_add_to_cart: Callback<Product>,
}
pub enum Msg {
AddToCart,
}
impl Component for AtcButton {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self { props, link }
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::AddToCart => self.props.on_add_to_cart.emit(self.props.product.clone()),
}
true
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.props = props;
true
}
fn view(&self) -> Html {
let onclick = self.link.callback(|_| Msg::AddToCart);
html! {
<button class="product_atc_button" onclick=onclick>{"Add To Cart"}</button>
}
}
}
// src/components/mod.rs
+ mod atc_button;
+ mod navbar;
mod product_card;
+ pub use atc_button::AtcButton;
+ pub use navbar::Navbar;
pub use product_card::ProductCard;
๊ทธ๋ฆฌ๊ณ ์๋ก์ด AtcButton
์ปดํฌ๋ํธ๋ฅผ ProductCard
์ ProductDetail
์์ ์ฌ์ฉํ์.
// src/components/product_card.rs
+ use crate::components::AtcButton;
use crate::route::Route;
use crate::types::Product;
use yew::prelude::*;
use yew_router::components::RouterAnchor;
pub struct ProductCard {
props: Props,
}
#[derive(Properties, Clone)]
pub struct Props {
pub product: Product,
- pub on_add_to_cart: Callback<()>,
+ pub on_add_to_cart: Callback<Product>,
}
impl Component for ProductCard {
// No changes
fn view(&self) -> Html {
type Anchor = RouterAnchor<Route>;
- let onclick = self.props.on_add_to_cart.reform(|_| ());
html! {
<div class="product_card_container">
<Anchor route=Route::ProductDetail(self.props.product.id) classes="product_card_anchor">
<img class="product_card_image" src={&self.props.product.image}/>
<div class="product_card_name">{&self.props.product.name}</div>
<div class="product_card_price">{"$"}{&self.props.product.price}</div>
</Anchor>
- <button class="product_atc_button" onclick=onclick>{"Add To Cart"}</button>
+ <AtcButton product=self.props.product.clone() on_add_to_cart=self.props.on_add_to_cart.clone() />
</div>
}
}
}
// src/pages/product_detail.rs
use crate::api;
+ use crate::components::AtcButton;
use crate::types::Product;
use anyhow::Error;
use yew::format::Json;
use yew::prelude::*;
use yew::services::fetch::FetchTask;
// No changes
#[derive(Properties, Clone)]
pub struct Props {
pub id: i32,
+ pub on_add_to_cart: Callback<Product>,
}
impl Component for ProductDetail {
// No changes
fn view(&self) -> Html {
if let Some(ref product) = self.state.product {
html! {
<div class="product_detail_container">
<img class="product_detail_image" src={&product.image}/>
<div class="product_card_name">{&product.name}</div>
<div style="margin: 10px 0; line-height: 24px;">{&product.description}</div>
<div class="product_card_price">{"$"}{&product.price}</div>
- <button class="product_atc_button">{"Add To Cart"}</button>
+ <AtcButton product=product.clone() on_add_to_cart=self.props.on_add_to_cart.clone() />
</div>
}
}
// No changes
}
}
๋ง์ง๋ง์ผ๋ก, cart_products
๋ฅผ Home
์์ App
์ผ๋ก ์ฎ๊ธด๋ค.
// src/app.rs
+ use crate::components::Navbar;
+ use crate::types::{CartProduct, Product};
use yew::prelude::*;
use yew_router::prelude::*;
use crate::pages::{Home, ProductDetail};
use crate::route::Route;
+ struct State {
+ cart_products: Vec<CartProduct>,
+ }
- pub struct App {}
+ pub struct App {
+ state: State,
+ link: ComponentLink<Self>,
+ }
+ pub enum Msg {
+ AddToCart(Product),
+ }
impl Component for App {
- type Message = ();
+ type Message = Msg;
type Properties = ();
- fn create(_: Self::Properties, _link: ComponentLink<Self>) -> Self {
+ fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
+ let cart_products = vec![];
- Self {}
+ Self {
+ state: State { cart_products },
+ link,
+ }
}
- fn update(&mut self, _msg: Self::Message) -> ShouldRender {
+ fn update(&mut self, message: Self::Message) -> ShouldRender {
+ match message {
+ Msg::AddToCart(product) => {
+ let cart_product = self
+ .state
+ .cart_products
+ .iter_mut()
+ .find(|cp: &&mut CartProduct| cp.product.id == product.id);
+ if let Some(cp) = cart_product {
+ cp.quantity += 1;
+ } else {
+ self.state.cart_products.push(CartProduct {
+ product: product.clone(),
+ quantity: 1,
+ })
+ }
+ true
+ }
+ }
- true
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
+ let handle_add_to_cart = self
+ .link
+ .callback(|product: Product| Msg::AddToCart(product));
+ let cart_products = self.state.cart_products.clone();
- let render = Router::render(|switch: Route| match switch {
- Route::ProductDetail(id) => html! {<ProductDetail id=id/>},
- Route::HomePage => html! {<Home/>},
+ let render = Router::render(move |switch: Route| match switch {
+ Route::ProductDetail(id) => {
+ html! {<ProductDetail id=id on_add_to_cart=handle_add_to_cart.clone() />}
+ }
+ Route::HomePage => {
+ html! {<Home cart_products=cart_products.clone() on_add_to_cart=handle_add_to_cart.clone()/>}
+ }
});
html! {
+ <>
+ <Navbar cart_products=self.state.cart_products.clone() />
<Router<Route, ()> render=render/>
+ </>
}
}
}
// src/pages/home.rs
// No changes
struct State {
products: Vec<Product>,
- cart_products: Vec<CartProduct>,
get_products_error: Option<Error>,
get_products_loaded: bool,
}
+ #[derive(Properties, Clone)]
+ pub struct Props {
+ pub cart_products: Vec<CartProduct>,
+ pub on_add_to_cart: Callback<Product>,
+ }
pub struct Home {
+ props: Props,
state: State,
link: ComponentLink<Self>,
task: Option<FetchTask>,
}
pub enum Msg {
- AddToCart(i32),
GetProducts,
GetProductsSuccess(Vec<Product>),
GetProductsError(Error),
}
impl Component for Home {
type Message = Msg;
- type Properties = ();
+ type Properties = Props;
- fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
+ fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let products = vec![];
- let cart_products = vec![];
link.send_message(Msg::GetProducts);
Self {
props,
state: State {
products,
- cart_products,
get_products_error: None,
get_products_loaded: false,
},
link,
task: None,
}
}
fn update(&mut self, message: Self::Message) -> ShouldRender {
match message {
Msg::GetProducts => {
self.state.get_products_loaded = false;
let handler =
self.link
.callback(move |response: api::FetchResponse<Vec<Product>>| {
let (_, Json(data)) = response.into_parts();
match data {
Ok(products) => Msg::GetProductsSuccess(products),
Err(err) => Msg::GetProductsError(err),
}
});
self.task = Some(api::get_products(handler));
true
}
Msg::GetProductsSuccess(products) => {
self.state.products = products;
self.state.get_products_loaded = true;
true
}
Msg::GetProductsError(error) => {
self.state.get_products_error = Some(error);
self.state.get_products_loaded = true;
true
}
- Msg::AddToCart(product_id) => {
- let product = self
- .state
- .products
- .iter()
- .find(|p: &&Product| p.id == product_id)
- .unwrap();
- let cart_product = self
- .state
- .cart_products
- .iter_mut()
- .find(|cp: &&mut CartProduct| cp.product.id == product_id);
- if let Some(cp) = cart_product {
- cp.quantity += 1;
- } else {
- self.state.cart_products.push(CartProduct {
- product: product.clone(),
- quantity: 1,
- })
- }
- true
- }
}
}
- fn change(&mut self, _: Self::Properties) -> ShouldRender {
+ fn change(&mut self, props: Self::Properties) -> ShouldRender {
+ self.props = props;
true
}
fn view(&self) -> Html {
let products: Vec<Html> = self
.state
.products
.iter()
.map(|product: &Product| {
- let product_id = product.id;
html! {
- <ProductCard product={product} on_add_to_cart=self.link.callback(move |_| Msg::AddToCart(product_id))/>
+ <ProductCard product={product} on_add_to_cart=self.props.on_add_to_cart.clone()/>
}
})
.collect();
- let cart_value = self
- .state
- .cart_products
- .iter()
- .fold(0.0, |acc, cp| acc + (cp.quantity as f64 * cp.product.price));
if !self.state.get_products_loaded {
// No changes
} else if let Some(_) = self.state.get_products_error {
// No changes
} else {
html! {
- <div>
- <div class="navbar">
- <div class="navbar_title">{"RustMart"}</div>
- <div class="navbar_cart_value">{format!("${:.2}", cart_value)}</div>
- </div>
<div class="product_card_list">{products}</div>
- </div>
}
}
}
}
์ด์ ProductDetail
ํ์ด์ง์์๋ ์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐ ํ ์ ์์ผ๋ฉฐ ๋ชจ๋ ํ์ด์ง์์ ๋ค๋น๊ฒ์ด์
๋ฐ๋ฅผ ๊ฐ๊ฒ ๋์๋ค.
Rust๋ง์ผ๋ก ์จ์ ํ๊ฒ SPA๋ฅผ ๋ง๋ค์ด๋ด๋ ๋ฐ ์ฑ๊ณตํ๋ค!
์ด ๋ฐ๋ชจ ๋งํฌ์ ๋ค์ด๊ฐ๋ฉด ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ ์ ์์ผ๋ฉฐ ์ฝ๋๋ Github ์ ์ฅ์์์ ํ์ธํ ์ ์๋ค. ๋ง์ฝ ์ง๋ฌธ์ด๋ ์ ์์ด ์๋ค๋ฉด, sheshbabu@gmail.com ๋ก ์ฐ๋ฝ ๋ฐ๋๋ค.
Yew ์ปค๋ฎค๋ํฐ๋ html!
์ด๋ Component
๋ฑ ์ฌ๋ฌ ์ถ์ํ๋ฅผ ์ ์ค๊ณํด ๋ React์ ์ต์ํ ๋์ ๊ฐ์ ์ฌ๋๋ค์ด ์ฆ์ ์์ํ ์ ์๊ฒ ํ๋ค. FetchTask์ ์ ๋ค๋ฌ์ด์ง์ง ์์ ๋ฉด์ด๋ ์์ธก ๊ฐ๋ฅํ(predictable) ์ํ ๊ด๋ฆฌ์ ๋ถ์กฑ, ๋ฌธ์์ ๋ถ์กฑ ๋ฑ ๋ฌธ์ ๊ฐ ์์ง๋ง ์ด๋ฌํ ์ด์๋ค์ ํด๊ฒฐํด ๋๊ฐ๋ค๋ฉด React๋ Vue์ ์ข์ ๋์์ด ๋ ๊ฐ๋ฅ์ฑ์ด ์๋ค๊ณ ์๊ฐํ๋ค.
์ฝ์ด์ค์ ๊ณ ๋ง๋ค! ์ด๋ฐ ๊ธ์ ๋ ๋ฐ์๋ณด๊ณ ์ถ๋ค๋ฉด twitter๋ฅผ ํ๋ก์ฐํ๋ผ!