Today's AntipatternAll Posts
테마
GitHubToday's AntipatternAll Posts

안티패턴을 통해 더 나은 코드를 작성하는 방법을 배워보세요. 개발자들이 실수하는 패턴들을 분석하고 개선방안을 제시합니다.

연결하기

© 2025 Smelly.dev All rights reserved.

June 27, 2025

localStorage 오남용: 위험하고 비효율적인 상태 관리 🔒

보안
상태관리
아키텍처

Summary

localStorage는 단순 문자열 저장소로, 민감한 데이터나 복잡한 객체/배열을 저장하기에 부적합합니다. XSS 공격에 취약하며, 동기적 특성으로 인해 메인 스레드를 블록하여 성능 문제를 야기할 수 있고, 데이터 양이 많아지면 관리가 어렵습니다. 대신 적절한 보안 메커니즘과 비동기 저장소를 활용하거나, 서버에 의존하는 것이 좋습니다.

Why Wrong?

왜 이 패턴이 문제인가요?

localStorage는 브라우저에 데이터를 영구적으로 저장할 수 있는 편리한 웹 스토리지 API이지만, 이를 오남용할 경우 심각한 보안 취약점, 성능 문제, 그리고 유지보수성 저하를 초래할 수 있습니다. 많은 개발자들이 localStorage의 단순성 때문에 애플리케이션의 복잡한 상태나 민감한 데이터를 무분별하게 저장하곤 합니다.

1. 치명적인 보안 취약점 (XSS 공격에 매우 취약)

localStorage에 저장된 모든 데이터는 클라이언트 사이드 JavaScript를 통해 쉽게 접근할 수 있습니다. 이는 교차 사이트 스크립팅(XSS) 공격에 매우 취약하다는 것을 의미합니다. 공격자가 악성 스크립트를 애플리케이션에 주입하는 데 성공하면, localStorage.getItem('userToken')과 같은 간단한 코드를 통해 사용자 인증 토큰, 개인 식별 정보 등 민감한 데이터를 탈취할 수 있습니다. 이는 HTTP Only 쿠키가 클라이언트 스크립트 접근을 막아주는 것과 대조됩니다.

2. 동기(Synchronous) API로 인한 성능 저하

localStorage의 모든 작업(저장, 읽기, 삭제)은 동기적으로(Synchronously) 동작합니다. 이는 데이터 저장 또는 읽기 작업이 완료될 때까지 메인 스레드가 블록된다는 의미입니다. 대량의 데이터를 저장하거나 읽으려 할 때 UI가 일시적으로 멈추는 '프리즈(freeze)' 현상이 발생할 수 있으며, 이는 사용자 경험(UX)을 심각하게 저해합니다. 특히 구형 브라우저나 저사양 기기에서는 그 영향이 더욱 두드러집니다.

3. 제한적인 저장 용량 및 복잡한 객체 직렬화/역직렬화

localStorage는 브라우저별로 약 5MB에서 10MB 정도의 제한적인 저장 용량을 가집니다. 또한, 오직 문자열(string) 데이터만 저장할 수 있습니다. 따라서 객체나 배열과 같은 복잡한 데이터를 저장하려면 JSON.stringify()를 사용하여 문자열로 직렬화해야 하고, 다시 읽을 때는 JSON.parse()를 사용하여 역직렬화해야 합니다. 이 과정에서 데이터 타입의 불일치, JSON 파싱 오류 등 예기치 않은 버그가 발생할 수 있으며, 복잡한 데이터 구조의 상태 관리를 더욱 어렵게 만듭니다.

4. 확장성과 유지보수의 어려움

애플리케이션의 상태가 복잡해지고 데이터가 많아질수록, localStorage에 직접 의존하는 방식은 데이터의 일관성을 유지하고 디버깅하는 것을 매우 어렵게 만듭니다. 여러 탭이나 창에서 동일한 localStorage 데이터에 접근할 때 발생할 수 있는 동기화 문제 또한 고려해야 합니다.

How to Fix?

어떻게 수정해야 할까요?

localStorage의 한계를 이해하고, 각 데이터의 특성과 애플리케이션의 요구사항에 맞춰 적절한 스토리지 솔루션을 선택해야 합니다.

1. 민감한 데이터는 클라이언트에서 저장하지 마세요

사용자 인증 토큰, 비밀번호, 결제 정보, 개인 식별 정보(PII) 등 민감한 데이터는 절대로 클라이언트 측 localStorage에 저장해서는 안 됩니다. 이러한 정보는 서버 측 세션 관리, 보안이 강화된 백엔드 저장소, 또는 HTTP Only 및 Secure 속성이 설정된 쿠키를 사용하여 관리해야 합니다. 토큰 기반 인증의 경우, 리프레시 토큰은 HTTP Only 쿠키에 저장하고, 액세스 토큰은 메모리에만 유지하는 방식을 고려할 수 있습니다.

2. 비동기 웹 스토리지 API 활용 (IndexedDB)

대용량 데이터나 복잡한 구조의 데이터를 클라이언트 측에 저장해야 한다면, IndexedDB와 같은 비동기 웹 스토리지 API를 사용해야 합니다. IndexedDB는:

  • 비동기적: 메인 스레드를 블록하지 않아 UI 응답성을 유지합니다.
  • 대용량 저장: 훨씬 큰 용량의 데이터를 저장할 수 있습니다.
  • 구조화된 데이터 저장: 객체 지향 데이터베이스처럼 JSON 객체를 직접 저장하고 인덱싱하여 효율적인 쿼리 및 검색이 가능합니다.

단순히 세션(브라우저 탭/창이 닫히면 사라지는) 동안만 유지해야 하는 데이터는 sessionStorage를 활용할 수 있습니다.

3. 전역 상태 관리 라이브러리 사용

애플리케이션의 복잡한 UI 상태는 Redux, Zustand, Recoil, MobX 등 전용 상태 관리 라이브러리를 사용하여 관리하는 것이 좋습니다. 이들 라이브러리는 상태 변경 로직을 중앙 집중화하여 예측 가능하고 디버깅하기 쉬운 코드를 작성할 수 있도록 돕습니다. 필요에 따라 이러한 라이브러리들은 Redux-persist와 같은 플러그인을 통해 상태를 영속화(persistence)할 수 있는 기능을 제공하며, 이는 localStorage보다 더 추상화되고 안전한 방식으로 상태를 관리할 수 있게 해줍니다.

4. localStorage는 제한적으로 활용

localStorage는 비민감하고 단순한 데이터(예: 사용자 UI 테마 설정, 마지막 방문 페이지, 임시 저장된 폼 데이터)를 저장하는 용도로만 제한적으로 사용해야 합니다. 이러한 데이터는 사용자 경험을 개선하는 데 도움이 되지만, 보안이나 성능에 큰 영향을 주지 않습니다.

요약: localStorage는 간단한 키-값 문자열 저장을 위한 도구이지, 복잡한 상태 관리나 보안이 요구되는 데이터 저장에 적합한 솔루션이 아닙니다. 데이터의 종류와 용도에 따라 적절한 저장소를 선택하는 것이 중요합니다.

Before Code (Bad)

// 🚨 안티패턴: localStorage에 민감 정보 및 복잡한 객체 직접 저장

import React, { useState, useEffect } from 'react';

function UserProfileEditor() {
  // 사용자 토큰을 localStorage에 직접 저장 (XSS 취약점 발생 가능)
  useEffect(() => {
    const token = 'very-sensitive-jwt-token-12345'; // 실제 환경에서는 로그인 시 받아옴
    localStorage.setItem('authToken', token);
    console.log('authToken 저장됨:', localStorage.getItem('authToken'));
  }, []);

  // 복잡한 사용자 프로필 객체를 localStorage에 직접 저장 (성능 및 유지보수 문제)
  const [userProfile, setUserProfile] = useState(() => {
    const savedProfile = localStorage.getItem('userProfile');
    // 문자열 -> 객체 변환 시 에러 처리 미흡
    return savedProfile ? JSON.parse(savedProfile) : {
      name: '',
      email: '',
      preferences: { theme: 'light', notifications: true }
    };
  });

  useEffect(() => {
    // 객체 -> 문자열 변환 시 성능 저하 및 에러 발생 가능성
    localStorage.setItem('userProfile', JSON.stringify(userProfile));
  }, [userProfile]);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setUserProfile(prevProfile => ({ ...prevProfile, [name]: value }));
  };

  const handlePreferenceChange = (key, value) => {
    setUserProfile(prevProfile => ({
      ...prevProfile,
      preferences: { ...prevProfile.preferences, [key]: value }
    }));
  };

  return (
    <div>
      <h2>내 프로필 편집</h2>
      <input
        type="text"
        name="name"
        value={userProfile.name}
        onChange={handleChange}
        placeholder="이름"
      />
      <input
        type="email"
        name="email"
        value={userProfile.email}
        onChange={handleChange}
        placeholder="이메일"
      />
      <div>
        <label>
          테마:
          <select value={userProfile.preferences.theme} onChange={(e) => handlePreferenceChange('theme', e.target.value)}>
            <option value="light">Light</option>
            <option value="dark">Dark</option>
          </select>
        </label>
      </div>
      <div>
        <label>
          알림 받기:
          <input
            type="checkbox"
            checked={userProfile.preferences.notifications}
            onChange={(e) => handlePreferenceChange('notifications', e.target.checked)}
          />
        </label>
      </div>
      <p>저장된 토큰 (개발자 도구에서 확인 가능): {localStorage.getItem('authToken')}</p>
      <p>저장된 프로필 (개발자 도구에서 확인 가능): {localStorage.getItem('userProfile')}</p>
    </div>
  );
}

export default UserProfileEditor;

After Code (Good)

// ✅ 개선된 패턴: localStorage는 단순 설정에만 활용하고, 민감/복잡 데이터는 다른 저장소 고려

import React, { useState, useEffect } from 'react';
// 전역 상태 관리 라이브러리 (예: Zustand) 또는 Context API 사용을 가정
// import { useAuthStore } from './stores/authStore'; 
// import { useUserProfileStore } from './stores/userProfileStore';

function UserProfileEditor() {
  // 1. 사용자 토큰: localStorage에 직접 저장하지 않고, HTTP Only 쿠키나 서버 세션으로 관리
  // 클라이언트 JS에서 직접 접근 불가 (보안 강화)
  // const { token } = useAuthStore(); // 예시: 전역 상태 관리 라이브러리에서 토큰을 메모리에만 유지

  // 2. 복잡한 사용자 프로필: IndexedDB 또는 서버 DB에 저장
  // IndexedDB는 비동기 및 대용량 데이터 저장에 적합
  // const [userProfile, setUserProfile] = useUserProfileStore(); // 예시: 전역 상태 관리 라이브러리 + IndexedDB 연동

  // 예시를 위해 단순화된 로컬 상태 사용 (실제로는 전역 상태 관리 라이브러리 권장)
  const [userProfile, setUserProfile] = useState({
    name: '김개발',
    email: 'dev@example.com',
    preferences: { theme: 'light', notifications: true }
  });

  // 3. localStorage는 간단한 사용자 UI 설정 등 비민감 데이터에만 활용
  const [theme, setTheme] = useState(() => {
    return localStorage.getItem('userTheme') || 'light';
  });

  useEffect(() => {
    localStorage.setItem('userTheme', theme);
    console.log('사용자 테마 설정 저장됨:', theme);
  }, [theme]);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setUserProfile(prevProfile => ({ ...prevProfile, [name]: value }));
  };

  const handlePreferenceChange = (key, value) => {
    if (key === 'theme') {
      setTheme(value); // 테마는 localStorage로 관리
    } else {
      // 그 외의 프로필 설정은 userProfile 상태로 관리 (IndexedDB 또는 서버 동기화)
      setUserProfile(prevProfile => ({
        ...prevProfile,
        preferences: { ...prevProfile.preferences, [key]: value }
      }));
    }
  };

  return (
    <div>
      <h2>내 프로필 편집</h2>
      <input
        type="text"
        name="name"
        value={userProfile.name}
        onChange={handleChange}
        placeholder="이름"
      />
      <input
        type="email"
        name="email"
        value={userProfile.email}
        onChange={handleChange}
        placeholder="이메일"
      />
      <div>
        <label>
          테마:
          <select value={theme} onChange={(e) => handlePreferenceChange('theme', e.target.value)}>
            <option value="light">Light</option>
            <option value="dark">Dark</option>
          </select>
        </label>
      </div>
      <div>
        <label>
          알림 받기:
          <input
            type="checkbox"
            checked={userProfile.preferences.notifications}
            onChange={(e) => handlePreferenceChange('notifications', e.target.checked)}
          />
        </label>
      </div>
      {/* 토큰은 클라이언트 JS에서 직접 접근 불가하므로 표시하지 않음 */}
      {/* <p>저장된 토큰 (서버/HTTP Only 쿠키로 관리)</p> */}
      <p>저장된 테마 (localStorage): {localStorage.getItem('userTheme')}</p>
      <p>저장된 프로필 (IndexedDB 또는 서버 DB로 관리): {JSON.stringify(userProfile)}</p>
    </div>
  );
}

export default UserProfileEditor;