[wecode]세션정리-Side Effect/useEffect/Clean up Effect

🙋🏻‍♀️·2022년 5월 17일
0

wecode

목록 보기
35/40

📖20220516 useEffect 오후세션

  1. useEffect훅을 활용해 다양한 Side Effect(부수효과를 일으킬 수 있다.
  2. React에서 일어나는 UI RenderingSide Effect(부수 효과)의 차이를 구분하여 설명할 수 있다.
  3. useEffect Hook을 활영해 원하는 타이밍(의존성 배열)에 Side Effect(부수 효과)를 일으킬 수 있으며, 이 과정에서 일어나는 컴포넌트 렌더링 과정을 설명할 수 있다.



1. Side Effect

✍️멘토님 몸풀기 함수

/* 템플릿 리터럴로 네임을 출력하는 함수*/
const introduce = (name) => `Hello ${name};
// input: name;
// output: Hello(name);
// introduce name을 인풋으로 받아서 Hello name을 리턴하는 함수
const sum = {num1, num2} => num1+num2;
// input: num1, num2
// output: num1 + num2
// sum함수는 num1, num2를 인풋으로 받아서 num1+num2를 리턴하는 함수
const Component = {state, props} => JSX
// input: state,props
// output: JSX
// 리액트 컴포넌트를 한문장으로 정의하자면 state,props를 받아서 JSX로 리턴하는 함수이다.



1. What is Side Effect?

1-1. Rendering in React

React에서 함수 컴포넌트의 rendering이란 state, props를 기반으로 UI 요소를 그려내는 행위이다. 렌더링의 결과물은 UI요소, 그러니까 화면에 JSX 문법으로 무엇이 나타날지를 적어둔 컴포넌트들이라고 할 수 있다. 렌더링 결과물에 영향을 주는 요소는 state와 props이다. 이를 달리 표현하면 (state,props)=>UI라고도 표현할 수 있다. 즉 input(state&props)에 따라 다른 output(UI)을 표현하는 함수(function)와도 구조적으로 동일하다.


(위 그림은 왼쪽에서 출발한 데이터가 render라는 함수를 거쳐 오른쪽의 JSX 요소로 리턴된 예시이다.)



1-2. Side Effect

부작용, 부수 효과라고도 부르는 Side Effect는 일상 생활의 맥락에서는 부정적인 의미로 사용되는 경우가 많다. 예를 들면 "감기약을 먹었는데 졸음이 오는 부작용이 있다"고 표현하는 경우가 이러한 용법에 해당한다. 하지만 프로그래밍 측면에서의 Side Effect는 단순히 부정적인 의미로만 사용되지 않는다. 아래 인용처럼 함수가 어떤 동작을 할 때, input-output 이외의 다른 값을 조작한다면, 이 함수에는 Side Effect(부수 효과)가 있다고 표현한다.

컴퓨터 과학에서 함수가 결과값 이외에 다른 상태를 변경시킬 때 **부작용**이 있다고 말한다.(wikipedia)

let count = 0

function greetWithSideEffect(name) { //input
 count = count + 1 // Side Effect! (함수 외부에 있는 값)

return ${name}님 안녕하세요! // output
}

greetWithSideEffect()라는 함수는 이름을 받아 인삿말을 리턴하는 함수이다.

하지만 이 함수는 단순히 input과 output만 존재하는 함수가 아니다. 실행하는 중간에 함수 외부 세계에 있는 count라는 변수를 조작한다. 이는 함수의 결과값이 이외의 다른 상태를 변경시키는 행위에 해당하므로 Side Effect가 있다고 할 수 있다.

이러한 Side Effect는 React의 함수 컴포넌트에서도 일어날 수 있다.

그렇다면 함수 컴포넌트에서의 Side Effect는, 렌더링이 아니고 외부 세계에 영향을 주는 어떠한 행위이다.

대표적으로 Data Fetching(백엔드에서 API 통해서 데이터 가져오는것=>외부에서 가져옴..리액트 내부에서 가져오는것 아님) , DOM에 직접 접근(ex. 외부에 있는 Event Listener 등록,리액트에선 JSX쓰기 때문에 DOM에 직접 접근할 일이 거의 없음. 근데 특정 상황에서 DOM에 직접 접근해야 하는 상황이 생김.. 근데 DOM은 컴포넌트 안에 있는 값이아님), 구독(ex. setInterval=> 주기적으로 실행해주는 함수. 어떤걸 넣으면 시간에 따라서 실행되는 함수.시간의 흐름을 구독)과 같은 행위들이 있다. 이들은 모두 컴포넌트에서 꼭 필요한 대표적인 Side Effect들이다.




const sumOne = (num) => {
return num+1;
}
// 1 출력
// input output
// 본질적인 역할만 하기 때문에 side effect (x), pure function

const sumOne = (num) => {
console.log("num =", num)

return num+1;
}
// side effect(x) 
// console은 외부에 있는 어떤 물체. 
// 접근, 읽어오거나, 수정하거나 => side effect
// 외부에 있는 콘손 log

const plusNum = 30;
const sumNum = (num) => num + plusNum;
// 함수 바깥에 있는 plusNum을 읽어서 가져왔기때문에
// sideEffect (O)

let result = 0;

const sum = (num) => {
result = result + num;
}
// sideEffect (O)
// 함수 바깥에 있는걸 읽어옴




2. useEffect

✍️위 프로젝트엔 2개의 경로가 존재함("/" => 로 이동, "/second" => 로 이동

2. Effect Hook

2-1. useEffect

이전 챕터에서 side Effect에 대해서 알아보고, 함수형 컴포넌트의 side Effect는 어떤 것들이 있는지 알아보았다. 하지만 이런 Effect들을 함수의 body 자리(render)에서 실행시키면 안된다.
앞서 함수 컴포넌트의 리턴 값은 UI 요소라고 설명했고, state, props의 변화가 있을 때마다
함수가 실행된다고 했다. 이 말은 매 렌더링 때마다 함수 body에 있는 로직이 실행된다는 뜻이다.
또한 렌더링과 무관한 로직이 렌더링 과정에서 실행되기 때문에 렌더링 자체에 영향을 줘 성능 상 악영향을 끼칠 수도 있다.

function greeWithSideEffect({name}) { //
// Bad!
 document.title = `${name}님 안녕하세요!`; // Side Effect

 return <div>{`${name}님 안녕하세요!`}</div> // Output
 }

그래서 React에서는 이런 Side Effect를 일으키기 적절한 장소로서 useEffecthook을 제공한다.
공식 문서에서도 useEffect를 "React의 순수한 함수적인 세계에서 명령적인 세계로의 탈출구로 생각하세요"라고 설명하고 있다.
여기서 "순수한 세계"란 렌더링(Input->output)을 뜻하고, 렌더링 이외에 일으켜야 하는 Side Effect를 일으킬 탈출구로 useEffect를 사용하라는 의미이다.
useEffect는 Side Effect를 렌더링 이후에 발생시킨다.(예외:useLayoutEffect). useEffect가 수행되는 시점에 이미 DOM이 업데이트 되었음을 보장한다는 뜻이고, 바꿔 말하면 Side Effect가 렌더링에 영향을 주지 않도록 설계되었음을 의미한다.

import { useEffect } from `react`;

function greeWithSideEffect({ name }) { // Input
 useEffect(() => {
 	// Good!
    document.title = `${name}님 안녕하세요!`; // Side Effect
 }, [name]);

 return <div>{`${name}님 안녕하세요!`}</div>; // Output
}

만약 Side Effect 이후 업데이트 된 정보가 있어 새롭게 화면이 그려져야 할 경우(state, props의 업데이트) 새롭게 렌더링을 일으킨다.
함수 컴포넌트는 최신 state와 props를 반영한 화면을 리턴하게 된다. Effect를 일으킬 타이밍은 앞서 설명했던 useEffect의 두 번째 인자인 의존성 배열(Dependency Array)를 통해 표현하게 되는데, 이에 대해 조금 더 자세히 살펴보자.

2-2. Rendering Cycle with useEffect
useEffect는 다음과 같은 형태로 사용한다.
두 번째 인자에 감지할 값을 배열로 넘겨주게 되면 해당 값들이 변경되었을 때만 실행하게 할 수도 있다.

import { useEffect } from "react"

// 사용법
useEffect (실행시킬 동작, [타이밍] )
document.addEventListener("타이밍", 실행시킬 동작) // 추상화 한 예시

// 매 렌더링마다 Side Effect가 실행되어야 하는 경우
useEffect(() => {
 // Side Effect
})

// Side Effect가 첫 번째 렌더링 이후 한번 실행되고,
// 이후 특정 값의 업데이트를 감지했을 때마다 실행되어야 하는 경우
useEffect(() => {
 // Side Effect
},[value])

// Side Effect가 첫 번째 렌더링 이후 한번 실행되고,
// 이후 어떤 값의 업데이트도 감지하지 않도록 해야 하는 경우
useEffect(() => {
 // Side Effect
},[])

함수 컴포넌트의 렌더링은 기본적으로 아래 순서대로 일어납니다.

  1. 컴포넌트가 렌더링 됩니다. 최초로 진행되는 렌더링은 브라우저에 처음으로 이 컴포넌트가 보여졌다는 의미로 mount 라고 표현합니다.
  2. useEffect 첫 번째 인자로 넘겨준 함수(callback)가 실행됩니다(Side Effect).
  3. 다시 렌더링(re-render)이 일어납니다 (state나 props가 변경된 경우)
  4. useEffect는 두 번째 인자에 들어있는 의존성 배열을 체크합니다.
    4-1. 만약 두 번째 인자에 아무런 값도 넘기지 않았거나 / 인자로 넘긴 배열에 들어있는 값 중 업데이트된 것이 하나라도 있다면 첫 번째 인자로 넘겨준 함수(callback)가 실행됩니다(Side Effect).
    4-2. 하나도 없거나 빈 배열이라면, 아무런 일도 하지 않습니다.
  5. 만약 앞에서 일으킨 Effect에서 state나 props를 변경시켰다면 다시 렌더링이 일어납니다.
  6. (중략...)
  7. 컴포넌트가 필요 없어지면 화면에서 사라집니다. 컴포넌트가 브라우저의 화면에서 사라졌다는 의미로 unmount라고 표현합니다.





3. Clean up Effect

3-1. Render->Effect Callback->Clean up!

마지막으로 Effect 이후 일어나는 Cleanup Effect에 대해서 알아보자.
Cleanup Effect는 간단하게 설명하자면, 이전에 일으킨 Side Effect를 정리할 필요가 있을 때 사용한다. 앞에서 예시로 들었던 Side Effect 중 조금 더 익숙할 이벤트 리스너 케이스를 다시 한번 살펴보자. 다음 코드는 페이지에 스크롤 이벤트가 일어날 때마다 console에 현재 스크롤이 위치한 Y 좌표를 출력한다. Side Effect이므로 useEffect안에서 사용하고, 이벤트 리스너는 한번만 등록하면 되기 때문에 의존성 배열에는 빈배열을 넣어주었다.

useEffect(()=> {
 function handleScroll() {
  console.log(window.scrollY)
 }
 document.addEventListener("scroll",handleScroll)
 }, [])

하지만 이 페이지를 벗어났을 때 이 이벤트 리스너는 더 이상 필요없어질 수 있다. 이 경우 일으켰던 Effect를 정리해줘야 한다. 이 때마다 Cleanup Effect를 일으킬 수 있도록 useEffect 안에 해당 로직을 정리하는 동작을 정의해두면 된다. 아래 코드에서 하이라이트 해둔 부분처럼.

useEffect(()=> {
 function handleScroll() {
  console.**log**(**window**.scrollY)
 }
 document.addEventListener("scroll",handleScroll)
 **return** () => {
  document.removeEventListener("scroll",handleScroll)
 }
 }, [])

주의할 점은 단순히 컴포넌트가 생성되고, 사라지는 시점에만 Cleanup Effect가 실행되는 건 아니다. 다음 Effect가 일어나기 전에, 이전 Effect의 영향을 정리해줘야 한다는 컨셉을 꼭 기억하자!
다음 예시 코드는 state가 업데이트 될 때마다 Effect를 일으키고, 다음 Effect가 일어나기 전에 Cleanup Effect를 일으킨다.

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

 console.log("render",count);

 useEffect(() => {
  console.log("useEffect Callback",count);
  return () => {
  console.log("cleanUp",count);
 });
}, [count]);

 return <div onClick={()=>setCount(count+1)}>하잉</div>
}

export default Foo;
// 정답코드
render, 0
 useEffect Callback, 0

 // 클릭

 render, 1
 cleanUp, 0
 useEffect Callback, 1




다이어그램을 통해 다시 한번 Effect의 순서를 확인하면서 설명을 마무리하겠습니다.
Render와 Clean Up, Effect 사이의 관계를 주의 깊게 살펴주세요.

3-2. FAQ

하나의 useEffect 안에서 로직을 처리하기 힘들어요! (공식 문서)

여러 개의 useEffect를 사용하면 됩니다.

팁: 관심사를 구분하려고 한다면 Multiple Effect를 사용합니다 Hook이 탄생한 동기가 된 문제 중의 하나가 생명주기 class 메서드가 관련이 없는 로직들은 모아놓고, 관련이 있는 로직들은 여러 개의 메서드에 나누어 놓는 경우가 자주 있다는 것입니다.
(...) Hook을 이용하여 이 문제를 어떻게 해결할 수 있을까요? State Hook을 여러 번 사용할 수 있는 것처럼 effect 또한 여러 번 사용할 수 있습니다. Effect를 이용하여 서로 관련이 없는 로직들을 갈라놓을 수 있습니다

0개의 댓글