ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React.js] Hooks - useEffect()
    Front-End(Web)/React - 프레임워크(React, Next) 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 관리기능을 보완하기 위해 추가된 기능인 만큼 단점도 있을 것이고,

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

    반응형

    'Front-End(Web) > React - 프레임워크(React, Next)' 카테고리의 다른 글

    [React.js] Inline Style  (0) 2020.11.30
    [React.js] Stateful vs Stateless Components  (0) 2020.11.29
    [React.js] Hooks - useState()  (0) 2020.11.27
    [React.js] Lifecycle  (0) 2020.11.27
    [React.js] State  (0) 2020.11.26
Designed by Tistory.