패캠 React 앱 배포하기, 프로젝트
https://slides.com/woongjae/react2021
https://slides.com/woongjae/fds17th-11
https://slides.com/woongjae/fds17th-12
https://slides.com/woongjae/fds17th-13
https://slides.com/woongjae/redux2021#/2
https://slides.com/woongjae/reactts2021#/2
https://slides.com/woongjae/fds17th-14
https://react.vlpt.us/
SPA 프로젝트 배포 이해하기
git clone https://github.com/xid-mark/tic-tac-toe.git
cd tic-tac-toe
nvm use
nvm install 10.16.3
npm ci
npm run build
git clone https://github.com/xid-mark/tic-tac-toe.git
cd tic-tac-toe
nvm use
nvm install 10.16.3
npm ci
npm run build
npm run build를 사용하면 production 모드로 빌드되며 'build' 폴더에 파일들이 생성된다. 파일들을 웹서버를 통해 유저들이 접근 가능
이렇게 만들어진 build 폴더를 static 서버로 띄우면 된다. 파일 하나 하나를 파일 서버처럼 사용하는 서버를 이야기 한다.
하면 위와 같이 static 폴더 안에 파일들이 존재하게 된다.
문제점은 보통 static 서버는 라우팅 되는 html 파일들이 없다. SPA방식의 경우 파일이 없으면 index.html을 내려주어야지 제대로 작동한다.
그래서 4가지 방식으로 한번 배포를 해보자
serve패키지로 배포
npm install serve -g
npx serve -s build
build 폴더를 배포해보자
-s 가 붙지 않으면 주소가 틀린경우 404가 뜨고 -s가 붙으면 index.html을 내려준다.
AWS S3
build 폴더안에 있는 내용들을 모두 삽입하자
하면 버킷안에 파일들이 존재한다.
이 버킷을 정적웹으로 바꿀 수 있다. 정적웹으로 바뀐다는 것은 주소에 파일명을 입력하면 해당파일이 브라우저에서 열린다.
속성 > 정적 웹 호스팅
public하게 바꾸어주어야 한다. 잠금장치를 풀거나 파일들을 public하게 바꾸어야 한다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::react-web-app-test-tonyhan/*"
]
}
]
}
Node.js express
npm i express
static 안에 경로를 적어주자
server.js 파일안에 다음을 적어주자
const express = require('express')
const path = require('path')
const app = express()
app.use(express.static(path.join(__dirname, 'build')))
app.listen(9000)
node server.js
서버가 열려있는 것을 확인할 수 있다. 하지만 뒤에 이상한게 붙으면 이상한 페이지가 뜬다.
이렇게하면 express의 api를 추가할 수 있다.
NginX
git clone https://github.com/xid-mark/tic-tac-toe.git
cd tic-tac-toe
ubuntu에 node 설치하기
https://github.com/nvm-sh/nvm
nano ~/.profile에 정보를 넣어주자
그다음 exit 하고 재접속하면 node 명령어가 먹힌다.
nvm use
nvm install 10.16.3
npm ci
npm run build
하면 build가 완료된다.
이젠 nginX를 설치하자
sudo apt-get update
sudo apt-get upgrade
인증키 다운 받기
wget http://nginx.org/keys/nginx_signing.key
apt-key에 추가
sudo apt-key add nginx_signing.key
key 삭제
sudo rm -rf nginx_signing.key
sources에 내용을 추가하자
sudo nano /etc/apt/sources.list
deb http://nginx.org/packages/mainline/ubuntu/ trusty nginx
deb-src http://nginx.org/packages/mainline/ubuntu/ trusty nginx
다시 업데이트/업그레이드
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install nginx
nginx -v
설치완료!
nginX가 잘 뜨는 것을 확인할 수 있다.
nginX에 build한 결과물을 올려주자
기존 파일 삭제
sudo rm -rf /etc/nginx/sites-available/default
sudo nano /etc/nginx/sites-available/default
server {
listen 80;
server_name localhost;
root /home/ubuntu/tic-tac-toe/build;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
위의 내용을 넣어주자
sudo service nginx restart
잘 실행되는 것을 확인할 수 있다.
React 로 만드는 쇼핑몰 프로젝트
npx create-react-app prototype-shop
web font를 추가해주자
public > index.html 에서 title 태그 아래에 적어주자.
<link
href="https://fonts.googleapis.com/css?family=Roboto&display=swap"
rel="stylesheet"
/>
src/index.css에 있는 내용을 아래의 것으로 교체하자
/* reset */
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* apply a natural box layout model to all elements, but allowing views to change */
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
button:focus {
outline: 0;
}
/* prototypes */
:root {
--primary: #021d49;
--primary-alpha: rgba(2, 29, 73, 0.5);
--secondary: rgb(255, 102, 97);
--secondary-alpha: rgba(0, 255, 126, 0.5);
--gray: #f6f6f6;
--gray-alpha: rgba(246, 246, 246, 0.9);
--black-color: #222223;
--red-color: #ff4d4f;
}
::selection {
background: var(--secondary);
}
::-moz-selection {
background: var(--secondary);
}
body,
button {
font-family: "Roboto", sans-serif;
font-size: 1.4rem;
background: var(--gray);
color: var(--primary);
}
.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;
}
@media (min-width: 768px) {
.container {
display: grid;
grid-template-columns: 0.7fr 0.3fr;
grid-template-rows: auto 1fr auto;
}
}
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
a:active,
a:hover {
outline-width: 0;
}
a {
color: var(--primary);
outline: none;
text-decoration: none;
}
a:focus,
a:hover,
a:active,
a.active {
color: var(--secondary);
text-decoration: underline;
}
header {
grid-column: span 2;
padding: 1vw;
text-align: center;
background-color: white;
}
header .btn__area {
margin-top: 40px;
margin-bottom: 40px;
}
header a {
display: inline-block;
}
header button {
display: inline-block;
font-weight: 700;
border-radius: 4px;
cursor: pointer;
transition: color 0.3s ease 0s, border-color 0.3s ease 0s,
background-color 0.3s ease 0s;
text-decoration: none;
border: 2px solid rgb(255, 102, 97);
background-color: rgb(255, 102, 97);
font-size: 1.2rem;
line-height: 22px;
padding: 15px 24px;
color: rgb(255, 255, 255) !important;
line-height: 22px;
}
.header__container {
margin-top: 50px;
margin-left: auto;
margin-right: auto;
padding-left: 0.4rem;
padding-right: 0.4rem;
display: flex;
flex-direction: column;
max-width: 1000px;
}
header .title {
font-size: 3rem;
line-height: 4rem;
font-weight: bold;
}
header .subtitle {
margin-top: 24px;
font-size: 22px;
line-height: 28px;
margin: 16px 0px 0px;
color: rgb(71, 71, 71);
font-weight: 500;
vertical-align: baseline;
}
.logo {
width: 25vw;
min-width: 160px;
max-width: 380px;
}
aside {
flex: 2;
padding: 0.4rem;
}
main {
padding: 0.4rem;
}
footer {
grid-column: span 2;
padding: 30px;
padding-left: 0px;
text-align: right;
font-size: 0.8em;
vertical-align: middle;
}
h1 {
margin-bottom: 1em;
font-size: 1.3em;
font-weight: bold;
}
hr {
border: 0;
height: 0;
border-top: 1px solid var(--gray);
margin-bottom: 20px;
}
.payment {
max-width: 400px;
margin: 0 auto;
}
.payment-logo {
padding-top: 6vh;
width: 100%;
}
.float--right {
float: right !important;
padding: 0.8rem;
}
.float--left {
float: left !important;
padding: 0.8rem;
}
.prototypes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(185px, 1fr));
grid-gap: 20px;
}
.prototype {
display: grid;
grid-template-columns: 100px 1fr;
grid-gap: 10px;
border: 0.01rem solid #fff;
background: #fff;
display: flex;
display: -ms-flexbox;
-ms-flex-direction: column;
flex-direction: column;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.prototype .prototype__body p {
padding: 0.8rem;
font-size: 1rem;
line-height: 1.4;
}
.prototype .prototype__footer {
padding: 0.8rem;
}
.prototype .prototype-header:last-child,
.prototype .prototype-body:last-child,
.prototype .prototype-footer:last-child {
padding-bottom: 0.8rem;
}
.prototype .prototype-image {
padding-top: 0.8rem;
}
.prototype .prototype-image:first-child {
padding-top: 0;
}
.prototype__artwork:first-child {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.prototype {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
.prototype .prototype__body .prototype__title {
padding: 0.8rem;
padding-bottom: 0;
font-size: 1rem;
line-height: 1.4;
font-size: 1.2rem;
color: var(--secondary);
}
.prototype .prototype__body .prototype__price {
padding-top: 0.1rem;
width: 100%;
color: var(--primary-alpha);
}
.prototype .prototype__body .prototype__desc {
font-size: 0.8rem;
padding-bottom: 0.8rem;
}
.prototype__artwork {
width: 100%;
}
.prototype__edit {
width: 100%;
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
.prototype__edit:hover {
cursor: pointer;
opacity: 0.4;
}
/* Button */
.btn {
-webkit-appearance: none;
-moz-appearance: none;
background: #fff;
border: 0.05rem solid var(--secondary);
border-radius: 6px;
color: var(--secondary);
cursor: pointer;
display: inline-block;
line-height: 1rem;
outline: none;
text-align: center;
text-decoration: none;
transition: all 0.2s ease;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: middle;
white-space: nowrap;
font-size: 0.9rem;
height: 2rem;
padding: 0.45rem 0.6rem;
}
.btn:focus,
.btn:hover {
background: var(--gray);
border-color: var(--primary);
text-decoration: none;
}
.btn[disabled],
.btn:disabled,
.btn.disabled {
cursor: default;
opacity: 0.5;
pointer-events: none;
}
.btn.btn--primary {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn.btn--primary:focus,
.btn.btn--primary:hover {
background: var(--secondary);
border-color: var(--secondary);
color: var(--primary);
}
.btn.btn--primary:active,
.btn.btn--primary.active {
background: var(--primary);
border-color: var(--primary);
color: var(--secondary);
}
.btn.btn--secondary {
background: var(--secondary);
border-color: var(--secondary);
color: var(--primary);
}
.btn.btn--secondary:focus,
.btn.btn--secondary:hover {
background: var(--primary);
border-color: var(--primary);
color: var(--gray);
}
.btn.btn--secondary:active,
.btn.btn--secondary.active {
background: var(--primary);
border-color: var(--primary);
color: var(--secondary);
}
.btn.btn--link {
background: transparent;
border-color: transparent;
color: var(--primary);
}
.btn.btn--link:focus,
.btn.btn--link:hover,
.btn.btn--link:active,
.btn.btn--link.active {
color: var(--secondary);
}
.btn.btn--clear {
background: transparent;
border: 0;
color: currentColor;
height: 0.8rem;
line-height: 0.8rem;
margin-left: 0.2rem;
margin-right: -2px;
opacity: 1;
padding: 0;
text-decoration: none;
width: 0.8rem;
}
.btn.btn--clear:hover {
color: #00ffb9;
opacity: 0.95;
}
.btn.btn--clear::before {
content: "\2715";
}
.btn .icon {
vertical-align: -10%;
}
/* Icon */
.icon {
box-sizing: border-box;
display: inline-block;
font-size: inherit;
font-style: normal;
height: 1em;
position: relative;
text-indent: -9999px;
vertical-align: middle;
width: 1em;
}
.icon::before,
.icon::after {
display: block;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.icon--forward::before {
border: 1px solid currentColor;
border-bottom: 0;
border-right: 0;
content: "";
height: 0.62em;
width: 0.62em;
}
.icon--forward::after {
background: currentColor;
content: "";
height: 13px;
width: 1px;
}
.icon--forward::after {
left: 46%;
}
.icon--forward::before {
transform: translate(-50%, -50%) rotate(135deg);
}
.icon--plus::before,
.icon--cross::before {
background: currentColor;
content: "";
height: 1px;
width: 100%;
}
.icon--plus::after,
.icon--cross::after {
background: currentColor;
content: "";
height: 100%;
width: 1px;
}
.icon--cross::before {
width: 100%;
}
.icon--cross::after {
height: 100%;
}
.icon--cross::before,
.icon--cross::after {
transform: translate(-50%, -50%) rotate(45deg);
}
.icon--delete::before {
border: 1px solid currentColor;
border-bottom-left-radius: 0.1rem;
border-bottom-right-radius: 0.1rem;
border-top: 0;
content: "";
height: 0.65em;
top: 60%;
width: 0.75em;
}
.icon--delete::after {
background: currentColor;
box-shadow: -0.25em 0.2em, 0.25em 0.2em;
content: "";
height: 1px;
top: 0.05rem;
width: 0.55em;
}
/* Orders */
.order {
border: 0.05rem solid var(--gray);
border-radius: 6px;
display: flex;
display: -ms-flexbox;
-ms-flex-direction: column;
flex-direction: column;
background: #fff;
}
.order .total {
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding: 0.8rem;
font-size: 1.1rem;
}
.order .order-nav {
-ms-flex: 0 0 auto;
flex: 0 0 auto;
}
.order .body {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
padding: 0.6rem 0.8rem;
padding-bottom: 0;
}
/* item */
.item {
align-content: space-between;
align-items: center;
display: flex;
display: -ms-flexbox;
-ms-flex-align: center;
-ms-flex-line-pack: justify;
}
.item .action {
-ms-flex: 0 0 auto;
flex: 0 0 auto;
}
.item .action .price {
-webkit-appearance: none;
-moz-appearance: none;
background: #fff;
display: inline-block;
line-height: 1rem;
outline: none;
text-align: center;
text-decoration: none;
transition: all 0.2s ease;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: middle;
white-space: nowrap;
font-size: 0.9rem;
height: 2rem;
padding: 0.45rem 0.6rem;
background: transparent;
border-color: transparent;
color: var(--primary);
cursor: default;
opacity: 0.5;
pointer-events: none;
}
.item video {
margin: 0.4rem 0.4rem 0.4rem 0.2rem;
border-radius: 4px;
max-width: 30px;
}
.item .content {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
}
.item .title {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-webkit-margin-before: 0px;
line-height: 1rem;
font-size: 0.9rem;
}
.item .sold-out {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-webkit-margin-before: 0px;
line-height: 1rem;
font-size: 0.9rem;
color: var(--red-color);
}
.item.item-centered .title,
.item.item-centered .item-subtitle {
margin-bottom: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* empty */
.empty {
background-color: #fff;
border: 0.05rem solid #fff;
color: var(--primary-alpha);
border-radius: 6px;
padding: 3.2rem 1.6rem;
text-align: center;
}
.empty .title,
.empty .subtitle {
margin: 0.8rem auto;
}
.empty .subtitle {
font-size: 0.9rem;
}
App.js에 위와 같이 Header 컴포넌트 아래에 우리가 작성할 것들을 모두 집어 넣어다.
필요한 컴포넌트들을 모두 만들어놓자
정상적으로 문제없이 작동하는 것을 확인할 수 있다.
// components/Header.jsx
export default function Header() {
return (
<header>
<div className="header__container">
<div className="title">Awesome Prototypes in Shop</div>
<div className="subtitle">
Check out what other designers have created using ProtoPie—download
these examples to learn exactly how they made their interactions.
</div>
<div className="btn__area">
<a href="https://www.protopie.io" target="_BLANK" rel="noreferrer">
<button>Try ProtoPie Yourself</button>
</a>
</div>
</div>
</header>
);
}
그 다음 헤더에 위의 코드를 너어주자.
// components/Footer.jsx
export default function Footer() {
return (
<footer>
<p>© 2021 Mark Lee. All rights reserved.</p>
</footer>
);
}
footer에는 위의 코드를 넣어주자.
// components/Prototypes.jsx
export default function Prototypes() {
return (
<main>
<div className="prototypes">
상품 리스트
</div>
</main>
);
}
export default function Orders() {
return (
<aside>
<div className="empty">
<div className="title">You don't have and orders</div>
<div className="subtitle">Click on a + to add aan order</div>
</div>
</aside>
)
}
구성품에 따라 다른 태그를 사용했다.
Header -> header
Orders -> aside
Prototypes -> main
Footer -> footer
상품 리스트
// components/Prototypes.jsx
const prototypes = [
{
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 function Prototypes() {
return (
<main>
<div className="prototypes">{prototypes.map(prototype => {
const {id, thumbnail, title, price, desc, pieUrl} = prototype;
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">
<i className="icon icon--plus"></i>
</div>
{title}
</div>
<p className="prototype__price">$ {price}</p>
<p className="prototype__description">{desc}</p>
</div>
</div>)
})}</div>
</main>
)
}
components/Prototypes.jsx에 위의 내용을 잠시 넣어주자.
상품 선택과 삭제를 주문에서 보여주기
파일을 만들어서 위와같이 작성해주자.
그리고 providers 폴더를 만들어서 AppStateProviders.jsx 도 만들어주자.
이건 Context를 제공해주는 Provider로 기본 props는 children이다. 그래고 AppStateContext.Provider을 이용해서 value 안에 객체 형태로 앞으로 모든 컴포넌트에 제공해줄 데이터를 넣을 것이다. 그리고 그 내용물로는 받아온 children을 객체로 반환해줄 것이다.
prototypes의 useState안에는 우리가 상수처럼 넣었던 Prototypes.jsx 파일안에 있는 데이터를 짤라서 넣어주자
그리고 AppStateProvider 파일 안에 우리가 필요할 법한 addToOrder, remove, removeAll 함수도 만들어놓자.
이렇게 만들어진 value를 전체전역으로 공유하기 위해 앱상위에 공유해주자.
이렇게하면 AppStateProvider가 모든 것을 가지고 있게 된다.
Prototypes에 useContext를 사용하면 Context 데이터를 사용할 수 있게 된다.
실재로 화면이 잘 뜨는 것을 확인할 수 있다.
이번에는 폴더안에 hooks 폴더를 만들고 usePrototypes.js 파일도 만들어주자.
그리고 이렇게 prototypes를 반환해주면 구지 useContext함수를 안쓰고도 바로 prototypes를 뽑아올 수 있다.
이런식으로 Prototypes 파일안에서 쓸 수 있다.
그리고 아래쪽에 있던 버튼을 이 함수와 연결해주자.
훅을 새로 만들어주자
해당 훅을 가져와서 사용하자.
잘 작동하는지 보기 위해 log를 찍어보자
버튼을 눌러보니 정상적으로 id가 출력된다.
새로운 훅을 만들자 orders를 받을 수 있는 훅이다.
버튼을 눌러보니 orders와 quantity가 올라가는 것을 확인할 수 있다.
이제 orders에 가진것이 없는 경우 원래 있던 것을 반환해주고 그외의 경우에는 orders에 선택된것을 반환해주자.
이제 prototype 변수에 id와 동일한 값을 받아와서 내부에서 적절하게 처리해주자.
클릭한것의 갯수와 가격이 나오는 것을 확인할 수 있다.
x 아이콘을 추가해주었다.
click 함수를 사용하기위해 useActions에서 remove를 가지고 오자.
참고로 아까 코드 짤때 body를 order 안에 넣어야 하는데 깜빡했다. 함께 넣어주자.
아래쪽에 가격을 계산한 결과인 total을 넣어주자.
totalPrice는 위와 같이 작성해주자. 하지만 그냥 위와같이 작성하면 글자가 아닌 숫자로 반환되기 때문에 reduce 함수를 사용해주고 추가적으로 orders가 변화하는 것에 따라 업데이트 되도록해주자.
전체삭제 버튼을 만들어주자.
total 아래쪽에 버튼을 만들어주자.
그리고 아직 안사용하지만 checkout 버튼도 만들어놓자.
이제 Provider에 remove와 removeAll 함수를 작성해주자.
완성했다.
Author And Source
이 문제에 관하여(패캠 React 앱 배포하기, 프로젝트), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@tonyhan18/패캠-React-프로젝트저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)