Skip to content

Instantly share code, notes, and snippets.

@ninanung
Last active May 6, 2022 15:30
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ninanung/767ca722befa8b0affe51ffa0064296b to your computer and use it in GitHub Desktop.
Save ninanung/767ca722befa8b0affe51ffa0064296b to your computer and use it in GitHub Desktop.
때늦은 React Hooks 시리즈 4탄 - useCallback/useRef

이 글의 코드는 저자의 Github에서 확인할 수 있습니다.

때늦은 React Hooks 시리즈 4탄 - useCallback/useRef

useCallback

React에서 컴포넌트가 다시 렌더링 될 때에는 컴포넌트 안에 선언된 함수들을 새로 생성합니다. 결국 계속해서 렌더링 되면 함수도 계속해서 새로 생성된다는 얘기입니다. 딱 들어봐도 비효율의 냄새가 나지 않습니까? 이런 비효율적인 작태를 React팀이 그냥 보고 넘어갔을 리 없겠죠. 그래서 나온것이 useCallback입니다. 저번 글의 useMemo와 비슷하게 퍼포먼스와 효율성을 위한 개념으로 만들어 졌습니다. 해당 기능을 사용하기 전의 코드를 먼저 보겠습니다.

const UseCallbackExample = () => {
  const [string, setString] = useState('');
  const [stringList, setStringList] = useState([]);

  const change = (e) => {
      setString(e.target.value);
  }

  const insert = () => {
    const newList = stringList.slice();
    newList.push(string);
    setStringList(newList);
  }

  const sum = (list) => {
    console.log('문자들을 합치는 중입니다...');
    let stringSum = '';
    for(let value of list) {
      stringSum += value + ' ';
    }
    return stringSum;
  }

  const result = useMemo(() => sum(stringList), [stringList]);

  return (
    <div>
      <input type='text' onChange={change}/>
      <button onClick={insert}>문자열 추가</button>
      {result}
    </div>
  )
}

이 코드는 저번 글에서 사용했던 코드와 거의 같습니다만, useCallback의 예를 위해 input의 onChange를 위해 change함수를 만들었습니다. 해당 코드에 있는 함수들, insert, changesum은 위에서 설명한 것과 같이 렌더링 될 때 마다 재생성될 터입니다. 이를 해결하기 위한 방법은 useMemo와 비슷합니다. 해당 함수를 useCallback을 이용해서 다시 만들어 보겠습니다.

const UseCallbackExample = () => {
  const [string, setString] = useState('');
  const [stringList, setStringList] = useState([]);

  const change = useCallback((e) => {
    setString(e.target.value);
  }, []);

  const insert = useCallback(() => {
    const newList = stringList.slice();
    newList.push(string);
    setStringList(newList);
  }, [string, stringList]);

  const sum = useCallback((list) => {
    console.log('문자들을 합치는 중입니다...');
    let stringSum = '';
    for(let value of list) {
      stringSum += value + ' ';
    }
    return stringSum;
  }, []);

  const result = useMemo(() => sum(stringList), [stringList, sum]);

  return (
    <div>
      <input type='text' onChange={change}/>
      <button onClick={insert}>문자열 추가</button>
      {result}
    </div>
  )
}

우선 change함수부터 보시면, 해당 함수는 두번째 인자로 빈 배열을 줬기 때문에 최초의 렌더링 시에만 함수가 생성되고 이후에는 생성되지 않습니다. insert함수는 stringstringList가 변경될 때에만 함수를 재생성합니다. 이러한 차이는 왜 나오는 걸까요? 분명히 change함수에서도 setString을 사용했는데 말이죠. 이는 useMeomouseCallback이나 동일한 법칙입니다. 해당 함수 안에서 state를 사용할 때(해당 값에 의존할때)는 반드시 두번째 인자인 배열 안에 추가시켜 주어야 합니다. change함수는 해당 state를 사용하지는 않고 변경하기만 했으므로 의존성이 없어서 추가시키지 않아도 됐지만, insert함수의 경우에는 stringListstrint을 사용하고 있기때문에 추가해줘야 했습니다. sum함수도 동일한 방식으로 매개변수 이외에는 다른 값을 사용하고 있지 않기 때문에 빈 배열을 두번째 값으로 넘겼습니다.

useMemo는 숫자, 문자열, 객체등의 일반적인 값에 사용하고, useCallback은 함수에 사용하면 됩니다. 위의 코드로 생각해보면, useMemo를 통해서는 문자열을 result변수에 넘겨줬지만, useCallback를 통해서는 change, insert, sum등의 함수를 넘겨줬습니다. 두가지 기능을 사용하기 전에, 어떻게 하면 효율적인 방식이 될지 고민하는 시간이 필요할 것 같습니다.

useRef

해당 기능을 사용하기 위해서는 우선 ref에 대한 내용을 알고 있어야 합니다. 공식 페이지에 자세하게 설명되어 있으니 참고하시면 될 것 같습니다. 문서를 읽으셨거나 이미 알고 계셨다면 아시겠지만, ref는 특정 element의 현상을 발생시키는 역할을 합니다. 예컨데, input의 포커스를 이동하거나 동영상을 재생하거나 하는 동작이 있습니다. useRef는 그런 점에 있어서, 함수형 컴포넌트에서 ref사용을 편하게 해주는 역할을 합니다. 예제에서는 focus를 변경하는 부분을 해보겠습니다.

const UseRefExample = () => {
  const [string, setString] = useState('');
  const [stringList, setStringList] = useState([]);

  // Here's useRef!
  const inputText = useRef();

  const change = useCallback((e) => {
    setString(e.target.value);
  }, []);

  const insert = useCallback(() => {
    const newList = stringList.slice();
    newList.push(string);
    setStringList(newList);

    // Here's focusing!
    inputText.current.focus();
  }, [string, stringList]);

  const sum = useCallback((list) => {
    console.log('문자들을 합치는 중입니다...');
    let stringSum = '';
    for(let value of list) {
      stringSum += value + ' ';
    }
    return stringSum;
  }, []);

  const result = useMemo(() => sum(stringList), [stringList, sum]);

  return (
    <div>
      <input type='text' ref={inputText} onChange={change}/>
      <button onClick={insert}>문자열 추가</button>
      {result}
    </div>
  )
}

자 코드를 보실까요. 사실 위의 코드는 useCallback의 설명에서 사용한 코드에 단 두줄의 코드와 약간의 추가를 했을 뿐입니다. 실제 동작도 간단합니다. 버튼을 누를경우 focus가 자동으로 input에 이동합니다. useRef의 사용법은 이처럼 간단합니다. 변수를 선언하고 html에서 테그에 ref를 등록한 후 사용하면 됩니다. 이러한 것 말고도 useRef의 사용법은 하나가 더 있습니다. 클래스형 컴포넌트의 로컬변수 선언을 useRef를 사용하여 함수형 컴포넌트에서도 할 수 있다는 겁니다. 기존의 클래스형 컴포넌트에서는 이런 방식이었습니다.

import React from 'react';

class LocalVar extends React.Component {
  localVar = 1;

  increaseLocalVar = () => {
    this.localVar++;
  }
  
  render() {
    return (
      <div>
        {this.localVar}
      </div>
    )
  }
}

export default LocalVar;

하지만 함수형 컴포넌트에서는 useRef를 사용합니다.

import React, {useRef} from 'react';

const LocalVar = () => {
  const localVar = useRef(1);

  const increaseLocalVar = () => {
    localVar.current = localVar.current + 1;
  }

  return (
    <div>
      {localVar}
    </div>
  )
}

export default LocalVar;

아마 모두 아시겠지만, 로컬변수는 state와 달리 렌더링에 영향을 주지 않으므로 그 점을 유의하여 사용해야 합니다.

자 이렇게 useCallbackuseRef를 모두 알아보았습니다. 아직 React Hooks에서 useReducer를 설명하지 않았습니다만, 이는 Redux를 알아야 하는 부분이기 때문에 우선은 패스하도록 하겠습니다. 다음 시간에는 React Hooks를 이용한 Router에 대한 얘기를 해볼까 합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment