[NextJS/TroubleShooting] - localStorage 데이터 저장

ZenTechie·2023년 8월 10일
0

Troubleshooting

목록 보기
2/9
post-thumbnail

weather-NextJS 마이그레이션 중 마주친 에러 해결

결과물

도시 목록 렌더링

localStorage 저장


왜 localStorage를 사용했을까?

✅ localStorage를 사용한 이유는, 별도의 데이터베이스를 사용하고 싶지 않았고 프로젝트의 규모도 크지 않아서 localStorage로도 충분히 구현이 가능하다고 판단했다.


일단, 내가 생각한 로직은 아래와 같다.

  1. 초기 렌더링 시, localStorage에 저장되어 있는 값을 가져와서 '저장한 도시 목록'을 렌더링 한다.
  2. 사용자가 키워드를 검색한다.
  3. 키워드를 포함하는 도시(나라)들의 결과를 목록으로 보여준다.
  4. 사용자가 목록에서 원하는 도시를 선택하면, 해당 도시의 정보가 localStorage에 저장된다.
  5. '저장(선택)한 도시 목록'업데이트(re-render) 된다.

기존

기존의 코드는 아래와 같고, localStorage에 값이 들어가지만 새로고침 시 모두 초기화되는 에러가 발생했다. 또한, 기능 구현에 초점을 맞추어서 예외를 따로 처리하지 못했다는 아쉬움이 있다.

  const [searchTerm, setSearchTerm] = useState<string>(''); // 검색 키워드
  const [searchResults, setSearchResults] = useState<any[]>([]); // 키워드 결과
  const [selectedCity, setSelectedCity] = useState<any>([]); // 선택한 도시
  const [storage, setStorage] = useState<any> ([JSON.parse(localStorage.getItem('selectedCity')!)]); // 기존 localStorage의 값 

  // 도시 검색 기능
  const handleSearch = (e: any) => {
    const input = e.target.value.toLowerCase();
    const filteredCountries: any[] = countriesData.countries.filter((country) =>
      country.country.toLowerCase().startsWith(input)
    );
    setSearchTerm(input);
    if (input === '') setSearchResults([]);
    else setSearchResults(filteredCountries);
  };

  // 도시 선택 시, 해당 도시 추가
  const handleClick = (city: any[]) => {
    setSelectedCity((prev) => {
      const updated = [...prev, city];
      const prevCity = JSON.parse(localStorage.getItem('selectedCity')!);
      if (prevCity !== null && prevCity !== undefined) prevCity.push(updated);
      localStorage.setItem('selectedCity', JSON.stringify(prevCity));
      return updated;
    });
    setSearchTerm('');
  };

  // 저장된 도시 목록 불러오기
  useEffect(() => {
    const prevCities: string | null = localStorage.getItem('selectedCity'); // 값이 있는지 확인
    if (prevCities) {
      setStorage(JSON.parse(prevCities));
    }
  }, []);

기존 코드의 의도는,
1. 렌더링 시, localStorage에 저장된 값을 가져와 storage 상태에 저장한다.
2. 도시를 선택했을 때 해당 도시들을 배열(selectedCity)로 만들어, localStorage에 저장한다.

기존 코드는 현재 시간 기준 바로 어제 작성했는데, 대체 무슨 의도를 보여주려했는지 잘 모르겠다. 😥
지금보니 정말 엉망인 코드다..

수정 후

수정된 코드는 아래와 같다.

  const [searchTerm, setSearchTerm] = useState<string>(''); // 검색 바
  const [searchResults, setSearchResults] = useState<any[]>([]); // 검색 결과
  const [storage, setStorage] = useState<any>(); // 저장된 도시 목록

  // 도시 검색 기능
  const handleSearch = (e: any) => {
    const input = e.target.value.toLowerCase();
    const filteredCountries: any[] = countriesData.countries.filter((country) =>
      country.country.toLowerCase().startsWith(input)
    );
    setSearchTerm(input);
    if (input !== '') setSearchResults(filteredCountries);
  };

  // 도시 선택 시 해당 도시 추가
  const handleClick = (city : any) => {
    let curStorage = JSON.parse(localStorage.getItem('selectedCity')!);
    setSearchTerm('');
    setSearchResults([]);

    // 이미 추가된 도시인지 확인
    const alreadyExist = curStorage.some((item: any) => item.country === city.country);
    if (alreadyExist) return; // 추가된 도시라면 추가하지 않음

    // 추가하지 않았을 경우 추가한다.
    curStorage.push(city);
    localStorage.setItem('selectedCity', JSON.stringify(curStorage));
    setStorage(curStorage);
  };

  // 첫 렌더링 시, 저장된 도시 목록 불러오기
  useEffect(() => {
    const curStorage = JSON.parse(localStorage.getItem('selectedCity')!);
    if (curStorage === null) {
      localStorage.setItem('selectedCity', JSON.stringify([]));
      return;
    }
    setStorage(curStorage);
  }, []);

  // 도시 선택 시, re-render
  useEffect(() => {

  }, [storage]);

도시 선택 시 해당 도시 추가하기

잘 생각해봤을 때, 기존의 selectedCity(state)는 필요가 없다는 걸 알았다. 기존에는 도시를 선택하면 이를 selectedCity에 넣어서, 한번에 localStorage로 넣으려는 의도였던 것 같은데 그러지 않아도 된다.

단순하게, handleClick인자로 들어오는 city처리하기만 하면 된다.
의도대로 로직이 흘러가게 하려면, 먼저 localStorage의 값을 불러와 저장해야 한다.
왜냐하면, localStorage.setItem()을 하게되면, 이는 기존의 값에 이어 붙이는게 아닌, 기존의 값을 대체한다는 의미이기 때문이다.

그 후, 중복된 도시를 추가하면 안되므로 선택한 도시가 이미 localStorage저장이 되어있는지 확인해야 한다.

이는 some 함수를 사용했다.
some배열 또는 객체에서, 조건에 맞는 찾고자 하는 값이 있는지 없는지의 결과를 true, false로 반환해준다.

사용 방법: [배열 또는 객체].some(조건)

이미 저장된 도시가 아니라면, curStorage에 추가한다.(push()) 그리고 curStoragelocalStorage에 저장하면 된다.(setItem())
✅ 이때, localStorage문자열만 저장이 가능하므로 JSON.stringify()를 사용한다.


첫 렌더링 시, 저장된 도시 목록 불러오기

만약, localStorage에 저장된 도시 목록(selectedCity)이 없다면 null을 반환할 것이다. 이 null을 처리하지 않으면, storage(state)null이 들어가는 참사가 발생한다.

따라서, 예외를 처리해줬다.

curStorage === null 이라면, localStorage에 []을 저장한다.
[]을 저장하냐면, 도시 목록을 배열로 저장하여 이를 map()을 사용하여 화면에 뿌려주기 위함이다.


도시 선택 시, re-render

도시를 선택했을 때, 선택한 도시가 바로 목록에 나타나도록 하려했다.

처음에 아래와 같은 코드로 작성했을 때, useEffect()무한히 호출됐다.
그야 당연한게, setStorage()가 실행되면 의존성으로 인해, useEffect()가 실행되고 다시 setStorage()가 실행되기 때문이다.

useEffect(() => {
    const curStorage = JSON.parse(localStorage.getItem('selectedCity')!);
    if (curStorage === null) {
      localStorage.setItem('selectedCity', JSON.stringify([]));
      return;
    }
    setStorage(curStorage);
  }, [storage]);

그래서 아래의 코드를 추가했다.

useEffect(() => {
  
}, [storage]);

딱히, 수행해야 하는 코드가 없으므로 빈 {}로 뒀다.
이렇게 사용해도 되는 코드인지는 확실하지 않아서, 추가적으로 확인을 해볼 것이다.

생각보다 쉽게 구현이 가능할 거라고 생각했는데, 의외로 복병이었다.

어떤 함수를 써서, 기능을 구현해야 하는지를 알고 있는데 막상 구현했을 때 원하는대로 흘러가지 않는다면 그림이나 글로 프로토타입을 작성해보는 것이 좋다.

참고자료

JavaScript Some Function
JavaScript JSON.parse
JavaScript JSON.stringify
localStorage 조작하기

profile
데브코스 진행 중.. ~ 2024.03

0개의 댓글