ttaeng_99 2020. 11. 28. 16:58
반응형

🤷 Rendering 후에 사용되는 코드들

컴포넌트가 렌더링된 후에 사용되는 코드들이 있다. 이러한 코드들이 쓰이는 이유는 다음과 같다.

  • 백앤드 사이드에서 변경된 데이터를 패치
  • 데이터의 변동을 확인하기 위함
  • Timers(setTimeout 등) 및 Intervals(setInterval) 의 운용
  • 위 조건들에 따라 필요시 DOM에 변화를 주기 위해

 

useEffect() 역시 렌더링 후의 동작을 위해 사용되는 Hooks로, 클래스형 컴포넌트의 Lifecycle 중 componentDidMount(), componentDidUpdate(), componentWillUnmount() 의 기능을 담당한다고 생각하면 된다.


📒 useEffect()

useState와 마찬가지로 React 라이브러리에서 import를 해줘야 한다.(중괄호 {} 반드시!)

이후, useEffect(()=>{callback function}) 양식에 맞춰 이후 동작을 콜백함수 형태로 지정하면 된다.

import React, { useState, useEffect } from 'react';	// useEffect 가져오기

export default function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {					// useEffect 동작 정의
    alert(`Count: ${count}`)
  })

  const handleClick = () => {
    setCount((prevCount) =>  prevCount + 1);
  };


  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>
        Click me
      </button>
    </div>
  );
}

* 이러한 useEffect 내 동작을, 간단하게 'Effect'라고 칭한다.

 

- Cleanup Function(Effect 제거)

 

대표적으로 이벤트리스너가 있다. DOM에 렌더링하면서 부여한 이벤트리스너는, 컴포넌트 삭제시 같이 없애줘야 한다.

Cleanup 없이 남아있다면, 메모리 소모가 많아지며 re-render 됬을 때 이벤트리스너 중복으로 인한 버그의 위험도 있다.

 

그래서, Updating(re-render), Unmounting 단계의 useEffect() 함수는 Clean up Function 반환을 포함하는 경우가 많다.

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

export default function Counter() {
  const [clickCount, setClickCount] = useState(0);
  
  useEffect(() => {
    document.addEventListener('mousedown', increment);
    // Cleanup Function
    return () => {
      document.removeEventListener('mousedown', increment);
    }
  })

  const increment = () => {
    setClickCount(prevCount => prevCount + 1)
  }

  return (
      <h1>Document Clicks: {clickCount}</h1>
  );
}

Document에 마우스를 누르면 +1되는 이벤트리스너를 추가했는데, 지워주지 않으니 +2, +3, +4.. 로 증가하는 것을 볼 수 있었다.

이벤트리스너가 남아서 누적됨에 따라 발생한 현상이었다. 이를 위해, Cleanup Function을 반환해서 이전 Effect를 미리 삭제하는 것이다.

 

- Dependency Array

 

어떤 Effect는 최초 Mount 시에만 발동하고, 이후 re-render 시에는 미발동해야하는 경우가 있을 수 있다.

이처럼, Effect를 특정 시점에서 발동하기 위해 useEffect() Hooks에서 사용되는 개념이 Dependency Array이다.

 

1. undefined: 모든 render 및 re-render 시 작동

지금까지 사용된 코드들 참고!

 

2. [] 빈 배열: Effect가 최초에만 작동, 이후엔 필요가 없음

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

export default function Timer() {
  const [time, setTime] = useState(0);
  const [name, setName] = useState('');

  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime((time) => time + 1)
    }, 1000)
    return () => {
      clearInterval(intervalId)
    }
  }, [])				// * dependency array

  const handleChange = ({ target }) => {
    setName(target.value)
  }

  return (
    <>
      <h1>Time: {time}</h1>
      <input value={name} onChange={handleChange} type='text'/>
    </>
  );
}

<input>에 값을 입력하면 onChange 이벤트리스너로 인해 계속해서 re-render된다.

그로 인해, <h1> {time}을 useEffect()의 callback function이 계속 작동하면서 clearInterval이 간섭되는 것이다.

이를 방지하기 위해, useEffetct(callback, dependency array) 두번째 인자로 빈 배열([])을 넣어 최초 렌더링시에만 callback이 실행되도록 한다.

 

3. [variable]: 변수가 바뀌는 경우에만 Effect 작동

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]);			// count가 바뀐 경우에만 useEffect() callback 작동

📒 Rules of Hooks

  1. only call Hooks at the top level
  2. only call Hooks from React functions

Hook을 사용하면서, 위 2가지 규칙을 명심해야한다. Hook은 다른 구문에 포함되지 않은 상위레벨, Hook은 리액트 함수형에서만 운용.

 

1. Hook은 다른 구문에 포함되지 않은 상위레벨에 작성

//Wrong way (if 안에 effect hook) 
if (userName !== '') {
  useEffect(() => {
    localStorage.setItem('savedUserName', userName);
  });
}


//Right way
useEffect(() => {
  if (userName !== '') {
    localStorage.setItem('savedUserName', userName);
  }
});

* if (items) {useState()} 대신, useState(null)로 빈 기본값을 부여하는 방법.

* if (!items) {useEffect()} 대신, useEffect(callback, [])로 dependency array를 통해 비어있을 때(최초) 한번만 callback 작동하는 방법.

 

 

2. Hook은 리액트 함수형 컴포넌트에서만 운용 가능

  • Hook을 다른 Javascript 함수나 클래스형 컴포넌트에서 사용할 수 없다.
  • 단, Hook들을 활용한 Custom Hook 제작시 사용 가능하다. (참고: reactjs.org/docs/hooks-custom.html (공식사이트))

📒 Seperating Hooks의 이점

이 코드에서, useEffect() 부분을 주목하자. menu, news-feed, friends 세개 response를 1개 Effect로 일괄 처리중이다.

import React, { useState, useEffect } from 'react';
import { get } from './mockBackend/fetch';

export default function SocialNetwork() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // Promise.all로 3개의 response를 받는다.
    Promise.all([get('/menu'), get('/news-feed'), get('/friends')]).then(
      ([menuResponse, newsFeedResponse, friendsResponse]) => {
        setData({
          menu: menuResponse.data,
          newsFeed: newsFeedResponse.data,
          friends: friendsResponse.data
        });
      }
    );
  }, []);

  return (
    <div className='App'>
      <h1>My Network</h1>
      {!data || !data.menu ? <p>Loading..</p> : ( 	
        <nav>
          {data.menu.map((menuItem) => (
            <button key={menuItem}>{menuItem}</button>
          ))}
        </nav>
      )}
      <div className='content'>
        {!data || !data.newsFeed ? <p>Loading..</p> : ( 
          <section>
            {data.newsFeed.map(({ id, title, message, imgSrc }) => (
              <article key={id}>
                <h3>{title}</h3>
                <p>{message}</p>
                <img src={imgSrc} alt='' />
              </article>
            ))}
          </section>
        )}
        {!data || !data.friends ? <p>Loading..</p> : (  
          <aside>
            <ul>
              {data.friends
                .sort((a, b) => (a.isOnline && !b.isOnline ? -1 : 0))
                .map(({ id, name, isOnline }) => (
                  <li key={id} className={isOnline ? 'online' : 'offline'}>
                    {name}
                  </li>
                ))}
            </ul>
          </aside>
        )}
      </div>
    </div>
  );
}

 

해당 부분을 세 개의 Effect로 나눈 예시이다. 보시다시피, useState와 useEffect를 각각 사용했다.

기존처럼 하나의 Effect로 처리하는 것보다 코드 가독성, 데이터 관리 효율 및 안정성이 우수하다.

  const [menu, setMenu] = useState(null);
  useEffect(() => {
    get('/menu').then((response) => {
      setMenu(response.data);
    });  }, []);

  const [newsFeed, setNewsFeed] = useState(null);
  useEffect(() => {
    get('/news-feed').then((response) => {
      setNewsFeed(response.data);
    });  }, []);
    
  const [friends, setFriends] = useState(null);
  useEffect(() => {
    get('/friends').then((response) => {
      setFriends(response.data);
    });  }, []);

확실히, 함수형 컴포넌트가 클래스형에 비해 작성이 용이한 것을 느꼈다.

Hooks 기능은 위처럼 코드를 기능별로 분리할 뿐 아니라, 이를 통해 에러를 줄이고 사용이 용이하게 해준다.

 

하지만, class의 state 관리기능을 보완하기 위해 추가된 기능인 만큼 단점도 있을 것이고,

앞으로 리액트 프로젝트 간 두 컴포넌트를 비교하면서 이런 부분들을 알아봐야겠다.

반응형