로컬스토리지 데이터 구조 변경 시 발생하는 문제와 해결 방법

로컬스토리지 데이터 구조 변경 시 발생하는 문제와 해결 방법

들어가며

최근, 내가 개발 중이던 여행 예약 플랫폼에서 로컬스토리지(LocalStorage) 관련 이슈를 발견하고 해결하는 과정이 있었다. 현재 서비스에서는 사용자의 최근 검색 기능(항공편 및 호텔 검색 시 입력한 날짜, 목적지, 인원수 등의 정보를 저장하는 기능)을 제공하고 있는데, 이 기능을 개선하면서 로컬스토리지의 데이터 구조를 변경해야 했다.

테스트 과정에서 예상치 못한 문제가 발견되었다. 새로운 코드가 적용되더라도 사용자의 로컬스토리지에는 기존 데이터 구조가 그대로 남아있기 때문에, 새 코드와의 구조적 불일치가 발생할 수 있었던 것이다. 실제로 테스트 환경에서 이전 버전의 데이터를 시뮬레이션했을 때, 새로운 코드가 기존 데이터와 맞지 않아 최근 검색 결과가 정상적으로 렌더링되지 않는 문제가 발생했다.

이 문제는 단순히 코드 수정만으로는 해결할 수 없었다. 이미 사용자들이 저장해 둔 기존 데이터를 어떻게 처리할지에 대한 고려가 필요했기 때문이다. 다행히 배포 전에 발견하여 조치할 수 있었지만, 앞으로는 이런 문제를 미리 예방할 수 있는 방법에 대해 고민하게 되었다.

사실 로컬스토리지의 데이터 구조 변경은 개발 과정에서 흔히 마주치는 문제다. 새로운 기능 추가나 기존 구조 개선 시 데이터 구조 변경이 필연적으로 발생할 수 있는데, 이미 저장된 데이터와의 호환성을 고려하지 않으면 예상치 못한 오류로 이어지기 쉽다. 그렇다면 이러한 문제를 효과적으로 예방하고 해결하려면 어떻게 해야 할까? 이번 경험을 통해 고민한 해결책을 공유하면서, 다른 개발자들은 어떻게 대처하고 있는지 함께 논의하고자 한다.

발생 가능한 문제점

데이터 구조가 변경되면 사용자의 기존 데이터와 최신 코드 간에 구조적 불일치가 발생할 수 있다. 이로 인해 참조 오류가 나타날 수 있으며, 심한 경우 갑작스러운 오류 메시지나 화면 렌더링 문제가 발생할 수도 있다.

해결 방안

처음 떠올릴 수 있는 방법은 로컬스토리지 데이터를 모두 삭제하는 것이지만, 이는 사용자의 데이터 손실로 이어지기 때문에 좋은 해결책이라고 보기 어렵다. 새로운 키를 사용하는 방식도 가능하지만, 이 경우 기존 데이터가 그대로 남아 있어 메모리 낭비가 발생할 수 있다.

개인적으로 가장 효과적이라고 생각하는 방식은 점진적 마이그레이션이다. 애플리케이션이 실행될 때 기존 데이터의 구조를 검사하고, 구 버전 데이터가 있다면 이를 읽어 새로운 구조로 변환한 후 저장하고, 기존 데이터는 삭제하는 방식이다.

다음은 점진적 마이그레이션의 간략한 개념적 예시 코드다.

※ 아래 코드는 실제 구현이 아닌 예시용 코드다.

function migrateSearchData() {
  const oldData = localStorage.getItem('travelSearches')

  if (oldData) {
    try {
      const oldSearches = JSON.parse(oldData)

      const newSearches = {
        version: '2.0',
        lastUpdated: new Date().toISOString(),
        items: oldSearches.map((search) => ({
          id: search.id || Date.now(),
          destination: search.destination,
          dates: {
            start: search.startDate,
            end: search.endDate,
          },
          travelers: search.peopleCount || 1,
        })),
      }

      localStorage.setItem('travelSearches_v2', JSON.stringify(newSearches))
      localStorage.removeItem('travelSearches')
    } catch (error) {
      console.error('마이그레이션 실패:', error)
    }
  }
}

그러나 이 방법에도 한계가 존재한다. 예를 들어, 새 데이터 구조에 필수인 데이터가 기존 데이터에 없는 경우가 있다. 이런 상황에서는 다음과 같은 두 가지 방법을 고려해 볼 수 있다.

  1. 기본값 설정 방식: 부족한 데이터는 기본값으로 채워 넣는다. 다만 정확한 데이터가 아닐 가능성이 있다.
  2. 사용자 공지 후 초기화 방식: 최근 검색 기록과 같이 정확도가 중요한 기능에서는 사용자에게 데이터 변경 사실을 알리고 데이터를 초기화하는 방법이 더 적절할 수 있다.

미래 지향적 설계 방안

예를 들어, 항공편 검색 기록은 'flightSearches'라는 키에, 호텔 검색 기록은 'hotelSearches'라는 키에, 사용자 설정은 'settings'라는 별도의 키에 저장한다면, 이후 한 가지 데이터 구조만 변경되어도 다른 데이터는 영향을 받지 않는다. 이러한 모듈화 접근법을 사용하면 유지 관리가 훨씬 쉬워진다.

const TravelStorage = {
  settings: {
    save(data) {
      localStorage.setItem('settings', JSON.stringify({ version: '1.0', data }))
    },
    load() {
      return JSON.parse(localStorage.getItem('settings') || '{"version":"1.0","data":{}}')
    },
  },

  flights: {
    save(searches) {
      localStorage.setItem('flightSearches', JSON.stringify({ version: '1.0', data: searches }))
    },
    load() {
      return JSON.parse(localStorage.getItem('flightSearches') || '{"version":"1.0","data":[]}')
    },
    clear() {
      localStorage.removeItem('flightSearches')
    },
  },

  hotels: {
    save(searches) {
      localStorage.setItem('hotelSearches', JSON.stringify({ version: '1.0', data: searches }))
    },
    load() {
      return JSON.parse(localStorage.getItem('hotelSearches') || '{"version":"1.0","data":[]}')
    },
    clear() {
      localStorage.removeItem('hotelSearches')
    },
  },
}

// 사용 예시
TravelStorage.flights.save([{ destination: 'Tokyo', date: '2025-03-01' }])
const flights = TravelStorage.flights.load()

결론

로컬스토리지 데이터 구조 변경 시, 데이터의 중요도와 성격에 따라 점진적 마이그레이션이나 초기화를 선택하는 것이 적절하다. 특히, 초기 개발 단계부터 데이터를 모듈화하여 관리한다면 이후 데이터 구조 변경에서 오는 어려움을 크게 줄일 수 있을 것이다.

내 경우, 결국 점진적 마이그레이션 방식을 선택하여 서울-오사카 왕복 2인 검색 기록이나 도쿄 3박 4일 호텔 검색 기록과 같은 기존 데이터를 최대한 보존하면서 새로운 데이터 구조로 옮겼다. 다행히 배포 전에 문제를 발견하여 사용자들은 불편 없이 서비스를 이용할 수 있었다.