[React] Lifecycle와 useEffect()

Lifecycle 개념

Lifecycle (생명주기)

여기에 Clock 컴포넌트가 하나 있다.

import React, { useState } from "react";

export default function Clock() {
	const [date, setDate] = useState(new Date());
    
    return (
    	<div>
        	<h1>Hello, world!</h1>
            <h2>It is {date.toLocalTimeString()}.</h2>
        </div>
    )
}

결과는 다음과 같다.

이 컴포넌트가 렌더링 되는 순간의 시간이 화면에 보여지게 된다.

이 말은 즉, 시간이 흘러도 화면의 시간은 멈춰있다.

 

그래서, 화면에 보여지는 시간을 1초마다 업데이트 해주고 싶다!

1초 마다 setDate(new Date())를 실행해서 화면을 업데이트 해주자!

javascript에서는 window.setInterval(() => {}, 1000); 을 사용했다.

 

과연 이 setInterval 함수는 Clock 컴포넌트 중 어디에 넣어야 할까?

import React, { useState } from "react";

export default function Clock() {
	const [date, setDate] = useState(new Date());
    
    window.setInterval(() => {
    	setDate(new Date())
    }, 1000)
    
    return (
    	<div>
        	<h1>Hello, world!</h1>
            <h2>It is {date.toLocalTimeString()}.</h2>
        </div>
    )
}

 

코드를 이렇게 짠다면 어떻게 될까?

Clock 컴포넌트에 렌더링 될 때 setInterval 함수가 등록이 된다.

1초 후에 setDate를 사용해서 state 값을 업데이트 하면 Clock이 re-rendering 된다.

그렇게 되면 setinterval 함수가 다시 등록이 된다. 

...

이런 방법은 옳지 않다!! ❌

 

이런 문제를 해결하기 위해서는 아래의 시점을 이해할 필요가 있다.

1. 컴포넌트가 처음으로 렌더링 될 때 (=DOM에 mount)

2. 컴포넌트가 DOM에서 빠졌을 때 (=unmount)

 

1번 시점에 window.setInterval()을 호출하고, 2번 시점에 window.clearInterval() 함수를 호출한다.

그렇다면 아까와 같이 setInterval 함수가 계속 걸리는 상황은 발생하지 않게 된다.

 

함수 컴포넌트와 클래스 컴포넌트의 차이점에 대해 알아보자.

함수 컴포넌트 클래스 컴포넌트
비교적 최근에 나옴 리액트 초기에 나옴
hook() 함수로
컴포넌트의 mount, unmount 되는 시점에 제어할 수 있음
생명 주기에 따른 메서드에 따라 제어할 수 있음

 

Lifecycle (생명 주기)

사이트 링크 : 

https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

 

React Lifecycle Methods diagram

Fully interactive and accessible React Lifecycle Methods diagram.

projects.wojtekmaj.pl

 

Lifecycle 클래스형 컴포넌트

Clock 컴포넌트 코드를 살펴보자.

import React from "react";

export default class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocalTimeString()}.</h2>
      </div>
    );
  }
}

코드 설명 

constructor : 초기화를 담당하는 영역. 우리가 사용할 state에 대한 정의를 constructor에서 한다.

render 함수를 실행하면 해당 컴포넌트가 첫번째 렌더에 실행이 된다.

 

constructor와 render 사이에 아래의 코드를 추가하자.

tick() {
    this.setState({
      date: new Date(),
    });
  }

tick은 클래스에 해당하는 메서드로, setState() 를 실행한다.

 

tick 함수 밑에 아래의 코드를 추가하자.

componentDidMount() {
    console.log("componentDidMount");
    this.timerID = setInterval(() => this.tick(), 1000);
  }

setInterval() 함수를 통해 1초에 한 번씩 tick 함수를 실행하도록 한다.

timerID는 state가 아니기 때문에 constructor에서 생성하지 않았다.

componentDidMount : component가 mount 되었을 때 실행되는 함수

 

여기서 tick 함수가 호출이 될 때, setState를 통해 다시 한 번 더 render 함수가 실행된다.

이 때 componentDidUpdate 함수가 호출된다. 

componentDidMount 아래쪽에 이 코드를 추가하자.

componentDidUpdate() {
    console.log("componentDidUpdate");
    console.log(this.state.date);
  }

state가 변경될 때마다 componentDidUpdate()가 실행될 것이다.

 

컴포넌트가 제거될 때, componentWillUnmount 함수가 호출된다.

componentDidUpdate 아래쪽에 이 코드를 추가하자.

  componentWillUnmount() {
    console.log("componentWillUnmount");
    clearInterval(this.timerID);
  }

 

심화내용은 https://ko.reactjs.org/docs/react-component.html 이 사이트를 참고하자.

 

useEffect() hook

https://ko.reactjs.org/docs/hooks-intro.html

 

Hook의 개요 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

Hook은 React 버전 16.8부터 React 요소로 새로 추가되었다. Hook을 이용하여 기존 Class 바탕의 코드를 작성할 필요 없이 상태 값과 여러 React의 기능을 사용할 수 있다.

import React, { useState } from 'react';

function Example() {
  // "count"라는 새로운 상태 값을 정의합니다.  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

 

Hook을 만든 이유

1. 컴포넌트 사이에서 상태 로직을 재사용하기 어렵다.

2. 복잡한 컴포넌트들은 이해하기 어렵다.

3. Class는 사람과 기계를 혼동시킨다.

 

어떤 기술이 '왜' 나왔는지 아는 것은 중요하기 때문에, 시간을 들여 Hook  소개 글을 읽어보자.

 

Hook 결론 : React로부터 Class를 제거할 계획은 없다.

class 컴포넌트를 봤을 때 무조건 함수 컴포넌트로 바꿀 필요는 없다.

결정적으로, Hook은 존재하는 코드와 함께 나란히 작동함으로써 점진적으로 적용할 수 있다.

 

오늘 배울 내용을 바로,

⚡ Effect Hook

React 컴포넌트 안에서 데이터를 가져오거나 구독하고, DOM을 직접 조작하는 작업을 이전에도 종종 해보았을 것이다.

우리는 이런 동작을 "side effects" (또는 짧게 "effects")라고 한다.

왜냐하면 이것은 다른 컴포넌트에 영향을 줄 수도 있고, 렌더링 과정에서는 구현할 수 없는 작업이기 때문이다.

 

Effect Hook, 즉 useEffect는 함수 컴포넌트 내에서 이런 side effects를 수행할 수 있게 해준다. 

React class의 componentDidMount나 componentDidUpdate, componentWillUnmount와 같은 목적으로 제공되지만, 하나의 API로 통합된 것이다. (useEffect와 이 세 가지 메서드를 비교하는 예시가 Using the Effect Hook 문서에 있다.)

 

useEffect는 어떻게 생겼을까?

export default function App(){
	useEffect(() => {
    
    }, [])
    
    return (
    	<div>App</div>
    )
}

useEffect의 첫번째 인자로는 함수, 두번째 인자론는 배열을 받는다.

두번째로 받는 배열 인자는 optional이기 때문에 받아도 되고, 안 받아도 된다.

 

두번째 인자(배열)을 사용했을 때 : componentDidMount처럼 동작

두번째 인자(배열)을 사용하지 않았을 때 : componentDidMount + componentDidUpdate처럼 동작

 

두번째 인자(배열)을 사용하지 않았을 경우는 잘 쓰지 않는다.

왜? 아래와 같이 useEffect를 사용할 수 있기 때문이다.

export default function App(props){
	const [state, setstate] = useState(initialState)
	useEffect(() => {
    
    }, [state, props.a])
    
    return (
    	<div>App</div>
    )
}

위의 코드는

componentDidMount + 특정 값이 변경되었을 때에만 해당하는 componentDidUpdate처럼 동작한다.

state나 props.a가 변경되었을 때 함수가 실행된다.

useEffect의 인자로 받는 함수가 인자로 받는 배열에 의존하고 있는 형태이다. 

➡ 이런 배열을 의존성 배열(Array dependencies)라고 한다.

 

그렇다면 componentWillUnmount는 어떻게 할까?

export default function App(props){
	const [state, setstate] = useState(initialState)
	useEffect(() => {
    	return () => {
        	cleanup
        }
    }, [state, props.a])
    
    return (
    	<div>App</div>
    )
}

 

useEffect 첫번째 인자로 어떤 함수를 return 해주면 component가 unmount될 때 return되는 함수를 실행한다.

이렇게 return하는 함수를 'component cleanup한다' 라고 한다.

component가 unmount 될 때, 깨끗이 정리해야하는 코드를 return 안에 적으면 된다.

 

+)

useEffect를 사용하기 위해서는 import를 해주어야 한다.

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

 

useEffect를 사용한 아래의 예시를 보자.

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);

  // componentDidMount, componentDidUpdate와 같은 방식으로  useEffect(() => {    // 브라우저 API를 이용하여 문서 타이틀을 업데이트합니다.    document.title = `You clicked ${count} times`;  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

document.title을 업데이트하는 코드를 왜 useEffect를 통해 업데이트를 하는 것일까?

컴포넌트 자체는 렌더링 전까지는 그저 '함수'에 불과하기 때문에, document에 접근할 수 없다.

그러므로, 한 번 렌더링이 된 후 document에 접근할 수 있다.

➡ useEffect를 사용하여 mount 후에 실행이 되도록 한 것이다.

✨ 데이터 가져오기, 수동으로 React 컴포넌트의 DOM을 수정하는 것은 보통 useEffect 안에 넣어서 사용한다.

 

 

정리(Clean-up)를 이용하지 않는 Effects

React가 DOM을 업데이트 한 뒤 추가로 코드를 실행해야 하는 경우가 있다.

네트워크 리퀘스트, DOM 수동 조작, 로깅 등은 정리(clean-up)가 필요 없는 경우들이다. 이러한 예들은 실행 이후 신경 쓸 것이 없기 때문이다.

이러한 경우에는 clean-up 코드를 작성하지 않아도 된다. (optional이다!)

 

effect를 이용하는 TIP

tip : 관심사를 구분하려고 한다면 Multiple Effect를 사용한다.

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

함수를 분리하는 것처럼 useEffect도 기능에 따라 분리하는 것이 코드 가독성에 좋다.

 

 

Effect를 건너뛰어 성능 최적화 하기

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // count가 바뀔 때만 effect를 재실행합니다.

count가 업데이트 될 때만 useEffect를 실행할 수 있다.

불필요한 컴포넌트의 re-rendering을 발생하지 않는다.

 

더 자세한 내용은 아래를 참고하자.

https://ko.reactjs.org/docs/hooks-effect.html

 

Using the Effect Hook – React

A JavaScript library for building user interfaces

ko.reactjs.org