2019-08-10 TIL

2019-08-10

React

신규 Lifecycle Method

getDerivedStateFromProps(props, state)

이놈 다음에 shouldComponentUpdate 호출 그 다음에 render 호출

state 오브젝트를 리턴해 state를 업데이트 하거나, null을 리턴해 업데이트를 하지 않도록 한다.

하나의 use case를 위해 존재한다고 하는데 그 케이스란, props의 변화에 의해 컴포넌트의 내부 스테이트를 업데이트 할 때.

예를 들면, offset prop에 기초해서 현재 스크롤 방향을 기록하는 것 / source prop에 명시된 외부 데이터를 로딩할 때

오랫동안 componentWillReceiveProps는 추가적인 렌더 없이 props변화에 따라 state를 바꿀 수 있는 유일한 방법이었다.

이젠 아니라는 말인데, props나 state가 변하면 getDerivedStateFromProps에서 새 state를 만들어 리턴한다. 그러면 그 state를 보고 shouldComponentUpdate에서 렌더를 새로 할지 안할지 정해줄 수 있다.

Hooks를 사용하면?

Hooks를 사용해서도 getDerivedStateFromProps를 구현할 수 있다는데 위 내용들을 보고 나서는 잘 와닿지 않는다. 아래 코드를 보자.

function ScrollView({row}) {
  let [isScrollingDown, setIsScrollingDown] = useState(false);
  let [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) { // 
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}

어차피 함수형 컴포넌트는 매 번 새로 렌더된다. 클래스 컴포넌트 처럼 어떤 조건에 의해 render 메서드의 호출을 선택할 수 없다. 위 코드의 의의는 조건문에 의해 state를 업데이트 여부를 조작할 수 있다는데 있는 듯 하다.

함수형 컴포넌트의 re-render 여부를 선택하는 방법은 없을까?

shouldComponentUpdate는 Hooks로..

Hooks로 하는건 아니지만 가능하다. 클래스 컴포넌트보다 제한적인 듯 하지만..

React.memo를 사용한다.

const Button = React.memo((props) => {
  // your component
});

감싸주면 이 컴포넌트에 들어오는 props를 shallow compare해 변화가 있을 경우에만 re-render한다.

성능 최적화를 위한 수단으로, 렌더를 방지하기 위해 사용하는 것은 버그로 이어질 수 있으니 하지 말란다.

어쨌든, 두번째 인자도 넣을 수 있는데 전/후 props를 받아 re-render 여부를 결정해줄 수 있다.

function MyComponent(props) {
  /* render using props */
}
function areEqual(prevProps, nextProps) {
  // return true or false
  // false를 리턴하는 조건일 때 re-render된다
}
export default React.memo(MyComponent, areEqual);

이 방법은 내부 state 변화 의한 렌더를 컨트롤할 수는 없다. 테스트해보니 areEqual을 true로 고정해 외부 props 변화에 의해 re-render되지 않게 만들었지만, 내부 state가 변하면 변경사항이 렌더됨을 확인했다.

아무래도 React.memo 입장에서는 컴포넌트에 input으로 들어오는 props만을 모니터링 할 수 있고 컴포넌트 내부의 값이 변하는 것을 알 수 없기 때문일 것임.

갑자기 useMemo

단순히 함수에 memoization 기능을 씌워줌.

const avg = useMemo(() => getAverage(list), [list]);

이러면 list가 변할때만 콜백을 실행하며, 콜백이 리턴하는 계산 결과가 avg에 할당된다. (예제에서 list는 배열. 엘리먼트들의 평균값을 리턴하는 함수)

그런데 일반적인 memoization은 이전에 계산한 적 있는 input에 대해서는 이전에 계산해 저장해둔 결과값을 바로 출력해주는데, useMemo는 구현 방식 상 여기까진 구현에 포함돼있지 않다. 물론 저장 및 조회 로직을 추가하면 구현할 수 있겠다.

useEffect for cleanup

아래처럼 적으면 mount시 콜백을 실행하고 unmout시 cleanup 함수를 실행한다.

useEffect(() => {
  console.log("useEffect called");
  return () => {
    console.log("useEffect cleanup !");
  };
}, []);   // deps로 빈 어레이를 넣는게 중요

deps 빈어레이가 없으면 클린업 함수가 unmount 말고도 update시에도 실행된다. 이게 굳이 TIL에 적은 이유.

(추가) Dan Abramov 블로그 중 UI 런타임으로서의 React

Effect 안의 함수가 다른 변수에 의존성이 있는 경우 기계적으로 위처럼 빈 배열을 넣는다면 버그가 생길 수 있다.

예를 들어 아래 코드는 버그 가능성이 높습니다.

 useEffect(() => {
   DataSource.addSubscription(handleChange);
   return () => >DataSource.removeSubscription(handleChange);
 }, []);

[]는 “절대로 이 Effect를 갱신하지 마”라는 의미이기 때문에 버그 가능성이 높습니다. Effect는 바깥에 선언된 handleChange가 바뀌더라도 다시 실행되지 않습니다. 그리고 handleChange는 다른 props나 상태를 참조할 수도 있습니다.

 function handleChange() {
   console.log(count);
 }

갱신을 허용하지 않는다면 handleChange는 계속 첫 번째 렌더링에 있는 상태를 참조해야 하고 count는 내부에서 항상 0이어야 합니다.

이 문제를 해결하기 위해서 의존성 배열에 명시하세요. 함수를 포함해서 모든 변할 수 있는 것들을요.

useEffect(() => {
  DataSource.addSubscription(handleChange);
  return () =>
    DataSource.removeSubscription(handleChange);
}, [handleChange]);

코드에 따라서 렌더링 할 때마다 필요 없는 handleChange 때문에 필요 없는 구독이 발생할 수도 있습니다. useCallbak훅을 통해 해당 문제를 해소할 수 있습니다. 혹은 매번 다시 구독하게 만들 수도 있습니다. 예를 들어 브라우저의 addEventListener 이벤트는 굉장히 빠르기 때문에 어설픈 최적화로 더 많은 문제가 발생할 수 있습니다.


Minchang Kim
Minchang Kim
웹/앱 개발자 김민창입니다! 좋은 하루 되세요!