Woongjae Lee
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team
https://slides.com/woongjae/react-workshop-jun-2023
Senior Lead Software Engineer @NHN Dooray (2021~)
Lead Software Engineer @ProtoPie (2016 ~ 2021)
Microsoft MVP
TypeScript Korea User Group Organizer
Marktube (Youtube)
이 웅재
- 가장 기본적인 개발 환경
- 클라이언트 개발 환경의 가장 중요한 축
- 현실적으로 한번 정해진 Node 버전을 올리기는 생각보다 어렵다.
- 개발 환경과 빌드 환경의 Node.js 버전은 반드시 같아야 한다.
- 배포 환경의 Node.js 버전은 같지 않아도 된다. Link
내가 개발한 환경 그대로 다른 사람이 개발할 수 있는가
실행 런타임이 같아야 한다. 같은 Node.js 버전
내 소스 코드에서 사용하는 라이브러리들의 재현 재현이 가능한 디펜던시 정보
내가 로컬에서 빌드한 환경 그대로 CI 서버에서 그대로 빌드가 가능한 것인가
빌드를 하는 런타임이 같아야 한다. 같은 Node.js 버전
빌드에 포함되는 라이브러리들의 재현 재현이 가능한 디펜던시 정보
$ nvm install
$ nvm use
- 빠르면 좋겠고,
- 내가 개발한 환경 그대로 빌드 환경에서 사용되길 바라고
- npm 이 기본, yarn 의 도전, pnpm 의 도전, yarn berry 로의 진화
- import 하면 해석하여, node_modules 에서 가져감
- 의존성 헬을 만든 장본인
- 이미 설치된 모듈의 다른 버전이 필요하다면 해당 모듈을 필요로 하는 모듈 아래에 둠.
- 설치할 때마다 상이한 구조를 가지게 될 수 있음.
- package-lock.json 과 npm ci
- node_modules 를 사용함
- 병렬 설치로 빠른 설치 속도
- yarn.lock 파일 생성을 통한 의존성 구조를 고정
- 모노레포 지원
npm i yarn -g
- 여러 프로젝트에서 사용되는 dependencies 의 중복 저장을 막아야
- 디스크 공간 절약 및 설치 속도 향상
- 평탄하지 않은 node_modules 디렉토리 생성
- 팬텀 디펜던시 ㅜ
- zero install
nvm use 18
npm i yarn -g
yarn set version stable
mkdir yarn-berry-nextjs
cd yarn-berry-nextjs
yarn init
yarn add next react react-dom
yarn add @yarnpkg/sdks -D
yarn dlx @yarnpkg/sdks vscode
{
"name": "yarn-berry-nextjs",
"packageManager": "yarn@3.6.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^13.4.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@yarnpkg/sdks": "^3.0.0-rc.45"
}
}
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
import logo from "./logo.svg";
import "./App.css";
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState("A");
function click() {
setCount(count + 1);
setName("B");
}
console.debug(count, name);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
{count} {name}
</p>
<button onClick={click}>Learn React</button>
</header>
</div>
);
}
export default App;
import logo from "./logo.svg";
import "./App.css";
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState("A");
function click() {
setTimeout(() => {
setCount(count + 1);
setName("B");
}, 1000);
}
console.debug(count, name);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
{count} {name}
</p>
<button onClick={click}>Learn React</button>
</header>
</div>
);
}
export default App;
서스펜스를 사용하면 컴포넌트 트리의 일부가 아직 표시될 준비가 되지 않은 경우 해당 부분의 로딩 상태를 선언적으로 지정할 수 있다.
몇 년 전에 Suspense의 제한된 버전을 도입했다. 하지만 지원되는 사용 사례는 React.lazy 를 사용한 코드 분할뿐이었고, 서버에서 렌더링할 때는 전혀 지원되지 않았다.
React 18에서는 서버에서 Suspense에 대한 지원을 추가하고, 동시 렌더링 기능을 사용하여 기능을 확장했다.
React 18의 Suspense 는 transition API 와 함께 사용할 때 가장 잘 작동한다. 트랜지션 도중 일시 중단하면 React는 이미 표시된 콘텐츠가 폴백으로 대체되는 것을 방지한다. 대신 React는 데이터가 충분히 로드될 때까지 렌더링을 지연시켜 로딩 상태가 나빠지는 것을 방지한다.
$ git clone https://github.com/2woongjae/prototype-shop.git
$ cd prototype-shop
$ nvm install 14.16.1
$ nvm use 14.16.1
$ npm ci
$ npm i react-router-dom
import Home from "./pages/Home";
import Checkout from "./pages/Checkout";
import AppStateProvider from "./providers/AppStateProvider";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/checkout",
element: <Checkout />,
},
]);
function App() {
return (
<AppStateProvider>
<RouterProvider router={router} />
</AppStateProvider>
);
}
export default App;
import Header from "../components/Header";
import Prototypes from "../components/Prototypes";
import Orders from "../components/Orders";
import Footer from "../components/Footer";
export default function Home() {
return (
<>
<Header />
<div className="container">
<Prototypes />
<Orders />
<Footer />
</div>
</>
);
}
import { useNavigate } from "react-router-dom";
export default function Orders() {
const navigate = useNavigate();
const goOrderPage = useCallback(() => {
navigate("/checkout");
}, [navigate]);
return (
...
<button
className="btn btn--secondary"
style={{ width: "100%", marginTop: 10 }}
onClick={goOrderPage}
>
Checkout
</button>
...
);
}
import { useMemo, useCallback } from "react";
import useOrders from "../hooks/useOrders";
import usePrototypes from "../hooks/usePrototypes";
import { useNavigate } from "react-router-dom";
export default function Checkout() {
const orders = useOrders();
const prototypes = usePrototypes();
const navigate = useNavigate();
const totalPrice = useMemo(() => {
return orders
.map((order) => {
const { id, quantity } = order;
const prototype = prototypes.find((p) => p.id === id);
return prototype.price * quantity;
})
.reduce((l, r) => l + r, 0);
}, [orders, prototypes]);
const goHomePage = useCallback(() => {
navigate("/");
}, [navigate]);
if (orders.length === 0) {
return (
<button
className="btn btn--secondary"
style={{ width: "100%", marginTop: 10 }}
onClick={goHomePage}
>
Go Home
</button>
);
}
return (
<div className="checkout-container">
<h1>Checkout</h1>
<div className="body">
{orders.map((order) => {
const { id } = order;
const prototype = prototypes.find((p) => p.id === id);
return (
<div className="item" key={id}>
<div className="img">
<video src={prototype.thumbnail} />
</div>
<div className="content">
<p className="title">
{prototype.title} x {order.quantity}
</p>
</div>
<div className="action">
<p className="price">$ {prototype.price * order.quantity}</p>
</div>
</div>
);
})}
</div>
<div className="total">
<hr />
<div className="item">
<div className="content">Total</div>
<div className="action">
<div className="price">$ {totalPrice}</div>
</div>
</div>
<div
style={{
textAlign: "center",
}}
>
<button
className="btn btn--primary"
style={{ width: "485px", marginTop: 10 }}
>
Payment
</button>
<button
className="btn btn--primary"
style={{ width: "485px", marginTop: 10, marginLeft: 10 }}
onClick={goHomePage}
>
Cancel
</button>
</div>
</div>
</div>
);
}
.checkout-container {
margin-left: auto;
margin-right: auto;
padding-left: 0.4rem;
padding-right: 0.4rem;
display: flex;
flex-direction: column;
min-height: 100vh;
max-width: 1000px;
margin-top: 50px;
margin-bottom: 100px;
}
간단한 구조
Redux Toolkit 은 Redux 를 사용하는 데 필요한 복잡한 설정 작업들을 대신 처리해주기 때문에, 더 간단하고 직관적인 구조를 가질 수 있습니다.
간편한 코드 작성
Redux Toolkit 은 createSlice() 함수와 createAsyncThunk() 함수를 제공하여, 보일러플레이트 코드를 줄이고 간단한 코드 작성을 가능하게 합니다.
개발 생산성 향상
Redux Toolkit 을 사용하면 Redux 를 보다 쉽게 사용할 수 있기 때문에, 개발 생산성을 향상시킬 수 있습니다.
최적화된 성능
Redux Toolkit 은 내부적으로 immer.js 와 Redux Toolkit RTK Query 를 사용하여 성능 최적화를 지원합니다.
일부 기능의 제한
Redux Toolkit 은 Redux 의 기능 중 일부를 대신 처리하기 때문에, 모든 기능을 지원하지는 않습니다. 따라서 일부 기능을 직접 구현해야 할 수도 있습니다.
학습 곡선
Redux Toolkit 을 사용하기 위해서는 Redux 의 기본적인 개념과 구조를 이해하고 있어야 합니다. 따라서 Redux 에 익숙하지 않은 개발자에게는 학습 곡선이 있을 수 있습니다.
세부적인 컨트롤의 부재
Redux Toolkit 은 Redux 를 간편하게 사용할 수 있도록 지원하기 때문에, 세부적인 컨트롤을 위해서는 Redux 의 기본 기능을 사용해야 합니다.
의존성 추가
Redux Toolkit 을 사용하기 위해서는 redux 와 immer.js 등의 라이브러리에 대한 의존성이 추가됩니다.
git clone https://github.com/nhn-kai/prototype-shop.git prototype-shop-redux-toolkit
cd prototype-shop-redux-toolkit
nvm use 16
npm ci
npm i @reduxjs/toolkit react-redux
npm start
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import create from "./redux/create";
const store = create();
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./modules/rootReducer";
export default function create() {
const store = configureStore({
reducer: rootReducer,
});
return store;
}
import prototypes from "./prototypes";
import orders from "./orders";
const rootReducer = { prototypes, orders };
export default rootReducer;
import { createSlice } from "@reduxjs/toolkit";
import { createNamespace } from "../utils";
const namespace = createNamespace("prototypes");
const initialState = {
data: [
{
id: "pp-01",
title: "Kids-story",
artist: "Thomas Buisson",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Kids-story_1.mp4",
price: 10,
pieUrl: "https://cloud.protopie.io/p/8a6461ad85",
},
{
id: "pp-02",
title: "mockyapp",
artist: "Ahmed Amr",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/mockyapp.mp4",
price: 20,
pieUrl: "https://cloud.protopie.io/p/27631ac9d5",
},
{
id: "pp-03",
title: "macOS Folder Concept",
artist: "Dominik Kandravý",
desc: "Folder concept prototype by Dominik Kandravý.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/macOS_Folder_Concept_-_Folder_concept.mp4",
price: 30,
pieUrl: "https://cloud.protopie.io/p/acde5ccdf9",
},
{
id: "pp-04",
title: "Translator",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Translator.mp4",
price: 40,
pieUrl: "https://cloud.protopie.io/p/b91edba11d",
},
{
id: "pp-05",
title: "In-car voice control",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/In-car_voice_control.mp4",
price: 50,
pieUrl: "https://cloud.protopie.io/p/6ec7e70d1a",
},
{
id: "pp-06",
title: "The Adventures of Proto",
artist: "Richard Oldfield",
desc: `Made exclusively for Protopie Playoff 2021
Shout up if you get stuck!
For the full experience. View in the Protopie App.
#PieDay #PlayOff #ProtoPie`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/The_Adventures_of_Proto.mp4",
price: 60,
pieUrl: "https://cloud.protopie.io/p/95ee13709f",
},
{
id: "pp-07",
title: "Sunglasses shop app",
artist: "Mustafa Alabdullah",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/sunglasses_shop_app.mp4",
price: 70,
pieUrl: "https://cloud.protopie.io/p/6f336cac8c",
},
{
id: "pp-08",
title: "Alwritey—Minimalist Text Editor",
artist: "Fredo Tan",
desc: `This minimalist text editor prototype was made with ProtoPie by Fredo Tan.
---
Inspired by Writty, a simple writing app by Carlos Yllobre. Try out Writty at https://writtyapp.com.
---
ProtoPie is an interactive prototyping tool for all digital products.
---
Learn more about ProtoPie at https://protopie.io.`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/minimalist-text-editor.mp4",
price: 80,
pieUrl: "https://cloud.protopie.io/p/946f88f8d3",
},
{
id: "pp-09",
title: "Voice search for TV",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/TV.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/60ee64cda0",
},
{
id: "pp-10",
title: "Finance App Visual Interaction 2.0",
artist: "Arpit Agrawal",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Credit_Card_App.mp4",
price: 90,
pieUrl:
"https://cloud.protopie.io/p/09ce2fdf84/21?ui=true&mockup=true&touchHint=true&scaleToFit=true&cursorType=touch",
},
{
id: "pp-11",
title: "Whack-a-mole",
artist: "Changmo Kang",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Whack_a_mole.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/ab796f897e",
},
{
id: "pp-12",
title: "Voice Note",
artist: "Haerin Song",
desc: `Made by Haerin Song
(Soda Design)`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Voice_note_with_sound_wave.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/7a0d6567d2",
},
],
};
const { reducer } = createSlice({
name: namespace,
initialState,
});
export default reducer;
import { createSlice } from "@reduxjs/toolkit";
import { createNamespace } from "../utils";
const namespace = createNamespace("orders");
const initialState = { data: [] };
const {
actions: { addToOrder, remove, removeAll },
reducer,
} = createSlice({
name: namespace,
initialState,
reducers: {
addToOrder(state, action) {
const id = action.payload.id;
const finded = state.data.find((order) => order.id === id);
if (finded === undefined) {
state.data = [...state.data, { id: id, quantity: 1 }];
} else {
state.data = state.data.map((order) => {
if (order.id === id) {
return {
id,
quantity: order.quantity + 1,
};
} else {
return order;
}
});
}
},
remove(state, action) {
const id = action.payload.id;
state.data = state.data.filter((order) => order.id !== id);
},
removeAll(state) {
state.data = [];
},
},
});
export default reducer;
export { addToOrder, remove, removeAll };
export const createNamespace = (namespace) => `@prototype-shop/${namespace}`;
import Header from "./components/Header";
import PrototypesContainer from "./containers/PrototypesContainer";
import OrdersContainer from "./containers/OrdersContainer";
import Footer from "./components/Footer";
function App() {
return (
<>
<Header />
<div className="container">
<PrototypesContainer />
<OrdersContainer />
<Footer />
</div>
</>
);
}
export default App;
import { useCallback } from "react";
import { addToOrder as addToOrderAction } from "../redux/modules/orders";
import Prototypes from "../components/Prototypes";
import { useSelector, useDispatch } from "react-redux";
export default function PrototypesContainer() {
const prototypes = useSelector((state) => state.prototypes.data);
const dispatch = useDispatch();
const addToOrder = useCallback(
(id) => {
dispatch(addToOrderAction({ id }));
},
[dispatch]
);
return <Prototypes prototypes={prototypes} addToOrder={addToOrder} />;
}
const Prototypes = ({ prototypes, addToOrder }) => (
<main>
<div className="prototypes">
{prototypes.map((prototype) => {
const { id, thumbnail, title, price, desc, pieUrl } = prototype;
const click = () => {
addToOrder(id);
};
return (
<div className="prototype" key={id}>
<a href={pieUrl} target="_BLANK" rel="noreferrer">
<div
style={{
padding: "25px 0 33px 0",
}}
>
<video
autoPlay
loop
playsInline
className="prototype__artwork prototype__edit"
src={thumbnail}
style={{
objectFit: "contain",
}}
/>
</div>
</a>
<div className="prototype__body">
<div className="prototype__title">
<div className="btn btn--primary float--right" onClick={click}>
<i className="icon icon--plus" />
</div>
{title}
</div>
<p className="prototype__price">$ {price}</p>
<p className="prototype__desc">{desc}</p>
</div>
</div>
);
})}
</div>
</main>
);
export default Prototypes;
import Orders from "../components/Orders";
import { useCallback } from "react";
import {
remove as removeAction,
removeAll as removeAllAction,
} from "../redux/modules/orders";
import { useSelector, useDispatch } from "react-redux";
export default function OrdersContainer() {
const prototypes = useSelector((state) => state.prototypes.data);
const orders = useSelector((state) => state.orders.data);
const dispatch = useDispatch();
const remove = useCallback(
(id) => {
dispatch(removeAction({ id }));
},
[dispatch]
);
const removeAll = useCallback(
(id) => {
dispatch(removeAllAction());
},
[dispatch]
);
return (
<Orders
prototypes={prototypes}
orders={orders}
remove={remove}
removeAll={removeAll}
/>
);
}
import { useMemo } from "react";
export default function Orders({ prototypes, orders, remove, removeAll }) {
const totalPrice = useMemo(() => {
return orders
.map((order) => {
const { id, quantity } = order;
const prototype = prototypes.find((p) => p.id === id);
return prototype.price * quantity;
})
.reduce((l, r) => l + r, 0);
}, [orders, prototypes]);
if (orders.length === 0) {
return (
<aside>
<div className="empty">
<div className="title">You don't have any orders</div>
<div className="subtitle">Click on a + to add an order</div>
</div>
</aside>
);
}
return (
<aside>
<div className="order">
<div className="body">
{orders.map((order) => {
const { id } = order;
const prototype = prototypes.find((p) => p.id === id);
const click = () => {
remove(id);
};
return (
<div className="item" key={id}>
<div className="img">
<video src={prototype.thumbnail} />
</div>
<div className="content">
<p className="title">
{prototype.title} x {order.quantity}
</p>
</div>
<div className="action">
<p className="price">$ {prototype.price * order.quantity}</p>
<button className="btn btn--link" onClick={click}>
<i className="icon icon--cross" />
</button>
</div>
</div>
);
})}
</div>
<div className="total">
<hr />
<div className="item">
<div className="content">Total</div>
<div className="action">
<div className="price">$ {totalPrice}</div>
</div>
<button className="btn btn--link" onClick={removeAll}>
<i className="icon icon--delete" />
</button>
</div>
<button
className="btn btn--secondary"
style={{ width: "100%", marginTop: 10 }}
>
Checkout
</button>
</div>
</div>
</aside>
);
}
단순성
MobX 는 Redux 와 비교했을 때 상대적으로 단순합니다. MobX 는 state, action, reaction 의 세 가지 구성 요소로 이루어져 있으며, 이를 이용하여 간단하게 애플리케이션의 상태 관리를 할 수 있습니다.
학습 곡선
MobX 는 상대적으로 쉬운 상태 관리 라이브러리이지만, 다른 상태 관리 라이브러리와는 다른 개념을 사용하므로, 초기에 학습 곡선이 높을 수 있습니다.
비동기적인 데이터 스트림을 다루는 프로그래밍 패러다임 중 하나입니다. 리액티브 프로그래밍에서는 데이터의 상태 변화에 대해 반응(react)하는 것이 중요합니다. 이를 위해 리액티브 프로그래밍에서는 데이터 스트림의 생성, 변환, 필터링, 결합 등을 위한 다양한 연산자를 제공합니다.
리액티브 프로그래밍의 주요 개념으로는 옵저버블(Observable)과 옵서버(Observer)가 있습니다. 옵저버블은 데이터 스트림을 나타내며, 옵서버는 옵저버블에서 발생하는 데이터 변화를 관찰하고 처리합니다. 따라서, 옵저버블에서 데이터 변화가 발생하면 옵서버는 이를 감지하여 처리하게 됩니다.
리액티브 프로그래밍은 비동기적인 데이터 처리를 위해 많이 사용됩니다. 비동기적인 데이터 처리는 이벤트 기반(event-driven)이거나, 콜백(callback) 기반이거나, 프로미스(promise)나, 비동기 함수(async/await) 등을 통해 구현됩니다. 이러한 비동기적인 데이터 처리를 효율적으로 구현하기 위해 리액티브 프로그래밍에서는 다양한 라이브러리와 프레임워크가 제공됩니다.
git clone https://github.com/nhn-kai/prototype-shop.git prototype-shop-mobx
cd prototype-shop-mobx
nvm use 16
npm ci
npm i mobx mobx-react
npm start
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import PrototypesStore from "./stores/PrototypesStore";
import OrdersStore from "./stores/OrdersStore";
import { Provider } from "mobx-react";
const prototypesStore = new PrototypesStore();
const ordersStore = new OrdersStore();
ReactDOM.render(
<React.StrictMode>
<Provider prototypesStore={prototypesStore} ordersStore={ordersStore}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import { computed, makeObservable, observable } from "mobx";
export default class PrototypesStore {
data = [
{
id: "pp-01",
title: "Kids-story",
artist: "Thomas Buisson",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Kids-story_1.mp4",
price: 10,
pieUrl: "https://cloud.protopie.io/p/8a6461ad85",
},
{
id: "pp-02",
title: "mockyapp",
artist: "Ahmed Amr",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/mockyapp.mp4",
price: 20,
pieUrl: "https://cloud.protopie.io/p/27631ac9d5",
},
{
id: "pp-03",
title: "macOS Folder Concept",
artist: "Dominik Kandravý",
desc: "Folder concept prototype by Dominik Kandravý.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/macOS_Folder_Concept_-_Folder_concept.mp4",
price: 30,
pieUrl: "https://cloud.protopie.io/p/acde5ccdf9",
},
{
id: "pp-04",
title: "Translator",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Translator.mp4",
price: 40,
pieUrl: "https://cloud.protopie.io/p/b91edba11d",
},
{
id: "pp-05",
title: "In-car voice control",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/In-car_voice_control.mp4",
price: 50,
pieUrl: "https://cloud.protopie.io/p/6ec7e70d1a",
},
{
id: "pp-06",
title: "The Adventures of Proto",
artist: "Richard Oldfield",
desc: `Made exclusively for Protopie Playoff 2021
Shout up if you get stuck!
For the full experience. View in the Protopie App.
#PieDay #PlayOff #ProtoPie`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/The_Adventures_of_Proto.mp4",
price: 60,
pieUrl: "https://cloud.protopie.io/p/95ee13709f",
},
{
id: "pp-07",
title: "Sunglasses shop app",
artist: "Mustafa Alabdullah",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/sunglasses_shop_app.mp4",
price: 70,
pieUrl: "https://cloud.protopie.io/p/6f336cac8c",
},
{
id: "pp-08",
title: "Alwritey—Minimalist Text Editor",
artist: "Fredo Tan",
desc: `This minimalist text editor prototype was made with ProtoPie by Fredo Tan.
---
Inspired by Writty, a simple writing app by Carlos Yllobre. Try out Writty at https://writtyapp.com.
---
ProtoPie is an interactive prototyping tool for all digital products.
---
Learn more about ProtoPie at https://protopie.io.`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/minimalist-text-editor.mp4",
price: 80,
pieUrl: "https://cloud.protopie.io/p/946f88f8d3",
},
{
id: "pp-09",
title: "Voice search for TV",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/TV.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/60ee64cda0",
},
{
id: "pp-10",
title: "Finance App Visual Interaction 2.0",
artist: "Arpit Agrawal",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Credit_Card_App.mp4",
price: 90,
pieUrl:
"https://cloud.protopie.io/p/09ce2fdf84/21?ui=true&mockup=true&touchHint=true&scaleToFit=true&cursorType=touch",
},
{
id: "pp-11",
title: "Whack-a-mole",
artist: "Changmo Kang",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Whack_a_mole.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/ab796f897e",
},
{
id: "pp-12",
title: "Voice Note",
artist: "Haerin Song",
desc: `Made by Haerin Song
(Soda Design)`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Voice_note_with_sound_wave.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/7a0d6567d2",
},
];
constructor() {
makeObservable(this, {
data: observable,
prototypes: computed,
});
}
get prototypes() {
return this.data;
}
}
import { action, computed, makeObservable, observable } from "mobx";
export default class OrdersStore {
data = [];
constructor() {
makeObservable(this, {
data: observable,
orders: computed,
addToOrder: action,
remove: action,
removeAll: action,
});
}
get orders() {
return this.data;
}
addToOrder = (id) => {
const finded = this.data.find((order) => order.id === id);
if (finded === undefined) {
this.data = [...this.data, { id, quantity: 1 }];
} else {
this.data = this.data.map((order) => {
if (order.id === id) {
return {
id,
quantity: order.quantity + 1,
};
} else {
return order;
}
});
}
};
remove = (id) => {
this.data = this.data.filter((order) => order.id !== id);
};
removeAll = () => {
this.data = [];
};
}
import Header from "./components/Header";
import PrototypesContainer from "./containers/PrototypesContainer";
import OrdersContainer from "./containers/OrdersContainer";
import Footer from "./components/Footer";
function App() {
return (
<>
<Header />
<div className="container">
<PrototypesContainer />
<OrdersContainer />
<Footer />
</div>
</>
);
}
export default App;
import { inject, observer } from "mobx-react";
import Prototypes from "../components/Prototypes";
const PrototypesContainer = inject(({ prototypesStore, ordersStore }) => ({
prototypesStore,
ordersStore,
}))(
observer(({ prototypesStore, ordersStore }) => {
return (
<Prototypes
prototypes={prototypesStore.prototypes}
addToOrder={ordersStore.addToOrder}
/>
);
})
);
export default PrototypesContainer;
const Prototypes = ({ prototypes, addToOrder }) => (
<main>
<div className="prototypes">
{prototypes.map((prototype) => {
const { id, thumbnail, title, price, desc, pieUrl } = prototype;
const click = () => {
addToOrder(id);
};
return (
<div className="prototype" key={id}>
<a href={pieUrl} target="_BLANK" rel="noreferrer">
<div
style={{
padding: "25px 0 33px 0",
}}
>
<video
autoPlay
loop
playsInline
className="prototype__artwork prototype__edit"
src={thumbnail}
style={{
objectFit: "contain",
}}
/>
</div>
</a>
<div className="prototype__body">
<div className="prototype__title">
<div className="btn btn--primary float--right" onClick={click}>
<i className="icon icon--plus" />
</div>
{title}
</div>
<p className="prototype__price">$ {price}</p>
<p className="prototype__desc">{desc}</p>
</div>
</div>
);
})}
</div>
</main>
);
export default Prototypes;
import { inject, observer } from "mobx-react";
import Orders from "../components/Orders";
const OrdersContainer = inject(({ prototypesStore, ordersStore }) => ({
prototypesStore,
ordersStore,
}))(
observer(({ prototypesStore, ordersStore }) => {
return (
<Orders
prototypes={prototypesStore.prototypes}
orders={ordersStore.orders}
remove={ordersStore.remove}
removeAll={ordersStore.removeAll}
/>
);
})
);
export default OrdersContainer;
import { useMemo } from "react";
export default function Orders({ prototypes, orders, remove, removeAll }) {
const totalPrice = useMemo(() => {
return orders
.map((order) => {
const { id, quantity } = order;
const prototype = prototypes.find((p) => p.id === id);
return prototype.price * quantity;
})
.reduce((l, r) => l + r, 0);
}, [orders, prototypes]);
if (orders.length === 0) {
return (
<aside>
<div className="empty">
<div className="title">You don't have any orders</div>
<div className="subtitle">Click on a + to add an order</div>
</div>
</aside>
);
}
return (
<aside>
<div className="order">
<div className="body">
{orders.map((order) => {
const { id } = order;
const prototype = prototypes.find((p) => p.id === id);
const click = () => {
remove(id);
};
return (
<div className="item" key={id}>
<div className="img">
<video src={prototype.thumbnail} />
</div>
<div className="content">
<p className="title">
{prototype.title} x {order.quantity}
</p>
</div>
<div className="action">
<p className="price">$ {prototype.price * order.quantity}</p>
<button className="btn btn--link" onClick={click}>
<i className="icon icon--cross" />
</button>
</div>
</div>
);
})}
</div>
<div className="total">
<hr />
<div className="item">
<div className="content">Total</div>
<div className="action">
<div className="price">$ {totalPrice}</div>
</div>
<button className="btn btn--link" onClick={removeAll}>
<i className="icon icon--delete" />
</button>
</div>
<button
className="btn btn--secondary"
style={{ width: "100%", marginTop: 10 }}
>
Checkout
</button>
</div>
</div>
</aside>
);
}
import { observer } from "mobx-react";
import Prototypes from "../components/Prototypes";
import { useContext } from "react";
import { MobXProviderContext } from "mobx-react";
const PrototypesContainer = observer(() => {
const { prototypesStore, ordersStore } = useContext(MobXProviderContext);
return (
<Prototypes
prototypes={prototypesStore.prototypes}
addToOrder={ordersStore.addToOrder}
/>
);
});
export default PrototypesContainer;
git clone https://github.com/nhn-kai/prototype-shop.git prototype-shop-recoil
cd prototype-shop-recoil
nvm use 16
npm ci
npm i recoil
npm start
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { RecoilRoot } from "recoil";
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import Header from "./components/Header";
import PrototypesContainer from "./containers/PrototypesContainer";
import OrdersContainer from "./containers/OrdersContainer";
import Footer from "./components/Footer";
function App() {
return (
<>
<Header />
<div className="container">
<PrototypesContainer />
<OrdersContainer />
<Footer />
</div>
</>
);
}
export default App;
import { useCallback } from "react";
import { useRecoilValue, useRecoilState } from "recoil";
import prototypesState from "../recoil/atoms/prototypesState";
import ordersState from "../recoil/atoms/ordersState";
import Prototypes from "../components/Prototypes";
export default function PrototypesContainer() {
const prototypes = useRecoilValue(prototypesState);
const [orders, setOrders] = useRecoilState(ordersState);
const addToOrder = useCallback(
(id) => {
const finded = orders.find((order) => order.id === id);
if (finded === undefined) {
setOrders([...orders, { id, quantity: 1 }]);
} else {
setOrders(
orders.map((order) => {
if (order.id === id) {
return {
id,
quantity: order.quantity + 1,
};
} else {
return order;
}
})
);
}
},
[orders, setOrders]
);
return <Prototypes prototypes={prototypes} addToOrder={addToOrder} />;
}
import { atom } from "recoil";
const prototypesState = atom({
key: "prototypesState",
default: [
{
id: "pp-01",
title: "Kids-story",
artist: "Thomas Buisson",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Kids-story_1.mp4",
price: 10,
pieUrl: "https://cloud.protopie.io/p/8a6461ad85",
},
{
id: "pp-02",
title: "mockyapp",
artist: "Ahmed Amr",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/mockyapp.mp4",
price: 20,
pieUrl: "https://cloud.protopie.io/p/27631ac9d5",
},
{
id: "pp-03",
title: "macOS Folder Concept",
artist: "Dominik Kandravý",
desc: "Folder concept prototype by Dominik Kandravý.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/macOS_Folder_Concept_-_Folder_concept.mp4",
price: 30,
pieUrl: "https://cloud.protopie.io/p/acde5ccdf9",
},
{
id: "pp-04",
title: "Translator",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Translator.mp4",
price: 40,
pieUrl: "https://cloud.protopie.io/p/b91edba11d",
},
{
id: "pp-05",
title: "In-car voice control",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/In-car_voice_control.mp4",
price: 50,
pieUrl: "https://cloud.protopie.io/p/6ec7e70d1a",
},
{
id: "pp-06",
title: "The Adventures of Proto",
artist: "Richard Oldfield",
desc: `Made exclusively for Protopie Playoff 2021
Shout up if you get stuck!
For the full experience. View in the Protopie App.
#PieDay #PlayOff #ProtoPie`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/The_Adventures_of_Proto.mp4",
price: 60,
pieUrl: "https://cloud.protopie.io/p/95ee13709f",
},
{
id: "pp-07",
title: "Sunglasses shop app",
artist: "Mustafa Alabdullah",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/sunglasses_shop_app.mp4",
price: 70,
pieUrl: "https://cloud.protopie.io/p/6f336cac8c",
},
{
id: "pp-08",
title: "Alwritey—Minimalist Text Editor",
artist: "Fredo Tan",
desc: `This minimalist text editor prototype was made with ProtoPie by Fredo Tan.
---
Inspired by Writty, a simple writing app by Carlos Yllobre. Try out Writty at https://writtyapp.com.
---
ProtoPie is an interactive prototyping tool for all digital products.
---
Learn more about ProtoPie at https://protopie.io.`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/minimalist-text-editor.mp4",
price: 80,
pieUrl: "https://cloud.protopie.io/p/946f88f8d3",
},
{
id: "pp-09",
title: "Voice search for TV",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/TV.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/60ee64cda0",
},
{
id: "pp-10",
title: "Finance App Visual Interaction 2.0",
artist: "Arpit Agrawal",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Credit_Card_App.mp4",
price: 90,
pieUrl:
"https://cloud.protopie.io/p/09ce2fdf84/21?ui=true&mockup=true&touchHint=true&scaleToFit=true&cursorType=touch",
},
{
id: "pp-11",
title: "Whack-a-mole",
artist: "Changmo Kang",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Whack_a_mole.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/ab796f897e",
},
{
id: "pp-12",
title: "Voice Note",
artist: "Haerin Song",
desc: `Made by Haerin Song
(Soda Design)`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Voice_note_with_sound_wave.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/7a0d6567d2",
},
],
});
export default prototypesState;
import { atom } from "recoil";
const ordersState = atom({
key: "ordersState",
default: [],
});
export default ordersState;
import { useCallback, useMemo } from "react";
import { useRecoilValue, useRecoilState } from "recoil";
import prototypesState from "../recoil/atoms/prototypesState";
import ordersState from "../recoil/atoms/ordersState";
import Orders from "../components/Orders";
export default function OrdersContainer() {
const prototypes = useRecoilValue(prototypesState);
const [orders, setOrders] = useRecoilState(ordersState);
const totalPrice = useMemo(() => {
return orders
.map((order) => {
const { id, quantity } = order;
const prototype = prototypes.find((p) => p.id === id);
return prototype.price * quantity;
})
.reduce((l, r) => l + r, 0);
}, [orders, prototypes]);
const remove = useCallback(
(id) => {
setOrders((orders) => {
return orders.filter((order) => order.id !== id);
});
},
[setOrders]
);
const removeAll = useCallback(() => {
setOrders([]);
}, [setOrders]);
return (
<Orders
prototypes={prototypes}
orders={orders}
totalPrice={totalPrice}
remove={remove}
removeAll={removeAll}
/>
);
}
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import { atom } from "jotai";
const prototypesState = atom([
{
id: "pp-01",
title: "Kids-story",
artist: "Thomas Buisson",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Kids-story_1.mp4",
price: 10,
pieUrl: "https://cloud.protopie.io/p/8a6461ad85",
},
{
id: "pp-02",
title: "mockyapp",
artist: "Ahmed Amr",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/mockyapp.mp4",
price: 20,
pieUrl: "https://cloud.protopie.io/p/27631ac9d5",
},
{
id: "pp-03",
title: "macOS Folder Concept",
artist: "Dominik Kandravý",
desc: "Folder concept prototype by Dominik Kandravý.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/macOS_Folder_Concept_-_Folder_concept.mp4",
price: 30,
pieUrl: "https://cloud.protopie.io/p/acde5ccdf9",
},
{
id: "pp-04",
title: "Translator",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Translator.mp4",
price: 40,
pieUrl: "https://cloud.protopie.io/p/b91edba11d",
},
{
id: "pp-05",
title: "In-car voice control",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/In-car_voice_control.mp4",
price: 50,
pieUrl: "https://cloud.protopie.io/p/6ec7e70d1a",
},
{
id: "pp-06",
title: "The Adventures of Proto",
artist: "Richard Oldfield",
desc: `Made exclusively for Protopie Playoff 2021
Shout up if you get stuck!
For the full experience. View in the Protopie App.
#PieDay #PlayOff #ProtoPie`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/The_Adventures_of_Proto.mp4",
price: 60,
pieUrl: "https://cloud.protopie.io/p/95ee13709f",
},
{
id: "pp-07",
title: "Sunglasses shop app",
artist: "Mustafa Alabdullah",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/sunglasses_shop_app.mp4",
price: 70,
pieUrl: "https://cloud.protopie.io/p/6f336cac8c",
},
{
id: "pp-08",
title: "Alwritey—Minimalist Text Editor",
artist: "Fredo Tan",
desc: `This minimalist text editor prototype was made with ProtoPie by Fredo Tan.
---
Inspired by Writty, a simple writing app by Carlos Yllobre. Try out Writty at https://writtyapp.com.
---
ProtoPie is an interactive prototyping tool for all digital products.
---
Learn more about ProtoPie at https://protopie.io.`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/minimalist-text-editor.mp4",
price: 80,
pieUrl: "https://cloud.protopie.io/p/946f88f8d3",
},
{
id: "pp-09",
title: "Voice search for TV",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/TV.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/60ee64cda0",
},
{
id: "pp-10",
title: "Finance App Visual Interaction 2.0",
artist: "Arpit Agrawal",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Credit_Card_App.mp4",
price: 90,
pieUrl:
"https://cloud.protopie.io/p/09ce2fdf84/21?ui=true&mockup=true&touchHint=true&scaleToFit=true&cursorType=touch",
},
{
id: "pp-11",
title: "Whack-a-mole",
artist: "Changmo Kang",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Whack_a_mole.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/ab796f897e",
},
{
id: "pp-12",
title: "Voice Note",
artist: "Haerin Song",
desc: `Made by Haerin Song
(Soda Design)`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Voice_note_with_sound_wave.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/7a0d6567d2",
},
]);
export default prototypesState;
import { atom } from "jotai";
const ordersState = atom([]);
export default ordersState;
import { useCallback } from "react";
import { useAtom } from "jotai";
import prototypesState from "../jotai/atoms/prototypesState";
import ordersState from "../jotai/atoms/ordersState";
import Prototypes from "../components/Prototypes";
export default function PrototypesContainer() {
const [prototypes] = useAtom(prototypesState);
const [orders, setOrders] = useAtom(ordersState);
const addToOrder = useCallback(
(id) => {
const finded = orders.find((order) => order.id === id);
if (finded === undefined) {
setOrders([...orders, { id, quantity: 1 }]);
} else {
setOrders(
orders.map((order) => {
if (order.id === id) {
return {
id,
quantity: order.quantity + 1,
};
} else {
return order;
}
})
);
}
},
[orders, setOrders]
);
return <Prototypes prototypes={prototypes} addToOrder={addToOrder} />;
}
import { useCallback, useMemo } from "react";
import { useAtom } from "jotai";
import prototypesState from "../jotai/atoms/prototypesState";
import ordersState from "../jotai/atoms/ordersState";
import Orders from "../components/Orders";
export default function OrdersContainer() {
const [prototypes] = useAtom(prototypesState);
const [orders, setOrders] = useAtom(ordersState);
const totalPrice = useMemo(() => {
return orders
.map((order) => {
const { id, quantity } = order;
const prototype = prototypes.find((p) => p.id === id);
return prototype.price * quantity;
})
.reduce((l, r) => l + r, 0);
}, [orders, prototypes]);
const remove = useCallback(
(id) => {
setOrders((orders) => {
return orders.filter((order) => order.id !== id);
});
},
[setOrders]
);
const removeAll = useCallback(() => {
setOrders([]);
}, [setOrders]);
return (
<Orders
prototypes={prototypes}
orders={orders}
totalPrice={totalPrice}
remove={remove}
removeAll={removeAll}
/>
);
}
npx create-react-app react-swr-test
cd react-swr-test
nvm use 16
npm i swr
npm start
import useSWR from "swr";
const fetcher = (...args) => fetch(...args).then((res) => res.json());
export default function SwrTest1() {
const { data, error, isLoading } = useSWR(
"https://api.github.com/users",
fetcher
);
console.log(data, error, isLoading);
return (
<div>
<h1>SwrTest</h1>
{isLoading && <p>Loading...</p>}
{error && <p>Error!</p>}
{data && (
<div>
{data.map((user) => (
<p key={user.login}>{user.login}</p>
))}
</div>
)}
</div>
);
}
import useSWR from "swr";
const fetcher = (...args) => fetch(...args).then((res) => res.json());
function useGithubUser() {
const { data, error, isLoading } = useSWR(
"https://api.github.com/users",
fetcher
);
return { user: data, isLoading, error };
}
function UserList() {
const { user, isLoading, error } = useGithubUser();
return (
<div>
{isLoading && <p>Loading...</p>}
{error && <p>Error!</p>}
{user && <p>{user.map(({ login }) => login).join(",")}</p>}
</div>
);
}
function UserCount() {
const { user, isLoading, error } = useGithubUser();
return (
<div>
{isLoading && <p>Loading...</p>}
{error && <p>Error!</p>}
{user && <div>{user.length}</div>}
</div>
);
}
export default function SwrTest2() {
return (
<div>
<h1>SwrTest2</h1>
<UserList />
<UserCount />
</div>
);
}
nvm install 18
nvm use 18
npx create-next-app my-blog-new
클라이언트 단에서 돌아가지 않는 데이터베이스 및 API 등의 백엔드 서비스에 접근할 수 있다.
보안 키 값들이 클라이언트 단에 드러나지 않도록 지킬 수 있다.
data fetching 과 렌더링을 동일한 환경에서 수행할 수 있다.
서버에 렌더링을 캐싱할 수 있다.
번들링할 자바스크립트 양을 줄일 수 있다.
data fetching이 필요한 경우 Server
백엔드 자원에 접근해야 하는 경우 Server
클라이언트에 드러내면 안 되는 민감한 정보가 있을 때 Server
자바스크립트 코드를 줄여야 할 때 Server
click, change 리스너 등을 사용하여 대화형(상호작용) 컨텐츠를 구현하려는 경우 Client
'상태(state)'을 활용하는 경우 Client
브라우저 상에서만 지원하는 API(예: local storage와 같은 웹 스토리지를 다루는 API)를 사용하는 경우 Client
Next.js에 따르면 우선 위와 같은 기준을 두고, 가능한 경우에는 서버 컴포넌트로 만들고
따로 클라이언트 컴포넌트로 구현이 필요한 부분들만 추출하는 편이 좋다고 한다.
cd my-blog-new
npm i @sanity/client antd @ant-design/icons @sanity/block-content-to-react react-syntax-highlighter @sanity/client dayjs
npm run dev
import styles from "./page.module.css";
import SanityService from "../services/SanityService";
import Header from "./components/Header";
import Footer from "./components/Footer";
import BlogHeadline from "./components/BlogHeadline";
import BlogMainPost from "./components/BlogMainPost";
import BlogList from "./components/BlogList";
async function fetchData() {
const sanityService = new SanityService();
const home = await sanityService.getHome();
const posts = await sanityService.getPosts();
return {
home,
posts,
};
}
export default async function Home() {
const { posts, home } = await fetchData();
const mainPost = posts.find((p) => p.slug === home.mainPostUrl);
const otherPosts = posts.filter((p) => p.slug !== home.mainPostUrl);
return (
<div className={styles.container}>
<Header />
<BlogHeadline />
<BlogMainPost {...mainPost} />
<BlogList posts={otherPosts} />
<Footer />
</div>
);
}
import Header from "../../components/Header";
import SanityService from "../../../services/SanityService";
import Footer from "../../components/Footer";
import BlogPostDetail from "../../components/BlogPostDetail";
import BlogMainPost from "../../components/BlogMainPost";
import styles from "./page.module.css";
export async function generateStaticParams() {
const posts = await new SanityService().getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
async function fetchData(params) {
const posts = await new SanityService().getPosts();
const post = posts.find((p) => p.slug === params.slug);
return post;
}
export default async function Page({ params }) {
const data = await fetchData(params);
console.log(data);
return (
<div className={styles.container}>
<Header />
<BlogMainPost {...data} />
<BlogPostDetail blocks={data.content} />
<Footer />
</div>
);
}
npx serve out
nvm install 18
nvm use 18
npm create sanity@latest -- --template blog --create-project "My Blog Contents New" --dataset production
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'imageGallery',
title: 'ImageGallery',
type: 'object',
fields: [
defineField({
name: 'caption',
title: 'Caption',
type: 'string',
validation: (Rule) => Rule.required(),
}),
defineField({
name: 'images',
title: 'Images',
type: 'array',
options: {
layout: 'grid',
},
of: [
{
name: 'image',
title: 'Image',
type: 'image',
hotspot: true,
fields: [
{
name: 'alt',
title: 'alt',
type: 'string',
options: {
isHighlighted: true,
},
validation: (Rule) => Rule.required(),
},
],
validation: (Rule) => Rule.required(),
},
],
validation: (Rule) => Rule.required().max(4),
}),
],
})
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'imageGallery',
title: 'ImageGallery',
type: 'object',
fields: [
defineField({
name: 'caption',
title: 'Caption',
type: 'string',
validation: (Rule) => Rule.required(),
}),
defineField({
name: 'images',
title: 'Images',
type: 'array',
options: {
layout: 'grid',
},
of: [
{
name: 'image',
title: 'Image',
type: 'image',
hotspot: true,
fields: [
{
name: 'alt',
title: 'alt',
type: 'string',
options: {
isHighlighted: true,
},
validation: (Rule) => Rule.required(),
},
],
validation: (Rule) => Rule.required(),
},
],
validation: (Rule) => Rule.required().max(4),
}),
],
})
npm i @sanity/code-input
import {defineConfig} from 'sanity'
import {deskTool} from 'sanity/desk'
import {visionTool} from '@sanity/vision'
import {schemaTypes} from './schemas'
import {codeInput} from '@sanity/code-input'
export default defineConfig({
name: 'default',
title: 'My Blog Contents New',
projectId: 'hzh6fivm',
dataset: 'production',
plugins: [deskTool(), visionTool(), codeInput()],
schema: {
types: schemaTypes,
},
})
export default defineType({
title: 'Block Content',
name: 'blockContent',
type: 'array',
of: [
...
// You can add additional types here. Note that you can't use
// primitive types such as 'string' and 'number' in the same array
// as a block type.
defineArrayMember({
type: 'image',
options: {hotspot: true},
}),
defineArrayMember({
type: 'video',
options: {hotspot: true},
}),
defineArrayMember({
type: 'code',
options: {hotspot: true},
}),
defineArrayMember({
type: 'link',
options: {hotspot: true},
}),
defineArrayMember({
type: 'imageGallery',
options: {hotspot: true},
}),
],
})
By Woongjae Lee
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team