과제를 진행하면서 회원 관리 테이블을 만들었는데,
검색 필터링, 체크박스 선택, 삭제, 추가 기능을 구현하던 중, 기능은 잘 작동했지만 필터링 후 삭제하면 필터가 풀리는 문제가 발생했습니다.
이 문제를 해결하면서 바닐라 JS에서도 체계적인 상태 관리가 얼마나 중요한지 느꼈고, 그 과정을 공유하려고 합니다!
🚨 문제 상황: 필터링 후 삭제하면 필터가 풀린다?
재현 예시
- 성별: 여자로 필터링 → 14명 표시
- 그 중 3명을 체크해서 삭제
- 문제 발생! → 필터가 풀리고 전체 목록(25명)이 다시 표시됨
// 문제 코드
const handleDeleteSelected = () => {
const checkedCheckboxes = document.querySelectorAll(".member-checkbox:checked");
const selectedIds = Array.from(checkedCheckboxes).map((checkbox) =>
parseInt(checkbox.dataset.id)
);
// localStorage에서 삭제
const allMembers = getLocalStorage();
const updatedMembers = allMembers.filter(
(member) => !selectedIds.includes(member.id)
);
setLocalStorage(updatedMembers);
// 문제 부분: 전체 데이터로 렌더링!
renderTable(updatedMembers, tbody);
};
왜 이런 일이 발생했을까요?
삭제 후 updatedMembers(전체 데이터)를 그대로 렌더링했기 때문입니다!
사용자는 "여자"로 필터링한 상태인데, 코드는 그걸 기억하지 못하는거죠
🤔 상태 관리란?
상태(State)란 시간에 따라 변하는 데이터입니다.
웹 애플리케이션에서 상태의 예시:
- 로그인한 사용자 정보
- 장바구니에 담긴 상품 목록
- 현재 적용된 필터 조건 ← 우리의 문제!
- 체크박스 선택 여부
상태 관리란 이런 데이터를 어디에 저장하고, 어떻게 업데이트하고, 어떻게 동기화할지 결정하는 것입니다!
React에서는 useState, useReducer, Redux 같은 도구로 상태를 관리하지만,
바닐라 JS에서는 직접 설계 해야 합니다!!
전역 상태 vs localStorage 중심 설계
문제를 해결하기 위해 두 가지 방법을 고민했습니다.
방법 1: 전역 상태 변수로 관리하기
// 복잡한 방법
let currentFilteredData = []; // 필터링된 데이터 저장
const handleDeleteSelected = () => {
// 1. localStorage 업데이트
const allMembers = getLocalStorage();
const updatedMembers = allMembers.filter(...);
setLocalStorage(updatedMembers);
// 2. currentFilteredData도 업데이트
const updatedFilteredData = currentFilteredData.filter(...);
currentFilteredData = updatedFilteredData;
// 3. 렌더링
renderTable(updatedFilteredData, tbody);
};
문제점:
localStorage와currentFilteredData두 곳을 동시에 관리해야 함- 한쪽을 업데이트하는 걸 깜빡하면 데이터 불일치 발생!
- 코드가 복잡해짐
방법 2: localStorage를 Single Source of Truth로 사용하기
// 간단한 방법
const handleDeleteSelected = () => {
// 1. localStorage만 업데이트
const allMembers = getLocalStorage();
const updatedMembers = allMembers.filter(...);
setLocalStorage(updatedMembers);
// 2. 필터를 다시 적용해서 렌더링
const filteredData = searchFilter(updatedMembers);
renderTable(filteredData, tbody);
};
장점:
- localStorage 하나만 관리하면 됨!
- 필터는 필요할 때마다 재계산
- 데이터 일관성 보장
💡 Single Source of Truth란?
"데이터의 유일한 진실 공급원"이라는 의미로, 하나의 저장소만을 신뢰하고 다른 모든 데이터는 여기서 파생시키는 원칙입니다!
실제 트러블슈팅 과정
처음에는 "왜 필터가 풀리지?" 라고만 생각했는데 이후 전체 데이터를 렌더링하고 있다는 걸 알고난 후에는
currentFilteredData변수를 만들어서 현재 필터 상태를 저장- 삭제 후 필터를 다시 적용
라는 2가지 해결책을 생각했고 고민 끝에 2번을 선택했습니다!
이유:
- 상태를 여러 곳에 흩어놓으면 동기화 문제가 생김
- localStorage만 믿고 필터는 필요할 때 재계산하는 게 더 안전하다고 생각했습니다
*searchFilter 재사용
*
이과정에서 이미 구현해둔 searchFilter 함수를 재활용했습니다!
// filter.js에서 이미 만들어둔 함수
export const searchFilter = (data) => {
const name = (nameFilter?.value || "").trim().toLowerCase();
const gender = genderFilter?.value || "";
const role = roleFilter?.value || "";
// ...
return data.filter((member) => {
const matchName = !name || member.name.toLowerCase().includes(name);
const matchGender = !gender || gender === member.gender;
const matchRole = !role || role === member.role;
// ...
return matchName && matchGender && matchRole && ...;
});
};
이 함수는 현재 입력된 필터 조건을 읽어서 데이터를 걸러주기 때문에,
삭제 후 다시 호출하면 필터 상태가 유지됩니다!!
즉 ...
- Before: 전역 상태 관리 (복잡함)
// filter.js
let currentFilteredData = []; // 전역 변수
export const initializeFilter = () => {
const allMembers = getLocalStorage();
currentFilteredData = allMembers; // 초기화
renderTable(currentFilteredData);
};
export const handleFilterSubmit = (e) => {
e.preventDefault();
const allMembers = getLocalStorage();
const filteredData = searchFilter(allMembers);
currentFilteredData = filteredData; // 업데이트 (...)
renderTable(currentFilteredData);
};
// 다른 파일에서 사용
export { getCurrentFilteredData, setCurrentFilteredData };
// checkbox.js
import { getCurrentFilteredData, setCurrentFilteredData } from './filter.js';
const handleDeleteSelected = () => {
// ...
// 1. localStorage 업데이트
const allMembers = getLocalStorage();
const updatedMembers = allMembers.filter(...);
setLocalStorage(updatedMembers);
// 2. currentFilteredData도 업데이트 (...)
const currentData = getCurrentFilteredData();
const updatedFilteredData = currentData.filter(...);
setCurrentFilteredData(updatedFilteredData);
// 3. 렌더링
renderTable(updatedFilteredData);
};
문제점:
currentFilteredData를 여러 파일에서 import/export- 삭제할 때 두 곳 모두 업데이트해야 함
- 한쪽을 깜빡하면 버그 발생!
- After: localStorage 중심 (간단함)
// filter.js
export const initializeFilter = () => {
const allMembers = getLocalStorage(); // localStorage에서 가져오기
renderTable(allMembers, tbody);
};
export const handleFilterSubmit = (e) => {
e.preventDefault();
const allMembers = getLocalStorage(); // 항상 localStorage에서
const filteredData = searchFilter(allMembers);
renderTable(filteredData, tbody);
};
// checkbox.js
import { searchFilter } from './filter.js'; // 필터 함수만 import
const handleDeleteSelected = () => {
// ...
// 1. localStorage만 업데이트!
const allMembers = getLocalStorage();
const updatedMembers = allMembers.filter(...);
setLocalStorage(updatedMembers);
// 2. 현재 필터를 다시 적용!
const filteredData = searchFilter(updatedMembers);
renderTable(filteredData, tbody);
};
개선점:
- localStorage 하나만 관리
- 필터는 필요할 때마다 재계산
- 데이터 일관성 보장
- 코드가 훨씬 간단해짐
localStorage를 Single Source of Truth로 사용하는 방법
1. 모든 데이터 변경은 localStorage를 거친다
// 올바른 패턴
const addMember = (newMember) => {
const members = getLocalStorage(); // 1. 가져오기
members.push(newMember); // 2. 수정
setLocalStorage(members); // 3. 저장
const filtered = searchFilter(members); // 4. 필터 적용
renderTable(filtered, tbody); // 5. 렌더링
};
// 잘못된 패턴
let members = [...]; // 전역 변수에 저장
members.push(newMember);
renderTable(members, tbody);
// localStorage와 동기화 안 됨!
2. 계산된 값(필터링된 데이터)은 저장하지 않는다
// 올바른 패턴
const filteredData = searchFilter(getLocalStorage());
renderTable(filteredData, tbody);
// 잘못된 패턴
let filteredData = searchFilter(getLocalStorage());
// 이 변수를 계속 들고 다니며 관리 ⚠️
이유:
- 계산된 값을 저장하면 원본 데이터와 동기화 문제 발생
- 필요할 때마다 재계산하는 게 더 안전!
3. 각 기능에서 독립적으로 localStorage 접근
// filter.js
const handleFilterSubmit = () => {
const data = getLocalStorage(); // ✨
const filtered = searchFilter(data);
renderTable(filtered, tbody);
};
// checkbox.js
const handleDeleteSelected = () => {
const data = getLocalStorage(); // ✨
const updated = data.filter(...);
setLocalStorage(updated);
const filtered = searchFilter(updated);
renderTable(filtered, tbody);
};
// modal.js
const handleAddMember = () => {
const data = getLocalStorage(); // ✨
data.push(newMember);
setLocalStorage(data);
const filtered = searchFilter(data);
renderTable(filtered, tbody);
};
각 모듈이 독립적으로 localStorage에 접근하기 때문에:
- 모듈 간 의존성이 낮아짐
- 데이터 일관성 자동 보장
- 버그 발생 확률 감소!
✨ 결론: React 없이 상태 관리를 해보니 ...
이번 과제를 하면서 느낀 점은, 좋은 상태 관리의 공통 원칙에 대해 생각해봤습니다
- Single Source of Truth - 데이터는 한 곳에만
- 단방향 데이터 흐름 - 데이터 변경 → 렌더링
- 최소한의 상태 - 계산할 수 있는 건 저장하지 않기
- 명확한 책임 분리 - 각 모듈의 역할을 명확히
바닐라 JS로 이런 원칙을 직접 구현해보니,
- 왜 React가
useState를 제공하는지 - 왜 Redux가 단일 스토어를 강조하는지
- 왜
useEffect가 필요한지
이해가 되더라구요!
localStorage든, React든, 중요한 건 일관성 있는 상태 관리와 예측 가능한 데이터 흐름, 유지보수하기 쉬운 구조를 만드는 거라는 걸 배웠습니다!
'web' 카테고리의 다른 글
| Tailwind CSS Deep Dive (0) | 2026.07.08 |
|---|---|
| useEffect 더 잘 다루기 (ft. 의존성 배열 & clean up 함수) (0) | 2026.07.08 |
| HTML/CSS(Styled-Component) 프로젝트에 디자인 시스템 도입하기 (0) | 2026.07.08 |
| [React] 회원가입 폼을 구현하며⋯ (2) | 2026.07.07 |
| [Typescript] TS를 배워보자 (1) (0) | 2026.07.07 |