[Week13] React(TypeScript) 기반의 동적 UI 개발 (7) - 04/06

Kyulee·2026년 4월 7일

TIL 

목록 보기
63/90
post-thumbnail

useMemo, 화면 이동 시 데이터 전달, 우편번호 서비스

지난 시간에 이어 북스토어 프로젝트의 도서 주문과 주문 목록 화면을 구현하는 과정을 정리했습니다.


1. 성능 최적화를 위한 useMemo

useMemo 훅은 성능 최적화를 위해 연산된 값을 재사용할 수 있게 도와줍니다. 연산된 값을 메모리에 저장해두고, 의존성 배열에 있는 값이 바뀌지 않았다면 이전에 연산한 값을 그대로 재사용합니다. 값이 변경되었을 때만 다시 연산을 수행하기 때문에 불필요한 렌더링과 연산 부하를 줄일 수 있습니다.

import { useMemo } from 'react';

function OrderSummary({ cartItems }: Props) {
  // cartItems가 바뀔 때만 총액을 다시 계산합니다
  const totalPrice = useMemo(() => {
    return cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }, [cartItems]);

  return <div>총 금액: {totalPrice.toLocaleString()}</div>;
}

useMemo 를 사용할 때는 모든 곳에 남발하지 않는 것이 중요합니다. 단순한 연산에는 오히려 메모이제이션 비용이 더 클 수 있기 때문에, 연산이 복잡하거나 렌더링이 잦은 곳에 선택적으로 적용하는 것이 좋습니다.


2. 화면 이동 시 데이터 전달하기

화면을 이동하면서 특정 데이터를 함께 넘겨주어야 하는 상황이 생길 수 있습니다. 이때 react-router-dom 에서 제공하는 useNavigateuseLocation 훅을 활용할 수 있습니다.

데이터 보내기 — useNavigate

// Basket.tsx (데이터를 보내는 쪽)
import { useNavigate } from 'react-router-dom';

function Basket() {
  const navigate = useNavigate();

  const handleOrder = () => {
    // state 속성을 통해 이동할 경로로 데이터를 함께 넘겨줍니다
    navigate('/order', { state: orderData });
  };

  return <button onClick={handleOrder}>주문하기</button>;
}

데이터 받기 — useLocation

// Order.tsx (데이터를 받는 쪽)
import { useLocation } from 'react-router-dom';

function Order() {
  const location = useLocation();
  // location.state를 통해 전달받은 데이터를 꺼내서 사용합니다
  const orderDataFromBasket = location.state;

  return <div>{/* 주문 정보 렌더링 */}</div>;
}

form 안에서 button 사용 시 주의사항

form 태그 내부에서 button 태그를 사용할 경우, 기본 타입이 submit 으로 자동 지정됩니다. 폼 제출 목적이 아닌 단순 클릭 버튼으로만 사용하고 싶다면 반드시 type="button" 을 명시해야 합니다.

// ❌ type을 명시하지 않으면 form이 제출됩니다
<button onClick={handleAddress}>주소 검색</button>

// ✅ type="button"을 명시해야 합니다
<button type="button" onClick={handleAddress}>주소 검색</button>

3. 우편번호 서비스와 동적 스크립트 로딩

주소 검색 기능을 구현하기 위해 별도의 키 발급이나 사용량 제한이 없는 Daum 우편번호 서비스를 활용했습니다.

동적 스크립트 로딩이 필요한 이유

해당 서비스를 이용하려면 외부 스크립트를 불러와야 합니다. 하지만 이 스크립트는 프로젝트 전체가 아닌 주문 페이지에서만 필요하므로, index.html 에 전역으로 추가하는 대신 컴포넌트가 마운트될 때만 동적으로 로드하도록 구현했습니다. 불필요한 리소스를 줄여 초기 로딩 성능을 개선할 수 있습니다.

const SCRIPT_URL = '//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js';

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

function AddressSearch({ onCompleted }: Props) {
  useEffect(() => {
    // 컴포넌트가 마운트될 때 스크립트를 동적으로 추가합니다
    const script = document.createElement('script');
    script.src = SCRIPT_URL;
    script.async = true;
    document.head.appendChild(script);

    // 컴포넌트가 언마운트될 때 스크립트를 제거합니다
    return () => {
      document.head.removeChild(script);
    };
  }, []);

  const handleOpen = () => {
    new window.daum.Postcode({
      onComplete: (data: { address: string }) => {
        onCompleted(data.address);
      },
    }).open();
  };

  return (
    <button type="button" onClick={handleOpen}>
      주소 검색
    </button>
  );
}

useEffect 의 클린업 함수에서 스크립트를 제거하는 부분이 핵심입니다. 컴포넌트가 언마운트될 때 불필요하게 남아있는 스크립트 태그를 정리해 메모리 누수를 방지합니다.

profile
안녕하세요 매일의 배움을 기록으로 자산화하는 개발자 이규현입니다 😊

0개의 댓글