import { useEffect, useState } from 'react';
import axios from 'axios';
import Products from './Products';
import Options from './Options';
import ErrorBanner from './ErrorBanner';
const Type = ({ orderType }) => {
const [items, setItems] = useState([]);
const [error, setError] = useState(false);
useEffect(() => {
loadItems(orderType);
}, [orderType]);
const loadItems = async (orderType) => {
try {
const response = await axios.get(`http://localhost:4000/${orderType}`);
setItems(response.data);
console.log(response.data);
} catch (error) {
console.error(error);
}
};
const ItemComponents = orderType === 'products' ? Products : Options;
const optionItems = items.map((item) => (
<ItemComponents
key={item.name}
name={item.name}
imagePath={item.imagePath}
/>
));
return (
<>
{optionItems}
{error && <ErrorBanner message="에러가 발생하였습니다." />}
</>
);
};
export default Type;
const Options = ({ name }) => {
return (
<form>
<input type="checkbox" id={`${name} option`} />
<label htmlFor={`${name} option`}>{name}</label>
</form>
);
};
export default Options;
지금까지의 결과물
createContext 를 사용하여 컨텍스트 생성
사용할 부분에 (App 컴포넌트)를 컨텍스트의 Provider로 감싸줌
useContext로 value 가져와서 사용
/src/context 폴더 생성 후, OrderContext.js 생성
(createContext)
import { createContext } from 'react';
const OrderContext = createContext();
index.js 수정 (Provider로 감싸줌)
``` js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import Ordercontext from './context/OrderContext';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<OrderContext.Provider>
<App />
</OrderContext.Provider>
</React.StrictMode>
);
// OrderContext.js
import { createContext } from 'react';
const OrderContext = createContext();
export function OrderContextProvider(props) {
return <OrderContext.Provider value>{props.children}</OrderContext.Provider>;
}
-> props를 spread 연산자로 간단하게 표현 가능
return <OrderContext.Provider value>{props.children}</OrderContext.Provider>;
// 같은 컴포넌트이다.
return <OrderContext.Provider value {...props} />;
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { OrderContextProvider } from './context/OrderContext';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<OrderContextProvider>
<App />
</OrderContextProvider>
</React.StrictMode>
);
set
메서드를 사용함.// OrderContext.js
import { createContext, useMemo, useState } from 'react';
const OrderContext = createContext();
export function OrderContextProvider(props) {
const [orderCounts, setOrderCounts] = useState({
products: new Map(),
options: new Map(),
});
const value = useMemo(() => {
return [{ ...orderCounts }];
}, [orderCounts]); // 리렌더링 최적화를 위해
return <OrderContext.Provider value={value} {...props} />;
}
// OrderContext.js
import { createContext, useMemo, useState } from 'react';
const OrderContext = createContext();
export function OrderContextProvider(props) {
const [orderCounts, setOrderCounts] = useState({
products: new Map(),
options: new Map(),
});
const value = useMemo(() => {
function updateItemCount(itemName, newItemCount, orderType) {
const newOrderCounts = { ...orderCounts }; // 불변성을 위해 복사해둠
const orderCountsMap = orderCounts[orderType]; // products, options
orderCountsMap.set(itemName, parseInt(newItemCount)); // Map set
setOrderCounts(newOrderCounts); //
}
return [{ ...orderCounts }, updateItemCount];
}, [orderCounts]);
return <OrderContext.Provider value={value} {...props} />;
}
// OrderContext.js
const [totals, setTotals] = useState({
products: 0,
options: 0,
total: 0
})
useEffect(() => {
const productsTotal = calculateSubtotal("products", orderCounts);
const optionsTotal = calculateSubtotal("options", orderCounts);
const total = productsTotal + optionsTotal;
setTotals({
products: productsTotal,
options: optionsTotal,
total: total,
});
}, [orderCounts]);
const pricePerItem = {
products: 1000,
options: 500,
};
function calculateSubtotal(orderType, orderCounts) {
let optionCount = 0;
for (const count of orderCounts[orderType].values()) {
// Object.prototype.values()는 객체를 반환한다.
optionCount += count;
}
return optionCount * pricePerItem[orderType];
}
[참고]
- for ... of 문
- 반복 가능 객체에 대해 반복 (개별 속성값에 대해 실행)
type.jsx
import { useEffect, useState } from 'react';
import axios from 'axios';
import Products from './Products';
import Options from './Options';
import ErrorBanner from './ErrorBanner';
import { useContext } from 'react';
import { OrderContext } from '../context/OrderContext';
const Type = ({ orderType }) => {
const [items, setItems] = useState([]);
const [error, setError] = useState(false);
// useContext로 가져옴
const [orderData, updateItemCount] = useContext(OrderContext);
console.log(orderData, updateItemCount);
useEffect(() => {
loadItems(orderType);
}, [orderType]);
const loadItems = async (orderType) => {
try {
const response = await axios.get(`http://localhost:4000/${orderType}`);
setItems(response.data);
console.log(response.data);
} catch (error) {
console.error(error);
setError(true);
}
};
const ItemComponents = orderType === 'products' ? Products : Options;
const optionItems = items.map((item) => (
<ItemComponents
key={item.name}
name={item.name}
imagePath={item.imagePath}
/>
));
return (
<>
{optionItems}
{error && <ErrorBanner message="에러가 발생하였습니다." />}
</>
);
};
export default Type;
// Type.jsx
...
<ItemComponents
key={item.name}
name={item.name}
imagePath={item.imagePath}
updateItemCount={(itemName, newItemCount) => updateItemCount(itemName, newItemCount, orderType)}
/>
-> ItemComponents는 'Products' 또는 'Options' 이므로 각 컴포넌트에 props로 업데이트 함수를 넣어줌.
// Products.jsx
const Products = ({ name, imagePath, updateItemCount }) => {
console.log('products : ', name, imagePath);
// itemName은 name, newItemCount는 e.target.value
const handleChange = (e) => {
const currentValue = e.target.value;
updateItemCount(name, currentValue);
}
return (
<div style={{ textAlign: 'center' }}>
<img
src={`http://localhost:4000/${imagePath}`}
alt={`${name} product`}
style={{ width: '75%' }}
/>
<form style={{ marginTop: '10px' }}>
<label htmlFor="" style={{ textAlign: 'right' }}>
{name}
</label>
<input
type="number"
name="quantity"
min="0"
defaultValue={0}
style={{ marginLeft: '70x' }}
onChange={handleChange}
/>
</form>
</div>
);
};
export default Products;
// Options.jsx
const Options = ({ name, updateItemCount }) => {
return (
<form>
<input
type="checkbox"
id={`${name} option`}
onChange={(e) => {
updateItemCount(name, e.target.checked ? 1 : 0);
}}
/>
<label htmlFor={`${name} option`}>{name}</label>
</form>
);
};
export default Options;
// Type.jsx
return (
<div>
<h2>주문 종류</h2>
<p>하나의 가격</p>
<p>총 가격: {orderData.totals[orderType]}</p>
{optionItems}
{error && <ErrorBanner message="에러가 발생하였습니다." />}
</div>
);
// App.js
import OrderPage from './pages/OrderPage';
import SummaryPage from './pages/SummaryPage';
import CompletePage from './pages/CompletePage';
import { useState } from 'react';
function App() {
const [step, setStep] = useState(0);
return (
<div style={{ padding: '4rem' }}>
{step === 0 && <OrderPage setStep={setStep} />}
{step === 1 && <SummaryPage setStep={setStep} />}
{step === 2 && <CompletePage setStep={setStep} />}
</div>
);
}
export default App;
// OrderPage/index.js
import { useContext } from 'react';
import Type from '../../components/Type';
import { OrderContext } from '../../context/OrderContext';
const OrderPage = ({ setStep }) => {
const [orderData] = useContext(OrderContext);
return (
<div>
<h1>Travel Products</h1>
<div style={{ display: 'flex' }}>
<Type orderType="products" />
</div>
<div style={{ display: 'flex', marginTop: 20 }}>
<div style={{ width: '50%' }}>
<Type orderType="options" />
</div>
<div style={{ width: '50%' }}>
<h2>Total Price: {orderData.totals.total}</h2> <br />
<button onClick={() => setStep(1)}>주문</button>
</div>
</div>
</div>
);
};
export default OrderPage;
// SummaryPage/index.js
import { useState } from 'react';
const SummaryPage = ({ setStep }) => {
const [checked, setChecked] = useState(false);
return (
<div>
<form>
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
id="confirm-checkbox"
/>
<label htmlFor="confirm-checkbox">주문하려는 것을 확인하셨나요?</label>
<br />
<button disabled={!checked} type="submit" onClick={() => setStep(2)}>
주문 확인
</button>
</form>
</div>
);
};
export default SummaryPage;
step이라는 state를 통해 페이지 전환이 가능하다!
Array.form
- Map 객체를 배열로 만들어주기
- 배열로 만들어준 후, Array.prototype.map 매서드로 보여줌
⚠️ 참고 - 배열 구조분해 할당 시
- 만약 첫 번째 인자만 가져오고 싶다면?
const [first] = someArray; // 이렇게 하면 됨. const [first, _] = someArray; // 이런식으로 안해도 됨.
// SummaryPage/index.js
import { useContext } from 'react';
import { useState } from 'react';
import { OrderContext } from '../../context/OrderContext';
const SummaryPage = ({ setStep }) => {
const [checked, setChecked] = useState(false);
const [orderDetails] = useContext(OrderContext);
const productArray = Array.from(orderDetails.products);
const productList = productArray.map(([key, value]) => (
<li key={key}>
{value} {key}
</li>
));
return (
<div>
<h1>주문 확인</h1>
<h2>여행 상품: {orderDetails.totals.products}</h2>
<ul>{productList}</ul>
<form>
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
id="confirm-checkbox"
/>
<label htmlFor="confirm-checkbox">주문하려는 것을 확인하셨나요?</label>
<br />
<button disabled={!checked} type="submit" onClick={() => setStep(2)}>
주문 확인
</button>
</form>
</div>
);
};
export default SummaryPage;
Array.from(mapObject)
-> [[key, value], [key, value], [key, value]..]
로 변환// Map
new Map([
[
"America",
2
],
[
"England",
1
],
[
"Germany",
2
],
[
"Portland",
1
]
])
// Array
[
[
"America",
2
],
[
"England",
1
],
[
"Germany",
2
],
[
"Portland",
1
]
]
// SummatyPage/index.js
import { useContext } from 'react';
import { useState } from 'react';
import { OrderContext } from '../../context/OrderContext';
const SummaryPage = ({ setStep }) => {
const [checked, setChecked] = useState(false);
const [orderDetails] = useContext(OrderContext);
// Map 객체는 length가 아닌 size
const hasOptions = orderDetails.options.size > 0;
console.log(hasOptions);
let optionsDisplay = null;
if (hasOptions) {
const optionsArray = Array.from(orderDetails.options.keys());
const optionList = optionsArray.map((key) => <li key={key}>{key}</li>);
optionsDisplay = (
<>
<h2>옵션: {orderDetails.totals.options}</h2>
<ul>{optionList}</ul>
</>
);
}
const productArray = Array.from(orderDetails.products);
const productList = productArray.map(([key, value]) => (
<li key={key}>
{value} {key}
</li>
));
return (
<div>
<h1>주문 확인</h1>
<h2>여행 상품: {orderDetails.totals.products}</h2>
<ul>{productList}</ul>
{optionsDisplay}
<form>
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
id="confirm-checkbox"
/>
<label htmlFor="confirm-checkbox">주문하려는 것을 확인하셨나요?</label>
<br />
<button disabled={!checked} type="submit" onClick={() => setStep(2)}>
주문 확인
</button>
</form>
</div>
);
};
export default SummaryPage;
import axios from 'axios';
import { useContext } from 'react';
import { useEffect } from 'react';
import { OrderContext } from '../../context/OrderContext';
const CompletePage = () => {
const [orderData] = useContext(OrderContext);
useEffect(() => {
orderCompleted(orderData);
}, [orderData]);
const orderCompleted = async (orderData) => {
try {
const response = await axios.post(
'http://localhost:4000/order',
orderData
);
console.log(response);
} catch (error) {
console.error(error);
}
};
return <div>CompletePage</div>;
};
export default CompletePage;
import axios from 'axios';
import { useContext, useState } from 'react';
import { useEffect } from 'react';
import { OrderContext } from '../../context/OrderContext';
const CompletePage = ({ setStep }) => {
const [orderHistory, setOrderHistory] = useState([]);
const [loading, setLoading] = useState(true);
const [orderData] = useContext(OrderContext);
useEffect(() => {
orderCompleted(orderData);
}, [orderData]);
const orderCompleted = async (orderData) => {
try {
const response = await axios.post(
'http://localhost:4000/order',
orderData
);
setOrderHistory(response.data);
setLoading(false);
} catch (error) {
console.error(error);
}
};
const orderTable = orderHistory.map((item, key) => (
<tr key={item.orderNumber}>
<td>{item.orderNumber}</td>
<td>{item.price}</td>
</tr>
));
if (loading) {
return <div>LOADING...</div>;
} else
return (
<div style={{ textAlign: 'center' }}>
<h2>주문이 성공했습니다.</h2>
<h3>지금까지 모든 주문</h3>
<table>
<tbody>
<tr>
<th>number</th>
<th>price</th>
</tr>
{orderTable}
</tbody>
</table>
<br />
<button onClick={() => setStep(0)}>첫 페이지로</button>
</div>
);
};
export default CompletePage;
발견 에러
옵션에서 체크 후 해제 시, 다음 Summary 페이지에 나타나는 에러가 발견됨.
원인
화면단에 보여줄 때, options.keys로 다 보여줬는데,
Map 객체에서 한 번 체크한 이상 무조건 key-value가 남기 때문에
값이 1인 항목만 보여주어야 함.