개발/리액트

리액트 핵심만 훑어보자 #8 훅

brobro332 2024. 11. 23. 22:51
반응형

📘 『소플의 처음 만난 리액트』를 읽고 정리한 글입니다. 

 

훅(Hook)이 뭘까?

  • 기존의 함수 컴포넌트는 클래스 컴포넌트와는 다르게 코드도 굉장히 간결하고 별도로 state를 정의하거나 컴포넌트의 생명주기 함수를 사용할 수 없었다.
  • 이런 기능을 지원하기 위해 나온 것이 바로 훅이다.
  • 훅라는 단어는 갈고리를 뜻하는데, 원래 존재하는 어떤 기능에 갈고리를 거는 것처럼 끼어 들어가 같이 수행되는 것을 의미한다.
  • 이러한 훅의 이름은 모두 use로 시작한다.
  • 물론 개발자가 직접 커스텀 훅을 만들어 사용할 경우 이름을 개발자 마음대로 지을 수도 있지만 그러한 경우에도 이름 앞에 use를 명시해 주는 것이 올바르다.

 

useState

  • state를 사용하기 위한 훅이다.
function Counter(props) {
    var count = 0;
    
    return (
    	<div>
            <p>총 {count}번 클릭했습니다.</p>
            <button onClick={() => count++}>
            	클릭
            </button>
        </div>
    );
}
  • 상기 코드는 버튼을 클릭하면 카운트 값을 증가시킬 수는 있지만 재렌더링이 일어나지 않아 증가된 카운트 값이 화면에 표시되지 않는다.
  • 따라서 아래의 코드와 같이 state를 사용해서 값이 바뀔 때마다 재렌더링이 되도록 해야 할 필요성이 있다.

 

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>
    );
  • 위의 Counter(props) 메소드는 버튼이 눌렸을 때 setCount() 함수를 호출해서 카운트를 증가시킨다.
  • 그리고 count의 값이 변경되면 컴포넌트가 재렌더링되면서 화면에 증가된 카운트 값을 표시할 수 있다.
  • 클래스 컴포넌트에서는 setState() 함수 하나를 사용해서 모든 state 값을 업데이트할 수 있었지만 함수 컴포넌트에서는 변수 각각에 대해 set 함수가 따로 존재한다.

 

useEffect

  • 사이드 이펙트를 수행하기 위한 훅이다.
  • 사이드 이펙트라고 하면 부정적인 느낌을 가지고 있지만 리액트에서의 사이드 이펙트는 부정적인 의미는 아니다.
  • 리액트에서 말하는 사이드 이펙트는 서버에서 데이터를 받아오거나 수동으로 DOM을 변경하는 등의 일반적인 이펙트를 의미한다.
  • 클래스 컴포넌트에서 제공하는 생명주기 함수인 componentDidMount(), componentDidUpdate(), componentWillUnmount()와 동일한 기능을 하나로 통합해서 제공하며 다음과 같이 사용한다. 

 

useEffect(이펙트 함수, 의존성 배열);
  • 배열 안에 있는 변수 중에 하나라도 값이 변경되었을 때 이펙트 함수가 실행된다.
  • 기본적으로 이펙트 함수는 처음 컴포넌트가 렌더링 된 이후와 업데이트로 인한 재렌더링 이후에 실행된다.
  • 만약 이펙트 함수가 마운트, 언마운트 시에 단 한 번씩만 실행되게 하고 싶으면 의존성 배열에 빈 배열을 넣으면 된다.
  • 이렇게 하면 이펙트가 props나 state에 있는 어떤 값에도 의존하지 않는 것이 되므로 여러 번 실행되지 않는다.
  • 의존성 배열 없이 useEffect()를 사용하면 리액트는 DOM이 변경된 이후에 해당 이펙트 함수를 실행하라는 의미로 받아들인다.
  • 그래서 컴포넌트가 처음 렌더링 될 때를 포함해서 매번 렌더링 될 때마다 이펙트가 실행되는데, 결과적으로 componentDidMount(), componentDidUpdate()와 동일한 역할을 하게 된다.
  • 그렇다면 componentWillUnmount()은 어떻게 수행할까?
  • 결과만 말하자면 useEffect()에서 리턴하는 함수는 컴포넌트가 마운트 해제될 때 호출된다.
  • useEffect()는 자주 사용하기 때문에 구조를 꼭 기억해두어야 한다.

 

useMemo

const memoizedValue = useMemo(() => {
	return 함수(의존성 변수1, 의존성 변수2);
}, [의존성 변수1, 의존성 변수2]);
  • Memoized value를 반환하는 훅이다.
  • 의존성 배열에 들어 있는 변수가 변했을 경우에만 함수를 호출하여 새로운 결괏값을 반환하며, 그렇지 않은 경우는 기존 함수의 결괏값을 그대로 반환한다.
  • 컴포넌트가 다시 렌더링 될 때마다 연산량이 높은 작업의 반복을 피할 수 있다.
  • 렌더링이 일어나는 동안 실행해서는 안 될 작업을 useMemo에 넣으면 안된다는 점을 유의해야 한다.
  • 의존성 배열을 넣지 않을 경우 렌더링이 일어날 때마다 함수가 실행되며, 의존성 배열에 빈 배열을 넣을 경우 컴포넌트 마운트 시에만 함수가 실행된다.

 

useCallback

  • useMemo와 유사하지만 한 가지 차이점은 값이 아닌 함수를 반환한다. 
  • useCallback에서 파라미터로 받는 이 함수를 콜백이라고 부른다.
  • 의존성 배열에 따라 Memoized value를 반환한다는 점에서는 useMemo와 완전히 동일하다.
  • 만약 useCallback을 사용하지 않고 컴포넌트 내에 함수를 정의한다면 매번 렌더링이 일어날 때마다 함수가 새로 정의된다.
  • 따라서 해당 훅을 사용하여 특정 변수의 값이 변한 경우에만 함수를 다시 정의하여 반복을 없애주는 것이 올바르다.

 

useRef

  • 레퍼런스를 사용하기 위한 훅이다.
  • 리액트에서 레퍼런스란 특정 컴포넌트에 접근할 수 있는 객체를 의미한다.
  • useRef 훅은 레퍼런스 객체를 반환한다.
  • 레퍼런스 객체에는 .current라는 속성이 있는데 이는 현재 참조하고 있는 엘리먼트를 의미한다고 보면 된다.

 

const refContainer = useRef(초깃값);
  • 위 코드처럼 사용하면 된다.

 

function TextInputWithFocusButton(props) {
    const inputElem = useRef(null);
    
    const onButtonClick = () => {
    	inputElem.current.focus(); 	
    };
    
    return (
    	<div>
            <input ref={inputElem} type="text"/>
            <button onClick={onButtonClick}>Focus the input</button>
        </div>
    );
}
  • 상기 코드는 버튼을 클릭하면 input 태그를 포커싱하는 예제이다.
  • 매번 렌더링 될 때마다 항상 같은 ref 객체를 반환한다.
  • 주의해야 할 점은 useRef 훅은 내부의 current 속성이 변경되더라도 재렌더링을 일으키지 않는다는 것이다.
  • 따라서 ref에 DOM node가 연결되거나 분리될 경우 어떤 코드를 실행하고 싶다면 callbackref 방식을 사용해야 한다.
  • 이는 DOM node의 ref 속성에 useRef 레퍼런스 객체 대신 useCallback 반환 함수를 넣어주는 것이다.
  • 이렇게 되면 자식 컴포넌트가 변경되었을 때 알림을 받을 수 있고, 이를 통해 다른 정보들을 업데이트할 수 있다. 

 

function MeasureExample(props) {
	const [height, setHeight] = useState(0);
    
    const measuredRef = useCallback(node => {
    	if (node !== null) {
        	setHeight(node.getBoundingClientRect().height);
        }
    }, []);
    
    return (
    	<div>
        	<h1 ref={measureRef}>안녕, 리액트</h1>
            <h2>위 헤더의 높이는 {Math.round(height)}px입니다.</h2>
        </div>
    );
}
  • 위의 예제에서는 h1 태그의 높이값을 업데이트 하고 있다. 
  • useCallback의 의존성 배열로 빈 배열이 들어가 있으므로 마운트, 언마운트 시에만 콜백 함수가 호출된다. 

 

훅의 규칙

훅은 무조건 최상위 레벨에서만 호출해야 한다.

  • 여기서 말하는 최상위 레벨은 리액트 함수 컴포넌트의 최상위 레벨을 의미하는데, 반복문이나 조건문 또는 중첩된 함수들 안에서 훅을 호출하면 안 된다는 의미이다.
  • 이 규칙에 따라 훅은 컴포넌트가 렌더링 될 때마다 매번 같은 순서로 호출되어야 한다.

 

리액트 함수 컴포넌트에서만 훅을 호출해야 한다.

  • 일반 자바스크립트 함수에서 훅을 호출하면 안 된다.
  • 훅은 리액트 함수 컴포넌트에서 호출하거나 직접 만든 커스텀 훅에서만 호출할 수 있다.

 

커스텀 훅

  • 커스텀 훅을 만드는 이유는 여러 컴포넌트에서 반복적으로 사용되는 로직을 훅으로 만들어 재사용하기 위함이다.
  • 또한 파라미터로 무엇을 받을지, 어떤 것을 반환할지 개발자가 직접 정할 수 있다. 
  • 컴포넌트 명은 꼭 use로 시작하도록 하여 해당 컴포넌트에서 훅을 호출함을 명시해야 한다.

 

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

    if (isOnline === null) {
    	return '대기중...';
    }
    return isOnline ? '온라인' : '오프라인';
}
  • 상기 코드에는 커스텀 훅을 적용하고자 한다. 
  • isOnline이라는 state에 따라서 사용자의 상태가 온라인인지 여부를 텍스트로 보여주는 컴포넌트이다.

 

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

    return (
    	<li style={{ color : isOnline ? 'green' : 'black'}}>
            {props.user.name}
        </li>
    );
}
  • 위의 예제는 온라인 상태의 사용자의 이름만 초록색으로 표시하는 코드이다.
  • 코드를 보면 userStatus 컴포넌트와 useState(), useEffect() 훅을 사용하는 부분이 동일하다.
  • 즉 여러 곳에서 중복되는 코드이므로 추출하여 다음과 같이 커스텀 훅으로 만들 수 있다.

 

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

    return isOnline;
}
  • state와 관련된 중복 로직을 useUserStatus라는 커스텀 훅으로 추출해낸 것이다.
  • 이제 다음 코드와 같이 UserStatus와 UserListItem에 커스텀 훅을 적용하면 된다.

 

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>
    );
}
  • 위처럼 같은 커스텀 훅을 사용하는 두 개의 컴포넌트는 state를 공유하는 것일까?
  • 아니다. 단순히 state 연관 로직을 재사용이 가능하게 만든 것 뿐이다.
  • 여러 개의 컴포넌트에서 하나의 커스텀 훅을 사용하더라도 컴포넌트 내부의 state나 effects는 전부 분리되어 있다.

 

const userList = [
    { id : 1, name : 'Inje' },
    { id : 2, name : 'Mike' },
    { id : 3, name : 'Steve' }
];

function ChatUserSelector(props) {
    const [userId, setUserId] = userState(1);
    const isUserOnline = useUserStatus(userId);
    
    return (
      <div>
          <Circle color={isUserOnline ? 'green' : 'red'}/>
          <select
              value={userId}
              onChange={event => setUserId(Number(event.target.value))}
          >
              {userList.map(user => (
                  <option key={user.id} value={user.id}>
                      {user.name}
                  </option>
              ))}
          </select>
        </div>
    );
}
  • 훅 간의 데이터를 공유하는 예제 코드이다.
  • userId가 변경될 때마다 setUserId() 함수가 호출되고, useUserStatus 훅은 이전에 선택된 사용자를 구독 취소하고 새로 선택된 사용자의 온라인 여부를 구독한다.

 

오늘도 실습 한 번 해보자

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;
  • UseCounter.jsx 컴포넌트를 작성해 보자.
  • 해당 컴포넌트에 주어지는 props 값이 최초 카운트 수가 된다.
  • 이후로 increaseCount(), decreaseCount() 메소드를 선언하여 이벤트 함수와 함께 반환한다.

 

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

const MAX_CAPACITY = 10;

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

    useEffect(() => {
        console.log("useEffect() 메소드가 호출되었습니다.");
        console.log(`정원 도달여부 ${isFull}`);
    });

    useEffect(() => {
        setIsFull(count >= MAX_CAPACITY);
        console.log(`최근 개수: ${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 ManageTeamMembers;
  • 이번엔 ManageTeamMembers.jsx 컴포넌트를 작성해 보자.
  • useEffect()를 선언함으로써 Team 컴포넌트가 렌더링 될 때마다 useEffect() 메소드의 호출여부와 팀의 멤버 정원 도달 여부를 콘솔에 찍는다.
  • 또한 카운트 상태가 변할 때마다 최근 개수를 콘솔에 찍는다.
  • 가입 버튼을 클릭하면 멤버의 카운트 수가 증가하며, 정원이 꽉 찼을 경우 비활성화된다.
  • 탈퇴 버튼을 클릭하면 멤버의 카운트 수가 감소한다.
  • 또한 정원이 꽉 찰 경우 빨간 글씨로 "정원이 가득 찼습니다." 문구가 노출된다.

 

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

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

reportWebVitals();
  • 작성한 컴포넌트를 index.js에 추가해 주자. 

 

작성한 화면을 확인해 보자

 

  • 요런 느낌

 

  • 콘솔에는 다음과 같이 출력된다.
  • ManageTeamMembers.jsx 파일의 MAX_CAPACITY 변수 값에 도달할 경우 정원 도달여부가 true로 바뀐다.

 

마치며

훅은 굉장히 중요하고도 복잡한 기능이다.
본인도 사실 완벽히 숙지하지는 못했다. 

반복만이 살 길이다. 결국 무언가를 만들어보는 게 가장 중요하고, 잘못된 부분은 바로잡아가면 된다고 생각한다. 😎

이미지 출처

 

[React] Component 를 사용하는 기본적인 방법

컴포넌트는 React의 핵심 개념 중 하나이며, 이는 사용자 인터페이스(UI)를 구축하는 기반이다. 컴포넌트를 만들어보자. 우선 여기까지는 순수 JS의 선언 및 정의에 대한 구문이다. 리액트는 컴포

velog.io

 

소플 - soaple.io

소플

www.soaple.io