useCallback Hook과 arrow function에 대해서 질문드리겠습니다.

안녕하세요. 조금 전에 가입한
자바스크립트 개발을 시작한 지 얼마 되지 않은 새내기입니다.
최근에 React를 활용하고 있는데, 코드 리뷰를 받으면서
매니저로부터 arrow function를 포함한 익명 함수들을 콜백함수로 전달하는 것은
성능에 영향을 미칠 수 있기 때문에 useCallback을 활용하라는 피드백을 받았습니다.
최근 며칠 이 변환 작업때문에 스트레스를 많이 받았는데요.
useState를 통해 상태라도 업데이트시키는 로직이 들어가는 순간 바로 Too many re-renders라는
에러에 봉착하게 되고 그렇지 않으면 동작을 하지 않던가… 머리를 쥐어뜯었습니다.
그러다가 useCallback 함수 내부에서 리턴문을 익명 함수로 쓰게 되면 정상적으로 동작한다는
사실을 알게 되었습니다. 왜 그럴까요?
저는 마냥 익명 함수를 쓰면 성능에 문제가 생긴다 ==> useCallback으로 치환하자
라는 생각 밖에 없었는데 갑자기 useCallback 내에서 익명 함수를 리턴하면 동일한 정상 동작이 가능하다는 사실이
뭔가 잘 와닿지 않습니다. 이렇듯 같은 정상 결과를 반환하더라도, 결국은 익명 함수를 리턴하기 때문에
useCallback을 사용한 의미가 없고 그저 익명함수를 쓴 것과 같은 성능 악화를 가져오는건지, 그렇지 않은지에 대해서
아시는 분이 계시다면 답변 부탁드리겠습니다…!

예시 코드가 있으면 이해에 도움이 되겠다는 의견이 있어 추가하겠습니다!
급하게 작성하느라 정확한 코드는 아닐 수 있어서 대강 이런걸 말하는구나 라고 봐주시면 감사하겠습니다!

아래의 코드는 제가 위에서 말씀드렸던 일반적인 익명 함수의 경우입니다.
이 경우가 성능에 좋지 않은 영향을 끼친다더군요.

import React, { useState } from 'react';

const Component = () => {
  const [param, setParam] = useState('');
  ...

  return (
    ...
    <SomeComponent
      onClick={() => setParam('parameter')}
      {...others}
    />
  );
}

그리고 이 아래의 코드가 useCallback 을 사용했을 경우입니다.
예시와 좀 다를 수 있지만 제가 이러한 방식으로 매개변수가 있는 콜백 함수를
useCallback 으로 변환해 작성했을 때 에러가 발생하거나 정상적으로 동작하지 않았습니다.

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

const Component = () => {
  const [param, setParam] = useState('');

  const handleClick = useCallback((params) => {
    setParam(params);
  },[]);

  ...
  return (
    ...
    <SomeComponent
      onClick={handleClick('parameter')}
      {...others}
    />
  );
}

이 코드는 위 부분과의 차이점만을 작성했습니다.
너무 예시가 간결해서 이상하게 보일수도 있지만, 제 요지는
위에서와 같이 useCallback을 사용하면 상태가 계속 변화하고 리턴하면서 무한루프에 빠져
렌더링 에러가 발생하지만 아래와 같이 작성하면 정상적으로 동작합니다.

  const handleClick = useCallback((params) => {
    return () => setParam(params);
  },[]);

이 글을 통해서 전달하고자 하는 질문의 요지는 이렇습니다.
콜백에 익명함수를 쓰는 것은 성능에 안좋은 영향을 끼치기 때문에 useCallback을 사용하는
것이 권장되는데 반해, 이 Hook 안에서 익명 함수로 리턴을 하게 되면 동일한 정상 동작 반환을 하긴 하는데,
그냥 익명 함수만 사용한 것과 같은 성능 하락에 영향을 주는지? 아니면 그렇지 않기 때문에
Hook 내에 익명함수 반환하는 방식으로 같이 사용해도 좋은건지? 가 요점입니다.

읽어주셔서 감사합니다.

말씀하신 사항을 코드와 함께 설명해주시면 질문을 보다 정확히 이해할 수 있을 것 같습니다. :grinning:

추가했습니다!

const handleClick = useCallback((params) => { setParam(params); },[]);

이런식으로 작성할 경우 event 객체를 setParam의 인저로 바로 넣어주기 때문에 의도하지 않았다면 다른 결과를 얻으실 수도 있습니다.
제가 알기로 useCallback을 쓰는 경우는 함수 memoization을 통해 re-rendering을 막아주기 위해 사용합니다. 일반적으로 부모의 상태가 변하거나 해당 컴포넌트가 새로 렌더링 되면 해당 컴포넌트의 element가 re-rendering 되는데, React.memo를 사용해 컴포넌트 자체의 re-rendering을 막을 것입니다. 하지만 해당 컴포넌트에 이벤트 콜백 함수를 선언하고 자식 컴포넌트나 element에 전달 할 경우 함수도 새로 생성되어 re-rendering을 막고자 했던 부분이 새로운 함수때문에 변화가 있는 것으로 판단해 의도와 다르게 re-rendering이 될 수도 있기 때문인데요. useCallback을 사용해서 기존과 변화가 없으면 함수를 새로 생성하지 않아 불필요한 re-rendering을 막기 위해 사용하는 것으로 알고 있습니다. useCallback의 두번째 인자의 배열에는 참조하는 state를 넣어 해당 state가 변경될때마다 함수를 새로 생성하여 state를 제대로 참조할 수 있게 합니다. 만약 두번째 인자를 비운다면 처음 rendering 이후 함수가 새로 생성되지 않을 것입니다.
만약 arrow function으로 익명함수를 콜백함수에 직접 작성한다면 그 element는 무조건 re-rendering 될 것입니다.
그리고 일반적으로 useCallback에는 return으로 반환할 값이 없어서 사용하지 않는다고 합니다.
짧게 설명하기 힘들어 내용이 다소 명확하지 못해 죄송합니다만 부족한 부분은 공식문서나 Kent C. Dodds의 글을 참조하시면 도움이 될 것 같습니다.

1 Like

어느정도 감이 오는것같습니다. 답변 감사합니다!

1 Like

@park78951님이 설명해주셨듯이 useCallback을 써서 같은 memoization이 되기 때문에 이를 활용하면 하위 component들이 불필요하게 다시 렌더링되는 것을 막아 줄 수 있습니다.

@anthonyminyungi 님께서 질문에 언급해주신 내용에서 정정하고 싶은 부분이 있습니다.

  1. 특정 컴포넌트에 props로 함수를 전달할 때 그 함수가 익명함수, 기명함수의 여부는 성능과 무관합니다.
  2. useCallback을 우선적으로 활용하는 것을 권하지 않습니다. useCallback을 쓴다고 항상 성능이 더 좋아지는 것은 아닙니다.
    useCallback를 사용해서 생성한 함수를 props로 받는 컴포넌트의 렌더링을 회수를 줄일 수 있지만, useCallback을 사용하면 사용하지 않을 때보다 추가적으로 메모리 사용과 비교연산이 필요하게 됩니다. 따라서 렌더링을 줄여서 얻게되는 성능적 이득과 useCallback을 사용해서 성능적 손해, dependancy 설정이나 동시성 관련한 이슈로 useCallback이 더 많은 실수(버그)를 유발할 수 있는 점등을 생각해볼 필요가 있습니다. 이 때문에 useCallback을 써서 얻게되는 이득이 확실히 더 크다고 판단되지 않는 상황에서 우선적으로 useCallback로 wrap해서 사용하는 것보다 일단 useCallback을 사용하지 않다가 관련되서 문제가 생기면 useCallback을 검토해보는 것이 나은 접근방법이라 생각합니다.

관련해서 살펴볼 만한 글

2 Likes

당시에 해결한뒤 지금 발견했습니다.답변 주셔서 감사합니다!

늦은 답변이지만 … 뭔가 잘 못 이해한거 같아서 onClick에는 함수를 넣어줘야해요 위에는 () => 해서 함수 자체를 넣어줬는데 useCallback에서는 함수를 실행한 값을 넣어줬어요. 그래서 클릭하기도전에 렌더링이되면 함수가 바로 실행이되고 state가 바뀌고 바꼈으니 또 렌더링 되고 무한 반복 되는겁니다. 그래서 밑에하신 방법이 고안 되는거구 고차함수로 만드신겁니다. 커링이라고 해서 (parameter) => (event) => setParams() 처럼 만들수 있어요. 한번 실행이되도 함수형태로 한꺼풀 더 남아있어서 클릭할때 실행 되는겁니다.

1 Like

답변 감사드립니다!
그렇다면 onClick={()=> ~ } 와 같이 작성하는 것 보다는 handler = useCallback(() => () => ~ )와 같이 작성 후 onClick={handler}처럼 작성해주는 것이 옳다는 말씀으로 생각해도 될까요?

아니죠 onClick 안에는 함수를 넣어줘야 합니다. click을 하면 그 함수가 실행이 됩니다.
handler = useCallback(()=> () => ~, []);
으로 작성 하신경우에는 onClick={handler()) 를 넣어줘야합니다. 처음에 자동으로 실행이 되고 이제 () => ~ 이 남게 되죠. 함수가 남잖아요? 그러면 클릭하면 그 함수가 실행이 되는겁니당.

useCallback이 중요한게 아니고 지금 함수랑 고차 함수 onClick에 대해 이해를 하시면 될 것 같아요. useCallback은 그냥 메모이제이션해주는 것뿐.

그러니깐 onClick에 함수를 넣는 방법

  1. 처음 인자를 넘겨주지 않을 때
    const handler = (e) => console.log(e.target);
    onClick={handler} 이렇게 하면 클릭을 했을 때 첫번째 인자로
    onClick 자체에서 함수의 첫번재 인자로 event객체를넘겨줍니다.
    e.target을 log 하면 어떤 태그가 클릭 되었는지 볼 수 있습니다.

  2. 처음 인자를 넘겨주고 싶을 때
    const handler = (s) => (e) => console.log(s, e.target);
    onClick = {handler(“안녕하세요”)};
    이렇게 되면 처음에 s 매개변수에 "안녕하세요"가 들어간 채로 함수가 한번 실행이 됩니다. 그러면 (e) => console.log(s, e.target)이 남은 상태겠죠? s엔 "안녕하세요"가 들어가 있습니다. 클릭을 하면 남은 함수가 실행됩니다.

빠른 피드백 정말 감사드립니다.
설명해주신 덕분에 onClick prop에 함수를 넘겨주는 방법에 대해서는 이해가 됐습니다.
그렇다면 제 질문의 요점으로 돌아가서,

이 내용에 대해서는 어떻게 생각하시는지 여쭤봐도 될까요?
예를 들어, map으로 전개하여 배열의 요소를 onClick 함수에서 실행되는 함수의 인자로 전달하고자 할때,
다시 말해 onClick={()=> setParam('parameter')}와 같은 방식으로 작성하게 되면
말씀해주신 것처럼 클릭하면 실행될 함수를 전달해주는 역할만 할 뿐 성능에 악영향을
끼치는 것은 아니라는 것으로 이해하는 것이 맞을지 여쭤보고싶습니다.

성능에 좋지는 않을 것 같아요. 왜냐하면 저렇게 똑같은 함수들을 map으로 넘겨준다고 치면 배열의 개수만큼 () => setParam(‘parameter’) 함수가 생성되거든요. 그러면 메모리를 많이 잡아먹겠죠.
const handler = () => setParam(‘parameter’); 로 생성해놓고
onClick = {handler} 로 하면 handler는 한번 생성 했고 배열에 다 같은 함수를 넣어주는게 되겠죠 ? 최적화 한번 했습니다.

그런데 함수형 컴포넌트에서는 렌더링이 될 때마다 함수를 다시 실행하기 때문에 handler라는 함수가 재생성 되요. 그래서
const handler = useCallback(() => setParam(‘parameter’),[]);
를 하고 배열에 넘겨주면 useCallback이 캐싱을 해줘서 리렌더링이 되도 함수를 재생성하지 않고 캐싱한 함수를 사용합니다.
최적화 두번했습니당.

onClick 같이 jsx 안에 함수를 넣어줄 때는 useCallback으로 감싸주는게 좋아요. 자세한 것은 공식문서에 useCallback 읽어보세요. 두번째 인자가 중요합니다

1 Like

useCallback을 사용하는 것이 확실히 성능에 영향을 주는 경우도 있는게 맞군요…
친절한 답변 정말 감사드립니다! 많은 도움이 되었습니다.