[React] Hook(훅)

문지은·2023년 7월 13일
0

React

목록 보기
6/24
post-thumbnail

Hook

  • 리액트의 훅은 리액트의 state와 생명주기 기능에 갈고리를 걸어 원하는 시점에 정해진 함수를 실행되도록 만든 것
  • 이때 실행되는 함수를 훅이라고 함
  • 훅의 이름은 모두 use로 시작함

useState()

  • state를 사용하기 위한 훅
  • 함수 컴포넌트에서는 기본적으로 state라는 것을 제공하지 않기 때문에 클래스 컴포넌트처럼 state를 사용하기 위해서는 useState() 훅을 사용해야 함

사용방법

const [변수명, set 함수명] = useState(초깃값);
  • 파라미터로 state의 초깃값을 넣어 함수를 호출하면 리턴 값으로 배열이 나옴
  • 리턴된 배열에는 두 가지 항목이 있음.
    • state로 선언된 변수
    • 해당 state의 set 함수

예시

  • 아래 코드의 Counter 컴포넌트는 버튼을 클릭하면 카운트를 하나씩 증가시키고 현재 카운트를 보여주는 컴포넌트
import React, { useState } from "react";

function Counter(props) {
	var count = 0;
  	
  	return (
    	<div>
        	<p>{count}번 클릭했습니다.</p>
        	<button onClick={() => count++}>
              클릭
        	</button>
        </div>
    );
}
  • 이렇게 카운트를 함수의 변수로 선언해서 사용하게 되면 버튼 클릭 시 카운트 값을 증가시킬 수는 있지만, 재렌더링이 일어나지 않아 새로운 카운트 값이 화면에 표시되지 않을 수 있다.
    • 따라서 이런 경우에는 state를 사용해서 값이 바뀔 때마다 재렌더링이 되도록 해야한다.
  • 그렇다면 useState() 함수를 사용하는 코드로 바꿔보자.
    • 카운트 값을 state로 관리할 수 있다.
    • 버튼이 눌리면 setCount() 함수를 호출해서 카운트를 1 증가시키고, count 값이 변경되면 컴포넌트가 재렌더링되면서 화면에 새로운 카운트 값이 표시된다.
import React, { useState } from "react";

function Counter(props) {
	const [count, setCount] = useState(0);
  	
  	return (
    	<div>
        	<p>{count}번 클릭했습니다.</p>
        	<button onClick={() => setCount(count+1)}>
              클릭
        	</button>
        </div>
    );
}

useEffect()

  • side effect를 수행하기 위한 훅
    • 효과 혹은 영향을 뜻하는 effect
  • 다른 컴포넌트들에 영향을 미칠 수 있으며 렌더링 중에는 작업이 완료될 수 없기 때문에 렌더링이 끝난 이후에 실행되어야 하는 작업
  • componentDidMount(), componentDidUpdate(), componentWillUnmount()와 동일한 기능을 하나로 통합해서 제공함으로써 useEffect() 훅만으로 생명주기 함수와 동일한 기능을 수행할 수 있음.

사용 방법

useEffect(이펙트 함수, 의존성 배열);
  • 배열 안에 있는 변수 중에 하나라도 값이 변경되면 이팩트 함수 실행
  • 기본적으로 이펙트 함수는 처음 컴포넌트가 렌더링된 이후와 업데이트로 인한 재렌더링 이후에 실행됨
  • 만약 이펙트 함수가 마운트와 언마운트시에 단 한 번씩만 실행되게 하고 싶으면, 의존성 배열에 빈 배열([])을 넣으면 됨.
    • props와 state에 있는 어떤 값에도 의존하지 않는 것이 되므로 여러번 실행되지 않음
  • 의존성 배열은 생략 가능
    • 생략하면 컴포넌트가 업데이트될 때마다 호출됨

예시

위에서 작성했던 코드에 추가로 useEffect() 훅을 사용하여 클래스 컴포넌트에서 제공하는 componentDidMount(), componentDidUpdate()와 같은 생명주기 함수의 기능을 동일하게 수행하도록 만들어보자.

  • useEffect() 안에 있는 이펙트 함수에서는 브라우저에서 제공하는 API를 사용하여 document title을 업데이트한다.
import React, { useState, useEffect } from "react";

function Counter(props) {
	const [count, setCount] = useState(0);
  	
  	// componentDidMount, componentDidUpdate와 비슷하게 작동
  	useEffect(() => {
    	// 브라우저 API를 사용해서 document의 title 업데이트
      document.title = `${count}번 클릭했습니다.`;
    });
  
  	return (
    	<div>
            <p>{count}번 클릭했습니다.</p>
        	<button onClick={() => setCount(count+1)}>
              클릭
        	</button>
      	</div>
    );
}
  • 위 코드처럼 의존성 배열 없이 useEffect()를 사용하면 리액트는 DOM이 변경된 이후에 해당 이펙트 함수를 실행하라는 의미로 받아들임.
    • 컴포넌트가 처음 렌더링될 때를 포함해서 매번 렌더링될 때마다 이펙트가 실행됨
    • 위 코드의 경우 이펙트 함수는 처음 컴포넌트가 마운트되었을 때 실행되고 이후 컴포넌트가 업데이트될 때마다 실행됨
      • componentDidMount(), componentDidUpdate()와 동일한 역할을 하는 것!
    • 이펙트는 함수 컴포넌트 안에서 선언되기 때문에 해당 컴포넌트의 props와 state에 접근할 수도 있음

이번에는 componentWillUnmount()와 동일한 기능을 useEffect()로 구현하는 방법에 대해 알아보자.

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

function UserStatus(props) {
	const [isOnline, setIsOnline] = useState(null);
  
  	function handleStatusChange(status) {
    	setIsOnline(status.isOnline);
    }
  
  	useEffect(() => {
    	ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
      	return () => {
        	ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
        };
      };
    });

	if (isOnline === null) {
    	return '대기 중...';
    }
	return isOnline ? '온라인' : '오프라인';
}
  • 위 코드는 useEffect()에서 먼저 ServerAPI를 사용하여 사용자의 상태를 구독하고 이후 함수를 하나 리턴하는데 해당 함수 안에는 구독을 해지하는 API를 호출하도록 되어 있다.
    • useEffect()에서 리턴하는 함수는 컴포넌트가 마운트 해제될 때 호출된다.
    • 즉, componentWillUnmount() 함수가 하는 역할과 동일하다.
  • userEffect() 훅은 하나의 컴포넌트에 여러 개를 사용할 수도 있다.
function UserStatusWithCounter(props) {
  
	const [count, setCount] = useState(0);
	useEffect(() ={
		document.title =`${count}번 클릭했습니다.`;
	});

	const [isOnline, setIsOnline] = useState (null);
	useEffect(() => {
		ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
		return ( => {
			ServerAPI. unsubscribeUserStatus (props.user.id, handleStatusChange);
		};
	});

function handleStatusChange(status) {
	setIsOnline (status.isOnline);
}

정리

useEffect(() => {
	// 컴포넌트가 마운트 된 이후,
    // 의존성 배열에 있는 변수들 중 하나라도 값이 변경되면 실행
    // 의존성 배열에 빈배열([])을 넣으면 마운트와 언마운트시에 단 한 번씩만 실행
    // 의존성 배열 생략 시 컴포넌트 업데이트 시마다 실행
    
    return () => {
    	// 컴포넌트가 마운트 해제되기 전에 실행됨
    }

}, [의존성 변수1, 의존성 변수2, ...]);

useMemo()

  • Memoized value를 리턴하는 훅
  • 파라미터로 Memoized value를 생성하는 create 함수와 의존성 배열을 받는다.
    • 의존성 배열에 들어있는 변수가 변했을 경우에만 새로 create 함수를 호출하여 결괏값을 반환하며, 그렇지 않은 경우에는 기존 함수의 결괏값을 그대로 반환한다.
  • useMemo() 훅을 사용하면 컴포넌트가 다시 렌더링될 때마다 연산량이 높은 작업을 반복하는 것을 피할 수 있다.
const memoizedValue = useMemo(
	() => {
    	// 연산량이 높은 작업을 수행하여 결과를 반환
        return computeExpensiveValue(의존성 변수1, 의존성 변수2);
    },
    [의존성 변수1, 의존성 변수2]
);
  • useMemo()로 전달된 함수는 렌더링이 일어나는 동안 실행되기 때문에 일반적으로 렌더링이 일어나는 동안 실행돼서는 안될 작업을 useMemo()안에 넣으면 안됨.
  • 의존성 배열을 넣지 않을 경우 렌더링이 일어날 때마다 매번 함수가 실행되기 때문에 useMemo() 훅에 의존성 배열을 넣지 않는 것은 아무런 의미가 없음
    • 만약 의존성 배열에 빈 배열을 넣을 경우 컴포넌트 마운트시에만 함수가 실행됨

eslint-plugin-react-hooks 패키지

  • useMemo()에서는 의존성 배열을 잘 만들어 주는 것이 중요
  • eslint-plugin-react-hooks 패키지에서는 의존성 배열이 잘못 되어있는 경우에 자동으로 경고 표시를 해주며 고칠 방법을 제안해 줌
  • https://www.npmjs.com/package/eslint-plugin-react-hooks

useCallback()

  • useMemo() 훅과 유사한 역할을 하지만 값이 아닌 함수를 반환함
    • 의존성 배열과 콜백 함수를 파라미터로 받으며, 의존성 배열에 있는 변수 중 하나라도 변경되면 Memoized(메모이제이션 된) 콜백 함수를 반환
const memoizedCallback = useCallback(
	() => {
    	doSomething(의존성 변수1, 의존성 변수2);
    },
    [의존성 변수1, 의존성 변수2]
);
  • 만약 useCallback() 훅을 사용하지 않고 컴포넌트 내에 함수를 정의한다면 매번 렌더링이 일어날 때마다 함수가 새로 정의됨
    • useCallback() 훅을 사용하여 특정 변수의 값이 변한 경우에만 함수를 다시 정의하도록 해서 불필요한 반복 작업을 없애줌

useRef()

  • References를 사용하기 위한 훅으로 레퍼런스 객체를 반환함
    • 매번 렌더링될 때마다 항상 같은 ref 객체를 반환
  • References
    • 특정 컴포넌트를 접근할 수 있는 객체
    • .current라는 속성이 있는데 이것은 현재 레퍼런스(참조)하고 있는 엘리먼트를 의미
const refContainer = useRef(초깃값);
  • 파라미터로 들어온 초깃값으로 초기화된 레퍼런스 객체를 반환함
  • 반환된 레퍼런스 객체는 컴포넌트의 lifetime 전체에 걸쳐서 유지됨
  • .current 속성을 변경하는 것은 재렌더링을 일으키지 않으므로 ref에 DOM node가 연결되거나 분리되었을 경우 어떤 코드를 실행하고 싶다면 callback ref를 사용해야 함

예시

  • 아래는 useRef() 훅을 사용하여 버튼 클릭 시 <input>에 포커스를 하도록 하는 코드
function TextInputWithFocusButton(props) {
	const inputElem = useRef(null);
  
  	const onButtonClick = ( => {
	// `current`는 마운트된 input element를 가리킴
		inputElem.current. focus);
  	};
	return (
      <>
		<input ref={inputElem} type="text" /> 
        <button onClick={onButtonClick}>Focus the input</button>
	  </>
  );
}

훅의 규칙

  • 훅은 무조건 최상위 레벨에서만 호출해야 한다.
    • 최상위 레벨은 리액트 함수 컴포넌트의 최상위 레벨을 의미한다.
    • 즉, 반복문이나 조건문 또는 중첨된 함수들 안에서 훅을 호출하면 안된다.
    • 이 규칙에 따라 훅은 컴포넌트가 렌더링 될 때마다 매번 같은 순서로 호출된다.
  • 리액트 함수 컴포넌트에서만 훅을 호출해야 한다.
    • 일반적인 자바스크립트 함수에서는 훅을 호출할 수 없으며,
    • 리액트 함수 컴포넌트 혹은 직접 만든 커스텀 훅에서만 호출할 수 있다.
    • 이 규칙에 따라 리액트 컴포넌트에 있는 state와 관련된 모든 로직은 소스코드를 통해 명확하게 확인 가능하다.

Custom Hook

  • 리액트에서 기본적으로 제공되는 훅들 이외에 추가적으로 필요한 기능이 있다면 직접 만들어 사용할 수 있음
  • 컴포넌트에서 반복적으로 사용되는 로직을 훅으로 만들어 재사용함

커스텀 훅을 만들어야 하는 상황

  • 아래 코드의 UserStatus 컴포넌트는 isOnline이라는 state에 따라 사용자의 상태가 온라인인지 아닌지를 텍스트로 보여주는 컴포넌트
import React, { useState, useEffect } from "React";

function UserStatus(props) {
	const [isOnline, setIsOnline] = useState(null);
  
  	function handleStatusChange(status) {
    	setIsOnline(status.isOnline);
    }
  
  	useEffect(() => {
    	ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
      	return () => {
        	ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
        };
      };
    });

	if (isOnline === null) {
    	return '대기 중...';
    }
	return isOnline ? '온라인' : '오프라인';
}
  • 동일한 웹사이트에서 연락처 목록을 제공하는데 이때 온라인인 사용자의 이름은 초록색으로 표시해 주고 싶다고 가정하고, 이 컴포넌트의 이름을 UserListItem이라고 하자.
    • UserStatus와 useState(), useEffect() 훅을 사용하는 부분이 동일
import React, { useState, useEffect } from "React";

function UserListItem(props) {
	const [isOnline, setIsOnline] = useState(null);
  
  	function handleStatusChange(status) {
    	setIsOnline(status.isOnline);
    }
  
  	useEffect(() => {
    	ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
      	return () => {
        	ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
        };
      };
    });

	return (
    	<li style={{ color: isOnline ? 'green' : 'black' }}>
        {props.user.name}
        </li>
    );
}
  • 기존 리액트에서는 보통 이렇게 state와 관련된 로직이 중복되는 경우에는 render props 또는 HOC(higher-order-components)를 사용하지만 우리는 중복되는 코드를 추출하여 커스텀 훅으로 만들어보겠다.

커스텀 훅 추출하기

  • 커스텀 훅은 이름이 use로 시작하고 내부에서 다른 훅을 호출하는 하나의 자바스크립트 함수
    • 만약 이름이 use로 시작되지 않는다면 특정 함수의 내부에서 훅을 호출하는지를 알 수 없기 때문에 훅의 규칙 위반 여부를 자동으로 확인할 수 없다.
  • 아래 코드는 위에서 중복되었던 로직을 useUserStatus()라는 커스텀 훅으로 추출해 낸 것이다.
import React, { useState, useEffect } from "React";

function useUserStatus(userId) {
	const [isOnline, setIsOnline] = useState(null);
  
  	function handleStatusChange(status) {
    	setIsOnline(status.isOnline);
    }
  
  	useEffect(() => {
    	ServerAPI.subscribeUserStatus(props.user.id, handleStatusChange);
      	return () => {
        	ServerAPI.unsubscribeUserStatus(props.user.id, handleStatusChange);
        };
      };
    });

	return isOnline;
}

커스텀 훅 사용하기

  • 이제 중복되는 로직을 useUserStatus() 훅으로 추출했기 때문에 다음과 같이 코드를 변경할 수 있다.
function UserStatus(props) {
	const isOnline = useUserStatus(props.user.id);

	if (isOnline === null) {
    	return '대기 중...';
    }
	return isOnline ? '온라인' : '오프라인';
}

function UserListItem(props) {
	const isOnline = useUserStatus(props.user.id);

		return (
          <li style={{ color: isOnline ? 'green' : 'black' }}>
              {props.user.name}
          </li>
    );
}

실습

userCounter() 커스텀 훅 만들기

  • 초기 카운트 값을 파라미터로 받아 count라는 이름의 state를 생성하여 값을 제공하고 카운트 증가 및 감소를 편리하게 할 수 있도록 함수를 제공하는 훅을 만들어보자.
// src/chapter_07/userCounter.jsx

import React, { useState } from "react";

function useCounter(initialValue) {
    const [count, setCount] = useState(initialValue);

    const increaseCount = () => setCount((count) => count + 1);
    const decreaseCount = () => setCount((count) => Math.max(count - 1, 0));

    return [count, increaseCount, decreaseCount];
}

export default useCounter;

Accommodate 컴포넌트 만들기

  • 위에서 만든 useCounter()을 사용하는 함수 컴포넌트를 만들어보자.
  • 이 컴포넌트는 사람을 수용하는 시설에서 사용한다고 가정한다.
  • 앞에서 만든 useCounter() 훅을 사용하여 카운트를 관리하고, 최대 카운트 개수(MAX_CAPACITY)를 초과하면 경고 문구가 표시되도록 코드를 작성해보자.
  • useEffect() 훅의 작동 방식을 확인하기 위해 두 개의 useEffect() 훅을 사용한다.
    • 의존성 배열이 없는 useEffect() 훅은 컴포넌트가 마운트된 직후에 호출되며 이후 컴포넌트가 업데이트될 때마다 호출된다.
    • 의존성 배열이 있는 useEffect() 훅은 컴포넌트가 마운트된 직후에 호출되며, count 값이 바뀔 때마다 호출되는데 용량 상태 isFull을 state에 저장한다.
// src/chapter_07/Accommodate.jsx

import React, { useState, useEffect } from "react";
import useCounter from "./useCounter";

const MAX_CAPACITY = 10;

function Accommodate(props) {
    const [isFull, setIsFull] = useState(false);
    const [count, increaseCount, decreaseCount] = useCounter(0);

    useEffect(() => {
        console.log("======================");
        console.log("useEffect() is called.");
        console.log(`isFull: ${isFull}`);
    });

    useEffect(() => {
        setIsFull(count >= MAX_CAPACITY);
        console.log(`Current count value: ${count}`);
    }, [count]);

    return (
        <div style={{ padding: 16 }}>
            <p>{`${count}명 수용했습니다.`}</p>

            <button onClick={increaseCount} disabled={isFull}>
                입장
            </button>
            <button onClick={decreaseCount}>퇴장</button>

            {isFull && <p style={{ color: "red" }}>정원이 가득찼습니다.</p>}
        </div>
    );
}

export default Accommodate;

실행 하기

  • Accommodate 컴포넌트를 실제 화면에 렌더링 하기 위해 index.js 파일을 수정한다.
// src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import Accommodate from './chapter_07/Accommodate';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Accommodate />
  </React.StrictMode>
);

reportWebVitals();
  • 처음 출력된 로그를 보면 우리가 사용한 두개의 useEffect() 훅이 호출된 것을 알 수 있다.
  • 입장 버튼을 눌렀을 때도 두 개의 useEffect() 훅이 호출되고 카운트 값은 1 증가 한다.
    • 의존성 배열이 없는 useEffect() 훅이 호출된 이유는 컴포넌트가 업데이트 되었기 때문
    • 의존성 배열이 있는 useEffect() 훅이 호출된 이유는 count 값이 변경되었기 때문
  • 정원이 가들 찰 때까지 입장 버튼을 눌러보면 isFull 값이 true가 되면서 입장 버튼이 비활성화된다.
    • count를 의존성 배열로 갖고 있는 훅은 더이상 호출되지 않는다.

실습 전체 코드

References

profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈

0개의 댓글