useCallback 을 사용한 함수 재사용

Hyun·2021년 9월 27일
0

리액트 기초

목록 보기
11/18

useCallback 은 지난 글에서 다뤘던 useMemo 와 비슷한 hook 이다. useMemo 는 특정 결과값을 재사용 할 때 사용하는 반면, useCallback특정 함수를 새로 만들지 않고 재사용하고 싶을 때 사용한다.

이전에 App.js에서 구현했었던 onCreate, onRemove, onToggle, onUpdate, onModify 함수를 확인해보자.

const onCreate = () => {
    const newUser ={
      id: nextId.current,
      username:username,
      email:email
    }
    setUsers([
      ...users, newUser//users=객체모음, newUser=추가힐 객체
    ])
    setInput({
      username: '',
      email: ''
    })

    nextId.current += 1;
  }

const onRemove = (id) => {
    setUsers(users.filter(user => user.id !== id));
  }

const onToggle = (id) => {
    setUsers(
      users.map(user => user.id === id? {...user, active : !user.active} : user)
    )
  }

const onUpdate = () => {
    setUsers(
      users.map(user => user.id === id ? {...user, username: username, email : email} : user)
    )
    setInput({
      username: '',
      email: '',
      id: '',
    })
  }

const onModify = (user) => {
    setInput({
      username: user.username,
      email: user.email,
      id: user.id
    })
  }

이 함수들은 컴포넌트가 리렌더링 될 때 마다 새로 만들어진다. 함수를 선언하는 것 자체는 메모리도, CPU 도 리소스를 많이 차지하는 작업은 아니기 때문에 함수를 새로 선언한다고 해서 그 자체만으로 큰 부하가 생길 일은 없지만, 한번 만든 함수를 필요할때만 새로 만들고 그렇지 않으면 기존의 함수를 재사용하는 것은 여전히 중요하다.

useCallback 의 사용법은 아래와 같다.

App.js

...

const countActiveUsers = (users) => {
  console.log('활성 사용자 수를 세는중...');
  return users.filter(user => user.active === true).length;
}

const App = () => {

  const [input, setInput] = useState({
    username: '',
    email: '',
    id: ''
  }) 

  const {username, email, id} = input;

  const onChange =(e)=>{
    const {name, value} = e.target;
    setInput({
      ...input,
      [name]:value
    })
  }

  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false
    }
  ]);
  
  const nextId = useRef(4);

  const onCreate = useCallback(() => {
    const newUser ={
      id: nextId.current,
      username:username,
      email:email
    }
    setUsers([
      ...users, newUser//users=객체모음, newUser=추가할 객체
    ])
    setInput({
      username: '',
      email: ''
    })

    nextId.current += 1;
  },[username, email, users])

  const onUpdate = useCallback(() => {
    setUsers(
      users.map(user => user.id === id ? {...user, username: username, email : email} : user)
    )
    setInput({
      username: '',
      email: '',
      id: '',
    })
  },[users, id, username, email])

  const onRemove = useCallback((id) => {
    setUsers(users.filter(user => user.id !== id));
  }, [users])

  const onToggle = useCallback((id) => {
    setUsers(
      users.map(user => user.id === id? {...user, active : !user.active} : user)
    )
  }, [users])

  const onModify = useCallback((user) => {
    setInput({
      username: user.username,
      email: user.email,
      id: user.id
    })
  }, [])
  
  const count = useMemo(() => countActiveUsers(users), [users]);

  return(
    <div>
      <CreateUser username={username} email={email} onChange={onChange} onCreate={onCreate} onUpdate={onUpdate}/>
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} onModify={onModify}/>
      <div>활성사용자 수: {count}</div>
    </div>
  )
}

export default App;

주의할 점은, 함수 안에서 사용하는 상태 혹은 props 가 있다면 꼭 deps 배열에 포함시켜야 한다는 것이다.

useCallback 을 사용한 함수는 컴포넌트 렌더링시마다 새로 만들어지는 것이 아니라, deps 배열안의 값들이 변경될 때마다 함수를 새로 선언한다.

따라서 배열안의 값들이 변경되지 않는다면 기존의 함수를 재사용한다. 만약 deps 배열 안에 함수에서 참조하는 값을 넣지 않게 된다면, 함수 내에서 참조하는 값이 새로 변경됬음에도 불구하고 함수를 새로 선언하지 않고 변경되기 이전의 값을 참조한 기존의 함수를 재사용하게 된다.

그렇게 되면 함수 내에서 해당 값들을 참조할 때 가장 최신 값을 참조 할 것이라고 보장할 수가 없다. props로 받아온 함수가 있다면, 이 또한 deps 에 넣어주어야 한다.

useCallback 을 사용함으로써 바로 이뤄낼 수 있는 눈에 띄는 최적화는 없다. 컴포넌트 렌더링 최적화 작업을 해주어야만 성능이 최적화된다.

그 전에, 어떤 컴포넌트가 렌더링되고 있는지 확인하기 위해 React DevTools 라는 크롬 확장프로그램을 설치한다. 이후 React 탭이 개발자 도구에 나타나는데, 톱니바퀴 아이콘을 누르고 'Highlight Update' 를 체크해준다.

이 속성을 키면 리렌더링 되는 컴포넌트에 사각형 형태로 하이라이트되어 보여지게 된다. 현재는 아래와 같이 input 태그의 값이 바뀔 때에도 UserList 컴포넌트가 리렌더링 되고 있다. (전체를 감싸는 App 컴포넌트는 당연히 리렌더링된다.)


다음 글에서는 이 리렌더링을 막아보도록 하자.

추가 설명

onCreate 함수 코드에서 왜 dependency 에 nextId 변수가 들어있지 않은지 궁금증이 들 수 있다.

useRef 로 정의된 변수는 컴포넌트가 렌더링될 때 값이 초기화 되지 않고, 변수의 값이 변경될 때 컴포넌트가 리렌더링 되지도 않는다. 따라서 useRef 로 정의된 변수는 항상 stable 한 상태를 유지할 수 있으며, 결과적으로 변수를 참조하는 함수는 실행될때마다 항상 현재(최신)의 값을 참조할 수 있다.

이러한 이유로 useRef 로 정의된 변수를 dependency 에 넣을 필요가 없다 : )

 const nextId = useRef(4);

  const onCreate = useCallback(() => {
    const newUser ={
      id: nextId.current,
      username:username,
      email:email
    }
    setUsers([
      ...users, newUser//users=객체모음, newUser=추가할 객체
    ])
    setInput({
      username: '',
      email: ''
    })

    nextId.current += 1;
  },[username, email, users])

주의 사항

주의 사항까진 아니고, 리액트 공식 홈페이지에서 dependency 에 setState 함수와 dispatch 함수는 포함되지 않아도 괜찮다고 한다. 함수 동일성이 안정적이고 리렌더링 시에도 변경되지 않을 것이라는 것을 리액트가 보장한다고 한다.

profile
better than yesterday

1개의 댓글

comment-user-thumbnail
2022년 1월 10일

좋은 글 감사합니다. 혹시 onModify에는 deps로 빈 배열을 넣은 이유를 알 수 있을까요?

답글 달기