프로필

프로필 사진
Popomon
Frontend Developer
(2020/12 ~)

    카테고리

    포스트

    [Frontend/React] 11. 리액트 훅(hook)

    2020. 10. 3. 11:58

    꿈가게: To Do List - iOS

    꿈가게: To Do List - Android

    리액트 훅(hook)이란?

    컴포넌트에 기능을 추가할 때 사용하는 함수를 말합니다. 리액트 훅은 리액트 16.8부터 추가되었습니다. 이전에는 클래스형 컴포넌트를 사용하였으나, 리액트 훅이 도입되고 부터는 모두 함수형 컴포넌트를 사용할 수 있게 되었습니다. 리액트 훅을 사용한 함수형 컴포넌트는 클래스형 컴포넌트보다 장점이 많으며 리액트 팀에서도 훅에 집중하고 있습니다. 대표적인 리액트 훅에는 상태값을 추가하는 useState 함수와 서버 API 호출 및 이밴트 핸들러 등록을 처리하는 useEffect 함수가 있습니다.

     


    useState 훅의 사용법

    useState 상태값은 특정 타입의 값 뿐만 아니라 객체나 배열 값으로도 초기화 할 수 있습니다. 이렇게 여러개의 상태값을 한번에 관리하기 위해서는 useReducer 라는 훅을 사용하는 것이 더 적합합니다. 이 훅은 이후에 설명하도록 하겠습니다.

     

    const [list, setList] = useState([]);
    const [count, setCount] = useState(0);
    const [state, setState] = useState({ list: [], count: 0 });

     


    useState 훅에서 상태값 변경함수의 비동기 처리

    useState 훅을 사용해서 정의되는 상태값 변경 함수는 비동기 처리로 진행됩니다. 따라서 상태 변경 함수가 한번에 여러번 호출 되더라도 1만 증가하게 됩니다.

     

    export default function App() {
      const [count, setCount] = useState(0);
      
      function onClick() {
        setCount(count + 1);
        setCount(count + 1);
      }
      
      console.log('렌더링 호출!!');
      
      return (
        <div>
          <h2>{count}</h2>
          <button onClick={onClick}>증가</button>
        </div>
      )
    }

     

    2회 호출한 값을 얻기 위해서는 상태값 변경 함수에 함수를 입력하는 방법으로 해결할 수 있습니다.

     

    export default function App() {
      const [count, setCount] = useState(0);
      
      function onClick() {
        setCount(v => v + 1);
        setCount(v => v + 1);
      }
      
      console.log('렌더링 호출!!');
      
      return (
        <div>
          <h2>{count}</h2>
          <button onClick={onClick}>증가</button>
        </div>
      )
    }

     


    리액트 요소 내부 함수의 일괄처리

    onClick 이라는 함수는 button 이라는 리액트 요소에 입력이 되어 있기 때문에, 일괄처리로 실행됩니다. 따라서 상태값 변경 함수가 여러번 호출되더라도 상태값이 변경 될 때마다 렌더링을 하는 것이 아니라 상태값이 다 변경 된 후 함수의 로직이 마무리 되면 그 다음 렌더링이 발생합니다.

     

    그런데, 아래와 같이 외부에서 정의된 함수에 useState로 정의된 상태 변경 함수를 호출 한다면 결과가 달라집니다. 이러한 경우에는 일괄 처리를 하지 못하기 때문에, 호출 할 때마다 렌더링이 발생합니다.

     

    export default function App() {
      const [count, setCount] = useState(0);
      
      function onClick() {
        setCount(v => v + 1);
        setCount(v => v + 1);
      }
      
      // 일괄처리 불가능으로 2회 렌더링
      useEffect(() => {
        window.addEventListener('click', onClick);
        return () => window.removeEventListener('click', onClick);
      })
      
      console.log('렌더링 호출!!');
      
      return (
        <div>
          <h2>{count}</h2>
          <button onClick={onClick}>증가</button>
        </div>
      )
    }

     

    외부에서 함수를 등록하여 사용하는 경우에도 일괄 처리를 하는 방법이 있습니다. 아래와 같이 unstable_batchedUpdates 함수를 사용하면 됩니다.

     

    export default function App() {
      const [count, setCount] = useState(0);
      
      function onClick() {
        ReactDOM.unstable_batchedUpdates(() => {
          setCount(v => v + 1);
          setCount(v => v + 1);
        })
      }
      
      useEffect(() => {
        window.addEventListener('click', onClick);
        return () => window.removeEventListener('click', onClick);
      })
      
      console.log('렌더링 호출!!');
      
      return (
        <div>
          <h2>{count}</h2>
          <button onClick={onClick}>증가</button>
        </div>
      )
    }

     


    useEffect 훅으로 불필요한 API 호출을 막기

    useEffect 내부에 적용되는 로직은 렌더링이 완료 된 후, 비동기로 호출이 됩니다. useEffect 내부에서 첫번째 인자로 전달되는 함수를 부수효과 함수라고 부릅니다. 하지만, 렌더링이 자주 되는 요소가 있을 수 있습니다.

     

    예를 들어보겠습니다.  로그인 후 아이디와 이름이 표시되는 Profile 컴포넌트가 있습니다. 페이지가 새로고침 될 때마다 API를 호출하여 로그인 정보를 받아와야 할까요? 이러한 불필요한 API 호출을 막아주기 위해서는 useEffect의 두번째 인자로 전달되는 배열이 필요합니다.

     

    아래와 같이 userId 라는 값을 2번째 인자인 의존성 배열에 집어 넣으면 userId가 변경될 때에만 useEffect 안에 있는 부수 효과 함수가 실행됩니다. 따라서 불필요한 API 호출을 막아줄 수 있습니다.

     

    export default function Profile({ userId }) {
      const [user, setUser] = useState(null);
      useEffect(() => {
        getUserApi(userId).then(data => setUser(data));
      }, [userId]);
      
      return (
        // ...
      );
    }
    
    function getUserApi(userId) {
      // ...
    }

     

    여기서 의존성 배열에는 어떤것들이 들어가야 할까요? 부수 효과 함수에서 사용한 변수를 잘 봐야 합니다. 컴포넌트의 상태값이나 속성값 또는 컴포넌트의 내부에서 정의된 지역변수나 지역 함수 같은 것들은 모두 의존성 배열에 작성을 해줘야 합니다.

     

    getUserApi는 외부에 있는 함수이기 때문에 입력을 하지 않아도 되고, userId는 속성 값이기 때문에 입력을 해 주어야 합니다. 그리고 setUser 상태값 변경 함수는 변하지 않는다는 것이 보장되기 때문에 예외적으로 입력하지 않아도 괜찮습니다. Profile 내부의 지역변수나 지역 함수를 부수 효과 함수 내에서 사용했다면 의존성 배열에 추가해야 합니다.

     

    의존성 배열은 꼭 필요한 경우에만 입력하는 것이 좋습니다.  추후에 의존성 배열을 어떻게 관리해야 하는가에 대한 내용도 언급을 하도록 하겠습니다.

     

     

     

    useEffect 훅으로 이벤트 정의하기

    useEffect 함수가 리턴하는 함수는 컴포넌트가 언마운트 될때 1회는 꼭 실행되는 함수입니다. 따라서 이벤트 함수의 트리거를 해제하는 함수를 호출합니다. 두번째 인자로 의존성 배열 값을 빈 배열을 넣어주면, 컴포넌트가 생성될때만 호출되며, 컴포넌트가 사라질 때만 리턴 함수가 호출됩니다.

     

    따라서 이벤트를 트리거 하고 해제 하는 패턴으로 아래와 같이 사용하실 수 있습니다.

    아래의 컴포넌트는 마운트 될 때 mount 로그가 찍히고 언마운트 될 때 unmout 로그가 찍힙니다. 그 외에는 width 상태값이 변경되어도 부수 효과 함수가 실행되지 않습니다.

     

    mount 1 (마운트)

    mount 2 (언마운트)

     

    export default function WidthPrinter() {
      const [width, setWidth] = useState(window.innerWidth);
      useEffect(() => {
        const onResize = () => setWidth(window.innerWidth);
        window.addEventListener('resize', onResize);
        console.log('mount');
        
        return () => {
          window.removeEventListener('resize', onResize);
          console.log('unmount');
        }
      }, []);
      return <div>{`width : ${width}`}</div>;
    }

     

    만약 의존성배열을 제거하면 렌더링 할 때마다 부수 효과 함수가 실행되게 됩니다.

     

    초기 렌더링 시, render 1 이 찍히고나서 다시 렌더링 될 때마다 이전에 반환된 함수를 실행해서 render 2 가 찍히고 렌더링 완료 후 다시 부수 효과 함수가 실행되어 render 1 이 찍힙니다.

     

    따라서 결과는 아래와 같습니다. 우선 초기 마운트 시에 render 1 이 찍힙니다. width를 변경 할 때마다 render 2 / render 1 두개가 번갈아가면서 찍힙니다.

     

    render 1 (마운트)

    render 2 (렌더링 1)

    render 1 (렌더링 1)

    render 2 (렌더링 2)

    render 1 (렌더링 2)

    render 2 (렌더링 3)

    render 1 (렌더링 3)

    render 2 (렌더링 4)

    render 1 (렌더링 5)

    ...

    render 2 (언마운트)

     

     

    export default function WidthPrinter() {
      const [width, setWidth] = useState(window.innerWidth);
      useEffect(() => {
        const onResize = () => setWidth(window.innerWidth);
        window.addEventListener('resize', onResize);
        console.log('render 1');
        
        return () => {
          window.removeEventListener('resize', onResize);
          console.log('render 2');
        }
      });
      return <div>{`width : ${width}`}</div>;
    }

     


    커스텀 훅 만들기

    이름은 use로 시작하도록 만들어줍니다. 기존에 프로필 정보를 불러오는 API 호출 컴포넌트를 사용하여 렌더링과 커스텀 훅으로 코드를 분할하여 작성해 보겠습니다. 기존 코드는 아래와 같습니다.

     

    export default function Profile({ userId }) {
      const [user, setUser] = useState(null);
      useEffect(() => {
        getUserApi(userId).then(data => setUser(data));
      }, [userId]);
      
      return (
        // ...
      );
    }
    
    function getUserApi(userId) {
      // ...
    }

     

    위의 코드를 분리하여, 커스텀 훅을 먼저 만들어 보겠습니다.

     

    export default function useUser(userId) {
      const [user, setUser] = useState(null);
      useEffect(() => {
        getUserApi(userId).then(data => setUser(data));
      }, [userId]);
      return user;
    }
    
    function getUserApi(userId) {
      // ...
    }

     

    기존의 코드에서 커스텀 훅을 사용하는 코드로 변경해 보겠습니다.

     

    export default function Profile({ userId }) {
      const user = useUser(userId);
      return (
        // ...
      );
    }

     

    상당히 직관적인 코드로 변경이 되었습니다.

     


    마운트 훅 만들기

    서버사이드 렌더링을 하는 경우에 유용하다고 하는 마운트 훅을 만들어 보도록 하겠습니다.

     

    export default function useMounted() {
      const [mounted, setMounted] = useState(false);
      useEffect(() => {
        setMounted(true);
      }, [])
      return mounted;
    }

     


    훅 사용 시 지켜야 할 규칙

    하나의 컴포넌트에서 훅을 호출하는 순서는 항상 같아야 합니다. 따라서 if / for / 함수 안에서 호출이 되면 안됩니다.

    훅은 함수형 컴포넌트 또는 커스텀 훅 안에서만 호출되어야 합니다.

     

     

     

     


    출처 : 실전 리액트 프로그리맹 - 인프런 (추천합니다 !!)

     

    실전 리액트 프로그래밍 - 인프런

    [실전 리액트 프로그래밍] 책의 저자 직강! 리액트의 기초부터 실전 활용법까지 익힐 수 있습니다. 초급 프레임워크 및 라이브러리 웹 개발 Front-End React Redux 웹 개발 온라인 강의 리액트(React)를 �

    www.inflearn.com