카테고리 없음

React) useEffect

헬리이 2023. 2. 2. 21:49
728x90

앞에 Side Effect글과 이어서 useEffect에 대해정리해보겠다.

 

https://hayley-0616.tistory.com/17

이제 useEffect 차례다..

 

학습목표는 이것이었다..

- React 에서 Side Effect의 올바른 발생 시점 알기

- useEffect의 사용법 알기

- 조건부로 Side Effect 발생시키기

- Rendering & Effect Cycle 알기

- Clean Up Effect 알기

 

과연 나는 다 알았을 것인가.. 🙈😑🙈 

 

 

 

 

 React 에서 Side Effect의 올바른 발생 시점

 

- 렌더링을 Blocking 하지 않기 위해서 렌더링이 모두 다 완료되고 난 후 실행할 수 있어야 한다.

- 매 렌더링마다 실행되는 것이 아니라 내가 원할 때만 조건부로 실행할 수 있어야 한다.

 

 

위 두 가지 조건을 충족시키면서 발생시키는 것이 가장 좋은데,

다행히 React에서는 위의 요구사항을 모두 충족시키면서 편하게 side effect를 발생시킬 수 있게 도와주는 useEffect라는 훅(hook)이 이미 존재 한다고 한다. 

 

 

왜 렌더링이 모두 완료되고 난 후 실행되어야 할까??

렌더링 단계(UI를 만들어내는 과정)에서 side effect를 발생시키게 되면 두 가지 문제가 발생하기 때문이다.

 

1. side effect가 렌더링을 blocking 함            ->사용자와 짧은시간내에 interaction할수있는 시간을 너무 오래걸리게 된다.

2. 매 렌더링마다 side effect가 수행됨               -> state,props 변경될때마다 side effect가 발생되면 비효율적이다.

 

 

 

1. 랜더링 blocking을 하지않게 하기 위한 useEffect 이용 예시

const App=()=> {
  console.log("sideEffect2")
  console.log("render1");
//렌더링 확인 위해 return 바로위에 콘솔을 찍어봄 => 새로고침 할때마다 render가 찍힘 1
//그리고 JSX를 리턴하기 전 함수 본문에서 sideEffect실행 2  
  return (
    <>
    <h1>Hello!</h1>
    </>
  )
}

export default App;

여기서 useEffect 를 이용해서 렌더링을 blocking 하지 않고 렌더링 이후에 sideEffect가 발생되도록 하겠다.

 

 

 

 

useEffect사용법

useEffect는 함수이기 때문에 호출하는 방식으로 사용할 수 있다.

 

useEffect( 콜백함수) 

하나의 함수를 인자로 받고,  useEffect의 동작은 렌더링이 완료된 후에 콜백함수를 실행해 주는 것

이때, 콜백함수 : 단순히 어떤 동작을 하는 함수를 인자로 받는것

 

 

예시)

import React,{useEffect} from 'react';
// useEffect를 import 해주기

const App=()=> {
  console.log("sideEffect2")


  const printConsole= () => {
    console.log("SideEffect with useEffect")
  }
  useEffect(printConsole)

  //useEffect() 함수를 호출하는 시그널을 하나 만들고, 
  //전달될 콜백 함수 printConsole을 위에 작성했다.
  //그 후 useEffect()인자로 전달될 콜백함수를 넣어주었다. 

  console.log("render1");

  return (
    <>
    <h1>Hello!</h1>
    </>
  )
}

export default App;

console창

확인해보면 콘솔에 찍히는 순서는

 

 

1. 함수 본문 맨 윗쪽에서 실행한 sideEffect가 먼저 출력

 

2. 그 후 리턴문 위에있던  render 라는 콘솔이 출력됨

 

3. 마지막으로는 렌더링 후 useEffect를 이용해서 출력한 side Effect가 출력되었다.

 

 

 

즉, useEffect를 이용한 함수는  다른 함수보다 위에 있지만 다른 렌더링이 모두 완료되고 난 후에 실행할 수 있게 해주는 역할을 한다. 

= side effect가 blocking 하지 않는다. 

 

 

위에서는 useEffect에 들어갈 인자를 따로 선언했는데, 이렇게도 쓸 수 있다.

useEffect(()=>{
console.log("side effect with useEffect");
});

 

 

 

 

 

2. 매 렌더링마다 side effect가 실행되는것을 방지하기 위한 useEffect 이용 예시

import React, { useEffect, useState } from "react";

const App = () => {

  useEffect(()=>{
    console.log("side effect with useEffect")
  })
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  console.log("render")
  return (
    <>
      <div className="wrapper">
        <h1>count :{count}</h1>
        <button onClick={() => setCount(count + 1)}>up</button>
      </div>
      <input type="text" onChange={(e) => setText(e.target.value)} />
    </>
  );
};
export default App;
//useState를 2번 사용했다.
//첫 번째로는 count를 변경해주는 useState를 사용하여 초기값으로 0 을 주었고,
//<button>을 클릭할때마다 count state가 1씩 증가되도록 하였다.

//두 번째로는 text를 변경해주는 useState를 사용하여 초기값으로 빈 문자열을 주었다.
//<input>창이 변할때마다 text의 state가 변하도록 만들어 주었다.

//실제 변하는지 확인을 위해 return문 위에 console.log('render')을찍어보았고,
//useEffect를 이용하여 하나의 sideEffect를 발생시켜보았다.

새로고침 하자마자 render와 side effect가 한번 출력 된것을 볼 수있고,

 

count 나 text 값이 바뀔때마다 위에있는 함수들이 다시 호출될 것이고, 함수 본문에 있는 render와 side effect가 다시 매번 출력되게 되었다.

 

 

즉, 지금은 매 화면이 렌더링 될때마다 모든 함수가 실행되는것을 확인해보았다.

 

이 불필요한 작업을 해결하기 위한것이 또  useEffect를 활용한 것이다.

 

useEffect는 콜백 함수 외에 의존성 배열(dependencyArray)이라는 두번째 매개변수를 가진다.

의존성 배열은 이름에서부터 알 수 있듯이 배열의 형태이고, 이 배열이 바로 side effect의 발생 여부를 결정짓는 조건이다.

 

 

useEffect의 완전한 형태는 아래와 같다.

useEffect(콜백 함수, 의존성 배열);

 

 

useEffect의 의존성 배열을 인자로 전달하게 되면, 

 

useEffect는 처음동작으로 의존성 배열의 값이 이전 렌더링과 비교했을때 동일한지 검사를 하고, 함수 조건에 따라 콜백함수를 호출한다.

 

 

정리하자면, useEffect는 

첫번째 렌더링에서는 무조건 콜백함수를 호출하고,

두번째 렌더링부터는 의존성 배열의 값이 변했으면 콜백함수를 호출하고, 아니면 호출하지 않는다.

 

 

 

 

 

위의 코드를 이용한 예시

import React, { useEffect, useState } from "react";

const App = () => {

  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  useEffect(()=>{
    console.log("count changed")
  },[count])
//count의 값이 변했을때만 이 콜백함수를 호출하라 라는 의미
console.log("count", count);  
console.log('text',text)
console.log("render")
//각각 초기값으로 콘솔을 설정하고, 어떻게 찍히는지 확인
  return (
    <>
      <div className="wrapper">
        <h1>count :{count}</h1>
        <button onClick={() => setCount(count + 1)}>up</button>
      </div>
      <input type="text" onChange={(e) => setText(e.target.value)} />
    </>
  );
};
export default App;

 

첫 번째 렌더링때문에 콜백함수 side effect인 count changed가 마지막에 찍혔다.

 

count를 변화시켜서 리렌더링이 발생하였고, useEffect가 다시 호출되게 되었는데, 

 

이때, 의존성배열에 들어온 count의 값을 이전 렌더링과 비교하여 변한것이 확인되었기 때문에

 

count changed 라는콜백함수를 실행하고, 콘솔을 출력하게 되었다. 

 

그럼 [count]에 있는 count가 아닌 text의 값을 변경시켜보겠다.

 

명백하게 리렌더링은 되었고, count는 0, 텍스트는 1번 바뀌었고, 렌더링이 되었다는 render도 찍혔으나 한번 변화에서 count changed라는 콘솔은 찍히지 않았다.

 

 이렇게 의존성 배열을 전달하면 리엑트가 렌더링마다 의존성 배열을 검사하고, 이 값이 변했을때만 함수를 호출하게 되는것이 확인되었다. 

 

 

 

그러면, 반드시 첫 번째  렌더링 때에만 이펙트가 실행되기를 원하고, 두번째 부터 마지막까지 절대 호출되지 않기를 원하는 콜백함수가 있는경우에는 어떻게 표현할까?

 

 

-> useEffect의 의존성 배열 자리에 [ ]  빈 배열을 넣는 것이다.

 

 

 

: 빈 배열이 들어가게되면, 변화를 감지할 값이 없기 때문에 첫 번째 렌더링때만 이펙트가 호출되고, 그 이후에는 절때 실행되지 않게 된다. 

예시)

 useEffect(()=>{
    console.log("only first render")
  },[])

 

위의 함수에 이 useEffect를 추가를 해본다.

처음에는 only first render라는 이펙트가 발생했지만, 그 이후 다른값이 변화할때 에는 발생되지 않았다.

즉, 의존성 배열을 통해서 side effect가 매번 실행되게 하는것이 아니라, 원하는 시점에만 side effect를 실행할 수 있게 되었다. 

 

 

정리하자면, 

useEffect에서 첫 번째 인자인 콜백 함수는 실행시킬 동작을 결정하고

두 번째 인자인 의존성 배열은 실행시킬 타이밍을 결정짓는다.

 

 

 

 


Rendering & Effect Cycle 

   

 

이펙트가 useEffect가 발생하는 사이클과 컴포넌트의 렌더링과 함께 어떤 순서로 이루어 지는지 정리해보겠다.

 

  1. 컴포넌트가 렌더링 된다.  (최초로 진행되는 렌더링은 브라우저에 처음으로 이 컴포넌트가 보였다는 의미로 mount 라고 표현한다.)
  2. useEffect 첫 번째 인자로 넘겨준 콜백 함수가 호출된다. (Side Effect)
  3. 컴포넌트의 state 또는 props가 변경되었을 경우 리렌더링이 발생한다. (update)
  4. update가 확인이 되면, useEffect는 두 번째 인자에 들어있는 의존성 배열을 확인한다
a. 만약 의존성 배열이 전달되지 않았거나 / 의존성 배열 내부의 값 중 이전 렌더링과 비교했을 때 변경된 값이 하나라도 있다면 첫 번째 인자로 넘겨준 콜백 함수가 호출된다. (Side Effect)

b. 의존성 배열 내부의 값 중 이전 렌더링과 비교했을 때 변경된 값이 없다면 콜백 함수를 호출하지 않는다.

c. state 또는 props가 변경된다면 업데이트 다음에 useEffect의 의존성 배열을 확인하는  과정을 반복한다.

  1. 컴포넌트가 더 이상 필요 없어지면 화면에서 사라진다 의미로 unmount라고 표현한다.

 

 

 

 


Clean Up Effect

 

clean up?

: 기존에 발생했던 사이드 이펙트를 정리하고 치우는 것

 

어떤 종류의 side effect가 clean up이 필요할까?

그 기준은 해당 side effect가 지속적으로 남아있는가?를 생각해 보면 된다.

 

ex) 한번만 호출되는 이펙트

useEffect(()=>{
    console.log("Hello! friends!")
  })
  
 //이러한 함수로 console.log를 찍었을때 콘솔에는 한번밖에 호출되지 않아서 따로 clean up 을 해줄 필요가 없다.

 

 

 

ex) 지속적으로 호출되는 이펙트

useEffect(()=>{
    setInterval(()=>{
      console.log('interval');
    },1000);
  });
  //1000ms 마다 console.log('interval')을 출력하게 하는 함수를 만들었다.

이펙트가 실행되고 콘솔을 확인하니, interval 이라고 해서 계속 콘솔이 찍히고 있었다.

= 지속적으로 구독을 하면서 남아있는 이펙트이다. 

 

중요한것은 이 상태에서 다른 페이지로 넘어가도

이 interval 이펙트는 더이상 필요없어졌음에도 불구하고, 지속적으로 남아있어서 코드가 동작할 것이다. 

= 불필요한 동작에 자원을 낭비하며 비효율적으로 된다. 

 

이러한 이펙트 들이 clean up 이 필요한 이펙트 이다. 

 

 

 

clean up 하는 방법

: useEffect에 전달한 콜백 함수에서 clean up 을 하면서 동작하고 싶은 것을 함수 형태로 만들어서 return 한다.

 

 

ex) clean up 하기 전 예시 

import React, { useEffect, useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  useEffect(()=>{
    console.log("Hello! friends!")
  })
  useEffect(()=>{
    console.log('effect출력')
    const button=document.getElementById("consoleButton")
    const printConsole=()=>{
      console.log("button clicked")
    }
    button.addEventListener("click",printConsole)
  });
<<<<여기에 clean up>>>>>>>>>>


  console.log("render")


  return (
    <>
      <h1>Count: {count}</h1>
      <button onClick={()=>setCount(count+1)}>Up</button>
      <button id="consoleButton">print console</button>
    </>
  );
};
export default App;

//id가 consoleButton 인 버튼하나를 새롭게 추가해 주었고,이벤트리스너를 부착할 것이다.
//해당 eventlistener를 통해서 클릭이벤트가 발생할 때마다 'button clicked'라는 메세지를 출력하게 했다.
//그리고 해당 이펙트가 잘 출력되는지 확인하기 위해 'effect출력' 도 추가해 주었다.

첫번째 렌더링되었을 때
print console이라는 버튼을 눌렀을 때

여기서 count의 state 가 변경되어 리렌더링이 발생하게 되면 useEffect에 의존성배열을 따로 전달해 주지 않아서 매번 렌더링이 될 때마다 이펙트가 발생될 것이다. 

up버튼을 5번 누르고 print console을 눌러보았다.

리렌더링이 발생하면서 render, hello! firends! 가 찍혔고, 

 

effect가 한번더 발생해서 콘솔이 중첩되어 찍히는 것을 확인할 수 있었다.

 

 

 

여기서 clean up을 이용할 것인데, 

 

다음 effect가 발생하기 전에 기존에 부착해둔 eventListener를 제거 할 것이고,

 

그 후 새롭게 effect를 발생기키는 것을 할 계획이다. 

 

const cleanUp=()=>{
button.removeEventListener("click",printConsole)
};

//cleanUp 이라는 함수를 하나 만들고, 

//위에 이벤트리스너를 제거해 주기위해 removeEventListener 매서드를 활용했다.

//위 상태로는 clean up이 작동되지 않으니, return 해 주어야 한다.
return cleanUp;

위 함수를 처음 clean up 넣을 예정이었던 자리에 넣어주고 확인을 하게 되면, 

 

useEffect는 clean up함수를 다음 이펙트가 실행되기 전에 한번 호출해 준다.

 

그래서 원하는 동작으로 clean up 함수에 넣어두면, 다음 이펙트가 실행되기전에 이전 이펙트를 정리할 수 있게 되었다. 

 

return () => {
button.removeEventListener("click", printConsole);
};

변수를 선언하지 않고 이렇게 return에  바로 정의할수도 있는점 메모메모!

 

 

 


** 참고 clean up을 해야하는 함수들

a. setInterval (생길때 쓰는거)
a. clearInterval (clean up 할때 쓰는것)

b. setTimeout
b. clearTimeout

clear반대말이 set 이니까, set 하면 무조건 또 clear 해줘야한다.

비슷하게, add<>remove

 

 

 

 

728x90