FrontEnd/React

[React] 처음 만나는 리액트 훅

일락。 2024. 11. 26. 13:29

  리액트 훅을 알아보기 전에 Clock 컴포넌트 코드를 추가하자.  Clock 컴포넌트는 Date 타입의 today속성을 통해 자바스크립트가 기본으로 제공하는 날짜와 시간을 얻어온다.

import type {FC} from 'react'
import {Div, Title, Subtitle} from '../components'

export type ClockProps = {
  today: Date
}

const Clock: FC<ClockProps> = ({today}) => {
  return (
    <Div className="flex flex-col items-center justify-center h-screen bg-primary text-white">
      <Title className="text-5xl">{today.toLocaleTimeString()}</Title>
      <Subtitle className="mt-4 text-2xl">{today.toLocaleDateString()}</Subtitle>
    </Div>
  )
}
export default Clock

 

실행결과 : 컴포넌트가 정상 동작하지만 시간이 실시간으로 갱신되지 않는다.

 

 리액트 훅이란?

   리액트 프레임워크가 2019년 2월 16.8.0 버전부터 리액트 훅이라는 혁신적인 기능을 추가 하였고 반드시 함수 컴포넌트에서만 사용해야 한다.  리액트 훅은 useState, useEffect 등 'use' 라는 접두사가 이름에 들어가는 함수이다.  

용도
 컴포넌트 데이터 관리  useMemo
 useCallback
 useState
 useReducer 
 컴포넌트 생명 주기 대응  useEffect
 useLayoutEffect
 컴포넌트 메서드 호출  useRef
 useImperativeHandle
 컴포넌트 간의 정보 공유  useContext

 

  리액트 훅의 탄생 배경

   리액트 버전 16.8.0 이전 버전까지의 사용자 컴포넌트는 React.Component를 상속하고 render 메서드를 반드시 구현해야 하는 클래스 기반의 컴포넌트 였다.

 

  클래스 컴포넌트는 많은 기능을 추가하면 코드가 직관적이지 않다.  또한 클래스 내의 메서드의 생명주기를 알기 어려운 단점도 존재한다.  이런 복잡함과 모호함을 극복하기 위해 리액트 훅이 만들어졌다.

 

 리액트 훅 코드 패턴과 의존성 목록

  리액트 훅 함수는 매개변수의 갯수를 기준으로 나눌 수 있다.  코드 패턴의 매개변수 중 의존성_목록(dependency list)은 콜백 함수에서 사용되는 변수나 함수의 값이 일정하지 않고 수시로 변할 수 있을 때, 해당 변수나 함수를 아이템으로 갖는 배열을 의미한다. 

 

  즉, 의존성 배열에 들어있는 아이템 중 하나라도 값이 변경된다면 콜백 함수를 새로고침해 변한 값을 골백 함수에 반영한다.

매개변수 개수 훅 함수 코드패턴
1개  useState, useRef , useContext,
 useImperativeHandle
1) 훅_함수<값의_타입>(값)
  - const today: Date = useRef<Date>(new Date)
2) 훅_함수<값의_타입 | null>(값)

2개  useMemo, useCallback , useEffect,
 useReducer, useLayoutEffect
 1) 훅_함수<값의_타입> (콜백_함수, 의존성_목록)
  - useEffect(() => {}, [])

 

 setInterval API로 시계 만들기

  자바스크립트의 setInterval API를 통해 시간 변화를 갱신해줄 수 있다.  이를 통해 App컴포넌트가 갱신한 시각을 화면에 반영하기 위해서는 컴포넌트 내에 setInterval API를 포함시켜야 한다.

 

  다만 포함할 경우 App가 다시 랜더링 될 때마다 setInterval API 메서드가 호출되기 때문에 메모리 누수가 발생할 수 있다.  이를 해결하기 위해 처음 렌더링 될 때 한번만 호출되는 useEffect 훅 함수가 목적에 부합하다.

 

 useEffect 훅 사용하기

  useEffect의존성 목록에 있는 조건 중 어느 하나라도 충족되면 그때마다 콜백함수를 다시 실행하는 훅이다.  컴포넌트가 생성될 때 한 번만 실행되게 하기 위해서는 의존성 목록을 []로 만들면 된다.  즉, 의존성 목록의 아이템이 없을 경우([]) useEffect는 첫 번째 매개변수의 콜백 함수를 한 번만 실행한다.

useEffect(콜백_함수, 의존성_목록)
콜백_함수 = () => {}
// 단 한번만 수행되는 의존성 목록 아이템이 없는 useEffect
useEffect(() => {}, [])

 

 useEffect를 사용해 App를 코드를 추가한다.

import {useEffect} from 'react'
import Clock from './pages/Clock'

export default function App() {
  let today = new Date()
  useEffect(() => {
    console.log('useEffect called')
    const duration = 1000
    const id = setInterval(() => {
      today = new Date()
      console.log('today', today.toLocaleTimeString())
    }, duration)
    return () => clearInterval(id)
  }, [])
  return (
    <main>
      <Clock today={today} />
    </main>
  )
}

 

  해당 소스를 실행할 경우 화면갱신이 정상적으로 되지 않으며 node에서는 오류가 발생했다고 로그를 확인할 수 있다.

 일단은 useRef 훅을 today에 적용하라는 것 같다.

src\App.tsx
  Line 10:15:  Assignments to the 'today' variable from inside React Hook useEffect 
   will be lost after each render. To preserve the value over time, 
   store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, 
   you can move this variable directly inside useEffect  react-hooks/exhaustive-deps

 

  또한 화면은 그대로지만 로그는 갱신되는 것을 확인할 수 있다.

 

 useRef 훅 사용하기

  useRef를 코드에 추가해 적용해보자.  일단 적용 시 nodejs에 로그가 발생하지 않지만 아직도 화면상의 시간이 갱신되고 있지 않음을 확인할 수 있다.  왜냐하면 useRef 훅은 컴포넌트를 다시 렌더링 하지 않기 때문이다.

import {useEffect, useRef} from 'react'
import Clock from './pages/Clock'

export default function App() {
  let today = useRef(new Date())
  useEffect(() => {
    console.log('useEffect called')
    const duration = 1000
    const id = setInterval(() => {
      today.current = new Date()
      console.log('today', today.current.toLocaleTimeString())
    }, duration)
    return () => clearInterval(id)
  }, [])
  return (
    <main>
      <Clock today={today.current} />
    </main>
  )
}

 

실행결과 : 로그는 갱신되지만 컴포넌트가 다시 렌더링되고 있지않다.

 

 useState 훅 사용하기

  리액트는 useState훅을 제공하며 다음처럼 사용한다.  useState가 반환하는 세터(setter)는 현재 값이 변경되면 컴포넌트를 자동으로 다시 랜더링하는 기능을 제공한다.

const [현재 값, 세터] = useState(초기값)
세터 = (새로운_값) => void

 

 

  코드에 useState를 추가한다.  useState 훅을 사용해 현재 시각 time, 시간을 변경하는 setToday() 함수를 얻고있다.

 setInterval함수 내에 1초마다 setToday(new Date())를 호출해 today값을 변경하고 있다.  값이 변경되었기 때문에 useState는 컴포넌트를 자동으로 다시 렌더링하여 화면 상의 시간이 변화하는 것을 확인할 수 있다.

import {useEffect, useRef, useState} from 'react'
import Clock from './pages/Clock'

export default function App() {
  const [today, setToday] = useState(new Date())
  useEffect(() => {
    console.log('useEffect called')
    const duration = 1000
    const id = setInterval(() => {
      setToday(new Date())
    }, duration)
    return () => clearInterval(id)
  }, [])
  return (
    <main>
      <Clock today={today} />
    </main>
  )
}

 

  커스텀 훅이란?

  리액트 훅은 여러 훅 함수를 조합해 하나의 새로운 훅 함수를 선언할 수 있으며 그 함수를 커스텀 훅(custom hook)이라고 한다.  커스텀 훅은 리액트 훅 뿐만 아니라 기존에 만든 커스텀 훅 함수를 조합해서도 만들 수 있다.

 

  커스텀 훅 함수는 '훅' 이라는 의미를 강조하고자 함수 이름 앞에 'use'라는 접두어를 붙여 생성한다.  useInterval, useClock 두 커스텀 훅 함수를 작성하자.

// useInterval.js
import {useEffect} from 'react'

export const useInterval = (callback: () => void, duration: number = 1000) => {
  useEffect(() => {
    const id = setInterval(callback, duration)
    return () => clearInterval(id)
  }, [callback, duration])
}

// useClock.ts
import {useState} from 'react'
import {useInterval} from './useInterval'

export const useClock = () => {
  const [today, setToday] = useState(new Date())
  useInterval(() => setToday(new Date()), 2000)
  return today
}

 

  이 두 커스텀 훅을 통해 App.tsx를 작성한다.  이로서 좀더 정리정돈 된듯한 소스를 구성할 수 있다.

import {useClock} from './hook'
import Clock from './pages/Clock'

export default function App() {
  const today = useClock()
  return <Clock today={today} />
}

 

 리액트 훅 함수의 특징

  • 같은 리액트 훅을 여러 번 호출할 수 있다.
  • 함수 몸통이 아닌 몸통 안 복합 실행문의 { } 안에서 호출할 수 없다.
  • 비동기 함수를 콜백 함수로 사용할 수 없다.

  특징들을 살펴보기 위해 훅을 호출하는 코드를 추가한다.  useState, useEffect훅을 각각 2번씩 호출해 총 4번 호출됨을 확인함으로 훅이 여러번 호출될 수 있음을 알 수 있다.  

export default function App() {
    const [x, setX] = useState(0)
    const [y, setY] = useState(0)
    
    useEffect(() => {}, [])
    useEffect(() => {}, [])
}

 

  이번에는 지역변수 블록 안에서 훅의 호출을 알아보기 위해 코드를 추가한다.  두가지 케이스를 확인해 보면 전부 { } 안에 훅이 선언된 것을 확인할 수 있으며 이런 방식의 호출을 하면 안된다.

export default function App() {
    {    // 지역 변수 블록
        const [x, setX] = useState<number>(0)
    }
    // if 문 또한 { } 안에서 호출되기 때문에 잘못된 훅 선언이다.
    if(true) {
        const [x, setX] = useState<number>(0)
    }
}

 

  마지막으로 비동기 콜백함수가 입력받을 수 없는지 확인하기 위해 코드를 추가한다.  이렇듯 비동기 함수를 선언해서 훅을 사용하면 안됨을 알 수 있다.

export defualt function App() {
    // useEffect의 콜백함수를 async() 비동기 함수로 구현하면 안된다.
    useEffect(async() => {
        await Promise.resolve(1)
    }, [])
}