[태그:] React

  • React 실전 가이드:중급

    React 입문 가이드를 통해 기본기를 다지셨다면, 이제 한 단계 더 나아갈 시간입니다. 이 중급 가이드에서는 애플리케이션의 성능을 최적화하고, 더 복잡한 데이터를 효율적으로 다루며, 코드의 품질을 높이는 실전적인 기술들을 다룹니다.

    목차

    1. Hooks 심화 학습: useMemouseCallback

      • 불필요한 렌더링은 이제 그만!
      • 언제 사용해야 할까?
    2. 데이터 페칭(Data Fetching) 마스터하기

      • useEffect + Axios 패턴의 한계
      • 서버 상태 관리의 구세주: React Query
    3. 성능 최적화와 React.memo

      • React.memo로 컴포넌트 렌더링 최적화하기
      • useCallback과의 시너지
    4. 컴포넌트 생명주기(Lifecycle)와 useEffect

      • useEffect로 생명주기 흉내 내기
      • Clean-up 함수: 메모리 누수 방지
    5. UI 라이브러리 활용

      • Material-UI (MUI)로 개발 속도 높이기

    1. Hooks 심화 학습: useMemouseCallback

    useStateuseEffect에 익숙해졌다면, 이제 성능 최적화를 위한 Hooks를 배울 차례입니다.

    • useMemo: 값(value)을 기억(memoization)합니다. 복잡한 연산의 결과값을 저장해두고, 의존성 배열(deps)의 값이 변경될 때만 다시 계산합니다. 이를 통해 불필요한 연산을 줄일 수 있습니다.

    • useCallback: 함수(function)를 기억합니다. 컴포넌트가 리렌더링될 때마다 함수가 새로 생성되는 것을 방지합니다. 자식 컴포넌트에 props로 함수를 내려줄 때, 불필요한 리렌더링을 막는 데 특히 유용합니다.

    import React, { useState, useMemo, useCallback } from 'react';
    
    function Calculator({ a, b }) {
      // a 또는 b가 변경될 때만 expensiveCalculation 함수가 다시 실행됩니다.
      const result = useMemo(() => expensiveCalculation(a, b), [a, b]);
    
      // 컴포넌트가 리렌더링 되어도 이 함수는 재생성되지 않습니다.
      const handleSave = useCallback(() => {
        saveResult(result);
      }, [result]);
    
      return (
        <div>
          <p>결과: {result}</p>
          <ChildComponent onSave={handleSave} />
        </div>
      );
    }

    2. 데이터 페칭 마스터하기

    2.1. useEffect + Axios 패턴의 한계

    입문 과정에서 배운 useEffectAxios를 사용한 데이터 페칭은 간단한 작업에는 유용하지만, 실무에서는 여러 한계에 부딪힙니다.

    • 로딩(loading), 에러(error) 상태를 직접 useState로 관리해야 합니다.
    • 데이터 캐싱(caching)이 없어, 같은 데이터를 여러 번 요청하게 됩니다.
    • 코드가 길고 복잡해지기 쉽습니다.

    2.2. 서버 상태 관리의 구세주: React Query

    React Query는 데이터 페칭, 캐싱, 동기화, 서버 상태 업데이트 등 비동기 작업을 매우 쉽게 만들어주는 라이브러리입니다. 보일러플레이트 코드를 대폭 줄여줍니다.

    • 주요 장점:
      • 데이터를 가져오는 로직과 UI 로직을 분리
      • 자동 캐싱 및 백그라운드 업데이트
      • isLoading, isError, data 등의 상태를 자동으로 제공
    import { useQuery } from 'react-query';
    import axios from 'axios';
    
    // 데이터를 가져오는 함수
    const fetchTodos = async () => {
      const { data } = await axios.get('https://api.example.com/todos');
      return data;
    };
    
    function TodoList() {
      // useQuery 훅 하나로 로딩, 에러, 데이터 상태를 모두 관리합니다.
      const { data: todos, isLoading, isError } = useQuery('todos', fetchTodos);
    
      if (isLoading) return <span>Loading...</span>;
      if (isError) return <span>Error fetching data</span>;
    
      return (
        <ul>
          {todos.map(todo => <li key={todo.id}>{todo.title}</li>)}
        </ul>
      );
    }

    3. 성능 최적화와 React.memo

    3.1. React.memo로 컴포넌트 렌더링 최적화하기

    React.memo는 고차 컴포넌트(HOC, Higher-Order Component)로, 컴포넌트를 감싸서 props가 변경되지 않으면 리렌더링을 방지하는 역할을 합니다.

    const MyComponent = (props) => {
      /* 렌더링 로직 */
    };
    
    // props가 변경될 때만 MyComponent가 리렌더링됩니다.
    export default React.memo(MyComponent);

    3.2. useCallback과의 시너지

    React.memo를 사용하더라도, props로 함수를 내려주는 경우 부모가 리렌더링될 때마다 함수가 새로 생성되어 React.memo가 무용지물이 될 수 있습니다. 이때 useCallback으로 함수를 기억시켜주면, React.memo가 제대로 동작하여 최적화 효과를 극대화할 수 있습니다.

    4. 컴포넌트 생명주기(Lifecycle)와 useEffect

    useEffect의 두 번째 인자인 의존성 배열(deps)을 어떻게 조작하느냐에 따라 클래스형 컴포넌트의 생명주기 메서드들을 흉내 낼 수 있습니다.

    • componentDidMount: useEffect(() => { ... }, []) (빈 배열)
    • componentDidUpdate: useEffect(() => { ... }, [dep1, dep2]) (의존성 지정)
    • componentWillUnmount: useEffect(() => { return () => { ... } }, []) (Clean-up 함수 반환)

    Clean-up 함수: 메모리 누수 방지

    컴포넌트가 사라질 때(unmount) 실행되는 return 함수는 매우 중요합니다. setInterval, setTimeout이나 외부 라이브러리 구독 등을 설정했을 때, 컴포넌트가 사라지기 전에 이를 해제하지 않으면 메모리 누수가 발생할 수 있습니다.

    useEffect(() => {
      const timerId = setInterval(() => {
        console.log('Tick');
      }, 1000);
    
      // 컴포넌트가 언마운트될 때 타이머를 정리합니다.
      return () => {
        clearInterval(timerId);
      };
    }, []);

    5. UI 라이브러리 활용

    모든 UI 컴포넌트를 직접 만드는 것은 비효율적일 수 있습니다. Material-UI(MUI), Ant Design, Chakra UI 같은 UI 라이브러리는 잘 디자인되고 검증된 컴포넌트들을 제공하여 개발 속도를 비약적으로 향상시켜 줍니다.


    이제 여러분은 React를 좀 더 깊이 있게 이해하고, 더 견고하고 성능 좋은 애플리케이션을 만들 준비가 되었습니다. 다음 단계로는 전역 상태 관리(Global State Management) 라이브러리인 ReduxZustand 등을 학습해보는 것을 추천합니다.

  • React 실전 가이드: 고급

    React의 기본과 중급 과정을 마스터한 당신, 이제는 프로덕션 레벨의 애플리케이션을 구축하고 최적화하는 고급 기술을 익힐 차례입니다. 이 가이드에서는 상태 관리 아키텍처, 재사용 가능한 로직 추상화, 렌더링 최적화, 그리고 프로덕션 배포 전략까지, 실무에서 마주하는 복잡한 문제들을 해결하기 위한 고급 주제들을 다룹니다.

    목차

    1. 고급 상태 관리 아키텍처

      • 클라이언트 상태 vs 서버 상태
      • React Query 심화: Mutations, Invalidation, Optimistic Updates
      • 전역 상태 관리: Zustand, Recoil, Redux Toolkit
    2. 재사용성을 극대화하는 Custom Hooks

      • 나만의 Hook 만들기
      • Custom Hooks 디자인 패턴
    3. 렌더링 최적화의 정점

      • React.lazySuspense를 이용한 코드 분할(Code Splitting)
      • 동시성(Concurrency) 렌더링: useTransitionuseDeferredValue
    4. 견고한 애플리케이션 설계

      • 디자인 패턴: 제어 컴포넌트 vs 비제어 컴포넌트
      • 폴더 구조: 대규모 애플리케이션을 위한 아키텍처
    5. 프로덕션 준비와 배포

      • 환경 변수 관리
      • Docker를 이용한 컨테이너화

    1. 고급 상태 관리 아키텍처

    1.1. 클라이언트 상태 vs 서버 상태

    현대 React 애플리케이션의 상태는 두 가지로 나뉩니다.

    • 서버 상태(Server State): 서버 API로부터 받아오는 데이터. 비동기적이고, 원격으로 관리되며, 다른 사람에 의해 변경될 수 있습니다. (예: 게시글 목록, 사용자 정보)
    • 클라이언트 상태(Client State): UI의 현재 상태. 동기적이고, 오직 클라이언트에서만 관리됩니다. (예: 다크 모드 여부, 모달 창 열림 상태)

    이 둘을 명확히 분리하고 각기 다른 도구로 관리하는 것이 중요합니다. 서버 상태는 React Query로, 클라이언트 상태는 useState, useReducer 또는 전역 상태 관리 라이브러리로 다루는 것이 효율적입니다.

    1.2. React Query 심화

    useQuery를 넘어, 데이터를 변경하는 작업을 위한 고급 기능을 알아봅니다.

    • useMutation: 서버의 데이터를 생성(Create), 수정(Update), 삭제(Delete)할 때 사용합니다.
    • Query Invalidation: mutation 성공 후, 관련된 query를 무효화시켜 자동으로 데이터를 최신 상태로 다시 가져오게 하는 강력한 기능입니다. (queryClient.invalidateQueries('todos'))
    • Optimistic Updates: 서버 응답을 기다리지 않고 UI를 먼저 업데이트하여 사용자 경험을 극대화하는 기법입니다. mutation이 실패하면 원래 상태로 롤백합니다.

    1.3. 전역 상태 관리

    Props drilling을 피하고 여러 컴포넌트가 공유하는 클라이언트 상태를 효율적으로 관리하기 위해 전역 상태 관리 라이브러리를 사용합니다. Redux가 전통적인 강자였지만, 최근에는 더 간결하고 사용하기 쉬운 Zustand, Recoil, Jotai 등이 많이 사용됩니다.

    2. 재사용성을 극대화하는 Custom Hooks

    반복되는 로직을 여러 컴포넌트에서 사용해야 할 때, Custom Hook을 만들어 로직을 추상화하고 재사용할 수 있습니다. Custom Hook은 이름이 use로 시작하는 JavaScript 함수입니다.

    // 화면의 너비를 추적하는 Custom Hook 예제
    import { useState, useEffect } from 'react';
    
    function useWindowWidth() {
      const [width, setWidth] = useState(window.innerWidth);
    
      useEffect(() => {
        const handleResize = () => setWidth(window.innerWidth);
        window.addEventListener('resize', handleResize);
        return () => window.removeEventListener('resize', handleResize);
      }, []);
    
      return width;
    }
    
    // 사용법
    function MyComponent() {
      const width = useWindowWidth();
      return <p>Window width is: {width}px</p>;
    }

    3. 렌더링 최적화의 정점

    3.1. React.lazySuspense를 이용한 코드 분할

    애플리케이션이 커지면 초기 로딩 속도가 느려질 수 있습니다. React.lazy를 사용하면 컴포넌트를 동적으로 import하여, 해당 컴포넌트가 실제로 렌더링될 때까지 관련 코드의 로딩을 지연시킬 수 있습니다. Suspense는 코드가 로딩되는 동안 보여줄 fallback UI(예: 스피너)를 설정하는 데 사용됩니다.

    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    
    function MyComponent() {
      return (
        <div>
          <Suspense fallback={<div>Loading...</div>}>
            <OtherComponent />
          </Suspense>
        </div>
      );
    }

    3.2. 동시성(Concurrency) 렌더링

    React 18부터 도입된 동시성 기능은 긴급하지 않은 렌더링을 잠시 중단하고, 더 중요한 사용자 입력(예: 타이핑)을 먼저 처리하여 앱의 반응성을 높입니다.

    • useTransition: 상태 업데이트를 긴급하지 않은 것으로 표시하여, UI가 멈추는 현상 없이 부드러운 전환을 가능하게 합니다.
    • useDeferredValue: 값의 업데이트를 지연시켜, 해당 값을 사용하는 컴포넌트의 리렌더링이 다른 긴급한 렌더링을 막지 않도록 합니다.

    4. 견고한 애플리케이션 설계

    4.1. 디자인 패턴

    고급 컴포넌트를 설계할 때 자주 사용되는 패턴을 이해하는 것은 중요합니다. 예를 들어, 폼(Form)을 다룰 때 상태를 React에서 모두 제어하는 제어 컴포넌트(Controlled Component) 방식과, DOM 자체에 맡기고 필요할 때만 값을 가져오는 비제어 컴포넌트(Uncontrolled Component) 방식의 장단점을 이해하고 상황에 맞게 사용해야 합니다.

    4.2. 폴더 구조

    애플리케이션이 성장함에 따라 유지보수 가능한 폴더 구조를 갖는 것이 중요합니다. 일반적인 패턴으로는 기능(feature)별로 컴포넌트, hooks, API 호출, 타입 등을 그룹화하는 Feature-based 구조가 있습니다.

    5. 프로덕션 준비와 배포

    5.1. 환경 변수 관리

    개발, 스테이징, 프로덕션 환경에 따라 다른 API 주소나 키 값을 사용해야 합니다. React(create-react-app 기준)는 .env 파일을 통해 환경 변수를 관리하는 기능을 기본적으로 제공합니다. REACT_APP_ 접두사를 사용하여 변수를 정의하고, 코드에서는 process.env.REACT_APP_API_URL과 같이 접근합니다.

    5.2. Docker를 이용한 컨테이너화

    Docker는 애플리케이션과 그 의존성들을 컨테이너라는 격리된 환경으로 패키징하는 기술입니다. Docker를 사용하면 “제 컴퓨터에서는 잘 됐는데…”와 같은 문제를 없애고, 개발부터 프로덕션까지 일관된 환경에서 애플리케이션을 실행하고 배포할 수 있습니다.


    이 가이드에서 다룬 주제들은 React를 사용하여 복잡하고 성능이 뛰어난 실제 서비스를 구축하는 데 필수적인 요소들입니다. 꾸준한 학습과 실제 프로젝트 적용을 통해 React 전문가로 거듭나시길 바랍니다.

  • React-query로 데이터 패칭 최적화하기

    React 애플리케이션에서 데이터를 가져오는 작업, 즉 데이터 페칭(Data Fetching)은 핵심적인 부분입니다. 많은 개발자가 useEffectuseState, 그리고 Axios 같은 HTTP 클라이언트를 조합하여 이 문제를 해결합니다. 하지만 이 방식은 애플리케이션이 복잡해질수록 여러 문제점을 드러냅니다.

    이 글에서는 전통적인 데이터 페칭 방식의 한계를 살펴보고, React-query가 어떻게 이 문제들을 해결하고 데이터 페칭을 최적화하는지 알아봅니다.

    목차

    1. 전통적인 데이터 페칭의 문제점
    2. 서버 상태 관리 라이브러리, React-query
    3. 핵심 API로 최적화 시작하기
      • useQuery: 데이터 조회와 캐싱의 자동화
      • useMutation: 간편한 데이터 변경
      • queryClient.invalidateQueries: 데이터 동기화의 마법
    4. 사용자 경험을 극대화하는 고급 패턴
      • Optimistic Updates (낙관적 업데이트)
    5. 결론: 왜 React-query를 선택해야 하는가?

    1. 전통적인 데이터 페칭의 문제점

    useEffectuseState를 사용해 데이터를 가져오는 코드를 생각해봅시다.

    import React, { useState, useEffect } from 'react';
    import axios from 'axios';
    
    function TodoList() {
      const [isLoading, setIsLoading] = useState(false);
      const [isError, setIsError] = useState(false);
      const [data, setData] = useState(null);
    
      useEffect(() => {
        setIsLoading(true);
        axios.get('/api/todos')
          .then(response => {
            setData(response.data);
          })
          .catch(error => {
            setIsError(true);
          })
          .finally(() => {
            setIsLoading(false);
          });
      }, []);
    
      if (isLoading) return <div>로딩 중...</div>;
      if (isError) return <div>에러 발생!</div>;
    
      // ...
    }

    이 코드에는 몇 가지 비효율적인 부분이 있습니다.

    • 보일러플레이트 코드: isLoading, isError, data 상태를 모두 직접 선언하고 관리해야 합니다. 컴포넌트마다 이 코드가 반복됩니다.
    • 캐싱의 부재: 다른 컴포넌트에서 동일한 데이터를 요청하면, 불필요한 API 호출이 다시 발생합니다.
    • 상태 동기화의 어려움: 사용자가 할 일을 추가(Create)하거나 삭제(Delete)했을 때, 목록을 최신 상태로 다시 불러오는(refetch) 로직을 수동으로 구현해야 합니다.

    2. 서버 상태 관리 라이브러리, React-query

    React-query는 자신을 “데이터 페칭 라이브러리”가 아닌 “서버 상태(Server State) 관리 라이브러리”라고 소개합니다. 서버에서 받아온 비동기 데이터를 클라이언트의 UI 상태와 분리하여, 캐싱, 동기화, 업데이트를 매우 효율적으로 관리해줍니다.

    3. 핵심 API로 최적화 시작하기

    3.1. useQuery: 데이터 조회와 캐싱의 자동화

    useQuery는 데이터 조회를 위한 기본 Hook입니다. 위의 장황한 코드를 아래와 같이 단 몇 줄로 줄일 수 있습니다.

    import { useQuery } from 'react-query';
    import axios from 'axios';
    
    const fetchTodos = () => axios.get('/api/todos').then(res => res.data);
    
    function TodoList() {
      const { data, isLoading, isError } = useQuery('todos', fetchTodos);
    
      if (isLoading) return <div>로딩 중...</div>;
      if (isError) return <div>에러 발생!</div>;
    
      // ...
    }

    useQueryisLoading, isError, data 상태를 알아서 관리해줄 뿐만 아니라, 강력한 캐싱 기능을 제공합니다.

    • useQuery('todos', ...)의 첫 번째 인자인 'todos'는 이 데이터의 고유한 쿼리 키(Query Key)입니다.
    • React-query는 이 키를 기준으로 데이터를 캐시에 저장합니다.
    • 다른 컴포넌트에서 동일한 'todos' 키로 useQuery를 호출하면, API를 다시 호출하는 대신 캐시된 데이터를 즉시 반환하여 불필요한 네트워크 요청을 막습니다.
    • stale-while-revalidate 전략을 기본으로 사용하여, 캐시된 데이터를 먼저 보여준 후 백그라운드에서 데이터를 최신으로 업데이트하여 사용자 경험과 데이터 정합성을 모두 잡습니다.

    3.2. useMutation: 간편한 데이터 변경

    데이터를 생성, 수정, 삭제할 때는 useMutation을 사용합니다.

    import { useMutation } from 'react-query';
    
    const addTodo = (newTodo) => axios.post('/api/todos', newTodo);
    
    function AddTodoComponent() {
      const mutation = useMutation(addTodo);
    
      const handleAddTodo = () => {
        mutation.mutate({ title: '새로운 할 일', completed: false });
      };
    
      // ...
    }

    3.3. queryClient.invalidateQueries: 데이터 동기화의 마법

    useMutation의 가장 강력한 기능은 다른 쿼리와의 연동입니다. 할 일이 추가된 후, 목록을 어떻게 새로고침할 수 있을까요? invalidateQueries를 사용하면 됩니다.

    import { useMutation, useQueryClient } from 'react-query';
    
    function AddTodoComponent() {
      const queryClient = useQueryClient();
    
      const mutation = useMutation(addTodo, {
        onSuccess: () => {
          // 'todos' 쿼리를 무효화시켜 자동으로 다시 가져오게 합니다.
          queryClient.invalidateQueries('todos');
        },
      });
    
      // ...
    }

    mutation이 성공하면 onSuccess 콜백이 실행되고, queryClient.invalidateQueries('todos')'todos'라는 키를 가진 useQuery를 “오래된 데이터(stale)”로 만듭니다. 해당 쿼리를 사용하는 모든 컴포넌트는 자동으로 데이터를 리เฟetch하여 UI를 최신 상태로 업데이트합니다. 더 이상 수동으로 상태를 관리할 필요가 없습니다.

    4. 사용자 경험을 극대화하는 고급 패턴

    Optimistic Updates (낙관적 업데이트)

    서버의 응답을 기다리지 않고, mutation이 성공할 것이라고 “낙관”하여 UI를 먼저 업데이트하는 기법입니다. 사용자는 즉각적인 피드백을 받게 되어 앱이 매우 빠르게 느껴집니다. useMutationonMutateonError 옵션을 사용해 구현할 수 있으며, 만약 mutation이 실패하면 이전 상태로 롤백합니다.

    5. 결론: 왜 React-query를 선택해야 하는가?

    React-query는 단순히 코드를 줄여주는 것을 넘어, 애플리케이션의 데이터 페칭 로직을 근본적으로 최적화합니다.

    • 성능 향상: 자동 캐싱과 불필요한 API 호출 방지로 뛰어난 성능을 제공합니다.
    • 개발 경험 향상: 반복적인 로딩/에러 상태 관리를 제거하고, 선언적으로 데이터를 다룰 수 있게 해줍니다.
    • 사용자 경험 향상: stale-while-revalidate, Optimistic Updates 등의 패턴으로 사용자에게 빠르고 부드러운 인터페이스를 제공합니다.

    React 애플리케이션의 데이터 페칭을 한 단계 끌어올리고 싶다면, React-query는 단연 최고의 선택입니다.