웹 풀스택

[React / ts] Book Store 만들기 - ⑩ 주문 처리(주소 검색 구현)

kevinmj12 2025. 4. 28. 23:08

 

주소를 입력할 때 자주 볼 수 있는 화면이다.

위 창에서 주소를 검색한 뒤 주소를 클릭하면 검색된 주소 정보가 입력된다.

 

https://postcode.map.daum.net/guide

 

Daum 우편번호 서비스

우편번호 검색과 도로명 주소 입력 기능을 너무 간단하게 적용할 수 있는 방법. Daum 우편번호 서비스를 이용해보세요. 어느 사이트에서나 무료로 제약없이 사용 가능하답니다.

postcode.map.daum.net

위 서비스는 다음 링크에서 보다 자세히 안내되어 있으며 무료로 제약없이 사용할 수 있다.

이를 활용하여 주문을 처리하는 페이지를 구현해보자.

 

// FindAddressButton.tsx

import Button from "../common/Button";
import { useEffect } from "react";
import { SCRIPT_URL } from "../../constants/address";

interface FindAddressButtonProps {
  onCompleted: (address: string) => void;
}

const FindAddressButton: React.FC<FindAddressButtonProps> = ({
  onCompleted,
}) => {
  const handleOpen = () => {
    new window.daum.Postcode({
      oncomplete: (data: any) => {
        onCompleted(data.address as string);
      },
    }).open();
  };

  useEffect(() => {
    const script = document.createElement("script");
    script.src = SCRIPT_URL;
    script.async = true;
    document.head.appendChild(script);

    return () => {
      document.head.removeChild(script);
    };
  }, []);

  return (
    <Button type="button" size="medium" scheme="normal" onClick={handleOpen}>
      주소 찾기
    </Button>
  );
};

export default FindAddressButton;

주소 찾기 기능을 수행하는 FindAddressButton.tsx이다.

버튼을 클릭할 시 handleOpen이 실행되고, new window.daum.Postcode()가 실행된다.

그런데 타입스크립트에서는 daum의 타입이 지정되어있지 않다는 에러가 발생할 것이다.

 

// /src/window.d.ts

interface Window {
  daum: {
    Postcode: any;
  };
}

그럴 때는 src폴더에 window.d.cs파일을 생성한 뒤 Window라는 interface를 지정해주고 daum, Postcode의 타입을 any로 지정해주면 해결할 수 있다.

 

이제 window.daum.Postcode()는 주소 검색 라이브러리를 호출하게 된다.

그리고 선택된 주소는 onCompleted()를 통해 부모에게 넘겨주게 된다.

 

useEffect(() => {
  const script = document.createElement("script");
  script.src = SCRIPT_URL;
  script.async = true;
  document.head.appendChild(script);

  return () => {
    document.head.removeChild(script);
  };
}, []);

useEffect()에서는 SCRIPT_URL(주소 검색 URL)을 <script>태그로 동적으로 추가한다.

그리고 종료될 때 스크립트를 제거하여 필요할 때만 주소 검색 스크립트를 로딩하도록 하였다.

 

// Order.tsx

import { useLocation, useNavigate } from "react-router-dom";
import Title from "../components/common/Title";
import CartSummary from "../components/cart/CartSummary";
import { CartStyle } from "./Cart";
import Button from "../components/common/Button";
import InputText from "../components/common/InputText";
import { useForm } from "react-hook-form";
import { Delivery, OrderSheet } from "../models/order.model";
import FindAddressButton from "../components/order/FindAddressButton";
import { useAlert } from "../hooks/useAlert";
import { order } from "../api/order.api";

interface DeliveryForm extends Delivery {
  addressDetail: string;
}

const Order = () => {
  const location = useLocation();
  const orderDataFromCart = location.state;
  const { totalCounts, totalPrice, firstBookTitle } = orderDataFromCart;
  const { showAlert, showConfirm } = useAlert();
  const navigate = useNavigate();

  const {
    register,
    handleSubmit,
    setValue,
    formState: { errors },
  } = useForm<DeliveryForm>();

  const handlePay = (data: DeliveryForm) => {
    const orderData: OrderSheet = {
      ...orderDataFromCart,
      delivery: {
        ...data,
        address: `${data.address} ${data.addressDetail}`,
      },
    };

    console.log(orderData);

    showConfirm("주문을 진행하시겠습니까?", () => {
      order(orderData).then(() => {
        showAlert("주문이 처리되었습니다.");
        navigate("/orderlist");
      });
    });
  };

  return (
    <>
      <Title size="large">주문서 작성</Title>

      <CartStyle>
        <div className="content">
          <div className="order-info">
            <Title size="medium" color="text">
              배송 정보
            </Title>
            <form className="delivery">
              <fieldset>
                <label>주소</label>
                <div className="input">
                  <InputText
                    inputType="text"
                    {...register("address", { required: true })}
                  />
                </div>
                <FindAddressButton
                  onCompleted={(address) => {
                    setValue("address", address);
                  }}
                />
              </fieldset>
              {errors.address && (
                <p className="error-text">주소를 입력해주세요</p>
              )}

              <fieldset>
                <label>상세 주소</label>
                <div className="input">
                  <InputText
                    inputType="text"
                    {...register("addressDetail", { required: true })}
                  />
                </div>
              </fieldset>
              {errors.address && (
                <p className="error-text">상세 주소를 입력해주세요</p>
              )}

              <fieldset>
                <label>수령인</label>
                <div className="input">
                  <InputText
                    inputType="text"
                    {...register("receiver", { required: true })}
                  />
                </div>
              </fieldset>
              {errors.address && (
                <p className="error-text">수령인을 입력해주세요</p>
              )}

              <fieldset>
                <label>전화번호</label>
                <div className="input">
                  <InputText
                    inputType="text"
                    {...register("contact", { required: true })}
                  />
                </div>
              </fieldset>

              {errors.address && (
                <p className="error-text">전화번호를 입력해주세요</p>
              )}
            </form>
          </div>
          <div className="order-info">
            <Title size="medium" color="text">
              주문 상품
            </Title>
            <strong>
              {firstBookTitle} 등 {totalCounts}권
            </strong>
          </div>
        </div>
        <div className="summary">
          <CartSummary totalCounts={totalCounts} totalPrice={totalPrice} />
          <Button
            size="large"
            scheme="primary"
            onClick={handleSubmit(handlePay)}
          >
            결제하기
          </Button>
        </div>
      </CartStyle>
    </>
  );
};

export default Order;