간단한 쇼핑몰 예제

47243 단어 ReactReact18React

간단한 쇼핑몰

Clayful

npm install clayful --save

이번에는 Strapi처럼 쉽게 데이터를 관리할 수 있는 Clayful을 사용해보려고 한다. 쇼핑몰 프로젝트에서 프론트는 React를 사용하여 설계하고, 백단은 Clayful로 관리하는 것이다.


회원가입과 store를 생성해주면 개발 페이지에서 API 접근 토큰을 얻을 수 있다. 이것을 이용하여 react와 연결해주면 된다.


//index.js
import clayful from "clayful/client-js";

clayful.confog({
   client:"토큰",
});
clayful.install("request", require('clayful/plugins/request-axios')(axios))

React와 Clayful을 연동하는 부분이다.


//LandingPage.js
const LandingPage = () => {
  const Product = clayful.Product;

  const options = {
    query: {
      page: 1,
    },
  };

  Product.list(options, (err, res) => {
    if (err) {
      console.log(err);
    }

    console.log(res);
  });
  return <div>랜딩페이지</div>;
};

공식 문서에서 나와있는 대로 정상적으로 clayful이 react에 연동되었나 테스트해보았다. 아무것도 등록하지 않아서 이와 같이 나타났지만 연동은 된 것을 확인했다.


React-router-dom

//App.js
    <Routes>
      <Route path="/" element={<LandingPage />} />
      <Route path="/login" element={<LoginPage />} />
      <Route path="/register" element={<RegisterPage />} />
    </Routes>
BrowserRouterHistory API를 사용해 URL과 UI를 동기화하는 라우터
RoutesRoute에 매치되는 첫번째 요소를 렌더링
RouteComponent 속성에 설정된 URL과 현재 경로가 일치하면 해당하는 컴포넌트를 렌더링.
Linka태그와 비슷한 역할로 to속성에 설정된 링크로 이동. 기록이 history 객체에 저장

요즘 Next.js만 사용하다보니 react-router-dom이 조금 헷갈려서 정리해보았다. 이제 본격적으로 설계해보자.


React18 - ReactDOM.render warning

import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

프로젝트를 CRA로 생성할 때, 최근에 업데이트된 React18로 된 것 같다. 그리고 ReactDOM.render는 React18에서 사용하지 않는 것 같다. ReactDOM.render하던 것을 root.render로 변경해주었다.


Login & Register

Clayful 개발자 문서를 보면서 회원가입과 로그인 기능을 구현했다. 로그인 정보는 로그인 성공시 반환하는 id값과 token을 localStorage에 저장했고, contextAPI를 이용하여 로그인 유지를 해주었다.


  • 로그인 부분
//LoginPage.js
    Customer.authenticate(payload, (err, res) => {
      if (err) {
        console.error(err);
        return;
      }
      const data = res.data;
      localStorage.setItem("customerUid", data.customer);
      localStorage.setItem("accessToken", data.token);
      navigate("/");
    });

payload는 email과 password가 들어가있는 객체이다.


  • ContextAPI
//utils/AuthContext.js
export const AuthContext = createContext();

const AuthContextProvider = ({ children }) => {
  const [isAuth, setIsAuth] = useState(false);

  const isAuthenticated = () => {
    const Customer = clayful.Customer;
    const options = {
      customer: localStorage.getItem("accessToken"),
    };
    
    Customer.isAuthenticated(options, (err, res) => {
      if (err) {
        console.error(err.code);
        setIsAuth(false);
        return;
      }
      const data = res.data;

      if (data.isAuthenticated) {
        setIsAuth(true);
      } else {
        setIsAuth(false);
      }
    });
  };

  const AuthContextData = {
    isAuth,
    isAuthenticated,
  };

  return (
    <AuthContext.Provider value={AuthContextData}>
      {children}
    </AuthContext.Provider>
  );
};

ContextAPI를 사용할 부분은 utils 디렉터리에 생성해주었다. Context를 먼저 생성한다. 그리고 로그인 성공할 경우 저장해두었던 localStorage를 이용하여 토큰을 가져오고, Customer.isAuthenticated로 Clayful에 현재 해당 토큰이 로그인중인지 체크한다. 이에 맞게 정보를 저자한 후 Provider에 value로 넘겨줄 로그인 정보 isAuthisAuthenticatedAuthContextData로 넘겨주고 있다.

로그아웃의 경우도 여기에서 함수를 생성해서 Provider로 넘겨주면 될 것이다. isAuth는 false로 만들고, localStorage에서 토큰을 제거한 후에 원하는 위치로 리다이렉트 시키면 된다.


//App.js
    <AuthContextProvider>
      <Routes>
        <Route path="/" element={<LandingPage />} />
        <Route path="/login" element={<LoginPage />} />
        <Route path="/register" element={<RegisterPage />} />
      </Routes>
    </AuthContextProvider>

생성해준 Provider를 App에서 감싸주기만 하면 App과 App 하위 컴포넌트들에서는 value에 저장해준 값들을 사용할 수 있다.


//LoginPage.js
isAuthenticated();

utils의 AuthContext.js에서 생성해준 isAuthenticated함수를 로그인 성공했을 경우에 사용해주면 완성이다. (로그아웃을 누르면 signOut 함수 넣기)


Products UI

상품들 출력

상품 정보를 Clayful에 등록했다. 애플스토어 클론이 목적이였지만.. 그냥 나의 마음대로 농산물 파는 페이지로 변경했다.


//LandingPage.js
const LandingPage = () => {
  const Product = clayful.Product;
  const [items, setItems] = useState([]);

  useEffect(() => {
    const options = {
      query: {
        page: 1,
      },
    };

    Product.list(options, (err, res) => {
      if (err) {
        console.error(err);
        return;
      }
      console.log(res.data);
      setItems(res.data);
    });
  }, []);

  const renderCards = items.map((item) => {
    const { _id, thumbnail, name, price } = item;
    if (item) {
      return (
        <ItemContainer key={_id}>
          <Link to={`/product/${_id}`}>
            <ItemImage src={thumbnail.url} alt={name} />
            <p>{name}</p>
            <p>From {price.original.formatted}</p>
          </Link>
        </ItemContainer>
      );
    }
  });

  return <Conteiner>{renderCards}</Conteiner>;
};

Clayful에서 product의 정보를 가져온다. 그리고 이 정보를 이용해서 UI를 출력해주면 끝이다. 나중에 상세 페이지를 위해서 클릭할 경우 링크를 타도록 했다.

평소에는 renderCards 함수 부분. 즉, map돌리는 부분을 그냥 컴포넌트 return 부분에 사용했는데 이처럼 함수로 따로 빼는것도 좋은 방법인 것 같다.

UI출력할 때, 반환되는 데이터를 console 찍어보고 위의 정보를 참고하여 코딩했다.


동적 라우팅 설정

//App.js
<Route path="product/:productid" element={<DetailProductPage />}/>>

이렇게 Route를 설정해주면 동적 라우팅 되어 해당 _id값에 맞는 페이지를 보여줄 수 있다.


css를 너무 대충 넣어서 이쁘진 않지만 데이터를 정상적으로 받아와 UI에 출력해주고 있는 것을 볼 수 있다.


dangerouslySetInnerHTML

description 부분은 나무위키 글을 긁어왔는데, 이와 같이 출력되었다. 브라우저 DOM에서 사이트 스크립팅 공격(XSS)을 방지하기 위해 고의적으로 이렇게 하는 것이다. 하지만 지금 우리에게 필요한 부분이 아니다.

    <Container>
      <div dangerouslySetInnerHTML={{ __html: description }} />
    </Container>

dangerouslySetInnerHTML 속성을 사용하면 쉽게 해결할 수 있다.


데이터에 맞게 state도 관리

  const deleteItemHandler = (itemId, price) => {
    const Cart = clayful.Cart;
    const options = {
      customer: localStorage.getItem("accessToken"),
    };
    Cart.deleteItemForMe(itemId, options, (err, res) => {
      if (err) {
        console.log(err);
        return;
      }
      removeItemFromState(itemId, price);
    });
  };

장바구니에서 X 버튼 클릭시 물품이 사라지는 기능을 구현하고 있었다. X를 누를경우 DB에서 데이터가 사라지도록 구현했다.

코드를 보면 삭제 성공시 removeItemFromState 함수를 실행하는것이 보인다. 이렇게 해주는 이유는 데이터에서 삭제가 되어도 현재 state에서는 그대로기 때문에 새로고침을 해야 반영이 된다. 새로고침을 하지 않고도 X 버튼을 클릭했을때, 유저가 바로 물품이 사라지는것을 보아야 하기 때문에 이 함수를 추가한 것이다.

여기서 뿐만 아니라 여러 군데에서 사용할 수 있다. 실시간으로 데이터가 변경되고 UI도 변경되어야 하는 상황에서 말이다. 또 하나의 예를 들어보자. 어느 게시글에 달린 댓글의 정보를 받아오는데, 그 댓글이 사라지면 DB에서도 제거되고 유저도 댓글이 삭제되는것을 실시간으로 보아야하는 경우에도 이와 비슷하다.


결제 요청

결제는 먼저 calyful의 연동클라이언트 -> 스토어 프론트 클라이언트에서 결제 방식 CRUD 하는것을 허용해주어야 한다.


장바구니에 담은 물품의 총 가격 + 배송비가격을 주문 총 가격으로 정했다. 그리고 배송하기 위한 정보 입력란 UI를 설계하고, state로 관리한다. 수취자 정보도 동일하다는 checkbox를 클릭하면 수취자 정보에 주문자의 정보가 자동으로 입력된다.


 const handlePayment = () => {
    const Cart = clayful.Cart;
    const Customer = clayful.Customer;
    const body = {
      name: { full: sendUserInfo.full },
      mobile: sendUserInfo.mobile,
    };
    const options = {
      customer: localStorage.getItem("accessToken"),
    };
    Customer.updateMe(body, options, (err, res) => {
      //에러처리

      let items = [];
      cartItem.item.map((item) => {
        let itemVariable = {};
        itemVariable.bundleItems = item.bundleItems;
        itemVariable.product = item.product._id;
        itemVariable.quantity = item.quantity.raw;
        itemVariable.shippingMethod = item.shippingMethod._id;
        itemVariable.variant = item.variant._id;
        itemVariable._id = item._id;
        return items.push(itemVariable);
      });

      const payload = {
        //주문자 정보, 수취자 정보
      };
      Cart.checkoutForMe("order", payload, options, (err, res) => {
        //에러처리
        Cart.emptyForMe(options, (err, res) => {
          //에러처리
          navigate("/history");
        });
      });
    });
  };

이제 PaymentPage에서 주문을 요청하자. 주문 버튼에 onClick 이벤트로 handlePayment 함수를 넣어주었다.

회원 정보를 PaymentPage에서 입력한 내용 토대로 업데이트하고, checkoutForMe까지 하면 emptyForMe로 장바구니를 비운 후 history 페이지(결제 내역 페이지)로 리다이렉트한다. payload 부분이 궁금하다면 clayful 문서에서 확인하자.

주소 찾는것은 react-daum-postcode 라이브러리를 이용했다.

주문을 클릭하면 이와 같이 history 페이지로 이동하고, 고구마랑 감자의 결제 내역이 나타난다.


마무리

clayful을 사용하면 쇼핑몰과 같은 프로젝트들을 간단하게 구현할 수 있다. 토이 프로젝트 정도로 좋은 것 같다.

이 간단한 프로젝트에서는 무통장입금을 했을 경우 결제 요청을 보내는 기능을 구현했다. 아임 포트를 연동한 결제 서비스도 구현해봐야겠다.

좋은 웹페이지 즐겨찾기