[React] useEffect, useMemo, useCallback

이호정·2022년 10월 13일
0

React

목록 보기
5/5

네이버 블로그에서 이전, 원글 작성일시 : 2021.07.25.03:19

오늘은 hook에 대해 좀 더 자세히 알아보자.

지난시간에 React Component의 Lifecycle에 대해 알아봤었다. useEffect는 어려웠다.

이 마음을 눈치채셨는지, 제로초 님의 다음 강의는 hook에 무게를 두고 있었다.

그럼, hook은 무엇인가?

React에서는 hook을 다음과 같이 설명한다.

Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 “연동(hook into)“할 수 있게 해주는 함수입니다. Hook은 class 안에서는 동작하지 않습니다. 대신 class 없이 React를 사용할 수 있게 해주는 것입니다.
출처 : https://ko.reactjs.org/docs/hooks-overview.html

react 컴포넌트의 state나 생명주기와 연관된 작업을 하는 녀석들이라고 대충 이해했다.
이전에 써왔던 useState, useRef 등 앞에 use가 붙는 녀석들은 모두 hook의 범주안에 들어간다고 보면 된다.

그렇다면 이제 이번에 배운 useEffect, useMemo, useCallback hook 대해 알아보자.
참고로 이녀석들은 모두 함수와 같이 배열을 인자로 받게되는데, 이 의미를 한번 제대로 이해해보자.




useEffect

지난 포스팅에서도 설명했듯, 이 녀석은 함수형 컴포넌트에서 LifeCycle을 관리할 때 사용하는 hook이다.
잠깐 복습을 해볼까,

React.useEffect(() => {
    console.log('componentDidMount or componentDidUpdate');

    return () => { console.log('componentWillUnmount'); }
}, [someState]);

useEffect안의 함수 실행문들은 componentDidMount 시점에만 실행되거나,
componentDidMount, componentDidUpdate, 모두 실행된다.(이는 두번째 인자인 배열에 따라 달라진다.)
return 뒤의 함수는 componentWillUnmount 시점에 실행된다.

자세히 알아보자

1. 두번째 인자, 배열

빈 배열을 그대로 두면 componentDidMount와 동일하게 동작한다.

만약에 비어있지 않다면?, useEffect의 첫번째 인자 함수는
componentDidMount, componentDidUpdate, 두 시점에서 모두 실행된다.
componentDidUpdate는 [ ] 안의 값이 변할때마다 실행된다.

"[ ] 안의 변수가 바뀌면 함수 실행"

즉 위 예시는 아래와 같이 동작한다.

렌더링 직후 -> componentDidMount or componentDidUpdate 출력
someState가 변할때마다 -> componentDidMount or componentDidUpdate 출력
컴포넌트가 unmount 될 때 -> componentWillUnmount 출력

참고로 [ ] 안에는 조건의 형태로도 넣을 수 있다. [ someState.length !== 0 ] 이렇게.
근데 되도록이면 이런 경우는 신중히 사용해야 할 것 같다.


2. componentDidMount만, 또는 componentDidUpdate만 따로 사용하고 싶다면

여러 방법이 있겠지만 간단하게 플래그를 사용하는 방법을 배웠다.

// ComponentDidMount
useEffect(() => {
    // 실행
}, []);

// ComponentDidUpdate
const mounted = useRef(false);
useEffect(() => {
    if(!mounted.current) {
        mounted.current = true;
    }
    else {
        // 실행
    }
}, [바뀌는값]);

useEffect에 대한 내용은 지난번 포스팅과 별반다르지 않다.
하지만 componentDidUpdate만 단독으로 사용하는 방법은 꽤 유용해보이니 잘 숙지해두자.




useMemo

로또 번호를 뽑아서 1초마다 한 숫자씩 표시하는 예제 중, 로또 번호를 무작위로 고르는 함수를 따로 작성했었다.
getWinNumbers라는 함수 였는데 이 함수의 반환값을 useState의 초기값으로 지정해뒀었다.

const hookPractice = () => {
    const [winNumbers, setWinNumbers] = React.useState(getWinNumbers());

    ...
}

전체 코드에는 1초마다 당첨공을 표시하기 위해 state 변경이 일어나는데,
state가 변경될 때마다 컴포넌트 다시 렌더링되고,
특히 함수 컴포넌트는 함수 전체가 렌더링시에 다시 실행되기 때문에,
결과적으로 state가 변경될때마다 getWinNumbers 함수가 매번 호출되는 문제가 발생했다.

다행히도, 처음 렌더링 직후에 setTimeout을 이용해 각 시간마다
함수를 미리 정의해놨기 때문에 동작에는 이상이 없었다.

하지만 만약 getWinNumbers 함수가 10초가 걸린다면?
우린 11초마다 당첨공을 표시할 수 있을 것이다.

이처럼, 복잡한 계산값 또는 오래걸리는 작업의 결과는 컴포넌트가 업데이트될 때 매번 실행되는게 아니라,
처음 실행되고 그 값을 어딘가에 저장하게끔 동작을 하는게 좋은데, 이걸 도와주는 녀석이 useMemo 함수이다.

useMemo를 이용하면, 위 예시는 이렇게 바뀐다.

const hookPractice = () => {
    const lottoNumbers = React.useMemo( () => getWinNumbers(), [ ] );
    const [winNumbers, setWinNumbers] = React.useState(lottoNumbers);

    ...
}

이렇게 되면, getWinNumbers()는 처음 한번만 실행되어 lottoNumbers에 저장되고
이후 컴포넌트 업데이트시에는 실행되지 않는다.

참고로 lottoNumbers에는 getWinNumbers()의 return 값이 저장된다.
함수 형식으로 표현되어야 하기때문에 헷갈릴 수 있다.

React.useMemo(함수, [ ]) 형식으로 사용하는데 익숙한 녀석이 보인다.
[ ], 배열, useEffect에서도 두번째 인자로 사용했었다. 가만보면 useEffect와 전달인자가 같은 점을 찾을 수 있다.
여기서도 비슷하게 사용되지만, 의미는 아래처럼 이해하는게 좋다.

"[ ]가 바뀌기 전까지 값을 기억(저장) "

기본적으로는 [ ] 안의 변수가 바뀌면 첫번째 인자로 전달된 함수를 실행하고,
그 반환값을 useMemo 함수의 반환값으로 전달하지만,
이는 달리말하면 [ ] 안의 변수가 바뀌기 전까지는 절대 전달된 함수가 실행될 일 없다는 뜻이다.
따라서 위와 같이 이해하는 것이 가능하다.




useCallback

useCallback도 useMemo와 비슷하다고 보면 된다. 대신 useMemo에서는 값을 저장하고 기억했다면,
useCallback은 함수를 기억하고 저장한다.

함수형으로 컴포넌트를 만들다 보면, 컴포넌트 안에 onClickBtn이라던지 함수를 정의할 일이 잦아진다.
이전에도 말했듯, 함수 컴포넌트는 다시 렌더링될 때, 함수 전체가 다시 실행되는데
이때 우리가 정의한 함수가 너무 길거나 해서 생성자체가 오래걸린다면? 뭔가 조치를 해야한다.
그래서 useMemo와 같이, useCallback을 이용하면 컴포넌트 안의 특정 함수를 기억해놓을 수 있다.

아래 예시를 보자

const onClickRedo = React.useCallback(() => {
    console.log('onClickRedo');
}, []);

사용법은 쉽다. 위에 보이는 것처럼 원하는 함수를 useCallback()으로 감싸기만 하면 된다.
두번째 인자 [ ] 추가도 잊지 말고. 두번째 인자 [ ] 는 useMemo와 동일하다.

"[ ] 안의 변수가 변하기 전까지 함수를 기억(저장)"

대신 첫번째 인자의 함수가 useMemo에서는 return값을 이용했는데,
useCallback에서는 기억하고 싶은 함수자체이다.

이 useCallback이 제대로 사용됐는지 확인은 쉽지않다. 함수가 생성되는 것을 확인할 수 없었기 때문이다.

여튼 저렇게 사용하면 컴포넌트가 처음 실행될 때, 사용자 정의 함수 자체를 생성하고 기억(저장)
컴포넌트 업데이트시, 기억(저장)된 함수를 불러와 실행! 할 수 있게된다.


useCallback을 이용할 때 몇가지 유의사항이 있다.

1. useCallback 안의 함수에서 state를 이용하려면 아래처럼 두번째 인자 [ ]안에 넣어줘야 한다.

const onClickRedo = React.useCallback(() => {
    console.log('onClickRedo');
    console.log(someState);
}, [someState]);

이유는 someState를 [ ]안에서 지워보고, 실행중에 someState가 변경되게끔 예제를 만들어보면 확인할 수 있다.

useCallback에 의해서 기억(저장)될 때의 someState를 그대로 기억하고 있기 때문에
someState의 값이 변경된 이후에 함수가 호출되어도, 변경되기 전의 값이 사용되기 때문이다.

따라서, useCallback 안에서 state를 이용하려고 할때는 꼭 두번째 인자 [ ] 안에 해당 state를 넣어줘야 한다.


2. 자식 컴포넌트에게 props로 함수를 전달하고자 한다면 무조건 useCallback을 써야한다.

부모 컴포넌트에 의해 자식 컴포넌트의 props가 변경되면 자식 컴포넌트도 같이 다시 렌더링이 일어난다.

부모 컴포넌트가 다시 렌더링 되면서 함수 전체가 재실행된다면, props로 넘겨준 함수도 새로 생성이 된다.
이때의 새로 생성된 함수와 props로 넘겨준 함수는 다르고, react에서 기본적으로 변경되었다고 인식하기 때문에
불필요한 자식 컴포넌트의 렌더링이 발생한다.

즉, 자식 컴포넌트의 쓸데없는 렌더링을 막기 위해,
props로 전달하는 함수는 무조건 useCallback을 이용해야한다.

React에서도 함수형 컴포넌트와 hook 사용을 권장(?)하기 때문에, 어렵지만서도 얼른 익숙해져야겠다.

0개의 댓글