[Next.js] URL State vs React State vs Global State




현대 웹 애플리케이션에서 상태 관리는 복잡성의 핵심입니다. 데이터가 어디에 저장되고 어떻게 관리되는지에 따라 애플리케이션의 성능, 사용자 경험, 그리고 개발 생산성이 크게 달라집니다.

이 글에서는 웹 개발에서 가장 중요한 세 가지 상태 유형인 URL State, React State, Global State의 특성과 적절한 사용 시나리오를 깊이 있게 살펴보겠습니다.


URL State: 브라우저 주소창이 곧 상태

URL State는 브라우저의 주소창에 저장되는 상태로, 쿼리 파라미터, 경로 파라미터, 해시 등을 통해 관리됩니다. 이는 웹의 가장 근본적인 상태 관리 방식입니다.

URL State의 특징

1. 공유 가능성(Shareability) URL을 통해 특정 상태를 다른 사용자와 쉽게 공유할 수 있습니다.

// 검색 결과 페이지의 URL
https://example.com/search?q=react&category=frontend&page=2

// 이 URL을 공유하면 다른 사용자도 동일한 검색 결과를 볼 수 있음

2. 북마크 가능성(Bookmarkability) 사용자가 특정 상태의 페이지를 북마크로 저장하고 나중에 정확히 같은 상태로 돌아올 수 있습니다.

3. 브라우저 히스토리 연동 뒤로 가기/앞으로 가기 버튼이 자연스럽게 작동하여 사용자 경험을 향상시킵니다.

4. SEO 친화적 검색 엔진이 URL 파라미터를 통해 페이지 내용을 더 잘 이해할 수 있습니다.

Next.js에서의 URL State 관리

'use client';

import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback } from 'react';

function ProductFilter() {
  const router = useRouter();
  const searchParams = useSearchParams();
  
  // URL에서 현재 필터 상태 읽기
  const category = searchParams.get('category') || 'all';
  const priceRange = searchParams.get('price') || 'any';
  const sortBy = searchParams.get('sort') || 'name';

  // URL 상태 업데이트 함수
  const updateFilters = useCallback((newFilters) => {
    const params = new URLSearchParams(searchParams);
    
    Object.entries(newFilters).forEach(([key, value]) => {
      if (value && value !== 'all' && value !== 'any') {
        params.set(key, value);
      } else {
        params.delete(key);
      }
    });

    // URL 업데이트 (페이지 새로고침 없이)
    router.push(`/products?${params.toString()}`);
  }, [router, searchParams]);

  return (
    <div className="filter-panel">
      <select 
        value={category}
        onChange={(e) => updateFilters({ category: e.target.value })}
      >
        <option value="all">전체 카테고리</option>
        <option value="electronics">전자제품</option>
        <option value="clothing">의류</option>
      </select>

      <select 
        value={priceRange}
        onChange={(e) => updateFilters({ price: e.target.value })}
      >
        <option value="any">모든 가격대</option>
        <option value="0-50000">5만원 이하</option>
        <option value="50000-100000">5만원~10만원</option>
      </select>

      <select 
        value={sortBy}
        onChange={(e) => updateFilters({ sort: e.target.value })}
      >
        <option value="name">이름순</option>
        <option value="price">가격순</option>
        <option value="rating">평점순</option>
      </select>
    </div>
  );
}

URL State 사용 시나리오

  • 검색 및 필터링: 검색어, 카테고리, 정렬 옵션
  • 페이지네이션: 현재 페이지 번호
  • 탭 상태: 활성 탭 정보
  • 폼 상태: 다단계 폼의 현재 단계
  • 모달 상태: 특정 모달의 열림/닫힘 상태 (공유 필요한 경우)

React State: 컴포넌트 내부의 로컬 상태

React State는 특정 컴포넌트 내에서만 사용되는 로컬 상태입니다. useState, useReducer 등을 통해 관리됩니다.

React State의 특징

1. 컴포넌트 범위(Component-Scoped) 해당 컴포넌트와 그 자식 컴포넌트에서만 접근 가능합니다.

2. 즉시성(Immediate) 상태 변경이 즉시 반영되며 네트워크 지연이 없습니다.

3. 임시성(Temporary) 컴포넌트가 언마운트되면 상태도 함께 사라집니다.

4. 격리성(Isolation) 다른 컴포넌트의 상태와 완전히 분리되어 있어 사이드 이펙트가 없습니다.

React State 활용 예시

'use client';

import { useState, useReducer } from 'react';

// 간단한 상태 관리 - useState
function CommentForm({ onSubmit }) {
  const [comment, setComment] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errors, setErrors] = useState({});

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // 클라이언트 사이드 검증
    if (!comment.trim()) {
      setErrors({ comment: '댓글을 입력해주세요' });
      return;
    }

    setIsSubmitting(true);
    setErrors({});

    try {
      await onSubmit(comment);
      setComment(''); // 성공 후 폼 리셋
    } catch (error) {
      setErrors({ submit: '댓글 등록에 실패했습니다' });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder="댓글을 입력하세요"
        disabled={isSubmitting}
      />
      {errors.comment && <p className="error">{errors.comment}</p>}
      
      <button type="submit" disabled={isSubmitting || !comment.trim()}>
        {isSubmitting ? '등록 중...' : '댓글 등록'}
      </button>
      {errors.submit && <p className="error">{errors.submit}</p>}
    </form>
  );
}

// 복잡한 상태 관리 - useReducer
const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, {
          id: Date.now(),
          text: action.payload,
          completed: false
        }],
        inputValue: ''
      };
    
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
    
    case 'SET_INPUT':
      return {
        ...state,
        inputValue: action.payload
      };
    
    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload
      };
    
    default:
      return state;
  }
};

function TodoList() {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [],
    inputValue: '',
    filter: 'all' // 'all', 'completed', 'active'
  });

  const filteredTodos = state.todos.filter(todo => {
    switch (state.filter) {
      case 'completed': return todo.completed;
      case 'active': return !todo.completed;
      default: return true;
    }
  });

  return (
    <div>
      <input
        type="text"
        value={state.inputValue}
        onChange={(e) => dispatch({ type: 'SET_INPUT', payload: e.target.value })}
        onKeyPress={(e) => {
          if (e.key === 'Enter' && state.inputValue.trim()) {
            dispatch({ type: 'ADD_TODO', payload: state.inputValue });
          }
        }}
      />
      
      <div>
        {['all', 'active', 'completed'].map(filter => (
          <button
            key={filter}
            className={state.filter === filter ? 'active' : ''}
            onClick={() => dispatch({ type: 'SET_FILTER', payload: filter })}
          >
            {filter}
          </button>
        ))}
      </div>

      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
            />
            <span className={todo.completed ? 'completed' : ''}>{todo.text}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

React State 사용 시나리오

  • 폼 입력 상태: 입력 필드 값, 검증 상태
  • UI 상태: 모달 열림/닫힘, 드롭다운 상태
  • 로딩 및 에러 상태: API 요청의 로딩, 에러 처리
  • 임시 계산값: 컴포넌트 내에서만 필요한 derived state
  • 애니메이션 상태: 컴포넌트별 애니메이션 제어

Global State: 애플리케이션 전역 상태

Global State는 애플리케이션 전체에서 접근 가능한 상태로, 여러 컴포넌트가 공유해야 하는 데이터를 관리합니다.

Global State의 특징

1. 전역 접근성(Global Accessibility) 애플리케이션의 어느 컴포넌트에서든 접근하고 수정할 수 있습니다.

2. 지속성(Persistence) 컴포넌트가 언마운트되어도 상태가 유지됩니다.

3. 공유성(Shareability) 여러 컴포넌트가 동일한 상태를 공유하고 동기화됩니다.

4. 복잡성(Complexity) 상태 변경의 영향 범위가 넓어 예측하기 어려울 수 있습니다.

Zustand를 활용한 Global State 관리

// stores/authStore.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useAuthStore = create(
  persist(
    (set, get) => ({
      // 상태
      user: null,
      isAuthenticated: false,
      accessToken: null,
      refreshToken: null,

      // 액션
      login: async (credentials) => {
        try {
          const response = await fetch('/api/auth/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(credentials),
          });

          if (!response.ok) throw new Error('Login failed');

          const { user, accessToken, refreshToken } = await response.json();

          set({
            user,
            isAuthenticated: true,
            accessToken,
            refreshToken,
          });

          return { success: true };
        } catch (error) {
          return { success: false, error: error.message };
        }
      },

      logout: () => {
        set({
          user: null,
          isAuthenticated: false,
          accessToken: null,
          refreshToken: null,
        });
      },

      updateUser: (userData) => {
        set(state => ({
          user: { ...state.user, ...userData }
        }));
      },

      // 토큰 갱신
      refreshAccessToken: async () => {
        const { refreshToken } = get();
        if (!refreshToken) return false;

        try {
          const response = await fetch('/api/auth/refresh', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ refreshToken }),
          });

          if (!response.ok) throw new Error('Token refresh failed');

          const { accessToken: newAccessToken } = await response.json();
          set({ accessToken: newAccessToken });
          return true;
        } catch (error) {
          // 리프레시 토큰도 만료된 경우 로그아웃
          get().logout();
          return false;
        }
      },
    }),
    {
      name: 'auth-store', // 로컬 스토리지 키
      partialize: (state) => ({
        user: state.user,
        isAuthenticated: state.isAuthenticated,
        refreshToken: state.refreshToken,
        // accessToken은 제외 (보안상 이유)
      }),
    }
  )
);

// stores/uiStore.js
export const useUIStore = create((set) => ({
  // 테마 상태
  theme: 'light',
  setTheme: (theme) => set({ theme }),
  toggleTheme: () => set((state) => ({ 
    theme: state.theme === 'light' ? 'dark' : 'light' 
  })),

  // 사이드바 상태
  sidebarOpen: false,
  toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
  closeSidebar: () => set({ sidebarOpen: false }),

  // 알림 상태
  notifications: [],
  addNotification: (notification) => set((state) => ({
    notifications: [...state.notifications, {
      id: Date.now(),
      ...notification,
    }]
  })),
  removeNotification: (id) => set((state) => ({
    notifications: state.notifications.filter(n => n.id !== id)
  })),

  // 글로벌 로딩 상태
  globalLoading: false,
  setGlobalLoading: (loading) => set({ globalLoading: loading }),
}));

컴포넌트에서 Global State 사용

'use client';

import { useAuthStore } from '@/stores/authStore';
import { useUIStore } from '@/stores/uiStore';

// 인증이 필요한 페이지 컴포넌트
function ProtectedPage() {
  const { user, isAuthenticated, logout } = useAuthStore();
  const { theme, toggleTheme } = useUIStore();

  if (!isAuthenticated) {
    return <div>로그인이 필요합니다.</div>;
  }

  return (
    <div className={`page ${theme}`}>
      <header>
        <h1>안녕하세요, {user.name}님!</h1>
        <button onClick={toggleTheme}>
          {theme === 'light' ? '다크 모드' : '라이트 모드'}
        </button>
        <button onClick={logout}>로그아웃</button>
      </header>
      {/* 페이지 컨텐츠 */}
    </div>
  );
}

// 로그인 폼 컴포넌트
function LoginForm() {
  const login = useAuthStore(state => state.login);
  const addNotification = useUIStore(state => state.addNotification);
  
  const [credentials, setCredentials] = useState({
    email: '',
    password: ''
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    const result = await login(credentials);
    
    if (result.success) {
      addNotification({
        type: 'success',
        message: '로그인이 완료되었습니다.',
      });
    } else {
      addNotification({
        type: 'error',
        message: result.error || '로그인에 실패했습니다.',
      });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={credentials.email}
        onChange={(e) => setCredentials(prev => ({
          ...prev,
          email: e.target.value
        }))}
        placeholder="이메일"
      />
      <input
        type="password"
        value={credentials.password}
        onChange={(e) => setCredentials(prev => ({
          ...prev,
          password: e.target.value
        }))}
        placeholder="비밀번호"
      />
      <button type="submit">로그인</button>
    </form>
  );
}

Global State 사용 시나리오

  • 사용자 인증 상태: 로그인 정보, 권한
  • 테마 및 설정: 다크/라이트 모드, 언어 설정
  • 장바구니 상태: 전자상거래의 장바구니 아이템
  • 알림 시스템: 글로벌 토스트, 알림 메시지
  • 애플리케이션 설정: 전역 환경 설정값

상태 유형별 비교 및 선택 가이드

특성 비교표

특성URL StateReact StateGlobal State
접근 범위전역 (URL 공유)컴포넌트 내부전역
지속성브라우저 히스토리컴포넌트 생명주기애플리케이션 생명주기
공유 가능성높음 (URL)낮음높음
SEO 영향있음없음없음
복잡성낮음낮음높음
성능 영향낮음낮음중간~높음

상태 선택 결정 트리

// 상태를 어디에 저장할지 결정하는 가이드
function decideStateLocation(stateDescription) {
  // 1. URL에 저장해야 하는가?
  if (
    stateDescription.needsSharing ||           // 다른 사용자와 공유 필요
    stateDescription.needsBookmarking ||       // 북마크 가능해야 함
    stateDescription.affectsSEO ||             // SEO에 영향
    stateDescription.isNavigation              // 네비게이션 관련
  ) {
    return 'URL State';
  }

  // 2. 전역 상태로 관리해야 하는가?
  if (
    stateDescription.usedInMultipleComponents || // 여러 컴포넌트에서 사용
    stateDescription.persistsAcrossPages ||      // 페이지 간 유지 필요
    stateDescription.isUserSession ||            // 사용자 세션 정보
    stateDescription.isGlobalSetting             // 전역 설정
  ) {
    return 'Global State';
  }

  // 3. 나머지는 React State
  return 'React State';
}

// 예시 사용
const searchState = decideStateLocation({
  needsSharing: true,        // 검색 결과 공유 필요
  needsBookmarking: true,    // 검색 결과 북마크 가능
  affectsSEO: true,         // 검색어가 SEO에 영향
  isNavigation: false
});
console.log(searchState); // "URL State"

const modalState = decideStateLocation({
  needsSharing: false,
  needsBookmarking: false,
  affectsSEO: false,
  usedInMultipleComponents: false,
  persistsAcrossPages: false
});
console.log(modalState); // "React State"

실제 프로젝트에서의 하이브리드 접근법

실제 애플리케이션에서는 세 가지 상태 유형을 조합하여 사용하는 것이 일반적입니다.

전자상거래 제품 목록 페이지 예시

'use client';

import { useSearchParams, useRouter } from 'next/navigation';
import { useState, useEffect } from 'react';
import { useCartStore } from '@/stores/cartStore';
import { useUIStore } from '@/stores/uiStore';

function ProductListPage() {
  // URL State - 검색, 필터, 정렬 (공유 가능해야 함)
  const searchParams = useSearchParams();
  const router = useRouter();
  
  const searchQuery = searchParams.get('search') || '';
  const category = searchParams.get('category') || 'all';
  const sortBy = searchParams.get('sort') || 'name';
  const page = parseInt(searchParams.get('page') || '1');

  // React State - UI 상태 (컴포넌트별 임시 상태)
  const [isLoading, setIsLoading] = useState(false);
  const [products, setProducts] = useState([]);
  const [selectedProduct, setSelectedProduct] = useState(null);
  const [isQuickViewOpen, setIsQuickViewOpen] = useState(false);

  // Global State - 장바구니, 전역 UI
  const addToCart = useCartStore(state => state.addToCart);
  const cartItems = useCartStore(state => state.items);
  const addNotification = useUIStore(state => state.addNotification);

  // URL 상태 업데이트 함수
  const updateURLState = (newParams) => {
    const params = new URLSearchParams(searchParams);
    Object.entries(newParams).forEach(([key, value]) => {
      if (value && value !== 'all') {
        params.set(key, value);
      } else {
        params.delete(key);
      }
    });
    router.push(`/products?${params.toString()}`);
  };

  // 제품 데이터 로딩 (URL 상태 변경 시)
  useEffect(() => {
    const loadProducts = async () => {
      setIsLoading(true);
      try {
        const response = await fetch(`/api/products?${searchParams.toString()}`);
        const data = await response.json();
        setProducts(data.products);
      } catch (error) {
        console.error('Failed to load products:', error);
      } finally {
        setIsLoading(false);
      }
    };

    loadProducts();
  }, [searchParams]);

  // 장바구니에 추가 (Global State 업데이트)
  const handleAddToCart = (product) => {
    addToCart(product);
    addNotification({
      type: 'success',
      message: `${product.name}이(가) 장바구니에 추가되었습니다.`,
    });
  };

  // 퀵뷰 열기 (React State 업데이트)
  const handleQuickView = (product) => {
    setSelectedProduct(product);
    setIsQuickViewOpen(true);
  };

  return (
    <div className="product-list-page">
      {/* 검색 및 필터 UI (URL State 제어) */}
      <div className="filters">
        <input
          type="text"
          value={searchQuery}
          onChange={(e) => updateURLState({ search: e.target.value, page: 1 })}
          placeholder="제품 검색..."
        />
        
        <select
          value={category}
          onChange={(e) => updateURLState({ category: e.target.value, page: 1 })}
        >
          <option value="all">전체 카테고리</option>
          <option value="electronics">전자제품</option>
          <option value="clothing">의류</option>
        </select>

        <select
          value={sortBy}
          onChange={(e) => updateURLState({ sort: e.target.value })}
        >
          <option value="name">이름순</option>
          <option value="price">가격순</option>
          <option value="rating">평점순</option>
        </select>
      </div>

      {/* 제품 목록 */}
      {isLoading ? (
        <div className="loading">로딩 중...</div>
      ) : (
        <div className="product-grid">
          {products.map(product => (
            <div key={product.id} className="product-card">
              <img src={product.image} alt={product.name} />
              <h3>{product.name}</h3>
              <p className="price">{product.price.toLocaleString()}원</p>
              
              <div className="actions">
                <button onClick={() => handleQuickView(product)}>
                  미리보기
                </button>
                <button onClick={() => handleAddToCart(product)}>
                  장바구니 추가
                </button>
              </div>
              
              {/* 이미 장바구니에 있는 제품 표시 */}
              {cartItems.some(item => item.id === product.id) && (
                <span className="in-cart">장바구니에 있음</span>
              )}
            </div>
          ))}
        </div>
      )}

      {/* 페이지네이션 (URL State) */}
      <div className="pagination">
        {page > 1 && (
          <button onClick={() => updateURLState({ page: page - 1 })}>
            이전
          </button>
        )}
        <span>페이지 {page}</span>
        <button onClick={() => updateURLState({ page: page + 1 })}>
          다음
        </button>
      </div>

      {/* 퀵뷰 모달 (React State) */}
      {isQuickViewOpen && selectedProduct && (
        <QuickViewModal
          product={selectedProduct}
          onClose={() => setIsQuickViewOpen(false)}
          onAddToCart={handleAddToCart}
        />
      )}
    </div>
  );
}

// 퀵뷰 모달 컴포넌트 (React State 활용)
function QuickViewModal({ product, onClose, onAddToCart }) {
  const [selectedSize, setSelectedSize] = useState('');
  const [quantity, setQuantity] = useState(1);

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        <button className="close-button" onClick={onClose}>×</button>
        
        <div className="product-details">
          <img src={product.image} alt={product.name} />
          <div className="info">
            <h2>{product.name}</h2>
            <p className="price">{product.price.toLocaleString()}원</p>
            <p className="description">{product.description}</p>
            
            {product.sizes && (
              <div className="size-selector">
                <label>사이즈:</label>
                {product.sizes.map(size => (
                  <button
                    key={size}
                    className={selectedSize === size ? 'selected' : ''}
                    onClick={() => setSelectedSize(size)}
                  >
                    {size}
                  </button>
                ))}
              </div>
            )}
            
            <div className="quantity-selector">
              <label>수량:</label>
              <button onClick={() => setQuantity(Math.max(1, quantity - 1))}>
                -
              </button>
              <span>{quantity}</span>
              <button onClick={() => setQuantity(quantity + 1)}>
                +
              </button>
            </div>
            
            <button
              className="add-to-cart-button"
              onClick={() => {
                onAddToCart({
                  ...product,
                  selectedSize,
                  quantity
                });
                onClose();
              }}
              disabled={product.sizes && !selectedSize}
            >
              장바구니에 추가
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

성능 최적화 고려사항

URL State 최적화

import { useDeferredValue } from 'react';
import { useSearchParams } from 'next/navigation';

function SearchResults() {
  const searchParams = useSearchParams();
  const searchQuery = searchParams.get('q') || '';
  
  // 빠른 입력에 대한 디바운싱 효과
  const deferredQuery = useDeferredValue(searchQuery);
  
  // deferredQuery를 사용하여 실제 검색 수행
  const { data: results } = useQuery(
    ['search', deferredQuery],
    () => searchAPI(deferredQuery),
    { enabled: !!deferredQuery }
  );

  return (
    <div>
      <p>검색어: {searchQuery}</p>
      {/* 결과는 디퍼된 쿼리로 표시 */}
      <SearchResultList results={results} />
    </div>
  );
}

Global State 최적화

// Zustand의 selector를 활용한 불필요한 리렌더링 방지
function UserProfile() {
  // 전체 auth state가 아닌 필요한 부분만 구독
  const userName = useAuthStore(state => state.user?.name);
  const userEmail = useAuthStore(state => state.user?.email);
  
  // 이렇게 하면 다른 auth 상태가 변경되어도 리렌더링되지 않음
  return (
    <div>
      <h1>{userName}</h1>
      <p>{userEmail}</p>
    </div>
  );
}

// 여러 값이 필요한 경우 shallow 비교 사용
import { shallow } from 'zustand/shallow';

function UserDashboard() {
  const { user, isAuthenticated } = useAuthStore(
    state => ({ user: state.user, isAuthenticated: state.isAuthenticated }),
    shallow
  );
  
  if (!isAuthenticated) return <LoginForm />;
  
  return <Dashboard user={user} />;
}

테스팅 전략

URL State 테스트

import { render, screen } from '@testing-library/react';
import { useRouter } from 'next/router';
import ProductList from '@/components/ProductList';

// Next.js 라우터 모킹
jest.mock('next/router', () => ({
  useRouter: jest.fn(),
}));

describe('ProductList URL State', () => {
  const mockPush = jest.fn();
  
  beforeEach(() => {
    useRouter.mockReturnValue({
      query: { category: 'electronics', page: '2' },
      push: mockPush,
    });
  });

  it('should display products based on URL parameters', () => {
    render(<ProductList />);
    
    // URL 파라미터에 따른 초기 상태 확인
    expect(screen.getByText('전자제품')).toBeInTheDocument();
    expect(screen.getByText('페이지 2')).toBeInTheDocument();
  });

  it('should update URL when filter changes', () => {
    render(<ProductList />);
    
    const categorySelect = screen.getByLabelText('카테고리');
    fireEvent.change(categorySelect, { target: { value: 'clothing' } });
    
    expect(mockPush).toHaveBeenCalledWith('/products?category=clothing&page=1');
  });
});

Global State 테스트

import { renderHook, act } from '@testing-library/react';
import { useAuthStore } from '@/stores/authStore';

describe('Auth Store', () => {
  beforeEach(() => {
    // 각 테스트 전에 상태 초기화
    useAuthStore.getState().logout();
  });

  it('should handle login correctly', async () => {
    const { result } = renderHook(() => useAuthStore());
    
    // Mock API 응답
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve({
          user: { id: 1, name: 'Test User' },
          accessToken: 'mock-token',
        }),
      })
    );

    await act(async () => {
      const loginResult = await result.current.login({
        email: 'test@example.com',
        password: 'password'
      });
      expect(loginResult.success).toBe(true);
    });

    expect(result.current.isAuthenticated).toBe(true);
    expect(result.current.user.name).toBe('Test User');
  });
});

마무리

URL State, React State, Global State는 각각 고유한 특성과 적합한 사용 사례를 가지고 있습니다. 효과적인 상태 관리를 위해서는:

선택 기준을 명확히 하기

  • 공유 가능성이 필요한가? → URL State
  • 컴포넌트 내부에서만 사용되는가? → React State
  • 여러 컴포넌트에서 공유되는가? → Global State

성능을 고려한 설계

  • 불필요한 리렌더링 최소화
  • 적절한 캐싱 및 메모이제이션 활용
  • 상태 구독 범위 최적화

유지보수성 확보

  • 상태의 소유권과 책임 명확화
  • 일관된 네이밍 컨벤션 사용
  • 적절한 테스트 커버리지 확보

현대 웹 애플리케이션에서는 이 세 가지 상태 유형을 적절히 조합하여 사용하는 것이 핵심입니다. 각 상태의 특성을 이해하고 프로젝트 요구사항에 맞는 최적의 조합을 선택하여 더 나은 사용자 경험과 개발자 경험을 만들어보세요.




댓글 남기기