React 개발을 하다 보면 useCallback이라는 훅을 심심치 않게 만나게 됩니다. "함수를 메모이제이션한다"는데, 이걸 왜 써야 하고 언제 써야 효과적일까요? 오늘은 useCallback의 필요성과 사용법을 쉽고 명확하게 알아보겠습니다.

 

함수가 새로 태어난다고? React 렌더링의 비밀

 

먼저 React 컴포넌트가 어떻게 동작하는지 떠올려 봅시다. 컴포넌트는 자신의 상태(state)나 부모로부터 받은 속성(props)이 변경되면 다시 렌더링됩니다. 이때 중요한 점은, 컴포넌트 함수 본문 전체가 다시 실행된다는 것입니다. 즉, 컴포넌트 내부에 정의된 함수들은 기본적으로 렌더링될 때마다 새로 만들어집니다.

function MyComponent() {
  const [count, setCount] = useState(0);

  // MyComponent가 리렌더링될 때마다 이 함수는 '새로운' 함수가 됩니다.
  const handleClick = () => {
    console.log("클릭!");
  };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>카운트 증가</button>
      {/* 자식 컴포넌트에 함수를 props로 전달 */}
      <MyButton onClick={handleClick} />
    </div>
  );
}

// React.memo로 최적화된 자식 컴포넌트
const MyButton = React.memo(({ onClick }) => {
  console.log("자식 버튼 리렌더링!");
  return <button onClick={onClick}>자식 버튼</button>;
});

 

위 코드에서 "카운트 증가" 버튼을 누르면 MyComponent가 리렌더링되고, handleClick 함수도 새로 만들어집니다.

 

 

그래서 뭐가 문제인데? 불필요한 리렌더링!

 

MyButton 컴포넌트는 React.memo로 감싸져 있습니다. React.memo는 props가 이전 렌더링과 정확히 같다면(참조 동일성) 리렌더링을 건너뛰는 똑똑한 기능이죠.

하지만 handleClick 함수는 렌더링마다 새로 만들어지기 때문에, MyButton 입장에서는 onClick prop이 계속 **다른 값(다른 함수의 참조)**으로 전달되는 셈입니다. JavaScript에서 함수는 객체이고, 새로 만들어진 함수는 이전 함수와 내용이 똑같아도 다른 객체니까요.

결국, MyButton은 실제로는 아무것도 변하지 않았는데도 부모(MyComponent)가 리렌더링될 때마다 불필요하게 같이 리렌더링됩니다. (콘솔에 "자식 버튼 리렌더링!"이 계속 찍히는 것을 확인해보세요.)

 

구세주 등장! useCallback

 

useCallback은 바로 이 문제를 해결하기 위해 탄생했습니다.

  • useCallback(fn, deps)은 함수 fn 자체를 메모이제이션합니다.
  • 의존성 배열 deps 안의 값이 변경되지 않는 한, 함수를 새로 만들지 않고 이전에 만들었던 함수의 참조를 그대로 반환합니다.
function MyComponent() {
  const [count, setCount] = useState(0);

  // useCallback으로 handleClick 함수를 메모이제이션
  // 의존성 배열이 비어있으므로([]) 이 함수는 처음 마운트될 때만 생성되고,
  // 이후에는 계속 같은 참조를 유지합니다.
  const handleClick = useCallback(() => {
    console.log("클릭!");
    // 만약 함수 안에서 count 같은 상태를 사용한다면 deps에 넣어줘야 합니다!
    // 예: console.log(count); -> useCallback(..., [count]);
  }, []); // 의존성 배열

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>카운트 증가</button>
      <MyButton onClick={handleClick} /> {/* 이제 동일한 참조가 전달됨 */}
    </div>
  );
}

// 자식 컴포넌트 (React.memo 사용)
const MyButton = React.memo(({ onClick }) => {
  console.log("자식 버튼 리렌더링!");
  return <button onClick={onClick}>자식 버튼</button>;
});

 

이제 "카운트 증가" 버튼을 눌러도 handleClick 함수의 참조는 동일하게 유지됩니다. React.memo는 onClick prop이 변경되지 않았다고 판단하고, MyButton은 더 이상 불필요하게 리렌더링되지 않습니다! (콘솔에 "자식 버튼 리렌더링!"이 찍히지 않아요!)

 

언제 useCallback을 써야 할까?

 

  1. React.memo로 최적화된 자식 컴포넌트에 함수를 props로 전달할 때: 자식 컴포넌트의 불필요한 리렌더링을 막아 성능을 개선할 수 있습니다. (가장 흔한 사용 사례!)
  2. 함수를 다른 Hook(useEffect, useMemo)의 의존성 배열에 포함시킬 때: 함수의 참조가 안정적이지 않으면 해당 Hook이 불필요하게 계속 실행될 수 있습니다. useCallback으로 함수 참조를 안정화시키면 이를 방지할 수 있습니다. (위의 시간 버그 해결 사례에서도 사용되었죠!)

 

주의! 남용은 금물

useCallback 자체도 함수를 비교하고 메모리에 저장하는 약간의 비용이 듭니다. 따라서 모든 함수를 useCallback으로 감쌀 필요는 없습니다. 위에서 설명한 것처럼 **참조 동일성이 꼭 필요한 경우(성능 최적화가 필요한 경우)**에 사용하는 것이 좋습니다.

 

마무리

useCallback은 React 애플리케이션의 성능을 최적화하는 데 유용한 도구입니다. 함수의 참조 동일성을 유지해야 하는 상황을 잘 파악하고 적재적소에 활용한다면, 더 부드럽고 효율적인 사용자 경험을 제공할 수 있을 것입니다.

안녕하세요! 개발하다 보면 예상치 못한 버그와 마주치곤 하죠. 특히 '시간'과 관련된 로직은 자칫 잘못 다루면 까다로운 문제를 일으키기도 합니다. 오늘은 React 애플리케이션에서 실시간으로 변하는 정보를 처리하다가 발생했던 '무한 루프' 버그와 그 해결 과정을 공유해 보려고 합니다. (특정 서비스 코드는 제외하고 개념 중심으로 설명할게요!)

어떤 상황이었을까요?

사용자에게 현재 상점의 운영 상태("영업 중", "준비 중", "휴무일")를 실시간으로 보여줘야 하는 기능이 있었습니다. 이를 위해 다음과 같은 로직이 필요했죠.

 

사용자에게 현재 상점의 운영 상태("영업 중", "준비 중", "휴무일")를 실시간으로 보여줘야 하는 기능이 있었습니다. 이를 위해 다음과 같은 로직이 필요했죠.

  1. 서버 또는 상태 관리 라이브러리(Redux, Zustand 등)에서 상점의 영업 시작 시간, 종료 시간, 휴무일 정보를 가져옵니다.
  2. 현재 시간과 비교하여 상태를 결정합니다.
  3. 일정 간격(예: 1분마다)으로 현재 상태를 다시 확인하여 업데이트합니다. (영업 시작/종료 시간이 되면 자동으로 상태가 바뀌어야 하니까요!)

 

 

겉보기엔 간단해 보였지만, 이 로직은 "Maximum update depth exceeded"라는 무시무시한 에러를 뿜어내며 앱을 멈추게 만들었습니다. 😱

 

 

무엇이 문제였을까요?

바로 React의 렌더링 방식과 '시간' 객체의 특성을 제대로 이해하지 못한 데 있었습니다.

 

 

  1. 매번 새로 태어나는 '현재 시간': 컴포넌트가 렌더링될 때마다 moment()나 new Date() 같은 코드로 현재 시간을 가져오면, 매번 새로운 시간 객체가 만들어집니다. 내용상 같은 시간이라도 메모리 주소는 다른, 별개의 객체인 거죠.
  2. 불안정한 의존성: 이 '새로운' 시간 객체나, 이 객체를 기반으로 계산된 값(예: 영업 시작/종료 시간 moment 객체)을 useEffect나 useCallback의 의존성 배열에 넣으면 어떻게 될까요? React는 "어? 의존성이 바뀌었네!"라고 판단하고 해당 Hook을 매번 다시 실행합니다.

  3. 멈추지 않는 업데이트: 만약 useEffect 안에서 상태(state)를 변경하는 로직이 있다면? 상태 변경 -> 리렌더링 -> 새로운 시간 객체 생성 -> 의존성 변경 감지 -> useEffect 재실행 -> 상태 변경 -> ... 이렇게 무한 루프에 빠지게 되는 것입니다. 특히 주기적으로 상태를 체크하는 setInterval이나 커스텀 훅(useInterval)과 결합되면 문제는 더 심각해질 수 있습니다.

 

어떻게 해결했을까요?

핵심은 불필요한 재생성을 막고, 참조 동일성을 유지하는 것이었습니다.

 

  1. 계산된 값 안정화 (useMemo): 영업 시작/종료 시간처럼, 특정 입력값(스토어에서 가져온 시간 문자열)이 바뀌지 않는 한 변하지 않아야 하는 값은 useMemo를 사용해 감싸주었습니다. 이렇게 하면 입력값이 그대로일 때는 이전에 계산했던 moment 객체의 참조를 그대로 반환해줘서, 의존성 배열에서 안정적인 값으로 사용할 수 있습니다. (아래 예시)

  2. '현재 시간'은 필요할 때만: 컴포넌트 최상단에서 const now = moment();를 선언하는 대신, 실제로 현재 시간이 필요한 함수 내부 (예: 상태 체크 함수)에서 호출하도록 변경했습니다. 이렇게 하면 렌더링마다 새로운 now 객체가 생성되어 의존성을 불안정하게 만드는 문제를 피할 수 있습니다.

  3. 함수도 안정화 (useCallback): 상태를 체크하는 함수(checkOpen 같은)가 다른 useEffect의 의존성으로 사용되거나, 자식 컴포넌트에 props로 전달된다면 useCallback으로 감싸주는 것이 좋습니다. (자세한 내용은 아래 useCallback 설명에서!)

  4. 똑똑한 상태 업데이트: 상태를 업데이트할 때, 현재 값과 변경될 값이 같다면 굳이 업데이트할 필요가 없겠죠? setState의 함수형 업데이트를 사용하여 이전 상태(prevChip)와 비교 후, 변경이 필요할 때만 업데이트하도록 수정했습니다. (아래 예시)
  5. 휴무일 로직 분리: 휴무일인지 판단하는 로직과 영업시간을 체크하는 로직은 서로 다른 관심사입니다. 휴무일 체크는 주로 날짜가 바뀔 때 한 번만 하면 되므로, 별도의 useEffect로 분리하고, 휴무일일 경우 시간 체크 인터벌을 멈추도록(delay 상태를 null로 설정) 처리했습니다.

 

계산된 값 안정화 (useMemo)

const startTime = useMemo(() => {
  if (!storeHours.start) return null;
  return moment(storeHours.start, 'HHmm');
}, [storeHours.start]); // storeHours.start가 바뀔 때만 새로 계산

const endTime = useMemo(() => {
  // ... endTime 계산 로직 (자정 넘김 처리 포함) ...
}, [storeHours.end, startTime]); // storeHours.end나 startTime이 바뀔 때만 새로 계산

 

 

똑똑한 상태 업데이트

setChip(prevChip =>
  prevChip !== 'OPEN' ? 'OPEN' : prevChip
);

 

 

배운 점

  • React에서 시간처럼 계속 변하는 값을 다룰 때는 객체의 참조 동일성에 유의해야 합니다.
  • useMemo와 useCallback은 불필요한 계산과 렌더링을 막아주는 강력한 도구입니다. (하지만 남용은 금물!)
  • 상태 업데이트는 꼭 필요할 때만 하도록 최적화하는 것이 좋습니다.
  • 로직의 관심사를 분리하면 코드를 이해하고 관리하기 쉬워집니다.

혹시 비슷한 문제로 골머리를 앓고 계셨다면, 이 글이 작은 도움이 되었기를 바랍니다! 😊

예시

function MyBenefit(): React.ReactElement {
  const [inputCpnNm, setInputCpnNm] = useState('');
  
  return 
    <MyBenefitInput
            setInputCpnNm={setInputCpnNm}
            inputCpnNm={inputCpnNm}
    />
}

 

 

 이렇게 해도 되고

type Props = {
  inputCpnNm: string;
  setInputCpnNm: (e) => void;
};
function MyBenefitInput(props: Props): React.ReactElement {
 어쩌고저쩌고
}

 

 이렇게 해도 되고

(setInputCpnNm 위에 마우스 올리면 타입 알려줌)

import React, { Dispatch, SetStateAction } from 'react';

type Props = {
  inputCpnNm: string;
  setInputCpnNm: Dispatch<SetStateAction<string>>;
};
function MyBenefitInput(props: Props): React.ReactElement {
 어쩌고저쩌고
}

 

 

e.stopPropagation();

 

 

 

 

 

 

https://pa-pico.tistory.com/20

 

[개념잡기] e.preventDefault() 와 stopPropagation() 의 차이

stopPropogation vs preventDefault e.preventDefault()와 e.stopPropagation()의 차이 두개의 코드 모두 이벤트 관련 동작에서 많이 사용되는 코드이다. 둘의 차이점은 무엇일까 알아보자. e.preventDefault() html 에서 a

pa-pico.tistory.com

 

mutate: (variables: TVariables, { onSuccess, onSettled, onError }) => void

 

The mutation function you can call with variables to trigger the mutation and optionally hooks on additional callback options.

변수를 사용하여 호출할 수 있는 mutation 함수는 mutation을 유발하고 추가 콜백 옵션을 선택적으로 연결할 수 있습니다.

 

 

 

🤓 variables

variables: TVariables

 

  • Optional(선택적)
  • mutateFn에 전달할 객체

 

 

🤓 onSuccess

onSuccess: (data: TData, variables: TVariables, context: TContext) => void

 

  • Optional(선택적)
  • 이 함수는 mutation함수가 성공하면 실행되고 mutation 결과데이터가 전달됩니다. 
  • Void 함수의 경우 반환된 값은 무시됩니다.

 

 

🤓 onError

onError: (err: TError, variables: TVariables, context: TContext | undefined) => void

 

  • Optional(선택적)
  • 이 함수는 mutation함수가 실패하면 실행되고 오류가 전달됩니다. 
  • Void 함수의 경우 반환된 값은 무시됩니다.

 

 

🤓 onSettled

onSettled: (data: TData | undefined, error: TError | null, variables: TVariables, context: TContext | undefined) => void

 

  • Optional(선택적)
  • 이 함수는 mutation함수가 성공하거나 실패하거나 둘 다 실행되고 mutation 결과데이터나 오류가 전달됩니다. 
  • Void 함수의 경우 반환된 값은 무시됩니다.

 

 

🤓 응용

const apiGetUpdateMutate = useMutation(apiGetUpdate, {
    onSuccess: (data, variables) => {
      const type = variables.type;
      const dvs = variables.dvs;

      if (type === PUSH) {
        //PUSH 값 변경 시 앱에 저장
        const pushvDvs =
          Data.list.filter(
            data => data.type === PUSH,
          )[0]?.dvs || '0';
        if (pushvDvs !== dvs)
          bridgeSetSettingInfo('PYN', dvs === '1' ? 'Y' : 'N');
      }
    },
    onError: (error, variables) => {
      if (variables.type === PUSH) {
        alert(
          '오류가 발생하였습니다. 잠시 후 다시 시도해주세요.',
        );
      }
    },
    onSettled: (data, error, variables) => {
      if (variables.type === PUSH) {
        queryClient.invalidateQueries([E_QUERY_KEY.LIST_YN, 'event']);
      }
      setIsInputDisabled(false); //더블클릭 방지
    },
  });

 

 

https://tanstack.com/query/latest/docs/framework/react/reference/useMutation

🤓useRef 언제 사용하지?

1️⃣  저장공간

ref는 state와 비슷하게 어떤 값을 저장하는 저장공간으로 사용된다.

State의 변화 ➡️ 렌더링 ➡️ 컴포넌트 내부 변수들 초기화
Ref의 변화 ➡️ No 렌더링 ➡️ 변수들의 값이 유지됨
State의 변화 ➡️ 렌더링 ➡️ 그래도 Ref의 값은 유지됨

 변경시 렌더링을 발생시키지 말아야하는 값을 다룰때 사용한다

(변화는 감지해야하지만, 그 변화가 렌더링을 발생시키면 안될때!!)

 

 

2️⃣  DOM요소에 접근

✅ useRef를 사용하면 손쉽게 input에 접근할 수 있다.

바닐라 자바스크립트의 getElementById, querySelector와 비슷하다.

 

<DOM요소 접근의 대표적인 예>

대표적으로 Input요소를 클릭하지 않아도 focus를 줄때 사용

 

 

 

🤓응용

 

우선 useRef import하기 (useState도 활용할거라 같이 import)

import React, { useRef, useState } from 'react';

 

 

변수 생성, ustState로 상태관리 

const foldRef = useRef<HTMLDivElement>(null);
const [foldStore, setFoldStore] = useState<boolean>(false);

 

 

button에 onClick으로 함수 적용

아래 접었다폈다 할 부분에 foldRef 참조

<button
  title="클릭으로 접고 펴는 버튼"
  onClick={() =>
    onFoldItem(foldRef, foldStore, setFoldStore)
  }
>

<dd className="content" ref={foldRef}>어쩌고 접었다폈다할 내용~</dd>

 

 

style.css

  .content {
    max-height: 0px;
    overflow: hidden;
    transition: max-height 0.3s ease-out;
  }

 

 

함수

/**
 * 목록 toggle(접기/펼치기)
 * @description 이벤트 내역보기 클릭 시 사용
 */
export const onFoldItem = (foldRef, foldStore, setFoldStore) => {
  if (!foldRef || !foldRef.current) {
    return;
  }

  const style = foldRef.current.style;

  if (foldStore) {
    style.maxHeight = '0';
  } else if (!foldStore) {
    style.maxHeight = `${foldRef.current.scrollHeight}px`;
  }
  setFoldStore(!foldStore);
};

 

 

 

https://hihiha2.tistory.com/19

가입 후 애플리케이션 등록하기

 

주소
https://developers.kakao.com/console/app

 

카카오계정

 

accounts.kakao.com

 

 

애플리케이션 등록

1. 내 애플리케이션 들어가서 애플리케이션 추가하기

 

 

 

등록 예시

 

2. 플랫폼 등록하기

애플리케이션 추가하기

 

등록 예시 화면

 

등록된 화면

 

앱 키 화면

 

 

env 파일에 앱 키 등록

JavaScript 키 등록

 

 

index.html에 script 연결

 

<script src="https://developers.kakao.com/sdk/js/kakao.js"></script>

 

 

App.jsx에 kakao 초기 설정

console로 확인해보기

  /**
   * @name kakao 설정
   */
  useEffect(() => {
    if (process.env.REACT_APP_KAKAO_KEY) {
      window.Kakao.init(process.env.REACT_APP_KAKAO_KEY);
      console.log(window.Kakao.isInitialized())
    }
  }, []);

 

초기화가 잘 되어 API를 가져왔는지 콘솔로 확인해보면 true값이 뜨고, false라면 초기화를 실패

 

함수 설정

  const shareKakao = () => {
    window.Kakao.Link.sendDefault({
      objectType: "feed",
      content: {
        title: "롯데마트 전단에 초대합니다.",
        description: "이하동문",
        imageUrl: `${IMAGE_PATH}/common/btn_share_gray.png`,
        link: {
          mobileWebUrl: process.env.REACT_APP_GO_URL,
        },
      },
      buttons: [
        {
          title: "함께 보기",
          link: {
            mobileWebUrl: process.env.REACT_APP_GO_URL,
          },
        },
      ],
    });
  };

 

        <button onClick={shareKakao}>
          <img
            src="https://developers.kakao.com/assets/img/about/logos/kakaolink/kakaolink_btn_medium.png"
            alt="카카오링크 보내기 버튼"
          />
        </button>

 

 

https://velog.io/@da__hey/React-React-Typescript%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%B9%B4%EC%B9%B4%EC%98%A4%ED%86%A1-%EB%A9%94%EC%8B%9C%EC%A7%80-%ED%94%8C%EB%9E%AB%ED%8F%BC-API-%EC%9D%B4%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0

 

[ React ] React, Typescript를 통해 카카오톡 메시지 플랫폼 API 이용해보기

카카오톡 메시지 플랫폼 API 이용해보기 💬 들어가며 사이드 프로젝트를 진행하면서 사용자 초대 링크를 카카오톡 공유 API를 통해 전송하는 기능을 구현하게 되었다. API에서 데이터를 가져오는

velog.io

 

1. 라이브러리 설치하기

npm install qrcode.react

혹은

yarn add qrcode.react

2. QR코드 띄우기

  • 라이브러리 설치 후 큐알 코드를 화면에 띄우는 것은 쉽다.
  • <QRCode /> element에 원하는 value값만 넘겨주면 된다.
import QRCode from 'qrcode.react';

const CreateCode = ({}: P) => {
  return (
    <Container>
      <QRCode value={QR에 담고 싶은 정보} />
    </Container>
  );
};

export default CreateCode;

 

 

 

참고: 

https://www.npmjs.com/package/qrcode.react

 

qrcode.react

React component to generate QR codes. Latest version: 4.2.0, last published: 22 days ago. Start using qrcode.react in your project by running `npm i qrcode.react`. There are 1013 other projects in the npm registry using qrcode.react.

www.npmjs.com

 

https://velog.io/@jiwonyyy/QR%EC%BD%94%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-qrcode.react

'Framework > React' 카테고리의 다른 글

[React] useRef 응용 - toggle기능 만들기  (1) 2025.02.04
[React] 카카오톡 메세지 공유하기  (0) 2025.01.13
[React] React-router 'Outlet'  (0) 2024.12.06
[React] Code Splitting & lazy  (0) 2024.12.04
[React] Redux - payload 적용  (0) 2024.12.03

+ Recent posts