Learn React / Hooks

user profile img

신현호

React

React Docs

Post Thumbnail

목차

    Hooks

    use

    Canary

    use Hook은 현재 ('23. 04. 18 기준) React의 Canary 채널과 실험 채널에서만 사용할 수 있습니다.

    usepromisecontext와 같은 데이터를 참조하는 Hook입니다. 기본 형태는 다음과 같습니다.

    jsx

    const value = use(resource)
    

    아래의 예제는 각각 promisecontext를 적용한 예제입니다.

    jsx

    import { use } from 'react'
    
    function MessageComponent({ messagePromise }) {
      const message = use(messagePromise)
      const theme = use(ThemeContext)
      // ...
    }
    

    use hook이 갖는 다른 훅과의 차이점은 다른 React Hook과 달리 useif와 같은 조건문과 반복문 내부에서도 호출할 수 있습니다.

    use hook을 사용하면 다음과 같은 작업이 가능해집니다.

    use를 사용하여 context를 참조할 수 있습니다.

    use hook의 인자로 context가 전달되면 useContext와 유사하게 작동합니다. 차이가 있다면 useContext는 컴포넌트의 최상위 수준에서 호출해야하지만 use의 경우 if같은 조건문이나 for같은 반복문 내부에서도 호출할 수 있습니다.

    이 케이스에서 useuseContext보다 유연하게 사용할 수 있습니다.

    jsx

    // when use 'useContext' hook
    import { useContext } from 'react'
    
    function Button() {
      const theme = useContext(ThemeContext)
      // ...
    }
    
    // when use 'use' hook
    import { use } from 'react'
    
    function Button({ show, children }) {
      if (show) {
        const theme = use(ThemeContext)
        const className = 'button-' + theme
    
        return <button className={className}>{children}</button>
      }
      return false
    }
    

    use를 사용하여 비동기처리를 진행할 수 있습니다.

    promise와 함께 사용되는 use의 경우 리액트에서만 사용되는 await로 이해하시면 됩니다.

    이전까지 우리는 컴포넌트 내부에서 비동기 처리를 진행해주기 위해서는 useEffect 훅을 사용해야 했습니다.

    jsx

    function ClientComponent() {
      useEffect(() => {
        const doAsync = async () {
          await doSomething('...')
        }
    
        doAsync()
      }, [])
    
      return <>...</>
    }
    

    하지만 use hook을 사용해준다면 해당 로직을 다음과 같이 변경해 줄 수 있습니다.

    jsx

    import { use } from 'react'
    
    function Component() {
      const data = use(promise)
    }
    

    위에서도 말했듯이, useiffor문 내부에도 사용이 가능하다고 했으므로 아래와 같은 예시도 가능합니다.

    jsx

    function Note({ id, shouldIncludeAuthor }) {
      const note = use(fetchNote(id))
    
      let byline = null
      // 조건부로 호출하기
      if (shouldIncludeAuthor) {
        const author = use(fetchNoteAuthor(note.authorId))
        byline = <h2>{author.displayName}</h2>
      }
    
      return (
        <div>
          <h1>{note.title}</h1>
          {byline}
          <section>{note.body}</section>
        </div>
      )
    }
    

    useCallback

    useCallback은 리렌더링 간에 함수 정의를 캐싱해 주는 Hook입니다.

    jsx

    const cachedFn = useCallback(fn, dependencies)
    

    아래의 예시를 보겠습니다. 아래 로직에서 어떤 부분이 문제가 될까요?

    jsx

    import { useCallback } from 'react'
    
    function ProductPage({ productId, referrer, theme }) {
      const handleSubmit = (orderDetails) => {
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails,
        })
      }
    }
    

    정답은 handleSubmit이라는 함수가 ProductPage가 리렌더링 될 때마다 재생성된다는 점 입니다.
    useCallback을 사용하면 이러한 함수를 캐싱하여 컴포넌트가 리렌더링되더라도 캐싱된 함수를 반환하여 불필요하게 함수를 추가 생성하는 일을 방지합니다.

    jsx

    import { useCallback } from 'react'
    
    function ProductPage({ productId, referrer, theme }) {
      const handleSubmit = useCallback(
        (orderDetails) => {
          post('/product/' + productId + '/buy', {
            referrer,
            orderDetails,
          })
        },
        [productId, referrer]
      )
      // ...
    }
    

    useCallback을 사용할 때는 두 가지 인자를 전달해야합니다.

    • 리렌더링 간에 캐싱할 함수 정의
    • 함수에서 사용되는 컴포넌트 내부의 모든 값을 포함하고 있는 의존성 목록

    최초 렌더링에서 useCallback으로 부터 반환되는 함수는 호출시에 전달할 함수입니다. useCallback은 의존성 배열에 들어간 값이 변경되기 전까지 리렌더링 간에 함수를 캐싱합니다.

    useContext

    useContext는 컴포넌트에서 context를 읽고 구독할 수 있는 Hook입니다.

    jsx

    const value = useContext(SomeContext)
    

    이를 사용하기 위해서는 기본적으로 상위 컴포넌트에서 createContext를 통해 context를 생성한 뒤 해당 컴포넌트를 Provider로 감싸줘야합니다.

    useContext는 특정 context에 대해 상위에서 가장 가까운 context provider를 찾습니다.

    context를 변경하고 싶다면 state를 선언한 뒤 provider에 전달하세요.

    jsx

    function MyPage() {
      const [theme, setTheme] = useState('dark')
      return (
        <ThemeContext.Provider value={theme}>
          <Form />
          <Button
            onClick={() => {
              setTheme('light')
            }}>
            Switch to light theme
          </Button>
        </ThemeContext.Provider>
      )
    }
    

    useDebugValue

    useDebugValue는 React DevTools에서 커스텀 Hook에 라벨을 추가할 수 있게 해주는 Hook입니다.

    jsx

    useDebugValue(value, format)
    

    useDebugValue는 아무것도 반환하지 않으며, 설정시 React DevTools에서 다음과 같이 표시됩니다.

    useDebugValue

    useDebugValue는 내부 구조가 복잡하여 검사하기 어려운 커스텀 Hook에 유용합니다.

    useDeferredValue

    useDeferredValue는 UI의 일부 업데이트를 지연시킬 수 있는 Hook입니다.

    jsx

    const deferredValue = useDeferredValue(value)
    

    useDeferredValue는 설정한 값의 지연된 버전을 가져옵니다.

    jsx

    import { useState, useDeferredValue } from 'react'
    
    function SearchPage() {
      const [query, setQuery] = useState('')
      const deferredQuery = useDeferredValue(query)
      // ...
    }
    

    초기 렌더링 중에 반환된 지연된 값은 사용자가 제공한 값과 같겠지만 업데이트가 발생하면 React는 먼저 이전 값으로 렌더링을 시도하고, 그 다음 백그다운드에서 다시 새 값으로 리렌더링을 시도합니다.
    요약하자면 일종의 낮은 우선순위를 지정하기 위한 Hook이라고 볼 수 있습니다.

    useDeferredValue에 전달하는 값은 문자열 및 숫자와 같은 원시값이거나, 컴포넌트 외부에서 생성된 객체여야 합니다. 렌더링 과정중에 새 객체를 생성하고 이 값을 useDeferredValue에 전달하게되면 렌더링ㅇ 할 때마다 값이 달라져 불필요한 백그라운드 리렌더링이 발생할 수 있습니다.

    useDeferredValue로 인한 백그라운드 리렌더링은 화면에 커밋될 때까지 Effects를 실행하지 않습니다. 백그라운드 리렌더링이 일시 중단되면 데이터가 로딩되고 UI가 업데이트된 후에 해당 Effects가 실행됩니다.

    위의 예제에서는 input의 값이 변경될 때마다 바로 리스트가 갱신이 됩니다. 하지만 일반적인 대체 UI 패턴에서는 결과 목록 업데이트를 지연하고 새 결과가 준비될 때 까지 이전 결과를 계속 표시합니다.

    useDeferredValue를 사용하면 값이 변경되어도 잠시동안 지연된 버전을 보여줄 수 있습니다.

    useEffect

    useEffect는 외부 시스템과 컴포넌트를 동기화하는 Hook입니다.

    jsx

    useEffect(setup, dependencies)
    

    useEffect는 그 사용방법이 상당히 많습니다. 대표적인 예시를 알아보도록 하겠습니다.

    컴포넌트를 외부 시스템과 연결하기

    jsx

    import { useEffect } from 'react'
    import { createConnection } from './chat.js'
    
    function ChatRoom({ roomId }) {
      const [serverUrl, setServerUrl] = useState('https://localhost:1234')
    
      useEffect(() => {
        const connection = createConnection(serverUrl, roomId)
        connection.connect()
        return () => {
          connection.disconnect()
        }
      }, [serverUrl, roomId])
      // ...
    }
    

    몇몇 컴포넌트들은 페이지에 표시되는 동안 네트워크나 브라우저 API, 또는 서드파티 라이브러리와 연결이 유지되어야 합니다.
    React에 제어되지 않는 이러한 시스템들을 외부 시스템이라고 부르며, React 컴포넌트를 외부 시스템과 연결하려면 useEffect를 사용해야합니다.

    useEffect는 2개의 인수를 받습니다.

    • 외부 시스템과 컴포넌트를 연결하는 설정 코드가 포함된 설정 함수
      • 외부 시스템과 연결을 해제하는 정리 코드가 포함된 정리 함수 또한 반환할 수 있습니다.
    • 위 함수 내부에서 사용하는 컴포넌트에서 비롯된 반응형 값들을 포함하는 의존성 배열

    React는 설정 함수와 정리 함수가 필요할 때마다 호출할 수 있으며, 이는 여러 번 호출될 수 있습니다.

    • 컴포넌트가 화면에 추가되었을 때 설정 코드가 동작합니다.
    • 의존성이 변경된 컴포넌트가 리렌더링 될 때마다 아래 동작을 수행합니다.
      • 먼저, 정리 코드가 오래된 props, state와 함께 실행됩니다.
      • 이후, 설정 코드가 새로운 props, state와 함께 실행됩니다.

    커스텀 Hook을 Effect로 감싸기

    Effect는 탈출구입니다. 'React 바깥으로 나가야 할 때'와 유즈케이스에 필요한 빌트인 솔루션이 없을 때 사용합니다.
    만약 Effect를 자주 작성해야 한다면 컴포넌트가 의존하고 있는 공통적인 동작들을 커스텀 Hook으로 추출해야한다는 신호일 수 있습니다.

    jsx

    function useChatRoom({ serverUrl, roomId }) {
      useEffect(() => {
        const options = {
          serverUrl: serverUrl,
          roomId: roomId,
        }
        const connection = createConnection(options)
        connection.connect()
        return () => connection.disconnect()
      }, [roomId, serverUrl])
    }
    

    위와 같이 작성하면 커스텀 hook은 effect의 로직을 조금 더 선언적인 API로 보일 수 있도록 숨겨줍니다.

    React로 작성되지 않은 위젯 제어하기

    가끔은 컴포넌트의 prop 또는 state를 외부 시스템과 동기화해야할 때가 있습니다.

    위의 예시처럼 React로 작성된 third-party 위젯이더라도 useEffect Hook을 통해 제어할 수 있습니다.

    Effect를 이용한 데이터 패칭

    컴포넌트에 데이터를 패칭할 때도 Effect를 사용할 수 있습니다. 만약 프레임워크를 사용하고 있다면 해당 프레임워크의 데이터 패칭 메커니즘을 사용하는 것이 Effect를 직접 작성하는 것보다 더 효율적일 것입니다.

    하지만 직접 Effect를 작성하여 데이터를 패칭하고 싶다면, 코드는 다음과 같을 수 있습니다.

    jsx

    import { useState, useEffect } from 'react'
    import { fetchBio } from './api.js'
    
    export default function Page() {
      const [person, setPerson] = useState('Alice')
      const [bio, setBio] = useState(null)
    
      useEffect(() => {
        let ignore = false
        setBio(null)
        fetchBio(person).then((result) => {
          if (!ignore) {
            setBio(result)
          }
        })
        return () => {
          ignore = true
        }
      }, [person])
    
      // ...
    }
    

    async / await을 사용한다면 아래와 같이 작성할 수 있습니다.

    jsx

    import { useState, useEffect } from 'react'
    import { fetchBio } from './api.js'
    
    export default function Page() {
      const [person, setPerson] = useState('Alice')
      const [bio, setBio] = useState(null)
      useEffect(() => {
        async function startFetching() {
          setBio(null)
          const result = await fetchBio(person)
          if (!ignore) {
            setBio(result)
          }
        }
    
        let ignore = false
        startFetching()
        return () => {
          ignore = true
        }
      }, [person])
    
      // ...
    }
    

    Effect에서 직접 데이터 페칭 로직을 작성하면 나중에 캐싱 기능이나 서버 렌더링과 같은 최적화를 추가하기 어려워집니다. 자체 제작된 커스텀 Hook이나 커뮤니티에 의해 유지보수되는 Hook을 사용하는 편이 더 간단합니다.

    useEffect를 쓸 때 다음을 주의하세요.

    Effect의 의존성은 선택할 수 없습니다.

    Effect 코드에서 사용하는 모든 반응형 값은 의존성으로 선언되어야 합니다. Effect의 의존성 배열은 코드에 의해 결정됩니다.

    jsx

    function ChatRoom({ roomId }) {
      // 이것은 반응형 값입니다
      const [serverUrl, setServerUrl] = useState('https://localhost:1234') // 이것도 반응형 값입니다
    
      useEffect(() => {
        const connection = createConnection(serverUrl, roomId) // 이 Effect는 이 반응형 값들을 읽습니다
        connection.connect()
        return () => connection.disconnect()
      }, [serverUrl, roomId]) // ✅ 그래서 이 값들을 Effect의 의존성으로 지정해야 합니다
      // ...
    }
    

    일반적으로 린터가 잘 설정되어 있다면 린터는 누락된 의존성이 있다면 이를 표시해줍니다. 의존성을 제거하려면 그것이 의존성이 되지 않아야 함을 린터에 증명해야합니다.

    jsx

    const serverUrl = 'https://localhost:1234' // 더 이상 반응형 값이 아님
    
    function ChatRoom({ roomId }) {
      useEffect(() => {
        const connection = createConnection(serverUrl, roomId)
        connection.connect()
        return () => connection.disconnect()
      }, [roomId]) // ✅ 모든 의존성이 선언됨
      // ...
    }
    

    위처럼 설정하면 serverUrl은 반응형 값이 아니게 되었으므로 의존성에 추가할 필요가 없습니다.

    Effect의 코드가 어떤 반응형 값도 사용하지 않는다면 그 의존성 목록은 비어있어야 합니다. []

    이전 state를 기반으로 state를 업데이트 할 때는 변경함수를 사용하세요.

    jsx

    // bad
    function Counter() {
      const [count, setCount] = useState(0)
    
      useEffect(() => {
        const intervalId = setInterval(() => {
          setCount(count + 1) // 초마다 카운터를 증가시키고 싶습니다...
        }, 1000)
        return () => clearInterval(intervalId)
      }, [count]) // 🚩 ... 하지만 'count'를 의존성으로 명시하면 항상 인터벌이 초기화됩니다.
      // ...
    }
    
    // good
    import { useState, useEffect } from 'react'
    
    export default function Counter() {
      const [count, setCount] = useState(0)
    
      useEffect(() => {
        const intervalId = setInterval(() => {
          setCount((c) => c + 1) // ✅ State 업데이터를 전달
        }, 1000)
        return () => clearInterval(intervalId)
      }, []) // ✅ 이제 count는 의존성이 아닙니다
    
      return <h1>{count}</h1>
    }
    

    bad의 예시에서는 count가 반응형 값이기 떄문에 의존성 배열에 추가해야 합니다. 그러나 count가 변경되는 것은 Effect가 정리된 후 다시 설정되는 것을 야기하므로 count는 계속 증가할 것입니다. 이상적이지 않은 방식입니다.

    이러한 현상을 방지하기 위해 state 변경함수를 사용하세요, 이처럼 변경하면 Effect는 더 이상 count에 의존하지 않게 됩니다.

    불필요한 객체의 의존성은 제거하세요

    Effect가 렌더링 중에 생성된 객체나 함수에 의존하게되면 너무 자주 실행될 수 있습니다.

    jsx

    const serverUrl = 'https://localhost:1234'
    
    function ChatRoom({ roomId }) {
      const [message, setMessage] = useState('')
    
      const options = {
        // 🚩 이 객체는 재 렌더링 될 때마다 새로 생성됩니다
        serverUrl: serverUrl,
        roomId: roomId,
      }
    
      useEffect(() => {
        const connection = createConnection(options) // 객체가 Effect 안에서 사용됩니다
        connection.connect()
        return () => connection.disconnect()
      }, [options]) // 🚩 결과적으로, 의존성이 재 렌더링 때마다 다릅니다
      // ...
    }
    

    jsx

    import { useState, useEffect } from 'react'
    import { createConnection } from './chat.js'
    
    const serverUrl = 'https://localhost:1234'
    
    function ChatRoom({ roomId }) {
      const [message, setMessage] = useState('')
    
      useEffect(() => {
        const options = {
          serverUrl: serverUrl,
          roomId: roomId,
        }
        const connection = createConnection(options)
        connection.connect()
        return () => connection.disconnect()
      }, [roomId])
      // ...
    }
    

    렌더링 중에 생성된 객체를 의존성으로 사용하지 말고 객체를 Effect 내부에서 생성하세요.

    useId

    useId는 접근성 어트리뷰트에 전달할 수 있는 고유 ID를 생성하기 위한 Hook입니다.

    jsx

    const id = useId()
    

    그러나 해당 hook을 리스트의 key를 생성하기 위해 사용하지 마세요.

    해당 hook은 다음과 같은 경우에 사용하면 유용합니다.

    • 접근성 어트리뷰트를 위한 고유 ID를 생성할 때
    • 여러 개의 연관된 엘리먼트의 ID를 생성할 때
    • 생성된 모든 ID에 대해 공유 접두사를 지정하고 싶을 때
    • 클라이언트와 서버에서 동일한 ID 접두사를 사용 할 때

    useImperativeHandle

    jsx

    useImperativeHandle(ref, createHandle, dependencies)
    

    useImperativeHandle은 ref로 노출되는 핸들을 사용자가 직접 정의할 수 있게 해주는 Hook입니다.

    기본적으로 컴포넌트는 자식 컴포넌트의 DOM 노드를 부모 컴포넌트에 노출하지 않습니다. 예를 들어 MyInput의 부모 컴포넌트가 <input> DOM 노드에 접근하려면 forwardRef를 사용해야합니다.

    jsx

    import { forwardRef } from 'react'
    
    const MyInput = forwardRef(function MyInput(props, ref) {
      return <input {...props} ref={ref} />
    })
    

    위에서 MyInput에 대한 ref는 <input> DOM 노드를 받게 됩니다. 하지만 useImperativeHandle을 사용하면 사용자 지정 값을 노출할 수 있습니다.

    jsx

    import { forwardRef, useRef, useImperativeHandle } from 'react'
    
    const MyInput = forwardRef(function MyInput(props, ref) {
      const inputRef = useRef(null)
    
      useImperativeHandle(
        ref,
        () => {
          return {
            focus() {
              inputRef.current.focus()
            },
            scrollIntoView() {
              inputRef.current.scrollIntoView()
            },
          }
        },
        []
      )
    
      return <input {...props} ref={inputRef} />
    })
    

    이렇게 useImperativeHandle을 사용하여 부모 컴포넌트에서 호출할 메서드만 있는 핸들을 노출할 수 있습니다.

    또, imperative handle을 통해 노출하는 메서드는 DOM 메서드와 정확히 일치할 필요 없습니다.

    useInsertionEffect

    useInsertionEffect는 layout effects가 실행되기 전에 전체 요소를 DOM에 주입할 수 있습니다.
    useInsertionEffect는 CSS-in-JS 라이브러리 작성자를 위한 것입니다. CSS-in-JS 라이브러리 작업 중에 스타일을 주입할 위치가 필요한 것이 아니라면, useEffect 또는 useLayoutEffect를 사용하세요.

    jsx

    useInsertionEffect(setup, dependencies)
    

    css

    .success {
      color: green;
    }
    

    전통적으로는 plain CSS를 사용해 React 컴포넌트의 스타일링을 진행했으나, 최근에는 JS 코드에서 직접 스타일을 작성하는 경우도 생겼습니다. 이를 CSS-in-JS라고 합니다.
    CSS-in-JS는 일반적으로 라이브러리 또는 도구를 사용해야 합니다. CSS-in-JS는 세 가지 일반적인 접근 방법이 있습니다.

    • 컴파일러를 사용하여 CSS 파일로 정적 추출
    • 인라인 스타일, 예: <div style={{ opacity: 1}}>
    • 런타임에 <style>태그 주입할

    CSS-in-JS를 사용하는 경우 일반적으로 앞의 두 가지 접근 방식을 사용하는게 좋고, 런타임에 style 태그를 주입하는 것은 다음과 같은 이유로 권장하지 않습니다.

    • 런타임 주입은 브라우저에서 스타일을 훨씬 더 자주 다시 계산하도록 합니다.
    • 런타임 주입이 React 생명주기 중에 잘못된 시점에 발생하면 속도가 매우 느려질 수 있습니다.

    첫 번째 문제는 해결할 수 없지만 두 번째 문제는 useInsertionEffect를 사용하여 해결할 수 있습니다.

    jsx

    // CSS-in-JS 라이브러리 안에서
    let isInserted = new Set()
    function useCSS(rule) {
      useInsertionEffect(() => {
        // 앞서 설명했듯이 <style> 태그의 런타임 주입은 권장하지 않습니다.
        // 하지만 꼭 주입해야 한다면 useInsertionEffect에서 주입하는 것이 중요합니다.
        if (!isInserted.has(rule)) {
          isInserted.add(rule)
          document.head.appendChild(getStyleForRule(rule))
        }
      })
      return rule
    }
    
    function Button() {
      const className = useCSS('...')
      return <div className={className} />
    }
    

    useLayoutEffect

    useLayoutEffect는 브라우저가 화면을 다시 그리기 전에 실행되는 useEffect입니다.
    다만, 해당 hook을 사용하면 성능이 저하될 수 있으므로 가능하면 useEffect를 사용해주세요.

    jsx

    useLayoutEffect(setup, dependencies)
    

    대부분의 컴포넌트는 렌더링을 위해 해당 컴포넌트의 화면상 위치와 크기를 알 필요가 없습니다. 컴포넌트가 JSX를 반환하면 브라우저가 컴포넌트의 레이아웃을 계산하고 화면을 다시 그리기 때문입니다.

    하지만 이것만으로 부족한 경우가 있을 수 있습니다. 그 예시로 마우스 커서를 올리면 툴팁이 요소 옆에 나타나는 경우를 생각해보세요. 충분한 공간이 있다면 툴팁은 요소 위에 나타나겠지만, 공간이 부족하다면 아래에 나타나야 합니다. 결국 투립을 올바른 위치에 렌더링하려면 툴팁의 높이를 알아야 하는 것입니다.

    이를 위해 두 번의 렌더링을 거쳐야 합니다.

    • 툴팁을 아무 위치에 렌더링합니다. (잘못된 위치여도)
    • 툴팁의 높이를 계산해서 위치를 결정합니다.
    • 올바른 위치에 툴팁을 다시 렌더링합니다.

    이 작업은 브라우저가 화면을 다시 그리기 전에 모두 이루어집니다.

    useLayoutEffectuseEffect간의 차이가 있다면 useLayoutEffect의 경우 브라우저가 화면을 다시 그리기 전에 처리되는 것을 보장하기 떄문에 브라우저가 화면은 다시 그리는 것을 딜레이시키지만, useEffect의 경우 그렇지 않습니다.

    위의 예제는 useEffect를 사용했습니다. 차이를 비교해보세요

    useMemo

    useMemo는 재렌더링 사이에 계산 결과를 캐싱할 수 있게 해주는 Hook입니다.

    jsx

    const cachedValue = useMemo(calculateValue, dependencies)
    

    useMemo를 사용하면 비용이 높은 로직의 재계산을 생략할 수 있습니다.

    jsx

    import { useMemo } from 'react'
    
    function TodoList({ todos, tab, theme }) {
      const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab])
      // ...
    }
    

    useMemo에 두 가지를 전달하여 계산을 캐싱할 수 있습니다.

    • () => 와 같이 인수를 받지 않고 계산하려는 값을 반환하는 계산 함수
    • 계산 내부에서 사용되는 컴포넌트 내의 모든 값을 포함하는 종속성 목록

    useMemo는 종속성이 변경되기 전까지 재렌더링 사이의 계산 결과를 캐싱합니다.

    jsx

    function TodoList({ todos, tab, theme }) {
      const visibleTodos = filterTodos(todos, tab)
      // ...
    }
    

    일반적으로 대부분의 계산은 매우 빠르기 때문에 문제가 되지 않으나, 큰 배열을 필터링 혹은 변환하는 경우 캐싱하는 것이 좋습니다.

    컴포넌트 재렌더링 건너뛰기

    경우에 따라 useMemo는 하위 컴포넌트 재렌더링 성능을 최적화하는데 도움이 될 수도 있습니다.

    jsx

    export default function TodoList({ todos, tab, theme }) {
      // ...
      return (
        <div className={theme}>
          <List items={visibleTodos} />
        </div>
      )
    }
    

    기본적으로 React는 컴포넌트가 다시 렌더링 될 때, 모든 자식 컴포넌트를 재귀적으로 다시 렌더링합니다. 위의 코드를 보면 TodoList가 props나 state의 변경으로 다시 렌더링되면 하위 컴포넌트인 List도 다시 렌더링됩니다.

    이 때 List를 다시 렌더링하는데 시간이 많이 걸린다면 memo를 통해 props가 똑같을 때 다시 렌더링 하는 것을 생략할 수 있습니다.

    jsx

    import { memo } from 'react'
    
    const List = memo(function List({ items }) {
      // ...
    })
    

    memo 대신 useMemo를 사용하여 캐싱할 수도 있습니다.

    jsx

    export default function TodoList({ todos, tab, theme }) {
      // 테마가 변경될 때 마다 다른 배열이 표시됩니다.
      const visibleTodos = filterTodos(todos, tab)
      return (
        <div className={theme}>
          {/* ... List의 props는 동일하지 않으며 매번 다시 렌더링 됩니다. */}
          <List items={visibleTodos} />
        </div>
      )
    }
    

    위의 예시에서 filterTodos는 항상 다른 배열을 생성합니다. 이럴 때 filterTodosuseMemo를 통해 캐싱한다면 연산을 최적화 할 수 있습니다.

    jsx

    export default function TodoList({ todos, tab, theme }) {
      // 재렌더링 사이에 계산을 캐싱하도록 React에 지시합니다...
      const visibleTodos = useMemo(
        () => filterTodos(todos, tab),
        [todos, tab] // ...따라서 해당 종속성이 변경되지 않는 한...
      )
      return (
        <div className={theme}>
          {/* ...List에 동일한 props가 전달되어 재렌더링을 생략할 수 있습니다. */}
          <List items={visibleTodos} />
        </div>
      )
    }
    

    다른 Hook의 종속성 메모화

    컴포넌트 본문에서 직접 생성된 객체에 의존하는 연산이 있다고 가정하겠습니다.

    jsx

    function Dropdown({ allItems, text }) {
      const searchOptions = { matchMode: 'whole-word', text }
    
      const visibleItems = useMemo(() => {
        return searchItems(allItems, searchOptions)
      }, [allItems, searchOptions]) // 🚩 주의: 컴포넌트 본문에서 생성된 객체에 대한 종속성
      // ...
    }
    

    이렇게 객체에 의존하는 것은 메모이제이션의 목적을 무색하게 합니다. useMemo를 적절하게 사용하여 바꿔봅시다.

    jsx

    function Dropdown({ allItems, text }) {
      const searchOptions = useMemo(() => {
        return { matchMode: 'whole-word', text }
      }, [text]) // ✅ text가 변경될 때만 변경
    
      const visibleItems = useMemo(() => {
        return searchItems(allItems, searchOptions)
      }, [allItems, searchOptions]) // ✅ allItems이나 searchOptions이 변경될 때만 변경
      // ...
    }
    

    이렇게 변경한다면 text가 변경되었을 때만 searchOptions가 변경될 것입니다.
    이보다 더 좋은 방법은 애초에 searchOptionsuseMemo 게산 함수의 내부에 선언하는 것입니다.

    jsx

    function Dropdown({ allItems, text }) {
      const visibleItems = useMemo(() => {
        const searchOptions = { matchMode: 'whole-word', text }
        return searchItems(allItems, searchOptions)
      }, [allItems, text]) // ✅ allItems이나 text가 변경될 때만 변경
      // ...
    }
    

    이제 연산은 text에 직접적으로 의존하게 됩니다.

    useOptimistic

    Canary

    useOptimistic Hook은 현재 ('23. 04. 18 기준) React의 Canary 채널과 실험 채널에서만 사용할 수 있습니다.

    useOptimistic은 UI를 낙관적으로 업데이트할 수 있게 해주는 Hook입니다.

    jsx

    const [optimisticState, addOptimistic] = useOptimistic(state, updateFn)
    

    useOptimistic은 비동기 작업이 진행 중일때 다른 상태를 보여줄 수 있게 해줍니다. 인자로 주어진 일부 상태를 받아, 네트워크 요청과 같은 비동기 작업 기간 동안 달라질 수 있는 그 상태의 복사본을 반환합니다.

    현재 상태와 작업의 입력을 취하는 함수를 제공하고, 작업이 대기 중일 때 사용할 낙관적인 상태를 반환합니다.

    낙관적인 상태란, 실제로 작업을 완료하는 데 시간이 걸리더라도 사용자에게 즉시 작업의 결과를 표시하기 위해 일반적으로 사용됩니다.

    jsx

    import { useOptimistic } from 'react'
    
    function AppContainer() {
      const [optimisticState, addOptimistic] = useOptimistic(
        state,
        // updateFn
        (currentState, optimisticValue) => {
          // merge and return new state
          // with optimistic value
        }
      )
    }
    

    useReducer

    useReducer는 컴포넌트에 reducer를 추가하는 Hook입니다.

    jsx

    const [state, dispatch] = useReducer(reducer, initialArg, init)
    

    useReducer는 현재 state와 dispatch 함수. 2개의 엘리먼트로 구성된 배열을 반환합니다.

    dispatch 함수는 state를 새로운 값으로 업데이트하고 리렌더링을 일으킵니다. dispatch의 유일한 인수는 action입니다.

    jsx

    const [state, dispatch] = useReducer(reducer, { age: 42 })
    
    function handleClick() {
      dispatch({ type: 'incremented_age' })
      // ...
    }
    

    React는 현재 statedispatch를 통해 전달된 action을 제공받아 호출된 reducer의 반환값을 통해 다음 state값을 설정합니다.

    React는 현재 state와 action을 reducer 함수로 전달합니다. reducer는 다음 state를 계산한 후 반환합니다.
    reducer는 다음과 같이 작성할 수 있습니다.

    jsx

    function reducer(state, action) {
      switch (action.type) {
        case 'incremented_age': {
          return {
            name: state.name,
            age: state.age + 1,
          }
        }
        case 'changed_name': {
          return {
            name: action.nextName,
            age: state.age,
          }
        }
      }
      throw Error('Unknown action: ' + action.type)
    }
    

    보통은 컨변션에 따라 switch문을 사용하며, switch는 각 case를 이용해 다음 state를 계산하고 반환합니다.

    Actions는 다양한 형태가 될 수 있지만, 컨벤션에 따라 액션이 무엇인지 정의하는 type 프로퍼티를 포함한 객체로 선언하는 것이 일반적입니다. type은 reducer가 다음 state를 계산하는데 필요한 최소한의 정보를 포합해야 합니다.

    jsx

    function Form() {
      const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 })
    
      function handleButtonClick() {
        dispatch({ type: 'incremented_age' })
      }
    
      function handleInputChange(e) {
        dispatch({
          type: 'changed_name',
          nextName: e.target.value,
        })
      }
      // ...
    }
    

    useRef

    useref는 렌더링에 필요하지 않은 값을 참조할 수 있는 Hook입니다.

    jsx

    const ref = useRef(initialValue)
    

    useRef는 처음에 제공한 초기값으로 설정된 단일 current 프로퍼티가 있는 ref객체를 반환합니다.

    ref는 변경되어도 리렌더링을 촉발하지 않습니다. 즉, ref는 컴포넌트의 시각적 출력에 영향을 미치지 않는 정보를 저장하는 데 적합합니다.

    ref 내부의 값을 업데이트하려면 current 프로퍼티를 수동으로 변경해야 합니다.

    jsx

    function handleStartClick() {
      const intervalId = setInterval(() => {
        // ...
      }, 1000)
      intervalRef.current = intervalId
    }
    

    ref를 사용하면 다음을 보장합니다.

    • (렌더링할 때마다 재설정되는 일반 변수와 달리) 리렌더링 사이에 정보를 저장할 수 있습니다.
    • (리렌더링을 촉발하는 state 변수와 달리) 변경해도 리렌더링을 촉발하지 않습니다.
    • (정보가 공유되는 외부 변수와 달리) 각각의 컴포넌트에 로컬로 저장됩니다.

    ref를 사용하면 다음과 같은 작업을 수행할 수 있습니다.

    ref로 DOM 조작하기

    jsx

    import { useRef } from 'react'
    
    function MyComponent() {
      const inputRef = useRef(null)
      // ...
      return <input ref={inputRef} />
    }
    
    function handleClick() {
      inputRef.current.focus()
    }
    

    ref를 사용하여 DOM을 조작하는 것은 특히 일반적입니다. useRef를 통해 ref를 생성하고 이를 JSX에 전달하면 DOM 노드는 <input>에 접근해 focus()같은 메서드를 호출할 수 있습니다.

    노드가 화면에서 제거되면 React는 ref의 current 프로퍼티를 null로 설정합니다.

    ref로 콘텐츠 재생성 피하기

    React는 초기에 ref 값을 한 번 저장하고, 다음 렌더링부터는 이를 무시합니다.

    jsx

    function Video() {
      const playerRef = useRef(new VideoPlayer())
      // ...
    }
    

    new VideoPlayer()의 결과는 초기 렌더링에만 사용되지만, 호출 자체는 이후의 모든 렌더링에서도 여전히 계속 이뤄집니다. 비싼 객체를 생성하는 경우 이는 낭비가 됩니다.

    jsx

    function Video() {
      const playerRef = useRef(null)
      if (playerRef.current === null) {
        playerRef.current = new VideoPlayer()
      }
      // ...
    }
    

    일반적으로 렌더링 중에 ref.current를 쓰거나 읽는 것은 허용되지 않지만 이 경우에는 결과가 항상 동일하고 초기화 중에만 조건이 실행되므로 충분히 예측할 수 있어 괜찮습니다.

    useState

    useState는 컴포넌트에 state 변수를 추가할 수 있는 Hook입니다.

    jsx

    const [state, setState] = useState(initialState)
    

    일반적으로 사용할 때 배열 구조분해를 사용하여 [something, setSomething]과 같은 state 변수의 이름을 지정하는 것이 규칙입니다.

    useState현재 state, state를 변경하고 리렌더링을 촉발하는 set함수 정확히 두 개의 값을 가진 배열을 반환합니다.

    이러한 set함수를 통해서 우리는 state를 다른 값으로 업데이트 하고 리렌더링을 촉발할 수 있습니다. 여기에는 다음 state를 직접 전달하거나, 이전 state로 부터 계산한 함수를 전달할 수도 있습니다.

    jsx

    const [name, setName] = useState('Edward')
    
    function handleClick() {
      setName('Taylor')
      setAge((a) => a + 1)
      // ...
    }
    

    state는 객체와 배열 또한 가능하지만. React에서 state는 읽기 전용으로 간주되므로, set함수를 통해 기존 객체를 변경하는 것이 아닌 새로운 객체를 만들어 교체해야합니다.

    jsx

    // bad
    // 🚩 state 안에 있는 객체를 다음과 같이 변경하지 마세요.
    form.firstName = 'Taylor'
    
    // good
    // ✅ 새로운 객체로 state를 교체합니다.
    setForm({
      ...form,
      firstName: 'Taylor',
    })
    

    useSyncExternalStore

    useSyncExternalStore는 외부 store를 구독할 수 있는 Hook입니다.

    jsx

    const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
    

    대부분의 React 컴포넌트는 props, state, context에서만 데이터를 읽습니다. 하지만 때로는 컴포넌트가 시간이 지남에 따라 변경되는 React 외부의 일부 저장소에서 데이터를 읽어야 하는 경우가 있습니다.

    • React 외부에 state를 보관하는 서드파티 상태 관리 라이브러리
    • 변경 가능한 값을 노출하는 브라우저 API와 그 변경 사항을 구독하는 이벤트

    이 때 useSyncExternalStore를 사용하면 외부 store를 구독할 수 있습니다.

    jsx

    import { useSyncExternalStore } from 'react'
    import { todosStore } from './todoStore.js'
    
    function TodosApp() {
      const todos = useSyncExternalStore(
        todosStore.subscribe,
        todosStore.getSnapshot
      )
      // ...
    }
    

    store에 있는 데이터의 snapshot을 반환합니다. 이 때 두 개의 함수를 인수로 전달해야 합니다.

    • subsribe 함수는 store에 구독하고 구독을 취소하는 함수를 반환해야 합니다.
    • getSnapshot 함수는 store에서 데이터의 스냅샷을 읽어야 합니다.

    React는 이 함수를 사용해 컴포넌트를 store에 구독한 상태로 유지하고 변경 사항이 있을 때 리렌더링합니다.

    브라우저의 API를 구독하기 위해 useSyncExternalStore를 사용할 수도 있습니다.

    해당 예제에서는 브라우저의 navigator.online이라는 속성을 구독하는 예제입니다.

    useTransition

    useTransition은 UI를 차단하지 않고 상태를 업데이트 할 수 있는 Hook입니다.

    jsx

    const [isPending, startTransition] = useTransition()
    

    useTransition은 어떠한 매개변수도 받지 않으며, 대기중인 transition의 여부상태 업데이트를 transition으로 표시할 수 있게 해주는 함수 두 항목이 있는 배열을 반환합니다.

    startTransition을 통해 state 업데이트를 transition으로 표시할 수 있습니다.

    jsx

    function TabContainer() {
      const [isPending, startTransition] = useTransition()
      const [tab, setTab] = useState('about')
    
      function selectTab(nextTab) {
        startTransition(() => {
          setTab(nextTab)
        })
      }
      // ...
    }
    

    Transition을 사용하면 느린 디바이스에서도 사용자 인터페이스 업데이트의 반응성을 유지할 수 있습니다.

    Transition을 사용하면 리렌더링 도중에도 UI가 반응성을 유지합니다. 예를 들어 사용자가 탭을 클릭했다가 마음이 바뀌어 다른 탭을 클릭하면 첫 번째 리렌더링이 완료될 때까지 기다릴 필요 없이 다른 탭을 클릭할 수 있습니다.

    useTransition 호출에서도 부모 컴포넌트의 state를 업데이트 할 수 있습니다.

    useTransition이 반환하는 isPending 값을 사용하여 transition이 진행중인지 사용자에게 표시할 수 있습니다.



    참고

    Profile Image

    신현호

    Frontend Developer

    프론트엔드 개발자를 꿈꾸고 있는 대학생입니다. 끊임없이 배우고 성장하는 개발자가 되기 위해 노력하고 있습니다.

    React Docs

    총 5개의 포스트가 존재합니다.