231218 TIL

thisisyjin·2023년 12월 18일
0

TIL 📝

목록 보기
101/113

React.js

주문 사이트 Project

  • 백엔드 서버 run 시켜주고, Type 컴포넌트를 보면 axios.get으로 백엔드 API를 호출하고 있음.
  • orderType은 products, options 중 하나 (Orderpage/index.jsx 참고)
  • 'products'이면 Products 컴포넌트를 보여주고, 'options'면 Options 컴포넌트를 보여줌.
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;
  • console.log(response.data)를 찍어보면 배열이 반환됨.
  • 이제 Options 컴포넌트를 체크박스 + 라벨(name)로 수정해주면 됨.
const Options = ({ name }) => {
  return (
    <form>
      <input type="checkbox" id={`${name} option`} />
      <label htmlFor={`${name} option`}>{name}</label>
    </form>
  );
};

export default Options;

지금까지의 결과물


Context API 사용

  • 여행 상품 수량을 올릴수록, 옵션을 추가할수록 priceState가 수정되어야 함.
  • 변경된 priceState를 전달하기 위해서는 컴포넌트 여러개를 타고 불필요한 코드 반복이 됨.
  • 이럴 때 Context API를 사용하자!
  1. createContext 를 사용하여 컨텍스트 생성

  2. 사용할 부분에 (App 컴포넌트)를 컨텍스트의 Provider로 감싸줌

  3. 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에서 아예 Provider 컴포넌트를 생성해서 임포트하기.
// 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} />;
  • index.js에서는 OrderContextProvider를 불러와서 감싸주면 됨. (children으로 App이 들어감)
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>
);

Map 객체

  • key - value 쌍으로 값을 저장.
  • Map 객체에서 업데이트를 할 때는 set 메서드를 사용함.
  • products, options에 대한 Map 객체를 생성하기.
  • context의 값(value)을 설정할 때, Products, Options에 대한 값 모두를 가지고 있어야 함.
  • 따라서 Map 객체를 이용하여 객체에 저장. (initialState)
// 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} />;
}

State 업데이트 함수 작성

  • orderCount를 업데이트하는 함수.
// 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 문
  • 반복 가능 객체에 대해 반복 (개별 속성값에 대해 실행)

Context Value 가져오기

  • 위에서 value 리턴을 배열로 하고있으므로 구조분해 할당으로 분리.

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;

총 가격(total) 보여주기

  • 여행 가격이 변하는 경우
  1. 상품의 개수가 변할 때
  2. 옵션의 체크가 변할 때
    -> OrderCounts가 변경됨
// Type.jsx
...

<ItemComponents
  key={item.name}
  name={item.name}
  imagePath={item.imagePath}
  updateItemCount={(itemName, newItemCount) => updateItemCount(itemName, newItemCount, orderType)}
  />

-> ItemComponents는 'Products' 또는 'Options' 이므로 각 컴포넌트에 props로 업데이트 함수를 넣어줌.

  1. Products 수정
// 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;
  1. Options 수정
  • 체크박스의 경우, e.target.checked가 true면 1이고, false면 0이다.
// 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>
);
  • 총 합계가 잘 변하는 것을 볼 수 있다.


step을 이용한 페이지 전환

  • Order Page = step 0
  • Summary Page = step 1
  • Complete Page = step 2
// 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를 통해 페이지 전환이 가능하다!


Summary Page 구현

  • 해당 페이지 UI 구현하기

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;
  • Map 객체를 배열로 바꿔준 것.
  • 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;

Complete Page 구현

  • 주문 완료 페이지 (완료 확인)
  • 해당 주문에 대한 POST 요청이 필요함.
  • order Number을 랜덤으로 지정 (response로 보내줌)
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;
  • response는 다음과 같다.
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인 항목만 보여주어야 함.

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글