React - useCallback

goodjam92·2023년 3월 26일
0

React

목록 보기
2/7
post-thumbnail

리액트를 사용하면서 useState, useEffect, useRef등은 써보았지만 아직 제대로 사용해보지 못한 훅들이 많았다...
그래서 이 참에 한 번 궁금했던 리액트의 hook몇 가지에 대해 알아보고자 한다.
그 첫번째는 바로 useCallback이 되시겠다!

useCallback

컴포넌가 처음 렌더링 될 때 내부에 선언된 함수를 새로 생성한다. 그리고 컴포넌트가 리렌더링되면 다시 새로운 함수로 생성한다. 이런 특성으로 인한 성능 문제가 발생할 수 있다.

useCallback : 리렌더링 간에 함수 정의를 캐시할 수 있는 hook

쉽게 얘기하면 useCallback의 첫번째 인자로 들어온 함수를 기억하여 두번째 인자로 넘어온 배열 요소의 값이 변경되기 전까지 재사용한다.
즉, 두번째 인자 값이 변하지 않으면 함수를 새로 생성하지 않고 기억 된 함수를 재사용한다는 의미이다.

  • useCallback(fn, dependencies) 두 가지 전달 요소
    • fn : 캐시하려는 함수 정의 값이다.
    • dependencies : 첫번째 인자의 함수(fn)가 의존해야하는 요소의 배열 useEffect의 종속성과 유사

컴포넌트 렌더링 시 함수를 새로 생선언하는 것은 성능 상 큰 문제가 되지 않기 때문에 단순히 컴포넌트 내에서 함수를 새로 생성하지 않기 위해 useCallback을 사용하는 것은 의미가 없다.

자바스크립트 함수 동등성

useCallback() 을 언제 사용해야하는지 이해하려면 함수 간의 동등함이 어떻게 결정되는지 알 필요가 있다.

const func1 = () => console.log("func");
const func2 = () => console.log("func");
console.log(func1 === func2); // false

자바스크립트에서 함수도 객체로 취급 되기 떄문에 메모리 주소에 의한 참조 비교가 일어난다.
이런 특성은 리액트 컴포넌트 내에서 어떤 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제가 발생할 수 있다.

의존 배열에 대한 요소로 함수를 넘길 때

useCallback 사용되지 않은 경우

function MyInfo({userId}) {
  const [userInfo, setUserInfo] = useState(null);
  
  const getInfo = () => 
	fetch(`https://getinfo-api.com/users/${userId}`)
      .then((res) => res.json())
	  .then(({user}) => info);	
  
  useEffect(()=> {
    getInfo().then((info) => setUserInfo(info));
  }, [getInfo]);
}

위의 코드를 예를 들어보면 useEffectgetInfo함수가 변경될 때마다 호출 된다. getInfo는 함수이기에 userId값에 상관없이 컴포넌트가 렌더링 될 때 마다 함수가 새로 생성되어 참조 값이 변경된다. 그러면 useEffect함수가 호출되어 info상태값이 바뀌고 다시 렌더링되고 또 다시 useEffect함수가 호출되는 악순환이 반복된다.

useCallback 사용 된 경우

function MyInfo({userId}) {
  const [userInfo, setUserInfo] = useState(null);
  
  const getInfo = useCallback (
    () => 
	fetch(`https://getinfo-api.com/users/${userId}`)
      .then((res) => res.json())
	  .then(({user}) => info),
   [userId]);	
  
  useEffect(()=> {
    getInfo().then((info) => setUserInfo(info));
  }, [getInfo]);
}

위와 같은 문제에서 useCallback을 이용하여 함수를 래핑하게 되면 getInfo 함수의 참조 값을 동일하게 유지한다. 따라서 userId가 변경되지 않는 한 useEffect는 재호출되지 않는다.

with React.memo()

useCallBack() hook 함수는 자식 컴포넌트 렌더링의 불필요한 렌더링을 막기 위해 memo() hook과 같이 사용할 수 있다.

예를 들면,

export const SmartLight = memo(function SmartLight({toggle, room, on}) {
  console.log({room, on});
  return (
  <button onClick={toggle}>
    {room} {on ? "💡" : "⬛"}
  </button>
  );
});

memo()함수로 컴포넌트를 감싸게 되면 해당 컴포넌트 함수는 props 값이 변경되지 않는 한 재호출되지 않습니다.

SmartLight컴포넌트를 적용시킬 컴포넌트 작성

function SmartHome() {
  const [bathOn, setBathOn] = useState(false);
  const [homeOfficeOn, setHomeOfficeOn] = useState(false);
  
  const toggleBath = () => setBathOn(!bathOn);
  const toggleHomeOffice = () => setHomeOfficeOn(!homeOfficeOn);
  
  return (
    <div>
  	  <SmartLight room="작업실" on={homeOfficeOn} toggle={toggleHomeOfficeOn}/>
      <SmartLight room="안방" on={bathOn} toggle={toggleBathOn} />
  )
    </div>
}

이 컴포넌트를 이용하여 안방의 불을 키면 다른 모든 방에 대한 컴포넌트 함수가 호출 되는 것이 콘솔에서 확인해볼 수 있다.

{room: "작업실", on: true}
{room: "안방", on: false}

조명을 키거나 끄는 방에 대한 SmartLight컴포넌트 함수만이 아닌 다른 방의 컴포넌트도 같이 호출되는 이유는 toggleBath(), toggleHomeOffice()함수 참조값이 SmartHome컴포넌트가 렌더링 될 때마다 새로 생성되어 바뀌어버리기 때문이다.

이 문제를 해결하려면 모든 toggle함수에 useCallback hook 함수로 감싸주어야 한다.

const toggleBath = useCallback (()=> 
  setBathOn(!bathOn) ,[bathOn]);
const toggleHomeOffice = useCallback(()=>
  setHomeOfficeOn(!homeOfficeOn), [homeOfficeOn]);

이제 다시 toggle을 해보면 해당 방에 대해서만 컴포넌트의 호출이 일어나는 것을 볼 수 있다.

{room: "작업실", on: true}

메모화된 콜백에서 상태 업데이트

경우에 따라 메모화된 콜백의 이전 상태를 기반으로 상태를 업데이트해야 할 수 있다.

function TodoList() {
  const [todos, setTodos] = useState([]);
  
  const handleAddTodo = useCallback((todo) => {
    const newTodo = { index: nextIndex++, todo };
    setTodos([...todos, newTodo]);
  }, [todos]);
}

위 함수는 todos를 종속성으로 지정하여 다음 할 일을 계산하는데 일반적으로 메모화된 함수는 가능한 한 적은 종속성을 갖는 것이 좋다.
다음 상태를 계산하기 위해서 일부 상태를 읽는 경우 대신 업데이트 함수를 전달하여 해당 종속성을 제거할 수 있다.

  const handleAddTodo = useCallback((todo) => {
    const newTodo = { index: nextIndex++, todo };
    setTodos(todos => [...todos, newTodo]);
  }, []);

useEffect가 반복되지 않도록 방지

useEffect내부에서 함수를 호출하고자 할 때

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const createOptions = useCallback(() => {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }, [roomId]); // roomId가 변경 될 때만 변경

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // createOptions가 변경될 때만 변경
  // ...

이렇게 작성하면 동일한 createOptions인 경우 리렌더링 시에도 동일한 기능을 수행한다.
그러나 함수 자체를 종속성으로 두지 않는 것이 훨씬 좋은 방법이면서 코드를 훨씬 간결하게 작성할 수 있다.

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    function createOptions() {
      return {
        serverUrl: 'https://localhost:1234',
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // roomId가 변경될 때만 변경

Custom hook 최적화

사용자 지정 hook을 작성하는 경우 반환되는 값이 함수인 경우 모든 함수를 useCallback으로 래핑하는 것이 좋다.

function useRouter() {
  const { dispatch } = useContext(RouterStateContext);

  const navigate = useCallback((url) => {
    dispatch({ type: 'navigate', url });
  }, [dispatch]);

  const goBack = useCallback(() => {
    dispatch({ type: 'back' });
  }, [dispatch]);

  return {
    navigate,
    goBack,
  };
}

hook 사용 시 필요할 때 코드를 최적화 할 수 있다는 이점이 있다.

끝으로..

React hook의 useCallback()에 대해 알아보았다!
이 함수를 어떤 상황에서 사용해야 하는지, 어떤 효과가 있는지도 알 수 있는 시간이었다.
성능 최적화에 적합한 hook이지만 무조건 적으로 사용하게 되면 오히려 코드가 복잡해지고, 유지보수가 어려워지는 역효과가 나타날 수 있다고하니 주의해서 사용해야 겠다.

다음시간에는 useCallback과 같이 사용 된useMemo에 대해서 알아봐야겠다!

.
.
.
.
.
.

참고 사이트
[daleseo] - useCallback 사용법
React 공식 문서 - useCallback

profile
습관을 들이도록 노력하자!

0개의 댓글