React - useEffect 부수기

김동하·2021년 2월 4일
2

react

목록 보기
17/31

모든 랜더링은 고유의 Prop과 State가 있다

  • count는 그저 숫자다.

state를 업데이트할 때마다, 리액트는 컴포넌트를 호출한다. 매 렌더 결과물은 고유의 counter상태 값을 살펴본다. 이 값은 함수 안에 상수로 존재하는 값이다. 랜더링 결과물에 숫자 값을 내장하는 것에 불과한 것!

  • setCount를 호출할 때, 리액트는 다른 count값과 함께 컴포넌트를 다시 호출한다. 그러면 리액트는 가산 최신의 랜더링 결과물과 일치하도록 DOM을 업데이트 한다.

  • 중요한 점은 여느 특정 랜더링 시 그 안에 있는 count 상수는 시간이 지난다고 바뀌는 것이 아니다!컴포넌트가 다시 호출되고, 각각의 랜더링 마다 격리된 고유의 count 값을 보는 것이다.

모든 랜더링은 고유의 이벤트 핸들러를 가진다

  • 이제 실험을 한다
  1. 카운터를 3까지 증가시킨다
  2. alert 버튼을 누른다
  3. setTimeout이 실행되기 전에 7까지 증가시킨다.

결과는 alert버튼을 클릭한 순간의 "3"을 보여준다. 즉, alert 버튼을 클릭할 때 state를 잡아둔 것이다!

count 값이 매번 별개의 함수를 호출마다 존재하는 상수값이기 때문에 우리의 함수는 여러번 호출되지만(랜더링 마다 한 번씩), 각각의 랜더링에서 함수 안의 count 값은 상수이자 독립적인 값(특정 랜더링 시의 상태)으로 존재한다.

모든 랜더링은 고유의 이펙트를 가진다

  • 그렇다면 useEffect가 어떻게 최신의 count값을 알고 있는 것일까?

count는 특정 컴포넌트 랜더링에 포함되는 상수라는 것을 알고 있다. 이벤트 핸들러는 그 랜더링에 "속한" count 상태를 본다. count는 특정 범위 안에 속하는 변수이기 때문! 변화하지 않는 이펙트 안에서 count 변수가 임의로 바뀌는 것이 아니라 이펙트 함수 자체가 매 랜더링마다 별도로 존재하는 것이다!

즉, 각각의 이펙트 함수는 그 랜더링에 "속한" props와 state를 본다!

모든 랜더링은 고유의… 모든 것을 가지고 있다

  • 과연 무슨 일이 일어날까?

우리가 생각한 그대로 console.log에 찍힌다. 하지만 class형은 다르다!

  • 만약 예제를 클래스 컴포넌트로 만들었다고 가정하면this.state.count는 특정 랜더링 시점의 값이 아니라 언제나 최신의 값을 가리킨다.

즉, 1, 2, 3이 아니라 최종 값을 누른 만큼 출력하는 것이다!

출처:https://rinae.dev/posts/a-complete-guide-to-useeffect-ko#tldr-too-long-didnt-read---%EC%9A%94%EC%95%BD

  • 이는 클로저로 고칠 수있다!

흐름을 거슬러 올라가기

정리하자면

컴포넌트의 랜더링 안에 있는 모든 함수는 (이벤트 핸들러, 이펙트, 타임아웃이나 그 안에서 호출되는 API 등) 랜더(render)가 호출될 때 정의된 props와 state 값을 잡아둔다.

만약 이벤트 안에 정의해둔 콜백에서 사전에 잡아둔 값을 쓰는 것이 아니라 최신의 값을 이용하고 싶을 때는 어떻게 할까? useRef를 사용해서 최신의 값을 불러보자!

클래스 컴포넌트에서는 이런 식으로 this.state에 값을 재할당한다. 미리 잡아둔 props, state와는 달리 특정 콜백에서 latestCount.current의 값을 읽어 들일 때는 언제나 같은 값을 보장하지 않는다.

그러면 클린업(cleanup)은 뭐지?

클린업의 본질은 되돌리는 것!

첫 랜더링에서ㅏ props가 {id:10}, 두 번째 렌더링에서 {id:20} 이라고 가정했을 때, 중요한 점은 리액트는 브라우저가 페인트 하고 난 뒤에야 이펙트를 실행한다는 것이다!

즉, 이전 이펙트는 새 props와 함께 리랜더링 되고 난 뒤에 클린업이 된다.

  1. 리액트가 {id:20}을 가지고 UI를 랜더링한다.
  2. 브라우저가 실제 그리기 시작한다. 화면에는 {id:20}이 반영된 UI를 볼 수 있다.
  3. 리액트가 {id:10}에 대한 이펙트 클린업을 한다.
  4. 리액트가 {id:20}에 대한 이펙트를 실행한다.
  • 이상한 점은 왜 prop이 {id:20}으로 바뀌고도 이젠 이펙트의 클린업이 여전히 예전 값인 {id:10}을 보는 걸까!?

정답은 바로

컴포넌트가 랜더링 안에 있는 모든 함수는 (이벤트 핸들러, 이펙트, 타임아웃이나 그 안에서 호출되는 API 등) 랜더가 호출될 때 정의된 props와 state 값을 잡아둔다.

  • 이펙트의 클린업은 "최신" prop를 읽지 않는다! 클린업이 정의된 시점의 랜더링에 있던 값을 읽는 것이다잉

라이프사이클이 아니라 동기화

  • 리액트는 우리가 지정한 props와 state에 따라 DOM과 동기화한다. 랜더링 시 “마운트” 와 “업데이트” 의 구분이 없다.

  • 이펙트도 같은 방식으로 생각하셔야 한다. useEffect 는 리액트 트리 바깥에 있는 것들을 props와 state에 따라 동기화 할 수 있게 한다!

위 코드는 묘하게 다르다. 렌더하는 jsx부분과 useEffect로 document.title이 동시에 존재!

컴포넌트의 마지막 결과물은 항상 같아야 한다. (props가 무엇이 오든 마지막 props를 렌더) 그렇다면 귀여운 컴포넌트 친구에게 무언가를 가르쳐보자.

리액트에게 이펙트를 비교하는 법을 가르치기

예를들어

<h1 className="Greeting">
  Hello, Dan
</h1>

요 친구를

<h1 className="Greeting">
  Hello, Yuzhi
</h1>

이렇게 바꾸면 리액트는 두 객체를 비교한다.

각가의 prop을 보고 children이 바뀌어서 DOM이 업데이트해야 함을 인지한다!

  • 예제

이펙트는 counter의 상태값을 사용하지 않는다. 이펙트는 document.titlename prop을 동기화하지만, name prop은 같습니다. document.title 을 매번 카운터 값이 바뀔때마다 재할당하는 것은 그다지 이상적으로 보이지 않는다! (어려움)

useEffect(() => {
  document.title = 'Hello, ' + name;
}, [name]); // 우리의 의존성

이렇게 불필요한 실행을 방지하기 위해 의존성 배열에 prop을 집어 넣는다.

이건 마치 우리가 리액트에게 “이봐, 네가 이 함수의 안을 볼 수 없는 것을 알고 있지만, 랜더링 스코프에서 name 외의 값은 쓰지 않는다고 약속할게.” 라고 말하는 것과 같습니다.

리액트에게 의존성으로 거짓말하지 마라

  • deps를 지정한다면, 컴포넌트에 있는 모든 값 중 그 이펙트에 사용될 값은 반드시 거기 있어야 한다. 예를 들어 데이터 불러오는 로직이 무한 루프에 빠질 수도 있고, 소켓이 너무 자주 반응할 수도 있습니다. 이런 문제를 해결하는 방법은 의존성을 제거하는 것이 아니다!

  useEffect(() => {
    fetchData();
  }, []); // 이게 항상 맞진 않다고 한다!!

의존성으로 거짓말을 하면 생기는 일

useEffect(() => {
  document.title = 'Hello, ' + name;
}, [name]);

요렇게 의존성 배열에 name 을 넣는다.

만약 의존성 배열에 []을 넣으면

출처 : https://rinae.dev/posts/a-complete-guide-to-useeffect-ko#tldr-too-long-didnt-read---%EC%9A%94%EC%95%BD

의존성을 솔직하게 적는 두 가지 방법

  • 예제

예를들어 매 초마다 숫자가 올라가는 카운터가 있다고 가정하자. 이때 의존성 배열에 []을 넣으면 한 번만 작동한다.

의존성을 솔직하게 적는 방법을 알아보자!

1. 컴포넌트 안에 있으면서 이펙트에서 사용되는 모든 값이 의존성 배열 안에 포함되도록 한다.

useEffect(() => {

  const id = setInterval(() => {
  
    setCount(count + 1);
  }, 1000);
  
  return () => clearInterval(id);
  
}, [count]);

count를 의존성 배열에 넣었다! 이제 count 값은 이펙트를 다시 실행하고 매번 다음 인터벌에서 setCount(count + 1) 부분은 해당 렌더링 시점의count 값을 사용할 것이다.

--> 여기서 문제는 count 값이 바뀔 때마다 인터벌은 해체되고 다시 설정되는 것!

2. 이펙트의 코드를 바꿔서 우리가 원하던 것 보다 자주 바뀌는 값을 요구하지 않도록 만드는 것이다.

그렇기 위해서 몇 가지 스킬을 살펴보장

이펙트가 자급자족 하도록 만들기

useEffect(() => {

  const id = setInterval(() => {
  
    setCount(count + 1);
  }, 1000);
  
  return () => clearInterval(id);
  
}, [count]);

이제 이펙트의 의존성에서 count를 제거해보자.

핵심 1 count를 쓰는 이유는 setCount를 위해서다. 이 경우에 스코프 안에서 count를 쓸 필요가 없다. 이전 상태를 기준으로 상태 값을 업데이트 하고 싶을 때는 setState함수 형태 업데이트를 사용하면 된다.

요렇게 콜백 형태로 setCount를 바꿔준다!

count 는 우리가 setCount(count + 1) 이라고 썼기 때문에 이펙트 안에서 필요한 의존성이었습니다. 하지만 진짜로 우리는 count 를 count + 1 로 변환하여 리액트에게 “돌려주기 위해” 원했을 뿐입니다. 하지만 리액트는 현재의 count 를 이미 알고 있습니다. 우리가 리액트에게 알려줘야 하는 것은 지금 값이 뭐든 간에 상태 값을 하나 더하라는 것입니다.

함수형 업데이트와 구글 닥스(Google Docs)

동기화에 대해 생각할 때 흥미로운 부분은 종종 시스템간의 “메세지”를 상태와 엮이지 않은 채로 유지하고 싶을 때가 있다는 것입니다. 예를 들어 구글 닥스에서 문서를 편집하는 것은 실제로 서버로 전체 페이지를 보내는 것이 아닙니다. 그렇다면 굉장히 비효율적이겠죠. 그 대신 사용자가 무엇을 하고자 했는지 표현한 것을 보냅니다.

오로지 필요한 최소한의 정보를 이펙트 안에서 컴포넌트로 전달하는게 최적화에 도움이 됩니다. setCount(c => c + 1) 같은 업데이터 형태는 setCount(count + 1) 보다 명백히 적은 정보를 전달합니다. 현재의 카운트 값에 “오염되지” 않기 때문입니다. 그저 행위(“증가”)를 표현할 뿐입니다. 리액트로 생각하기 문서에 최소한의 상태를 찾으라는 내용이 포함되어 있습니다. 그 문서에 쓰인 것과 같은 원칙이지만 업데이트에 해당된다고 생각하세요.

하지만 setCount(c => c + 1) 조차도 그리 좋은 방법은 아닙니다. 좀 이상해 보이기도 하고 할 수 있는 일이 굉장히 제한적입니다. 예를 들어 서로에게 의존하는 두 상태 값이 있거나 prop 기반으로 다음 상태를 계산할 필요가 있을 때는 도움이 되지 않습니다. 다행히도 setCount(c => c + 1) 은 더 강력한 자매 패턴이 있습니다. 바로 useReducer 입니다.

그의 등장.. 리듀서

액션을 업데이트로부터 분리하기

countstep 두 state가 있고 인터벌은 step의 입력값에 따라 count에 더한다.

여기서 의존성 배열에 step을 넣었다. 즉, step이 바뀌면 step만큼 count에 더해서 증가하는 것이다.

  • step 이 바뀐다고 인터벌 시계가 초기화되지 않는 것을 원한다면 어떻게 될까? 이펙트의 의존성 배열에서 step 을 제거하려면 어떻게 해야할까?

어떤 상태 변수가 다른 상태 변수의 현재 값에 연관되도록 설정하려고 한다면, 두 상태 변수 모두 useReducer 로 교체해야 할 수 있습니다.

setSomething(something => ...) 같은 코드를 작성하고 있다면, 대신 리듀서를 써보는 것을 고려하기 좋은 타이밍입니다. 리듀서는 컴포넌트에서 일어나는 “액션”의 표현과 그 반응으로 상태가 어떻게 업데이트되어야 할지를 분리합니다.

useReducer에 state와 dispatch를 가져오고 dispatch를 의존성 배열에 넣는다. 그리고 setCount 부분을 dispatch({ type: 'tick' })이렇게 대체한다!

리액트는 컴포넌트가 유지되는 한 dispatch 함수가 항상 같다는 것을 보장합니다. 따라서 위의 예제에서 인터벌을 다시 구독할 필요조차 없습니다.

이제 액션을 만들어보자. 이펙트 안에서 상태를 읽는 것 대신무슨 일이 일어났는지 알려주는 정보를 인코딩하는 것이 액션이고 액션을 디스패치한다! 이렇게 이펙트는 step 상태로부터 분리되어 있게 된다. 이펙트는 어떻게 상태를 업데이트 할지 신경쓰지 않고 단지 무슨 일이 일어났는지만 알려준다!!!! 짱이다!!

내가 여태까지 리덕스를 헛으로 배웠구나..

왜 useReducer가 Hooks의 치트 모드인가

여태까지 이펙트가 이전 상태를 기준으로 상태를 설정할 필요가 있을 때 어떻게 의존성을 제거하는지 공부했다. 하지만 다음 상태를 계산하는데 props가 필요하면 어떨까? 예를 들어 API가 <Count step={1} /> 인 것! 당연하지만 이럴 때 의존성으로 props.step 을 설정하는 것을 피할 순 없다.

--> 해답은 리듀서 그 자체를 컴포넌트 안에 정의하여 props를 읽도록 하면 된다!

너므 헷갈리는데. 일단 reducer 안에서 props에 접근할 수 있게 한 것이다. 이 경우 랜더링 간 dispatch의 동일성은 보장된다.

리액트는 그저 액션을 기억해 놓습니다. 하지만 다음 랜더링 중에 리듀서를 호출할 것입니다. 이 시점에서 새 props가 스코프 안으로 들어오고 이펙트 내부와는 상관이 없게 되죠.

이래서 useReducer 를 Hooks의 “치트 모드” 라고 생각하는 것입니다. 업데이트 로직과 그로 인해 무엇이 일어나는지 서술하는 것을 분리할 수 있도록 만들어줍니다. 그 다음은 이펙트의 불필요한 의존성을 제거하여 필요할 때보다 더 자주 실행되는 것을 피할 수 있도록 도와줍니다.

함수를 이펙트 안으로 옮기기

코드는 당연히 동작하지만 간단히 로컬 함수를 의존성에서 제외하는 헤결책은 컴포넌트가 커지면서 모든 경우를 다루고 있는지 보장하기 힘들다는 점이 있다!

이렇게 된다면 동기화에 실패한다! 해결하기 위해서는 함수를 이펙트 안에서 사용하면 된다!

이렇게 되면 이펙트 안에서 컴포넌트 범위 바깥에 있는 그 어떤한 것도 사용하지 않는다. 수정에 용이해지는 것!

하지만 저는 이 함수를 이펙트 안에 넣을 수 없어요

1. 함수가 컴포넌트 스코프 안의 어떠한 것도 사용하지 않는다면, 컴포넌트 외부로 끌어올려두고 이펙트 안에서 자유롭게 사용하면 됩니다.

이제 저 함수는 랜더링 스코프에 포함되지 않는다! 데이터 흐름에 영향을 받을 수 없기에 의존성 배열을 명시할 필요가 없어진 것이다.

2. useCallback

useCallback은 의존성 체크에 레이어를 하나 더 더하는 것! 즉, 문제를 다른 방식으로 해결하는데, 함수의 의존성을 피하기보다 함수 자체가 필요할 때만 바뀔 수 있도록 만드는 것이다!

  • 그렇다면 이 접근 방식이 왜 유용한지 생각해보자!

문제 입력을 받는 부분을 추가하여 임의의 query를 검색할 수 있다고 가정하자. 그래서 query를 인자로 받는 대신, getFetchUrl이 로컬 state로부터 읽어들인다!!!

으으 어렵다....

함수도 데이터 흐름의 일부인가?

이 패턴은 클래스 컴포넌트에서 사용하면 작동하지 않는다. 이것이 이펙트와 라이프사이클의 파이다!

fetchData도 클래스 메서드다. state가 바뀌었다고 메서드가 달라지지 않는다!

--> 해결 방법은 query 자체를 Child 컴포넌트에 넘기는 것 뿐!

클래스 컴포넌트에서, 함수 prop 자체는 실제로 데이터 흐름에서 차지하는 부분이 없습니다. 메소드는 가변성이 있는 this 변수에 묶여 있기 때문에 함수의 일관성을 담보할 수 없게 됩니다. 그러므로 우리가 함수만 필요할 때도 “차이” 를 비교하기 위해 온갖 다른 데이터를 전달해야 했습니다.

하지만 useCallback을 사용하면 함수는 명백하게 데이터 흐름에 포함된다!

--> useMemo도 있다.

결론 : useEffect를 더 자세히 알고 사용해보자!

출처 : https://rinae.dev/posts/a-complete-guide-to-useeffect-ko#tldr-too-long-didnt-read---%EC%9A%94%EC%95%BD

profile
프론트엔드 개발

0개의 댓글