카테고리 없음

브라우저 메모리와 상태 관리: 헷갈리는 개념들을 완전 정복하기

Kun Woo Kim 2025. 6. 1. 17:28
반응형

들어가며

프론트엔드 개발을 시작하면서 가장 혼란스러웠던 것 중 하나가 바로 상태 관리였습니다.

  • "useState는 새로고침하면 날아간다"
  • "localStorage에 저장하면 남아있다"
  • "TanStack Query는 서버 상태를 관리한다"

이런 말들을 들을 때마다 "도대체 데이터가 어디에 저장되는 거야?" 라는 궁금증이 생겼습니다. 브라우저 메모리? 그게 내 컴퓨터의 RAM인가? 서버 상태 관리? 클라이언트에서 서버를 관리한다고?

오늘은 이러한 헷갈리는 개념들을 시각적 자료와 함께 체계적으로 정리해보겠습니다. 각 상태 관리 방식이 실제로 어디에 저장되고, 언제까지 유지되며, 어떻게 동작하는지 명확하게 이해할 수 있도록 도와드리겠습니다.


브라우저 메모리 구조의 전체적인 이해

먼저 브라우저가 데이터를 저장하는 공간들을 시각적으로 살펴보겠습니다.

🏗️ 브라우저 메모리 아키텍처

📱 브라우저 프로세스
├── 🧠 RAM (휘발성 메모리)
│   ├── JavaScript 변수
│   ├── useState() 상태
│   ├── React 컴포넌트 상태
│   └── TanStack Query 캐시 (기본값)
│
└── 💾 디스크 저장소 (영구 저장)
    ├── localStorage
    ├── sessionStorage
    ├── IndexedDB
    ├── WebSQL (deprecated)
    └── 브라우저 캐시

📊 저장소별 특성 비교표

저장소 위치 용량 제한 지속성 동기/비동기 사용 용도
RAM 메모리 브라우저 한계 ❌ 페이지 새로고침 시 초기화 동기 임시 상태, 변수
localStorage 디스크 ~5-10MB ✅ 수동 삭제 전까지 영구 동기 설정값, 토큰
sessionStorage 디스크 ~5-10MB ⚠️ 탭 닫을 때까지 동기 세션 데이터
IndexedDB 디스크 ~50MB-무제한 ✅ 수동 삭제 전까지 영구 비동기 대용량 데이터

useState가 새로고침 시 초기화되는 이유

🔄 React 상태 라이프사이클

1. 페이지 로드
   ↓
2. React 앱 초기화
   ↓
3. useState 초기값 설정 → 🧠 RAM 저장
   ↓
4. 사용자 인터랙션 → 상태 업데이트 → 🧠 RAM 업데이트
   ↓
5. 새로고침 버튼 클릭
   ↓
6. 🗑️ RAM 초기화 → React 앱 재시작 → useState 초기값으로 복원

💡 실제 동작 예시

// 컴포넌트 마운트 시점
function Counter() {
  const [count, setCount] = useState(0); // 🧠 RAM에 0 저장

  const increment = () => {
    setCount(count + 1); // 🧠 RAM에서 값 업데이트
  };

  return (
    <div>
      <p>현재 카운트: {count}</p> {/* 🧠 RAM에서 값 읽기 */}
      <button onClick={increment}>+1</button>
    </div>
  );
}

결과: 버튼을 10번 눌러서 카운트가 10이 되어도, 새로고침하면 다시 0으로 돌아갑니다.

🧠 메모리 관점에서 본 상태 변화

시점 RAM 상태 화면 표시
페이지 로드 count: 0 "현재 카운트: 0"
버튼 3번 클릭 count: 3 "현재 카운트: 3"
새로고침 RAM 초기화 "현재 카운트: 0"
버튼 5번 클릭 count: 5 "현재 카운트: 5"

localStorage는 왜 새로고침 후에도 남아있을까?

💾 디스크 기반 저장소의 동작 원리

localStorage는 브라우저가 컴퓨터의 하드디스크(SSD/HDD)에 직접 파일로 저장합니다.

🖥️ 컴퓨터 디스크
└── 브라우저 폴더
    └── User Data
        └── Default
            └── Local Storage
                └── leveldb 파일들
                    ├── 000003.log
                    ├── CURRENT
                    └── MANIFEST-000001

📍 실제 저장 경로

운영체제 Chrome 저장 경로
Windows C:\Users\<사용자명>\AppData\Local\Google\Chrome\User Data\Default\Local Storage\
macOS ~/Library/Application Support/Google/Chrome/Default/Local Storage/
Linux ~/.config/google-chrome/Default/Local Storage/

🔄 localStorage 라이프사이클

1. 데이터 저장
   localStorage.setItem('key', 'value')
   ↓
2. 💾 디스크에 파일로 저장
   ↓
3. 새로고침 또는 브라우저 재시작
   ↓
4. 💾 디스크에서 파일 읽기
   ↓
5. localStorage.getItem('key') → 'value' 반환

💡 실제 동작 예시

// localStorage를 활용한 영구 카운터
function PersistentCounter() {
  // 🔍 페이지 로드 시 디스크에서 값 읽기
  const [count, setCount] = useState(() => {
    const saved = localStorage.getItem('count');
    return saved ? parseInt(saved) : 0;
  });

  const increment = () => {
    const newCount = count + 1;
    setCount(newCount);
    // 💾 디스크에 즉시 저장
    localStorage.setItem('count', newCount.toString());
  };

  return (
    <div>
      <p>영구 카운트: {count}</p>
      <button onClick={increment}>+1</button>
    </div>
  );
}

🆚 useState vs localStorage 비교

시나리오 useState 결과 localStorage 결과
초기 로드 count: 0 count: 0
버튼 5번 클릭 count: 5 (🧠 RAM) count: 5 (💾 디스크)
새로고침 count: 0 count: 5
브라우저 재시작 count: 0 count: 5
컴퓨터 재부팅 count: 0 count: 5

클라이언트 상태 vs 서버 상태: 완전 이해하기

🏷️ 상태 분류의 기준

상태를 분류하는 핵심은 "데이터의 출처와 변경 주체"입니다.

🌐 전체 상태
├── 📱 클라이언트 상태 (Client State)
│   ├── 출처: 사용자 인터랙션
│   ├── 변경: 클라이언트에서 직접 제어
│   └── 예시: 모달 열림/닫힘, 폼 입력값, 현재 탭
│
└── 🌍 서버 상태 (Server State)
    ├── 출처: 외부 API, 데이터베이스
    ├── 변경: 서버에서 변경, 클라이언트는 동기화
    └── 예시: 사용자 정보, 게시글 목록, 댓글

📊 상세 비교표

구분 클라이언트 상태 서버 상태
데이터 소스 브라우저 로컬 외부 API, 데이터베이스
변경 주체 사용자 인터랙션 서버, 다른 사용자
동기화 필요성 ❌ 불필요 ✅ 필수
최신성 보장 ✅ 항상 최신 ❌ 별도 확인 필요
네트워크 의존성 ❌ 오프라인 가능 ✅ 온라인 필요
캐싱 복잡도 🟢 단순 🔴 복잡

🎯 실제 예시로 이해하기

// 📱 클라이언트 상태 예시
function TodoApp() {
  const [isModalOpen, setIsModalOpen] = useState(false); // 클라이언트 상태
  const [selectedTab, setSelectedTab] = useState('all'); // 클라이언트 상태
  const [searchQuery, setSearchQuery] = useState(''); // 클라이언트 상태

  // 🌍 서버 상태 예시
  const { data: todos } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos // API 호출
  });

  const { data: user } = useQuery({
    queryKey: ['user'],
    queryFn: fetchCurrentUser // API 호출
  });

  return (
    <div>
      {/* 클라이언트 상태로 UI 제어 */}
      <TabBar selectedTab={selectedTab} onTabChange={setSelectedTab} />

      {/* 서버 상태로 데이터 표시 */}
      <TodoList todos={todos} user={user} />

      {/* 클라이언트 상태로 모달 제어 */}
      {isModalOpen && <AddTodoModal onClose={() => setIsModalOpen(false)} />}
    </div>
  );
}

TanStack Query: 서버 상태 관리의 혁신

🤔 왜 별도의 서버 상태 관리 라이브러리가 필요할까?

기존 useState만으로 서버 데이터를 관리할 때의 문제점들을 살펴보겠습니다.

❌ 기존 방식의 한계

// 🚫 useState로만 서버 데이터를 관리하는 경우
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    setError(null);

    fetchUser(userId)
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [userId]);

  // 같은 userId로 다른 컴포넌트에서도 fetchUser 호출 → 중복 요청!
  // 네트워크 오류 시 자동 재시도 없음
  // 데이터 캐싱 없음
  // 백그라운드에서 데이터 갱신 불가능
}

✅ TanStack Query의 해결책

// 🚀 TanStack Query로 개선된 버전
function UserProfile({ userId }) {
  const { data: user, isLoading, error, refetch } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000, // 5분간 캐시 유효
    retry: 3, // 실패 시 3번 재시도
  });

  // 동일한 queryKey로 다른 컴포넌트에서 호출해도 캐시된 데이터 사용!
  // 자동 에러 핸들링 및 재시도
  // 백그라운드 업데이트 지원
  // 메모리 효율적인 캐시 관리
}

🔄 TanStack Query 캐시 동작 플로우

1. useQuery 호출
   ↓
2. queryKey로 캐시 확인
   ├── 캐시 있음 → 🚀 즉시 데이터 반환 + 백그라운드 업데이트
   └── 캐시 없음 → 📡 API 요청 시작
   ↓
3. API 응답 받음
   ↓
4. 🧠 RAM 캐시에 저장
   ↓
5. 컴포넌트에 데이터 전달
   ↓
6. staleTime 지나면 다음 요청 시 백그라운드 갱신

📈 캐시 상태 전환 다이어그램

Fresh (신선) ←──staleTime──→ Stale (오래됨) ←──cacheTime──→ Removed (제거됨)
     ↑                           ↑                            ↑
즉시 사용 가능               백그라운드 갱신               메모리에서 삭제
네트워크 요청 안함            새 데이터로 교체              다음 요청 시 새로 fetch

⚙️ TanStack Query 설정 옵션과 동작

옵션 설명 기본값 예시 시나리오
staleTime 데이터를 신선하다고 간주하는 시간 0ms 사용자 정보: 5분, 실시간 데이터: 0초
cacheTime 캐시를 메모리에 보관하는 시간 5분 자주 사용하는 데이터: 10분, 일회성: 1분
retry 실패 시 재시도 횟수 3번 중요한 API: 5번, 선택적 데이터: 1번
refetchOnWindowFocus 창 포커스 시 자동 갱신 true 실시간성 중요: true, 정적 데이터: false

새로고침 시 데이터 동작 비교

🔄 각 상태 관리 방식의 새로고침 후 동작

// 시나리오: 사용자가 카운터를 5로 증가시킨 후 새로고침

// 1️⃣ useState - 🧠 RAM 저장
const [count, setCount] = useState(0);
// 새로고침 후: count = 0 (초기값으로 리셋)

// 2️⃣ localStorage - 💾 디스크 저장
const [count, setCount] = useState(() => {
  return parseInt(localStorage.getItem('count') || '0');
});
// 새로고침 후: count = 5 (디스크에서 복원)

// 3️⃣ TanStack Query - 🧠 RAM 캐시 (기본)
const { data: user } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser
});
// 새로고침 후: 캐시 없음 → API 재요청

// 4️⃣ TanStack Query + Persist - 💾 디스크 캐시
persistQueryClient({
  queryClient,
  persister: createSyncStoragePersister({
    storage: window.localStorage,
  }),
});
// 새로고침 후: 디스크에서 캐시 복원 → API 요청 없음

📊 새로고침 후 데이터 상태 매트릭스

상태 관리 방식 초기 로드 새로고침 후 브라우저 재시작 장점 단점
useState 초기값 초기값 초기값 🟢 단순함 🔴 데이터 손실
localStorage 저장된 값 저장된 값 저장된 값 🟢 영구 보존 🔴 수동 관리
TanStack Query API 요청 API 요청 API 요청 🟢 자동 캐싱 🔴 새로고침 시 재요청
TQ + Persist 캐시/API 캐시된 값 캐시된 값 🟢 캐시 보존 🔴 설정 복잡

📌 핵심 포인트:

  • RAM 저장소는 새로고침 시 모든 데이터가 초기화됩니다
  • 디스크 저장소는 브라우저를 껐다 켜도 데이터가 유지됩니다

실제 프로젝트에서의 적용 가이드

🎯 상황별 최적 상태 관리 전략

1️⃣ 소셜 미디어 앱 사례

function SocialMediaApp() {
  // 📱 UI 상태 - useState
  const [isMenuOpen, setIsMenuOpen] = useState(false);
  const [selectedTab, setSelectedTab] = useState('home');

  // 🔐 인증 정보 - localStorage (새로고침 후에도 로그인 유지)
  const [authToken] = useState(() => localStorage.getItem('token'));

  // 🌍 사용자 데이터 - TanStack Query (자동 캐싱)
  const { data: posts } = useQuery({
    queryKey: ['posts', selectedTab],
    queryFn: () => fetchPosts(selectedTab),
    staleTime: 30 * 1000, // 30초마다 갱신
  });

  // 👤 프로필 정보 - TanStack Query + 긴 캐시
  const { data: profile } = useQuery({
    queryKey: ['profile'],
    queryFn: fetchProfile,
    staleTime: 10 * 60 * 1000, // 10분간 캐시
  });
}

2️⃣ 전자상거래 앱 사례

function ShoppingApp() {
  // 🛒 장바구니 - localStorage (새로고침 후에도 유지)
  const [cartItems, setCartItems] = useState(() => {
    const saved = localStorage.getItem('cart');
    return saved ? JSON.parse(saved) : [];
  });

  // 🔍 검색/필터 - useState (페이지별 임시 상태)
  const [searchQuery, setSearchQuery] = useState('');
  const [priceRange, setPriceRange] = useState([0, 1000]);

  // 📦 상품 데이터 - TanStack Query
  const { data: products } = useQuery({
    queryKey: ['products', searchQuery, priceRange],
    queryFn: () => fetchProducts({ search: searchQuery, price: priceRange }),
    staleTime: 2 * 60 * 1000, // 2분간 캐시
  });
}

🔧 성능 최적화 팁

TanStack Query 캐시 최적화

// ✅ 좋은 예: 효율적인 캐시 설정
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // 1분: 일반적인 데이터
      cacheTime: 5 * 60 * 1000, // 5분: 메모리 보관
      retry: (failureCount, error) => {
        // 401 에러는 재시도 안함
        if (error.status === 401) return false;
        return failureCount < 3;
      },
    },
  },
});

// 📊 데이터 종류별 차별화된 설정
const { data: realtimeData } = useQuery({
  queryKey: ['realtime'],
  queryFn: fetchRealtimeData,
  staleTime: 0, // 항상 최신 데이터
  refetchInterval: 5000, // 5초마다 갱신
});

const { data: staticData } = useQuery({
  queryKey: ['static'],
  queryFn: fetchStaticData,
  staleTime: 24 * 60 * 60 * 1000, // 24시간 캐시
  cacheTime: 48 * 60 * 60 * 1000, // 48시간 보관
});

메모리 사용량 모니터링과 최적화

🔍 브라우저 개발자 도구로 메모리 확인하기

Chrome DevTools 메모리 분석

1. F12 → Performance 탭
2. 메모리 체크박스 활성화
3. 레코딩 시작 → 앱 사용 → 레코딩 종료
4. 메모리 사용 패턴 분석

📊 확인할 포인트:
- JavaScript Heap: useState, 변수들의 메모리 사용량
- DOM Nodes: 컴포넌트들의 메모리 사용량
- Event Listeners: 이벤트 핸들러 메모리 누수 확인

메모리 누수 방지 체크리스트

항목 확인 방법 해결 방법
useEffect 정리 return 문 확인 cleanup 함수 작성
이벤트 리스너 addEventListener 확인 removeEventListener 추가
타이머/인터벌 setInterval 확인 clearInterval 추가
구독(Subscription) Observer 패턴 확인 unsubscribe 호출
// ✅ 올바른 메모리 관리 예시
function Component() {
  useEffect(() => {
    const interval = setInterval(() => {
      // 주기적 작업
    }, 1000);

    const handleResize = () => {
      // 윈도우 리사이즈 처리
    };

    window.addEventListener('resize', handleResize);

    // 🧹 정리 함수: 컴포넌트 언마운트 시 실행
    return () => {
      clearInterval(interval);
      window.removeEventListener('resize', handleResize);
    };
  }, []);
}

미래의 상태 관리: 신기술 동향

🚀 차세대 상태 관리 기술들

1️⃣ React Server Components와 상태 관리

// 서버 컴포넌트에서 직접 데이터 페치
async function ProductList() {
  // 서버에서 실행되므로 클라이언트 상태 관리 불필요
  const products = await fetchProducts();

  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

2️⃣ Zustand + TanStack Query 조합

// 클라이언트 상태는 Zustand, 서버 상태는 TanStack Query
const useUIStore = create((set) => ({
  theme: 'light',
  sidebar: false,
  setTheme: (theme) => set({ theme }),
  toggleSidebar: () => set((state) => ({ sidebar: !state.sidebar })),
}));

function App() {
  const { theme, sidebar } = useUIStore(); // 클라이언트 상태
  const { data: user } = useQuery(['user'], fetchUser); // 서버 상태

  return (
    <div className={theme}>
      {sidebar && <Sidebar />}
      <UserProfile user={user} />
    </div>
  );
}

📈 성능 지표 및 벤치마크

상태 관리 방식 초기 로드 시간 메모리 사용량 네트워크 요청 개발 복잡도
useState만 🟢 빠름 🟢 적음 🔴 비효율적 🟢 낮음
Redux 🔴 느림 🔴 많음 🔴 비효율적 🔴 높음
TanStack Query 🟡 보통 🟡 보통 🟢 효율적 🟡 보통
TQ + Zustand 🟡 보통 🟡 보통 🟢 효율적 🟢 낮음

결론

💡 핵심 인사이트 정리

이번 글을 통해 우리는 브라우저 메모리의 구조부터 현대적인 상태 관리 전략까지 체계적으로 살펴보았습니다. 주요 포인트들을 다시 한번 정리해보겠습니다:

🎯 메모리 관점에서의 상태 분류

  • RAM (휘발성): useState, 일반 변수 🔄 새로고침 시 초기화
  • 디스크 (영구): localStorage, IndexedDB ✅ 브라우저 종료 후에도 유지

🔄 상태의 성격에 따른 분류

  • 클라이언트 상태: UI 제어, 사용자 인터랙션 🔧 useState, Zustand
  • 서버 상태: API 데이터, 외부 시스템 📡 TanStack Query, SWR

성능 최적화 전략

  • 데이터 특성에 맞는 캐시 전략 수립
  • 메모리 누수 방지를 위한 cleanup 함수 작성
  • 불필요한 API 호출 최소화

🚀 다음 단계 학습 가이드

  1. 기초 단계: useState와 useEffect로 상태 관리 패턴 익히기
  2. 중급 단계: TanStack Query로 서버 상태 관리 마스터하기
  3. 고급 단계: 상태 관리 라이브러리 조합으로 최적화하기
  4. 실무 단계: 팀 프로젝트에서 일관된 상태 관리 전략 수립하기

📚 마무리하며

처음에는 복잡해 보였던 상태 관리 개념들이 시각적 자료와 함께 정리되니 훨씬 명확해졌을 것입니다.

가장 중요한 것은 "어떤 도구를 사용할 것인가"가 아니라 "데이터의 성격을 파악하고 적절한 저장소를 선택하는 것"입니다.

React 개발에서 상태 관리는 단순히 라이브러리 사용법을 아는 것이 아니라, 브라우저의 메모리 구조와 데이터의 생명주기를 이해하는 것부터 시작됩니다. 이번 글이 여러분의 상태 관리 실력 향상에 도움이 되었기를 바랍니다.


참고 자료

반응형