Dandy Now!
  • [React.js] 리액트 19의 혁신적인 `use` 훅: 비동기 데이터와 컨텍스트를 더 스마트하게
    2025년 09월 04일 11시 11분 05초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    리액트 19의 혁신적인 use 훅: 비동기 데이터와 컨텍스트를 더 스마트하게!

    리액트 19의 등장은 프론트엔드 개발에 많은 변화를 예고하고 있다. 그중에서도 단연 눈길을 끄는 것은 바로 새로운 훅, use 훅이다. 이 훅은 기존 리액트 훅의 제약을 깨고, 비동기 데이터 처리와 컨텍스트 값 접근 방식을 혁신적으로 변화시킨다. 오늘은 use 훅이 무엇인지, 어떤 상황에서 사용하며, 리액트 쿼리 및 컨텍스트와는 어떤 관계를 맺는지 자세히 알아보자.


    1. use 훅이란 무엇인가?

    use 훅은 리액트 컴포넌트 내에서 PromiseContext의 값을 읽어올 수 있게 해주는 새로운 훅이다. 가장 큰 특징은 기존 훅들과 달리 iffor와 같은 조건문, 반복문 내부에서도 호출할 수 있다는 점이다. 이는 리액트 개발에 훨씬 더 큰 유연성을 제공한다.

    2. use 훅, 언제 사용하는가?

    use 훅은 크게 두 가지 주요 상황에서 빛을 발한다.

    2.1. 비동기 Promise의 결과 읽기

    use 훅의 가장 강력한 기능 중 하나는 비동기 작업(Promise)의 결과를 마치 동기 코드처럼 처리할 수 있게 해주는 것이다. use(promise)를 사용하면, 해당 Promise가 해결(resolve)될 때까지 리액트 컴포넌트는 Suspense에 의해 일시 중단된다. 데이터가 준비되면 컴포넌트는 다시 렌더링되며, 에러 발생 시 가장 가까운 Error Boundary가 이를 처리한다.

    이는 useEffectuseState를 조합하여 로딩, 에러, 데이터 상태를 수동으로 관리하던 기존의 복잡한 비동기 데이터 페칭 로직을 매우 간결하게 만들어준다.

    예제 코드:

    import { use, Suspense } from 'react';
    
    // (가정) 비동기적으로 사용자 데이터를 가져오는 함수
    async function fetchUserData(userId) {
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) {
        throw new Error('사용자 데이터를 불러오지 못했습니다.');
      }
      return response.json();
    }
    
    function UserProfile({ userId }) {
      // use 훅을 사용하여 Promise의 결과를 '동기적으로' 읽어온다.
      // Promise가 해결될 때까지 Suspense가 작동한다.
      const user = use(fetchUserData(userId)); 
    
      return (
        <div>
          <h2>{user.name}님의 프로필</h2>
          <p>이메일: {user.email}</p>
          <p>가입일: {user.createdAt}</p>
        </div>
      );
    }
    
    export default function App() {
      return (
        <Suspense fallback={<div>사용자 정보를 불러오는 중...</div>}>
          <UserProfile userId={123} />
        </Suspense>
      );
    }

    App 컴포넌트에서 UserProfile이 데이터를 기다리는 동안 Suspensefallback 메시지가 표시된다. 데이터가 성공적으로 로드되면 UserProfile이 렌더링된다.

    2.2. 컨텍스트(Context) 값 읽기

    use 훅은 useContext 훅과 유사하게 컨텍스트 값을 읽어올 수 있다. 하지만 useContext와 달리 if 문 안에서도 호출이 가능하다는 유연성을 제공한다. 이는 특정 조건에서만 컨텍스트 값이 필요한 경우에 유용하다.

    예제 코드:

    import { createContext, use } from 'react';
    
    // Context 생성
    const ThemeContext = createContext('light'); // 기본값: 'light'
    
    function ThemeToggle() {
      // use 훅으로 ThemeContext 값 읽기
      const theme = use(ThemeContext); 
    
      // 조건부 렌더링
      if (theme === 'dark') {
        return <button style={{ background: 'black', color: 'white' }}>다크 모드</button>;
      } else {
        return <button style={{ background: 'white', color: 'black' }}>라이트 모드</button>;
      }
    }
    
    export default function App() {
      return (
        // Provider로 'dark' 테마 제공
        <ThemeContext.Provider value="dark">
          <ThemeToggle />
        </ThemeContext.Provider>
      );
    }

    3. 리액트 쿼리(React Query)와 use 훅: 무엇이 다르고 어떻게 함께 쓰는가?

    use 훅의 등장으로 "리액트 쿼리 같은 데이터 페칭 라이브러리가 필요 없어지는 것 아닌가요?"라는 질문이 생길 수 있다. 결론부터 말하면, 아니다. 두 기술은 용도가 다르며, 상호 보완적으로 사용될 수 있다.

    3.1. 리액트 쿼리와 use 훅의 차이점

    • 리액트 쿼리 (@tanstack/react-query): 서버 상태(Server State) 관리에 특화된 강력한 라이브러리이다. 데이터 페칭, 캐싱, 동기화, 백그라운드 리페칭, 무한 스크롤 등 복잡한 서버 데이터 관리 로직과 성능 최적화 기능을 포괄적으로 제공한다.
    • use 훅: Promise와 Context 값을 '읽어오는' 리액트의 내장 메커니즘이다. 캐싱이나 백그라운드 리페칭과 같은 고급 서버 상태 관리 기능은 제공하지 않으며, 주로 Suspense와 연동하여 비동기 작업의 결과를 간결하게 처리하는 데 중점을 둔다.

    즉, 리액트 쿼리는 데이터 관리의 종합 솔루션이라면, use 훅은 비동기 결과를 다루는 리액트의 기본 도구이다.

    3.2. 리액트 쿼리와 use 훅 함께 사용하는 방법

    리액트 쿼리가 서버 상태를 효율적으로 관리하고, use 훅은 그 외의 비동기 작업이나 Context 값을 유연하게 처리하는 데 사용될 수 있다. 다음 두 가지 경우를 중심으로 살펴보자.

    1. 리액트 쿼리의 useQuery를 조건부로 호출하지 않고, 옵션으로 컨텍스트 값을 전달할 때

      • 핵심: 리액트 훅(useQuery 포함)은 항상 컴포넌트의 최상위 레벨에서 호출되어야 한다. use 훅으로 컨텍스트 값을 읽어온 후, 그 값에 따라 useQueryif 문 안에 넣는 것은 훅 규칙 위반이다.
      • 올바른 접근: use 훅은 컨텍스트 값을 읽는 데 사용하고, 그 값을 useQueryqueryKeyqueryFn, enabled 옵션으로 전달하여 동적인 쿼리를 구성한다.
      import { createContext, use } from 'react';
      import { useQuery } from '@tanstack/react-query';
      
      const UserRoleContext = createContext('guest'); // 기본값: guest
      
      // 역할에 따라 다른 데이터를 가져오는 쿼리 함수 (예시)
      async function fetchDataByRole(role) {
        if (role === 'admin') {
          const res = await fetch('/api/admin-data');
          return res.json();
        } else if (role === 'user') {
          const res = await fetch('/api/user-data');
          return res.json();
        }
        return null; // guest인 경우 데이터 없음
      }
      
      function Dashboard() {
        // use 훅으로 UserRoleContext의 값 읽기
        const userRole = use(UserRoleContext); 
      
        // useQuery는 항상 최상위에서 호출. userRole 값에 따라 쿼리 동작 제어.
        const { data, isLoading, error } = useQuery({
          queryKey: ['dashboardData', userRole], // userRole이 바뀌면 쿼리 재실행
          queryFn: () => fetchDataByRole(userRole),
          enabled: userRole !== 'guest', // guest일 때는 쿼리 비활성화
        });
      
        if (isLoading) return <div>데이터 로딩 중...</div>;
        if (error) return <div>에러 발생: {error.message}</div>;
        if (!data) return <div>{userRole === 'guest' ? '로그인이 필요하다.' : '데이터가 없다.'}</div>;
      
        return (
          <div>
            <h2>{userRole} 대시보드</h2>
            <pre>{JSON.stringify(data, null, 2)}</pre>
          </div>
        );
      }
      
      export default function App() {
          return (
              <UserRoleContext.Provider value="admin">
                  <Dashboard />
              </UserRoleContext.Provider>
          )
      }

      이 예제에서 use 훅으로 읽어온 userRole 값은 useQueryqueryKeyqueryFn에 인자로 전달되어 쿼리의 내용을 동적으로 변경하고, enabled 옵션을 통해 쿼리 실행 여부를 제어한다.

    2. 서버 데이터와 관련 없는 클라이언트 사이드 비동기 작업을 처리할 때

      • 핵심: 리액트 쿼리는 서버 상태 관리가 주 목적이다. 로컬 스토리지 접근, 웹 워커 통신, 클라이언트에서 발생하는 지연 로딩 등 서버와 무관한 비동기 작업은 use 훅으로 처리하여 역할 분담을 명확히 할 수 있다.
      import { useQuery } from '@tanstack/react-query';
      import { use } from 'react';
      
      // (가정) 로컬 스토리지에서 사용자 설정을 비동기적으로 불러오는 함수
      // 실제로는 localStorage API는 동기적이지만, 비동기 처리가 필요하다고 가정
      const getLocalSettingsAsync = () => {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve({ theme: 'dark', notifications: true });
          }, 300); // 0.3초 지연
        });
      };
      
      function UserSettings() {
        // 1. 리액트 쿼리를 사용하여 서버에서 사용자 프로필 정보 페칭
        const { data: serverProfile, isLoading: isProfileLoading } = useQuery({
          queryKey: ['userProfile'],
          queryFn: () => fetch('/api/profile').then(res => res.json()),
        });
      
        // 2. use 훅을 사용하여 서버와 무관한 비동기 클라이언트 로직 처리
        // Suspense에 의해 로딩될 때까지 기다린다.
        const localSettings = use(getLocalSettingsAsync()); 
      
        if (isProfileLoading) return <div>프로필 로딩 중...</div>;
        if (!serverProfile) return <div>프로필을 불러오지 못했다.</div>;
      
        return (
          <div>
            <h1>환영한다, {serverProfile.name}님!</h1>
            <h3>사용자 설정 (클라이언트)</h3>
            <p>테마: {localSettings.theme}</p>
            <p>알림: {localSettings.notifications ? '켜짐' : '꺼짐'}</p>
          </div>
        );
      }

      이 예시에서 useQuery는 서버 API로부터 사용자 프로필을 가져오는 데 집중하고, use 훅은 클라이언트 사이드의 비동기 작업(로컬 스토리지 설정 로딩)을 처리한다. 각자의 강점을 활용하여 더욱 효율적인 코드 작성을 가능하게 한다.


    ✨ 컨텍스트(Context)와 전역 상태 관리

    컨텍스트 (Context)란?

    리액트에서 컴포넌트 트리를 통해 데이터를 전달하는 메커니즘이다. 일반적으로 props를 통해 데이터를 전달하지만, 트리가 깊어지면 'prop drilling' 문제가 발생한다. 컨텍스트는 이 문제를 해결하기 위해 특정 데이터를 전역적으로 관리하여, 트리의 어느 위치에 있는 컴포넌트든 해당 데이터에 직접 접근할 수 있도록 해준다. Provider로 데이터를 제공하고, useContext 훅(또는 use 훅)으로 데이터를 소비한다.

    컨텍스트는 위에서 설명했듯이 데이터를 트리 아래로 전달하는 도구이지만, 그 자체로 전역 상태 관리 라이브러리는 아니다.

    컨텍스트 vs 전역 상태 관리 라이브러리 (Redux, Zustand 등)

    • 컨텍스트: 데이터를 전달하는 '방법'에 가깝다. Context.Providervalue가 변경되면 해당 컨텍스트를 사용하는 모든 하위 컴포넌트가 무조건 다시 렌더링된다. 이는 데이터가 자주 변경되는 경우 불필요한 렌더링을 유발하여 성능 저하의 원인이 될 수 있다. 주로 테마, 사용자 인증 정보, 언어 설정 등 자주 변경되지 않는 정적인 데이터를 공유할 때 적합하다.

    • 전역 상태 관리 라이브러리 (예: Zustand): 전역 상태를 효율적으로 관리하기 위한 복잡한 로직과 최적화 기법을 내장하고 있다. 상태의 특정 부분만 업데이트되더라도 해당 상태를 사용하는 컴포넌트만 선택적으로 다시 렌더링되도록 설계되어 있다. 또한 개발자 도구, 미들웨어 등 디버깅 및 확장 기능을 제공하여 잦은 업데이트가 발생하는 동적인 상태나 복잡한 로직을 관리하는 데 훨씬 효과적이다.

    결론적으로 컨텍스트는 단순한 전역 데이터 공유에는 충분하지만, 복잡하고 동적인 애플리케이션의 전역 상태 관리를 위해서는 리덕스, Zustand와 같은 전용 라이브러리를 고려하는 것이 좋다.


    728x90
    반응형
    댓글