앱에서 여행 상품과 옵션의 값을 더해 총 가격이 나오는 부분이 있다. 이 부분을 구현해보자 😎
해야할 일
테스트 작성
pages/OrderPage/tests/calculate.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Type from '../Type';
test("update product's total when products change", async () => {
render(<Type orderType="products" />);
// 여행 상품 가격은 0원부터 시작한다.
const productsTotal = screen.getByText('상품 총 가격: ', { exact: false }); // 상품 총 가격: 뒤에 다른 텍스트가 있어도 값을 가져옴
expect(productsTotal).toHaveTextContent('0');
// 아메리카 여행 상품 한 개 올리기
const americaInput = await screen.findByRole('spinbutton', {
name: 'America',
});
userEvent.clear(americaInput);
userEvent.type(americaInput, '1');
expect(productsTotal).toHaveTextContent('1000');
// 영국 여행 상품 3개 더 올리기
const englandInput = await screen.findByRole('spinbutton', {
name: 'England',
});
userEvent.clear(englandInput);
userEvent.type(englandInput, '3');
expect(productsTotal).toHaveTextContent('4000');
});
userEvent.clear()
input이나 textarea에 텍스트를 선택(select) 한 후 제거(delete) 해준다.
이 부분은 없어도 테스트 결과에 영향을 미치지 않음. 하지만 만약 현재 소스 코드 보다 위에서 같은 엘리먼트를 위한 userEvent를 사용한 경우 clear 해준 뒤 userEvent.type()
을 사용하는 것이 바람직!
테스트 실행
Fail
FAIL src/2-react-shop-test/pages/OrderPage/tests/calculate.test.js
✕ update product's total when products change (52 ms)
테스트에 대응하는 실제코드 작성
context를 이용하면
일반적인 react 애플리케이션에서 데이터는 위에서 아래로, 즉 부모에서 자식에게 props를 통해 정보를 내려주는 구조를 가지지만, 애플리케이션 안의 여러 컴포넌트들에 전해줘야 하는 props의 경우 이 과정이 매우 중복적이고 번거로울 수 있다. 이때 context를 이용하며 트리 단계마다 명시적으로 props를 넘겨주지 않고 많은 컴포넌트가 값을 공유하도록 만들 수 있음
context를 사용해서 할 일
context를 사용하는 방법
context 생성
contexts/OrderContext.js
import { createContext, useMemo } from 'react';
const OrderContext = createContext();
export function OrderContextProvider(props) {
return <OrderContext.Provider value {...props} />;
}
context는 Provider 안에서 사용 가능하기 때문에 Provider 생성
App.js
// ..
import { OrderContextProvider } from './contexts/OrderContext';
function App() {
return (
<div style={{ padding: '4rem' }}>
<OrderContextProvider>
<OrderPage />
</OrderContextProvider>
</div>
);
}
value로 넣을 데이터 만들어주기(필요한 데이터와 데이터를 업데이트 해줄 함수들)
필요한 데이터 형식 만들기
contexts/OrderContext.js
// ..
export function OrderContextProvider(props) {
// Map은 간단한 키와 값을 서로 연결(매핑)시켜 저장하며
// 저장된 순서대로 각 요소들을 반복적으로 접근할 수 있도록 함
const [orderCounts, setOrderCounts] = useState({
products: new Map(),
options: new Map(),
});
// value가 바뀔 때마다 OrderContext를 사용하는 모든 컴포넌트들이 모두 리렌더링됨
// 따라서 useMemo를 사용해서 성능을 최적화해준다.
const value = useMemo(() => [{ ...orderCounts }], [orderCounts]);
return <OrderContext.Provider value={value} {...props} />;
}
데이터를 업데이트 해주는 함수 만들기
context/OrderContext.js
// ..
export function OrderContextProvider(props) {
// ..
const value = useMemo(() => {
function updateItemCount(itemName, newItemCount, orderType) {
const newOrderCounts = { ...orderCounts };
console.log('newOrderCount before: ', newOrderCounts);
const orderCountsMap = orderCounts[orderType];
orderCountsMap.set(itemName, parseInt(newItemCount));
console.log('newOrderCount after: ', newOrderCounts);
setOrderCounts(newOrderCounts);
}
return [{ ...orderCounts }, updateItemCount];
}, [orderCounts]);
return <OrderContext.Provider value={value} {...props} />;
}
상품 Count를 이용한 가격 계산
context/OrderContext.js
//..
import { useEffect } from 'react';
const pricePerItem = {
products: 1000,
options: 500,
};
const calculateSubtotal = (orderType, orderCounts) => {
let optionCount = 0;
for (const count of orderCounts[orderType].values()) {
optionCount += count;
}
return optionCount * pricePerItem[orderType];
};
export function OrderContextProvider(props) {
// 상품 count를 이용한 가격 계산
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 value = useMemo(() => {
// totals 추가
return [{ ...orderCounts, totals }, updateItemCount];
}, [orderCounts, totals]);
}
orderContext 사용하기
contexts/OrderContext.js
export const OrderContext = createContext();
Type.js
// ..
import React, { useEffect, useState, useContext } from 'react';
import { OrderContext } from '../../contexts/OrderContext';
export default function Type({ orderType }) {
// ..
const [orderDatas, updateItemCount] = useContext(OrderContext);
// ..
}
context를 사용할 준비가 끝났다면 이 context를 통해 여행 상품, 옵션을 추가함에 따라 여행상품의 총 가겨과 옵션의 총가격 그리고 여행상품과 옵션의 총가격을 더한 전체 총 가격을 나타내보자
Products, Options 두 컴포넌트에 updateItemCount를 Prop로 전달
Type.js
// ..
import React, { useEffect, useState, useContext } from 'react';
import { OrderContext } from '../../contexts/OrderContext';
export default function Type({ orderType }) {
// ..
const [orderDatas, updateItemCount] = useContext(OrderContext);
const optionItems = items.map((item) => (
<ItemComponent
key={item.name}
name={item.name}
imagePath={item.imagePath}
// updateItemCount props 추가
updateItemCount={(itemName, newItemCount) => updateItemCount(itemName, newItemCount, orderType)}
/>
));
}
여행 가격은 각 상품의 숫자를 올리거나 내릴 때(Products 컴포넌트)
Products.js
function Products({ name, imagePath, updateItemCount }) {
const handleChange = (e) => {
const currentValue = e.target.value;
updateItemCount(name, currentValue);
};
return (
<div style={{ textAlign: 'center' }}>
{/* codes.. */}
<form style={{ marginTop: '10px' }}>
{/* codes.. */}
<input
style={{ marginLeft: 7 }}
type="number"
name="quantity"
min="0"
defaultValue={0}
onChange={handleChange} // add handleChange event
/>
</form>
</div>
);
}
옵션은 각 옵션의 체크박스를 체크하거나 제거할 때(Options 컴포넌트)
Options.js
const Options = ({ name, updateItemCount }) => {
return (
<form>
<input
type="checkbox"
id={`${name} option`}
onChange={(e) => updateItemCount(name, e.target.checked ? 1 : 0)}
/>
{/* codes.. */}
</form>
);
};
현재까지 구현한 context를 테스트하면 아래와 같다.
● update product's total when products change
TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
9 | const [items, setItems] = useState([]);
10 | const [error, setError] = useState(false);
> 11 | const [orderDatas, updateItemCount] = useContext(OrderContext);
| ^
에러가 나는 이유는?