본문 바로가기
02_STUDY/React

[ React ] 리액트 함수형 컴포넌트의 훅 Hook에 대해서 ( useState, useEffect, useContext, useMemo, useCallback, useRef )

by zestlumen 2023. 8. 10.

훅Hook이라고?

훅Hook은 우리가 만화영화에서 보던 후크선장의 그 훅Hook이 맞다.

갈고리를 사용해 함수형 컴포넌트에서 상태와 생명주기 기능을 특정한 시점에서

코드를 연결하거나 걸어두는 것처럼 사용해서 쉽게 상태 관리와 라이프사이클 기능을 사용할 수 있도록 도와준다.

 

(예를 들어,  useState 같은 경우 컴포넌트의 상태를 관리를 위한 훅인데

상태를 hook하고 끌어올려 컴포넌트에 사용할 수 있게 해준다.)

 

 

훅Hook이 왜 필요할까? 클래스 컴포넌트와의 비교

- 클래스 컴포넌트는 생명주기 메서드가 분산되어 있고, 상태관리, 생명주기 관련 로직이 복잡하게 얽혀있을 수 있다.

훅은 상태와 관련된 로직을 더 작은 단위로 분리해 관리하고 컴포넌트 구조를 간결하게 해준다.

 

- 클래스 컴포넌트에서는 상태 로직의 재사용이 어렵다. 함수형은 훅으로 재사용 가능

 

- 클래스 컴포넌트는 상태를 공유하거나 사이드 이펙트를 다루는 것이 어렵다.

훅은 useEffect로 사이드 이펙트를 선언적으로 처리 가능하다.

 

- 훅을 이용하면 컴포넌트 코드를 더 선언적으로 작성할 수 있다. 가독성 좋아진다.

 

- 클래스 컴포넌트의 경우 추가적인 메서드와 상속이 필요해 번들 크기가 커질 수 있다.

훅을 사용하면 필요한 로직만 선택 사용 가능해 번들 크기 줄일 수 있고 성능 최적화가 가능하다.

 

 

리액트 훅Hook은 'use'로 시작한다

1. useState Hook

상태(state)를 관리하기 위한 훅

const [ 변수명, set함수명 ] = useState(초기값);

 

useState()를 통해 초기값을 선언해주고 업데이트를 해야 한다.

useState함수는 초기값을 인자로 받으며 상태와 상태 변경 함수를 배열로 반환한다.

 

import React, { useState } from 'react';

function Counter() {
    const [count, setCount] = useState(0);
    
    return(
    	<div>
            <div>Count: {count}</div>
            <button onClick={() => setCount(count + 1)}>Add</button>
        </div>
    );
}

useState()를 통해 초기값을 0으로 설정한 상태 count와 업데이트할 수 있는 함수 setCount를 생성.

Add버튼을 클릭할 때마다 setCount를 호출하여 count 상태를 업데이트 한다.

컴포넌트가 리렌더링 되면서 화면에 표시되는 숫자가 변경된다.

 

 

 

2. useEffect Hook

사이드 이펙트를 관리하기 위한 훅,

컴포넌트가 렌더링 될 때마다 특정한 작업을 수행하도록 설정할 수 있는 함수이다.

함수 내에서 비동기적인 작업이나 사이드 이펙트를 처리할 수 있다.

useEffect()훅 하나만으로 생명주기 함수와 동일한 기능을 수행할 수 있다.

 

***리액트 컴포넌트 내에서 '사이드 이펙트'란 컴포넌트 상태나 props 값이 변경되었을 때

발생하는 부가적인 작업을 말한다. 컴포넌트 렌더링과는 직접적인 관련은 없지만,

컴포넌트의 동작이나 외부 환경에 영향을 미칠 수 있는 것들을 포함한다.

ex) 컴포넌트 상태가 변경될 때 API 요청, 서버에서 데이터 가져오기, 수동으로 DOM 조작 등

 

useEffect(effect 함수, 의존성 배열);

 

useEffect의 두번째 매개변수로 사용되는 의존성 배열(dependency array)

이펙트가 의존하고 있는 배열로, 특정한 값이나 변수들의 배열을 나타내며,

해당 값이 변경될 때에만 useEffect 내부의 코드가 실행되도록 설정하는 역할을 한다.

또한 컴포넌트의 동작을 정확하게 제어하고 불필요한 리렌더링을 방지해준다.

 

 

의존성 배열을 생략할 경우:  컴포넌트가 매번 렌더링 될 때마다 실행

 

useEfffect(() => {
	...
});

 

의존성 배열이 빈 배열[ ] 일 경우: 마운트와 언마운트시에 단 한번씩만 실행

 

useEfffect(() => {
	...
}, []);

 

의존성 배열이 state나 props일 경우: 내부 코드가 특정한 상태나 props 값이 변경될 때 실행

 

useEffect(() => {
  // count 상태가 변경될 때만 실행됨
}, [count]);

useEffect(() => {
  // username props가 변경될 때만 실행됨
}, [username]);

 

의존성 배열에 여러값을 포함시킬 경우: 배열 내 어떤 값이 변경되더라도 실행

useEffect(() => {
  // count나 username 중 하나라도, 어떤 값이라도 변경될 때 실행됨
}, [count, username]);

 

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

function FectchData() {
    const [data, setData] = useState([]);
    
    useEffect(()=>{
    	fetch('https://api.example.com/data')
            .then(response => response.json())
            .then(result => setData(result));
    }, []); 
    //처음 렌더링 될 때만 API에서 데이터 가져오기
    
    return(
        <div>
            <ul>
                {data.map(item => (
                	<li key={item.id}>{item.name}</li>
                ))}
            </ul>
        </div>
    );
}

 

3. useContext Hook

- 컴포넌트 간 데이터 공유를 간편하게 만들어주는 훅.

- 복잡한 props 전달이나 중간 컴포넌트를 거치지 않고 데이터를 공유할 수 있다.

- 컴포넌트 구조가 복잡해질 때 과도한 사용은 피하고 필요한 곳에만 사용하는 것이 좋다.

- 컨텍스트 값이 변경되지 않으면 컴포넌트가 리렌더링 될 때마다 useContext()가 호출되어

다시 값이 계산되지 않는다. 동일한 컨텍스트 값을 사용하면 성능상 좋은 장점이 있다.

- 주로 전역 상태 관리, 로그인 상태, 사용자 정보와 같은 인증 정보를 여러 컴포넌트에서 사용해야 하거나

어플리케이션 테마, 언어 설정과 같은 사용자 설정, 다국어 지원 등에 활용된다.

- 주로 상태 관리나 상태 업데이트에 useState()나 useReducer()훅과 함께 활용해

컴포넌트 간의 데이터 공유를 구현한다.

 

import React, {createContext, useContext } from 'react';

//context 생성
const MyContext = createContext();

//context를 사용할 component
function ComponentA() {
    //context값 가져오기
    const data = useContext(MyContext);
    return <p>Component A: {data}</p>;
}

function ComponentB() {
    //context값 가져오기
    const data = useContext(MyContext);
    return <p>Component B: {data}</p>;
}

//Provider component
export default function App() {
    const sharedData = "Hello, useContext!";
    
    return (
        <MyContext.Provider value={sharedData}>
            <ComponentA />
            <ComponentB />
        </MyContext.Provider>
    );
}

 

위 코드의 예시처럼 useContext 훅 사용 시에는 반드시 해당 컨텍스트 값을 제공하는 Provider 컴포넌트가 꼭 필요하다.

Provider 컴포넌트는 컨텍스트 값을 설정하고  이 값을 하위 컴포넌트들이 useContext를 통해 사용할 수 있도록 한다.

<MyContext.Provider value={sharedData}>
    <!--하위 컴포넌트들-->
</MyContext.Provider>

 

createContext()로 생성한 컨텍스트 객체인 MyContext의 value 라는 prop에

컨텍스트로 전달하려는 데이터를 넣는다. Provider 컴포넌트를 사용해 컨텍스트 값을 설정한 후,

그 아래의 모든 하위 컴포넌트에서 useContext 훅을 사용해 값을 사용할 수 있다.

 

useContext 훅 사용시 주의할 점

- 여러 개의 컨텍스트를 중첩된 경우 useContext() 훅을 사용할 때 컨텍스트 구별이 중요하다.

useContext()는 가장 가까이 있는 컨텍스트의 값을 반환한다.

(컨텍스트 값을 찾을 때 컴포넌트 트리를 위로 올라가면서 가장 가까이 있는 컨텍스트 값 반환)

- Provider 컴포넌트 내에서 컨텍스트 값이 변경될 때마다 하위 컴포넌트들은 리렌더링 된다.

불필요한 리렌더링을 줄이기 위해서는 컨텍스트 값이 자주 변경되지 않는 것이 좋다.

- 여러 컴포넌트에서 동일한 컨텍스트를 사용한다면, 하위 컴포넌트마다 별도의 Provider를

사용하는 것이 아닌 최상위 컴포넌트에서 한 번만 Provider를 사용하는 것이 좋다.

- Provider 컴포넌트가 최상위 계층에 위치하지 않을 시에는, 해당 컨텍스트가 필요한

모든 하위 컴포넌트가 제대로 동작하지 않기에 위치에 신경써야 한다.

- createContext()는 컨텍스트 객체 생성시 초기값을 제공하는데, Provider가 값을 제공하지 않을 때 사용된다.

- 컨텍스트 이름 명명은 관례적으로 대문자와 카멜케이스를 사용하며,

컨텍스트 이름 뒤에 Context를 붙이는 것이 일반적이다.

- useContext()사용 시에는 컨텍스트 의도를 이해하기 쉽게 변수명을 명확하게 지정하는 것이 좋다.

 

 

 

 

4.useMemo Hook

계산 비용이 큰 함수의 결과값을 캐싱하고, 의존성이 변경되었을 때에만 다시 계산하는 훅.

컴포넌트가 렌더링 될 때마다 계산되는 값을 캐싱하고, 의존성 배열의 값이 변경될 때마다 다시 계산한다.

주의할 점은 useMemo는 순수한 계산만을 위한 함수렌더링이 일어나는 동안 실행돼서는 안되는 작업,

사이드 이펙트를 가지는 코드나 함수 같은 것을 넣어서는 안된다. 연산량이 높은 작업에 많이 사용.

 

const memoizedValue = useMemo( 값 생성 함수, 의존성배열);

 

import React, { useMemo } from 'react';

function ExampleComponent({ a, b }) {
   const result = useMemo(() => {
       return a + b;
   }, [a, b]);
   
   return <div>Result: {result}</div>;
}

 

useMemo()의 경우에 의존성 배열을 넣지 않으면 아무런 의미가 없다.

(넣지 않을 시에 렌더링 될 때마다 매번 값 생성 함수가 실행되므로)

위의 코드로 보면 [a, b]내의 값이 변경될 때만 useMemo 내부 함수가 다시 실행되고 결과가 캐싱된다.

값이 변경되지 않으면, 컴포넌트가 리렌더링을 해도 이전에 계산한 결과가 재사용되어 성능을 최적화한다.

 

5.useCallback Hook

함수 자체를 캐싱하여 불필요한 함수가 다시 생성되는 것을 막고 성능을 향상시키는 데 사용된다.

자식 컴포넌트로 함수를 전달할 때 매번 새로운 함수 인스턴스가 생성되는 것을 방지하며,

자식 컴포넌트의 성능을 최적화하는 데 도움을 준다.

 

const memoizedCallback = useCallback( 콜백함수, 의존성 배열 );

 

 

useCallback(콜백함수,의존성배열)은 useMemo()훅을 사용한다면 아래와 같다.

useMemo(() => 콜백함수, 의존성 배열);

 

 

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

export default function Addition() {
    const [num1, setNum1] = useState(0);
    const [num2, setNum2] = useState(0);
    const [result, setResult] = useState(0);
    
    const addNumbers = useCallback(() => {
        setResult(num1 + num2);
    }, [num1, num2]);
    
    return(
    	<div>
           <input
              type='number'
              value={num1}
              onChange={e => setNum1(Number(e.target.value))}
           />
             +
           <input
              type='number'
              value={num2}
              onChange={e => setNum2(Number(e.target.value))}
           />
           <button onClick={addNumbers}>Add</button>
           <div>Result: {result}</div>
        </div>
    );
}

 

num1과 num2를 의존성 배열로 지정해 두 숫자가 변경될 때에 addNumbers가 재생성 되도록 하였다.

addNumbers는 num1과 num2를 더해 result에 저장하는 함수.

addNumbers는 매 랜더링마다 새로 생성되지 않고 기존에 생성한 함수를 재사용한다.

의존성 배열의 변수가 변했을 경우에만 콜백 함수를 다시 정의해서 리턴한다.

 

useMemo와 useCallback은

메모제이션(Memoization)을 활용하여 성능을 최적화 하는 데 사용하는 훅이지만

useMemo의 경우 값을 계산하고 캐싱하여 렌더링 결과를 최적화 하고,

useCallback은 함수를 캐싱하여 함수 재생성을 막아 성능을 최적화 하는 데 사용된다. ( 값이 아닌 함수 반환)

 

***메모제이션이랑 평소에 '메모한다'라고 하는 것과 비슷하게, 이전에 함수나 계산 결과를 저장해두고

동일한 입력값이 주어질 때 미리 저장된 결과를 반환하는 것으로 불필요한 계산을 줄이고 성능을 개선할 수 있다.

 

 

6.useRef Hook

React컴포넌트에서 DOM요소나  값을 참조하기 위한 훅.

컴포넌트에서 생성한 변수를 특정 DOM요소에 연결하거나 컴포넌트 사이에서 값을 공유할 수 있다.

useRef를 통해 생성된 ref객체는 컴포넌트의 생명주기 동안 유지되며, 렌더링과 상관없이 일관된 상태를 유지한다.

그렇기에 값이 변경되어도 컴포넌트가 다시 렌더링 되지 않는다.

따라서 컴포넌트 상태나 props와 연동해서 사용하지 않는 것이 좋다.

(값을 변경하면서 컴포넌트를 리렌더링 하고 싶다면 useState나 다른 상태 관리 훅을 사용해야 한다.)

 

const myRef = useRef(초기값);

 

useRef()를 사용해 ref 객체를 생성한다. 

파라미터로 들어온 초기값으로 레퍼런스 객체를 반환하는데

초기값이 필요 없으면 useRef()와 같이 호출하면 된다. ( undefined )

'.current' 프로퍼티를 통해 실제 DOM 요소나 다른 값을 참조한다.

 

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

function FocusInput() {
	//useRef로 input요소에 대한 ref 객체 생성
	const inputRef = useRef(null);
    
    //useEffect를 사용해 컴포넌트가 처음 마운트될 때 포커스를 설정
    useEffect(() => {
    	//.current프로퍼티를 이용해 inpuut요소에 접근해 포커스 설정
    	inputRef.current.focus(); 
    }, []);
    
    //input 요소에 useRef로 생성한 ref 객체 연결
    return <input ref={inputRef} />;
}

 

위와 같이 처음 마운트 될 때만 포커스를 설정하는 경우는 예로 들면 사용자 입력 폼이 처음 보여질 때

자동으로 첫 번째 입력 요소에 포커스가 가도록 하거나, 모달이나 팝업, 알림, 경고 메시지가 표시될 때

내부 요소에 포커스를 설정해 즉시 사용자가 확인 할 수 있도록 해서 사용자 경험을 개선하는 데 좋다.

 

 

가상 DOM이 아닌 실제 DOM에 접근하는 useRef()는 어디에 활용하는 걸까?

1. DOM조작

특정 DOM 요소에 직접적인 접근이 필요한 경우 

ex) 포커스 설정, 스크롤 위치 변경, 외부 라이브러리의 통합 등

2. 값 유지

상태(State)와 달리 useRef로 생성한 객체는 값이 변경되어도 컴포넌트 리렌더링을 유발하지 않는다.

일시적인 값의 유지가 필요할 때 활용하면 좋다.

3. 이전 값 저장

이전의 렌더링과 현재 렌더링 값을 비교할 때, useRef는 이전 값을 저장하여 활용할 수 있다.

4. 외부 라이브러리 관리

외부 라이브러리와 통합 시에 useRef를 사용해 라이브러리의 인스턴스를 관리하고 언마운트 시 정리할 수 있다.

ex) 차트라이브러리 (Chart.js, D3.js), 맵 라이브러리(Leaflet, Google Maps API), 텍스트 에디터 라이브러리(Draft.js, Quill), UI 라이브러리(Material-UI, Ant Design), 외부 인터렉션 라이브러리(ScrollMagic, Lottie) 등

 

7.Custom Hook

'use'로 시작해야 하며 개발자 마음대로 로직을 정의하고 공유할 수 있다.

ex) useFecth, useAuthentication...

다른 리액트 훅을 활용하거나 상태를 관리하고 특정한 기능을 추상화한 코드 포함 가능.

리액트 컴포넌트 내부에서만 사용되어야 하고 컴포넌트 상태나 props 활용 가능.

관례적으로 src 디렉토리 내의 hooks 폴더에 저장하는 것이 일반적이다.

 

 

*** 리액트 훅을 사용할 때 주의할 점

- 상태 업데이트 함수가 비동기적으로 동작한기 때문에 상태 변경 함수를 여러 번 호출해도

즉시 상태가 업데이트 되는 것이 아니라 리액트 내부 처리에 따라 변경될 수 있다. 

 

- 훅은 반드시 리액트 함수 컴포넌트의 최상위에서만 호출한다.

조건문, 루프, 중첩 함수 안에서 훅을 사용시 예상치 못한 동작이 발생할 수 있다.

(이는 컴포넌트 렌더링 주기와 관련이 있다. 컴포넌트의 상태State나 프로퍼티Props가 변경되면

리액트 컴포넌트를 다시 렌더링하고 이 때 컴포넌트 함수 내부 코드가 실행되며,

JSX에 의해 가상 돔에 변화가 기록되고. 가상 돔에 기록된 변화가 실제 돔에 반영되며 화면에 업데이트 된다.

리액트 훅은 컴포넌트의 상태 변화를 추적하고 렌더링이 진행되는 시점에 특정 작업을 수행하거나

상태를 업데이트 하게 된다. 그러나 조건문이나 루프, 중첩 함수 안에서 사용시에는

훅이 호출된 순서나 타이밍에 따라 상태 변화나 렌더링 주기에 맞지 않는 동작을 할 수 있다.

즉, 컴포넌트가 렌더링 될 때마다 매번 같은 순서로 호출되어야 한다.)

 

함수형 컴포넌트에서만 사용 가능하다. 클래스 컴포넌트에서는 사용할 수 없다.

 

- 컴포넌트의 재사용성을 고려해 로직을 추출하고 커스텀 훅을 만드는 것이 좋다.

 

- 모든 렌더링에서 호출되어야 하며 조건에 따라 훅을 생략하면 안 된다.