[react] useEffect

useEffect

useEffect

의존성 배열

function *Profile*({ userId }) {
  const[*user*,*setUser*]=*useState*();
  *useEffect*(() => {
    *fetchUser*(userId)*.then*((data) => *setUser*(data));
  }, []);
}

의존성 배열에 빈 배열을 넣어준다면, component가 mount될 때만 실행이 된다.
하지만 위의 경우에는 userId가 변경되었을 때, useEffect가 실행되지 않기 때문에 문제가 생길 수 있다.

function *Profile*({ userId }) {
  const[*user*,*setUser*]=*useState*();
  *useEffect*(() => {
    *fetchUser*(userId)*.then*((data) => *setUser*(data));
  }, [userId]);
}

위의 코드에 userId를 의존성 배열에 넣으면 Props로 넘어오는 userId가 변경될 때마다, useEffect가 실행이 된다.

아래의 경우는 detail을 의존성 배열에 넣어주지 않아서, detail이 변경되었을때 useEffect가 실행이 되지 않는다. 이러한 실수를 막기위해서 eslint를 사용해주는 것이 좋다.

function *Profile*({ userId }) {
  const[*user*,*setUser*]=*useState*();
  const[*detail*,*setDetail*]=*useState*();
  *useEffect*(() => {
    *fetchUser*(userId, detail)*.then*((data) => *setUser*(data));
  }, [userId]);
}

만약 의존성 배열이 필요없다면? mount될 때만, 실행하고 싶다면 어떻게 사용할 수 있을까?

아래와 같이 useOnMounted hook을 만들어서 사용하면, 이를 명시적으로 표현할 수 있다. 오…

//*useOnMounted.js*
*import* React, {useState, useEffect} *from* 'react';
*export* *default* function *useOnMounted*(effect){
  *useEffect*(effect, []);
}

//*Profile.js*
*import* useOnMounted *from* 'hooks/useOnMounted';
function *Profile*({ userId }) {
  const[*user*,*setUser*]=*useState*();
  const[*detail*,*setDetail*]=*useState*();
  *useOnMounted*(() => {
    *fetchUser*(userId, detail)*.then*((data) => *setUser*(data)););
  //*useEffect(() => {*
  //*fetchUser(userId, detail).then((data) => setUser(data));*
  //*}, [userId]);*
}

의존성 배열을 입력하지 않는 경우? 클로저 문제 발생할 수 있다.

function *MyComponent*() {
  const[*num1*,*setNum1*]=*useState*(0);
  const[*num2*,*setNum2*]=*useState*(0);
  *useEffect*(() => {
    const*id*=*setInterval*(()=>*console.log*(num1,num2),1000);
    *return* () => clearInterval(id);
  }, [num1]);
  *return* (
    <div>
      <button *onClick*={() => *setNum1*(num1 + 1)}>+1</button>
      <button *onClick*={() => *setNum2*(num2 + 1)}>+1</button>
    </div>
  );
}

위의 컴포넌트는 버튼을 누를때, num1, num2 state가 변한다.
하지만, useEffect의 의존성 배열에는 num2를 누락하였다.
num2 변경되어서 MyComponent를 렌더링할때, useEffect 내부의 콜백함수는 이전의 콜백함수를 기억했다가 다시 사용하게 된다. 왜냐하면, num2는 의존성 배열에 없기 때문이다.

이는 클로저 문제가 발생하게 되는데, 콜백함수의 lexical ?? 은 이전의 num1, num2를 가리키고 있기 때문이다.

async/await

//*bad*
*useEffect*(*async* () => {
  const*data*=*await fetchUser*(userId);
  *setUser*(data);
}, [userId]);

//*good*
*useEffect*(() => {
  *async* function *fetchAndSetUser*() {
    const*data*=*await fetchUser*(userId);
    *setUser*(data);
  }
  *fetchAndSetUser*();
}, [userId]);
const*fetchAndSetUser*=*useCallback*(
*async*function*fetchAndSetUser*(){
const*data*=*await fetchUser*(userId);
*setUser*(data);
},
[userId]
);
*useEffect*(() => {
  *fetchAndSetUser*();
}, [fetchAndSetUser]);

의존성 배열을 사용하지 않고 관리하는 법

의존성 배열을 사용하게 되면 여러가지 관리해야할 포인트가 늘어난다.

아래의 코드 같은 경우 의존성 배열을 통해서 useEffect를 관리하는데, 이 경우 fetchAndSetUser 함수를 useCallback으로 만들어 두지 않는다면, 컴포넌트가 렌더링 될때마다, 새로운 fetchAndSetUser 함수가 만들어지기 때문에, useEffect가 계속 실행되게 된다.
그렇기 때문에 fetchAndSetUser를 useCallback을 통해서 만들어 주어야하고, 이 함수가 제대로 memoized 되기 위해서는 내부의 상태 값에 대한 state를 또 의존성 배열에 넣어 주어야한다.

const*fetchAndSetUser*=*useCallback*(
*async*function*fetchAndSetUser*(){
const*data*=*await fetchUser*(userId);
*setUser*(data);
},
[userId]
);
*useEffect*(() => {
  *fetchAndSetUser*();
}, [fetchAndSetUser]);

의존성 배열을 입력하지 않고 if문을 통해서 처리하면, 클로저 문제가 해결되고 관리해야할 포인트가 줄어든다. 즉, 내부에 있는 값이 최신의 값을 유지하고 있기 때문에 클로저 문제가 생기지 않고, useCallback을 통해서 함수를 관리하지 않아도 된다.

*async* function *fetchAndSetUser*() {
  const*data*=*await fetchUser*(userId);
  *setUser*(data);
}
*useEffect*(() => {
  if (!user || *user.*id !== userId) {
    *fetchAndSetUser*(false);
  }
});

##

이전의 상태값을 기반으로 계산이 필요한 경우에 의존성 배열이 필요한 경우도 있다.

하지만 아래의 코드는 count가 바뀔때 마다, addEventListener와 removeEventListener를 반복하게 된다.

const[*count*,*setCount*]=*useState*(0);
*useEffect*(() => {
  function *onClick*() {
    *setCount*(count + 1);
  }
  *window.addEventListener*("click", onClick);
  *return* () => *window.removeEventListener*("click", onClick);
}, [count]);

이를 의존성 배열 없이 사용하려면 어떻게 해야할까?

함수적 갱신(functional update)을 해주는 방식을 사용할 수 있다. 이렇게 하면 의존성 배열을 사용하지 않고도 가능하다.

함수적 갱신을 사용하게 되면, 이전의 값을 토대로 값을 계산할 수 있기 때문에, 아래와 같이 빈배열을 넣어 mount될 때, 그리고 unmount 될때만 이벤트 리스너를 추가/해제 할 수 있게 된다.

const[*count*,*setCount*]=*useState*(0);
*useEffect*(() => {
  function *onClick*() {
    *setCount*((prev) => prev + 1);
  }
  *window.addEventListener*("click", onClick);
  *return* () => *window.removeEventListener*("click", onClick);
}, []);

##

function *Timer*({ initialTotalSeconds }) {
  const[*hour*,*setHour*]=*useState*(0);
  const[*minute*,*setMinute*]=*useState*(0);
  const[*second*,*setSecond*]=*useState*(0);
  *useEffect*(() => {
    const*id*=*setInterval*(()=>{
if(second){
}elseif(minute){
}elseif(hour){
}
},1000);
    *return* () => clearInterval(id);
  }, [hour, minute, second]);
}

위의 코드는 타이머를 보여주는 컴포넌트인데, 매초 second가 변경되기 때문에,
useEffect에서 계속 setInterval, clearInterval이 반복된다.

function *Timer*({ initialTotalSeconds }) {
  const[*state*,*dispatch*]=*useReducer*(reducer,{
*hour*:*Math.floor*(initialTotalSeconds/3600),
*minute*:*Math.floor*(initialTotalSeconds%3600/60),
*seconds*:*Math.floor*(initialTotalSeconds%60)
})
  const{*hour*,*minute*,*second*}=state;
  *useEffect*(() => {
    const*id*=*setInterval*(dispatch,1000);
    *return* () => clearInterval(id);
  }, []);
}

function *reducer*(state){
  const{*hour*,*minute*,*second*}=state;
  if(second){
    } else if (minute) {
    } else if (hour) {
    }
  }, 1000);
}

##

내용은 그대로인데, onClick 함수가 자주 변경되는 경우가 많다. 그래서 컴포넌트가 리렌더링 될 수가 있다.

function *MyComponent*({ onClick }) {
  *useEffect*(() => {
    *window.addEventListener*("click", () => {
      *onClick*();
    });
  }, [onClick]);
}
function *MyComponent*({ onClick }) {
  const*onClickRef*=*useRef*();
  *useEffect*(() => {
    *onClickRef.*current = onClick;
  });
  *useEffect*(() => {
    *window.addEventListener*("click", () => {
      *onClickRef.current*();
    });
  });
}

**주의** 아래와 같은 방식으로 ref를 수정해서는 안된다. 차후의 React에서는 렌더링이 중간에 취소될 수 있는데, (concurrent mode)
이렇게 사용되면 ref에 잘못된 값이 저장될 수 있다고 한다.

function *MyComponent*({ onClick }) {
  const*onClickRef*=*useRef*();
  *onClickRef.*current = onClick;
}

#React/hooks/use-effect