FrontEnd Develop/Project : Fashion Archive

Local Storage 사용법과 이를 활용한 최상위 컴포넌트(App.jsx)에서의 책임 분리

Frisbeen 2025. 1. 14. 22:19

서론

 

프론트엔드 프로젝트를 진행하다 보면 특정 이벤트(버튼 클릭, 항목 선택 등)에서 발생한 데이터를 로컬 스토리지에 저장해야 하는 경우가 종종 있습니다. 특히 React에서 이러한 작업을 할 때, 어떤 컴포넌트가 해당 작업을 수행할 책임을 가져야 하는지를 고민하게 됩니다.

 

이번 내용은 예전에 짰던 코드를 리팩토링하는 과정에서, 로컬스토리지의 책임을 최상위 컴포넌트가 다 지고 있는데 마음에 들지 않아서 분리를 시키려고 공부를 했던 내용입니다.

 

이번 글은 제가 진행하고 있는 패션 아카이브 프로젝트에서의 최상위 컴포넌트와 하위 컴포넌트 간의 책임 분리에 대해 알아보고, 예제 코드를 통해 올바른 방식으로 구현하는 방법을 설명하겠습니다.

 

 

상황

React 프로젝트에서 신발 쇼핑몰을 구현하는 과정에서, 사용자가 신발을 클릭하면 해당 신발의 ID를 로컬 스토리지에 저장하는 기능을 추가하고자 합니다.

 

해당 신발 ID를 통하여 최근 본 상품 목록을 반 영구적으로 저장하여, 사용자가 하여금 자신이 봤던 제품의 목록을 다시 사이트에 방문시 볼 수 있게하는 것이 목적입니다.

 

요구사항:

1. 사용자가 신발 이미지를 클릭하면 해당 ID를 로컬 스토리지에 저장합니다.

2. App 컴포넌트는 초기 설정 및 전역 상태 관리만 담당하며, 구체적인 작업(로컬 스토리지 저장 등)은 ShoeList 컴포넌트에서 처리해야 합니다.

 

 

예전 코드 방식 (최상위 컴포넌트에서 모든 책임을 처리)

/* App.jsx */
import Shoedata from './data/Shoedata';
import { useEffect, useState } from 'react';
import Navbar from './Components/Navbar';
import ShoeList from './Components/ShoeList';
import { Routes, Route } from 'react-router-dom';
import ShoeDetail from './Components/ShoeDetail';

function App() {
  const [shoes, setShoes] = useState(Shoedata);
  const pressedShoes = [];

  // **로컬 스토리지 초기화**
  useEffect(() => {
    localStorage.setItem('pressed', JSON.stringify(pressedShoes));
  }, []);

  // **로컬 스토리지에 신발 ID 저장**
  const postClickedShoe = (shoe) => {
    localStorage.setItem(shoe.id, JSON.stringify(shoe));
  };

  return (
    <div className="App">
      <Navbar />
      <Routes>
        <Route
          path="/"
          element={<ShoeList shoes={shoes} postClickedShoe={postClickedShoe} />}
        />
        <Route path="/detail/:id" element={<ShoeDetail shoes={shoes} />} />
      </Routes>
    </div>
  );
}

export default App;
/* ShoeList.jsx */
import React from 'react';

const ShoeList = ({ shoes, postClickedShoe }) => {
  return (
    <div className="shoe-list">
      {shoes.map((shoe) => (
        <div key={shoe.id} onClick={() => postClickedShoe(shoe)}>
          <h4>{shoe.title}</h4>
          <p>{shoe.price.toLocaleString()}원</p>
        </div>
      ))}
    </div>
  );
};

export default ShoeList;

 

문제점:

1. App.jsx에서 로컬 스토리지를 초기화하고 postClickedShoe 함수를 props로 ShoeList에 전달하는 방식은 프로젝트가 커질수록 유지보수가 어려워질 수 있습니다. 

 

-> 사실 위 프로젝트는 규모가 작은 프로젝트이기에, 유지 보수가 1번째 우선순위는 아닙니다. 그러나.. 습관을 직감적으로 이렇게 짜면 안돼겠다는 생각이 들었습니다.

 

2. postClickedShoe 함수는 App.jsx에서 관리하지만, 실제 이벤트가 발생하는 곳은 ShoeList.jsx입니다. 따라서 이 함수는 ShoeList에 정의하는 것이 더 적절합니다.

 

개선된 코드 방식 (책임 분리)

 

App.jsx

import Shoedata from './data/Shoedata';
import { useEffect, useState } from 'react';
import Navbar from './Components/Navbar';
import ShoeList from './Components/ShoeList';
import { Routes, Route } from 'react-router-dom';
import ShoeDetail from './Components/ShoeDetail';

function App() {
  const [shoes, setShoes] = useState(Shoedata);

  // **최초 로컬 스토리지 초기화**
  useEffect(() => {
    if (!localStorage.getItem('pressed')) {
      localStorage.setItem('pressed', JSON.stringify([]));
    }
  }, []);

  return (
    <div className="App">
      <Navbar />
      <Routes>
        <Route path="/" element={<ShoeList shoes={shoes} />} />
        <Route path="/detail/:id" element={<ShoeDetail shoes={shoes} />} />
      </Routes>
    </div>
  );
}

export default App;
import React from 'react';
import { useNavigate } from 'react-router-dom';

const ShoeList = ({ shoes }) => {
  const navigate = useNavigate();

  const handleShoeClick = (shoe) => {
    const pressedShoes = JSON.parse(localStorage.getItem('pressed')) || [];
    if (!pressedShoes.includes(shoe.id)) {
      pressedShoes.push(shoe.id);
      localStorage.setItem('pressed', JSON.stringify(pressedShoes));
    }
    navigate(`/detail/${shoe.id}`);
  };

  return (
    <div className="shoe-list">
      {shoes.map((shoe) => (
        <div key={shoe.id} onClick={() => handleShoeClick(shoe)}>
          <h4>{shoe.title}</h4>
          <p>{shoe.price.toLocaleString()}원</p>
        </div>
      ))}
    </div>
  );
};

export default ShoeList;

개선된 코드의 특징:

ShoeList 컴포넌트에서 로컬 스토리지를 직접 다룸.

App.jsx초기 설정 및 라우팅만 담당.

코드 구조가 더 간결하고 책임이 분리됨.

 

3. 코드 비교

GPT가 도와준 비교 표입니다.

상위 컴포넌트에서 하위 컴포넌트로 props를 전달하는 접근 방식은 좋으나,

상위 컴포넌트인 app.jsx는 사실 로컬스토로지를 크게 다루지 않습니다.

 

이 상황에서 모든 책임 ( 초기화 및 setItem 및 getItem) 을 app.jsx에서 짊어지는 것은 좋지 않다고 생각합니다.

 

 

의존성 배열을 비운 useEffect 문제 해결하기

 

접근은 너무 좋으나, 문제점이 있습니다. 

useEffect의 의존성 배열을 비우면 컴포넌트가 처음 렌더링될 때만 실행됩니다. 그런데 로컬 스토리지를 초기화할 때 빈 배열로 설정하면 페이지를 새로고침할 때마다 데이터가 초기화되는 문제가 발생합니다.

 

쉽게 설명하면, 현재 app.jsx에서 로컬스토리지의 데이터값을 설정할때 빈 배열을 setItem하고 있습니다.

useEffect의 의존성 배열을 비워놨기에 홈페이지가 처음 렌더링 되는 시점에 useEffect 내부의 코드가 실행됩니다.

즉, 내가 리렌더링을 (새로고침을) 누르는 순간 데이터가 다시 빈 배열을 로컬스토리지에 저장된다는것입니다.

 

해결 방법

아주 쉽습니다. (바로 setItem 하는 것이 아닌 먼저 Parsing을 실행하면 됩니다!)

로컬 스토리지에 데이터가 존재하는지 확인한 후, 존재하면 기존 데이터를 불러오고, 없으면 빈 배열로 초기화하면 됩니다.

1. 즉 먼저 로컬스토리지에 값을 가져온다 (Parsing 한다)

2. 만약 없다면 [ ] 빈 배열을 가지고 온다. ( false 일 경우 [])

 

  // 로컬스토리지를 확인하여 pressedShoes를 복원
  useEffect(() => {
    const savedShoes = JSON.parse(localStorage.getItem('pressed')) || [];
    setPressedShoes(savedShoes); // 로컬스토리지 값으로 상태 초기화
  }, []);

 

 

추가 : LocalStorage는 왜 Globally 작동되는가? 

 

다른 객체들은 Import 혹은 Props를 받아야 쓸 수 있는데 Local Storage는 Context도 아닌 녀석이 모든 페이지에 접근하여 가지고오고 심지어 값을 넣을 수 있습니다. 그 이유는..

 

LocalStorage는 글로벌 객체입니다!

 

localStorage는 브라우저 API로, 전역 객체 window의 일부입니다. 따라서 React 컴포넌트 간에는 서로의 파일에 상관없이 접근할 수 있습니다.

 

React는 브라우저에서 실행되기 때문에, 컴포넌트가 다르더라도 동일한 localStorage 객체를 공유합니다. 즉, 컴포넌트는 어디서든 동일한 window.localStorage에 접근합니다. 이는 동일한 브라우저 탭에서 저장된 모든 값에 대해 일관되게 접근할 수 있음을 의미합니다.

 

그럼 서버가 필요 없는거 아닌가요?

그렇지는 않은게 단점도 상당히 많습니다.

 

1. 비동기 문제:

localStorage동기적으로 작동합니다. 데이터를 저장할 때 딜레이는 없지만, 큰 데이터를 반복적으로 저장하면 렌더링 속도가 느려질 수 있습니다.

2. 데이터 정합성 문제:

여러 컴포넌트가 localStorage를 동시에 변경하면 데이터가 꼬일 수 있습니다. 따라서 상태 관리 라이브러리(예: Redux, Zustand)를 사용하거나 React의 Context API를 사용하여 데이터 관리를 하는 것이 좋습니다.

3. 보안:

localStorage에 민감한 정보를 저장하면 브라우저 개발자 도구로 쉽게 노출됩니다. 인증 토큰 등은 안전하지 않으므로 다른 방식(Session Storage 또는 쿠키) 사용을 고려해야 합니다.

 

 

결론

 

최상위 컴포넌트는 상태 및 전역 설정을 담당하고, 이벤트 핸들링과 같은 구체적인 작업은 해당 컴포넌트 내부에서 처리하는 것이 좋습니다. 

이번 프로젝트에서는 ShoeList 컴포넌트가 신발 클릭 이벤트를 처리하도록 변경하여 컴포넌트 간 책임 분리를 실현했습니다.

이 방식은 코드의 재사용성 및 유지보수성을 높이고, 프로젝트가 커져도 props 체인을 따라가야 하는 불편함을 줄입니다. 또한 Local Storage는 윈도우 글로벌 객체이므로 어디든 접근 할 수있지만 그만큼 단점도 많습니다,