
데이터 필터링
데이터 필터링은 웹 페이지에서 사용자가 원하는 정보만 선택적으로 볼 수 있게 해주는 강력한 기능이다.
예를들어, 대량의 국가 정보를 다루는 웹 페이지에서 지역별 필터링 기능이 없다면, 사용자는 필요한 정보를 찾는 데 많은 시간을 낭비하게 된다.
예시를 먼저 확인해보자.
예시
이제 예시도 봤으니, 지역 필터를 클릭하여 해당 지역 국가 정보를 보이거나 숨기는 HTML 필터링 시스템을 구현하는 방법을 알아보자.
HTML
데이터 필터링을 구현하기 위한 첫 단계는 적절한 마크업 구조를 설계하는 것이다.
국가 정보를 지역별로 필터링하기 위해서는 필터 버튼 영역과 국가 정보 영역으로 구성된 HTML 구조가 필요하다.
아래는 지역별 국가 정보 필터링을 위한 기본 HTML 구조 예시다.
<div class="container">
<div class="filter-container">
<div class="filter-buttons">
<button class="filter-btn active" data-region="all">모든 지역</button>
<button class="filter-btn active" data-region="JAPAC">아시아 태평양</button>
<button class="filter-btn active" data-region="AMR">아메리카</button>
<button class="filter-btn active" data-region="EMEA">유럽/중동/아프리카</button>
</div>
</div>
<div class="countries-container">
<div class="country-card" data-region="JAPAC">
<h4>한국</h4>
<p>인구: 5,170만 명</p>
<p>영토 크기: 100,210 km²</p>
<p>주요 언어: 한국어</p>
</div>
<div class="country-card" data-region="JAPAC">
<h4>일본</h4>
<p>인구: 1억 2,580만 명</p>
<p>영토 크기: 377,975 km²</p>
<p>주요 언어: 일본어</p>
</div>
<div class="country-card" data-region="AMR">
<h4>미국</h4>
<p>인구: 3억 3,100만 명</p>
<p>영토 크기: 9,833,517 km²</p>
<p>주요 언어: 영어</p>
</div>
<div class="country-card" data-region="EMEA">
<h4>영국</h4>
<p>인구: 6,800만 명</p>
<p>영토 크기: 242,495 km²</p>
<p>주요 언어: 영어</p>
</div>
</div>
</div>
위 HTML 구조에서 가장 중요한 요소는 데이터 속성(data-region)이다.
모든 국가 카드에는 해당 국가가 속한 지역을 data-region 속성으로 표시했다.
예를 들어, 한국과 일본은 ‘JAPAC’으로, 미국은 ‘AMR’으로, 영국은 ‘EMEA’로 표시된다.
또한 각 필터 버튼에도 어떤 지역을 필터링할지 data-region 속성으로 표시했다.
이 속성값을 기준으로 JavaScript에서 필터링 로직을 구현할 수 있다.
모든 필터 버튼에 초기에는 ‘active’ 클래스를 추가하여, 페이지가 로드될 때 모든 지역의 국가가 표시되도록 설정했다.
필터 기능을 활성화하면 JavaScript에서 이 클래스를 토글하여 필터 상태를 변경한다.
CSS
데이터 필터링의 시각적 효과와 사용자 경험을 향상시키기 위해 CSS 스타일을 적용해야 한다.
필터 버튼의 상태 변화와 국가 카드의 표시/숨김 효과를 스타일로 구현해보자.
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; background-color: #f8f9fa; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { text-align: center; margin-bottom: 30px; }
.filter-container { background-color: #fff; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-bottom: 20px; }
.filter-section { margin-bottom: 15px; }
.filter-section h4 { margin-top: 0; margin-bottom: 10px; }
.filter-buttons { display: flex; flex-wrap: wrap; gap: 10px; }
.filter-btn { padding: 8px 15px; border: none; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.3s ease; }
.filter-btn.active { background-color: #4361ee; color: white; }
.filter-btn:hover { opacity: 0.9; }
.countries-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
.country-card { background-color: #fff; border-radius: 5px; padding: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); transition: all 0.3s ease; }
.country-card h4 { margin-top: 0; border-bottom: 1px solid #eee; padding-bottom: 10px; }
.country-card.hidden { display: none; }
.reset-filters-btn { display: block; margin: 15px auto 0; padding: 8px 15px; border: none; border-radius: 4px; background-color: #dc3545; color: white; cursor: pointer; transition: all 0.3s ease; }
.reset-filters-btn:hover { opacity: 0.9; }
이 CSS 스타일의 핵심은 필터 상태에 따른 시각적 피드백과 숨김 효과다.
.filter-btn.active 클래스는 활성화된 필터 버튼을 시각적으로 구분하기 위한 스타일로, 배경색을 파란색으로 변경하고 텍스트를 흰색으로 표시한다.
사용자는 이를 통해 현재 어떤 필터가 활성화되어 있는지 직관적으로 인식할 수 있다.
.country-card.hidden 클래스는 필터링에 의해 숨겨진 국가 카드를 처리하는 스타일로, display: none을 사용하여 화면에서 완전히 제거한다.
JavaScript에서 필터 상태에 따라 이 클래스를 추가하거나 제거하여 카드의 표시 여부를 제어한다.
반응형 그리드 레이아웃(.countries-container)을 사용하여 다양한 화면 크기에 대응하며, 카드와 버튼에 transition 효과를 적용하여 상태 변화 시 부드러운 애니메이션을 제공한다.
이러한 디테일은 사용자 경험(UX)을 향상시키는 중요한 요소다.
JavaScript
데이터 필터링의 핵심 기능은 JavaScript를 통해 구현된다.
필터 버튼 클릭 시 해당 지역의 국가 정보를 보이거나 숨기는 동작을 JavaScript로 구현해보자.
document.addEventListener('DOMContentLoaded', function() {
const filterButtons = document.querySelectorAll('.filter-btn');
const countryCards = document.querySelectorAll('.country-card');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
this.classList.toggle('active');
const activeFilters = Array.from(filterButtons)
.filter(btn => btn.classList.contains('active'))
.map(btn => btn.getAttribute('data-region'));
if (this.getAttribute('data-region') === 'all') {
if (this.classList.contains('active')) {
filterButtons.forEach(btn => btn.classList.add('active'));
countryCards.forEach(card => card.classList.remove('hidden'));
} else {
filterButtons.forEach(btn => btn.classList.remove('active'));
countryCards.forEach(card => card.classList.add('hidden'));
}
return;
}
countryCards.forEach(card => {
const cardRegion = card.getAttribute('data-region');
if (activeFilters.includes(cardRegion) || activeFilters.includes('all')) {
card.classList.remove('hidden');
} else {
card.classList.add('hidden');
}
});
});
});
});
이 JavaScript 코드의 핵심 동작 원리는 다음과 같다.
- 필터 버튼 클릭 시 해당 버튼의 활성화 상태(active 클래스)를 토글한다.
- 현재 활성화된 모든 필터 버튼의 지역 값(data-region)을 배열로 수집한다.
- ‘모든 지역’ 버튼은 특별 케이스로 처리하여 모든 필터를 한 번에 켜거나 끌 수 있게 한다.
- 각 국가 카드에 대해 해당 카드의 지역(data-region)이 활성화된 필터에 포함되는지 확인한다.
- 포함되면 hidden 클래스를 제거하여 카드를 표시하고, 그렇지 않으면 hidden 클래스를 추가하여 카드를 숨긴다.
이 방식을 사용하면 여러 필터를 동시에 활성화하거나 비활성화할 수 있어 다양한 조합의 필터링이 가능하다.
예를 들어, JAPAC과 AMR 필터는 활성화하고 EMEA 필터는 비활성화하여 아시아와 아메리카 지역의 국가만 표시할 수 있다.
특히 ‘모든 지역’ 필터 버튼을 통해 사용자가 빠르게 모든 국가를 표시하거나 숨길 수 있어 편리하다.
이는 필터링 UI에서 중요한 사용자 경험(UX) 요소다.
데이터 속성
데이터 필터링에서 데이터 속성(data-*)은 매우 효과적인 도구다.
기본 예제에서는 단일 지역(data-region) 속성만 사용했지만, 더 복잡한 필터링을 위해 데이터 속성을 다양하게 활용할 수 있다.
아래는 여러 데이터 속성을 활용한 고급 필터링 예시다.
<div class="country-card" data-region="JAPAC" data-population="medium" data-language="asian">
<h4>한국</h4>
<p>인구: 5,170만 명</p>
<p>영토 크기: 100,210 km²</p>
<p>주요 언어: 한국어</p>
</div>
위 코드에서는 국가 카드에 지역(region) 외에도 인구 규모(population), 언어 그룹(language) 등 여러 데이터 속성을 추가했다.
이처럼 다양한 데이터 속성을 활용하면 더 정교한 필터링 시스템을 구현할 수 있다.
JavaScript에서는 이러한 속성들을 활용하여 복합 필터링을 구현할 수 있다.
function applyFilters() {
const activeRegions = getActiveFilters('region');
const activePopulations = getActiveFilters('population');
const activeLanguages = getActiveFilters('language');
countryCards.forEach(card => {
const cardRegion = card.getAttribute('data-region');
const cardPopulation = card.getAttribute('data-population');
const cardLanguage = card.getAttribute('data-language');
const regionMatch = activeRegions.length === 0 || activeRegions.includes(cardRegion);
const populationMatch = activePopulations.length === 0 || activePopulations.includes(cardPopulation);
const languageMatch = activeLanguages.length === 0 || activeLanguages.includes(cardLanguage);
if (regionMatch && populationMatch && languageMatch) {
card.classList.remove('hidden');
} else {
card.classList.add('hidden');
}
});
}
function getActiveFilters(filterType) {
return Array.from(document.querySelectorAll(`.filter-btn[data-type="${filterType}"].active`))
.map(btn => btn.getAttribute(`data-${filterType}`));
}
이 코드에서는 여러 종류의 필터(지역, 인구, 언어)를 동시에 적용하고 있다.
각 필터 타입마다 활성화된 값들을 수집하고, 모든 조건을 AND 연산으로 결합하여 모든 조건을 만족하는 카드만 표시한다.
이를 통해 “아시아 지역의 중규모 인구를 가진 국가”와 같은 복합적인 필터링이 가능해진다.
데이터 속성의 주요 장점은 HTML 요소에 직접 메타데이터를 저장할 수 있다는 점이다.
이는 DOM에 데이터를 명시적으로 연결하여 JavaScript에서 쉽게 접근하고 조작할 수 있게 해준다.
또한 데이터 속성은 CSS 선택자로도 사용할 수 있어, 특정 데이터 속성값을 가진 요소에 스타일을 적용할 수도 있다.
예를 들어, [data-region=”JAPAC”] 선택자를 사용하여 JAPAC 지역의 모든 카드에 특별한 스타일을 적용할 수 있다.
다중 필터
데이터 필터링에서 더 강력한 사용자 경험을 제공하기 위해 다중 필터 시스템을 구현해보자.
지금까지는 지역별 필터만 다루었지만, 이제는 인구 규모와 언어 그룹 등 여러 필터 유형을 조합하는 방법을 살펴보자.
먼저 HTML 구조에 여러 필터 유형을 추가한다.
<div class="filter-container">
<div class="filter-section">
<h4>지역별</h4>
<div class="filter-buttons">
<button class="filter-btn active" data-type="region" data-region="all">모든 지역</button>
<button class="filter-btn active" data-type="region" data-region="JAPAC">아시아 태평양</button>
<button class="filter-btn active" data-type="region" data-region="AMR">아메리카</button>
<button class="filter-btn active" data-type="region" data-region="EMEA">유럽/중동/아프리카</button>
</div>
</div>
<div class="filter-section">
<h4>인구 규모별</h4>
<div class="filter-buttons">
<button class="filter-btn active" data-type="population" data-population="all">모든 인구 규모</button>
<button class="filter-btn active" data-type="population" data-population="small">소규모 (5천만 미만)</button>
<button class="filter-btn active" data-type="population" data-population="medium">중규모 (5천만-2억)</button>
<button class="filter-btn active" data-type="population" data-population="large">대규모 (2억 이상)</button>
</div>
</div>
<div class="filter-section">
<h4>언어 그룹별</h4>
<div class="filter-buttons">
<button class="filter-btn active" data-type="language" data-language="all">모든 언어</button>
<button class="filter-btn active" data-type="language" data-language="indo-european">인도-유럽어족</button>
<button class="filter-btn active" data-type="language" data-language="asian">아시아어족</button>
<button class="filter-btn active" data-type="language" data-language="other">기타</button>
</div>
</div>
</div>
이렇게 각 필터 유형별로 버튼 그룹을 만들고, data-type 속성으로 필터 유형을 구분했다.
또한 각 버튼에는 해당 필터 유형에 맞는 데이터 속성(data-region, data-population, data-language)을 추가하여 필터 값을 지정했다.
다중 필터를 처리하기 위한 JavaScript 코드는 다음과 같다.
document.addEventListener('DOMContentLoaded', function() {
const filterButtons = document.querySelectorAll('.filter-btn');
const countryCards = document.querySelectorAll('.country-card');
let filterState = {
region: ['all', 'JAPAC', 'AMR', 'EMEA'],
population: ['all', 'small', 'medium', 'large'],
language: ['all', 'indo-european', 'asian', 'other']
};
filterButtons.forEach(button => {
button.addEventListener('click', function() {
const filterType = this.getAttribute('data-type');
let filterValue;
if (filterType === 'region') {
filterValue = this.getAttribute('data-region');
} else if (filterType === 'population') {
filterValue = this.getAttribute('data-population');
} else if (filterType === 'language') {
filterValue = this.getAttribute('data-language');
}
if (filterValue === 'all') {
if (this.classList.contains('active')) {
document.querySelectorAll(`.filter-btn[data-type="${filterType}"]`)
.forEach(btn => btn.classList.add('active'));
filterState[filterType] = [];
document.querySelectorAll(`.filter-btn[data-type="${filterType}"]`).forEach(btn => {
if (filterType === 'region') {
filterState[filterType].push(btn.getAttribute('data-region'));
} else if (filterType === 'population') {
filterState[filterType].push(btn.getAttribute('data-population'));
} else if (filterType === 'language') {
filterState[filterType].push(btn.getAttribute('data-language'));
}
});
} else {
document.querySelectorAll(`.filter-btn[data-type="${filterType}"]`)
.forEach(btn => btn.classList.remove('active'));
filterState[filterType] = [];
}
} else {
this.classList.toggle('active');
const allButton = document.querySelector(`.filter-btn[data-type="${filterType}"][data-${filterType}="all"]`);
const typeButtons = document.querySelectorAll(`.filter-btn[data-type="${filterType}"]:not([data-${filterType}="all"])`);
const allActive = Array.from(typeButtons).every(btn => btn.classList.contains('active'));
if (allActive) {
allButton.classList.add('active');
} else {
allButton.classList.remove('active');
}
filterState[filterType] = [];
document.querySelectorAll(`.filter-btn[data-type="${filterType}"].active`).forEach(btn => {
if (filterType === 'region') {
filterState[filterType].push(btn.getAttribute('data-region'));
} else if (filterType === 'population') {
filterState[filterType].push(btn.getAttribute('data-population'));
} else if (filterType === 'language') {
filterState[filterType].push(btn.getAttribute('data-language'));
}
});
}
applyFilters();
});
});
function applyFilters() {
countryCards.forEach(card => {
const cardRegion = card.getAttribute('data-region');
const cardPopulation = card.getAttribute('data-population');
const cardLanguage = card.getAttribute('data-language');
const matchesRegion = filterState.region.includes('all') || filterState.region.includes(cardRegion);
const matchesPopulation = filterState.population.includes('all') || filterState.population.includes(cardPopulation);
const matchesLanguage = filterState.language.includes('all') || filterState.language.includes(cardLanguage);
if (matchesRegion && matchesPopulation && matchesLanguage) {
card.classList.remove('hidden');
} else {
card.classList.add('hidden');
}
});
}
});
이 다중 필터 구현의 핵심은 filterState 객체로, 각 필터 유형별로 현재 활성화된 값들을 추적한다.
사용자가 필터 버튼을 클릭할 때마다 해당 필터 유형의 상태를 업데이트하고, 모든 필터 유형의 조건을 AND 연산으로 결합하여 국가 카드를 필터링한다.
특히 중요한 점은 각 필터 버튼에서 필터 타입과 필터 값을 가져오는 방식이다.
버튼의 data-type 속성으로 필터 유형을 파악한 후, 해당 유형에 맞는 데이터 속성(data-region, data-population, data-language)에서 값을 가져온다.
또한 ‘모든 xxx’ 버튼은 특별하게 처리하여 해당 유형의 모든 필터를 한 번에 켜거나 끌 수 있게 했다.
특정 유형의 모든 일반 필터가 활성화되면 자동으로 ‘모든 xxx’ 버튼도 활성화되는 논리를 구현했다.
이런 방식으로 복합적인 필터링 시스템을 구현하면 사용자는 매우 구체적인 조건으로 데이터를 필터링할 수 있다.
예를 들어, “아시아 지역의 중규모 인구를 가진 아시아어족 국가”와 같은 세부적인 필터링이 가능하다.
필터 상태 관리
데이터 필터링 시스템의 사용자 경험을 더욱 개선하기 위해 필터 상태를 저장하고 복원하는 기능을 추가해보자.
사용자가 페이지를 새로고침하거나 나중에 다시 방문했을 때 이전에 설정한 필터 상태를 유지하면 더 나은 경험을 제공할 수 있다.
localStorage를 활용하여 필터 상태를 저장하고 복원하는 코드는 다음과 같다.
document.addEventListener('DOMContentLoaded', function() {
const filterButtons = document.querySelectorAll('.filter-btn');
const countryCards = document.querySelectorAll('.country-card');
let filterState = {
region: ['all', 'JAPAC', 'AMR', 'EMEA'],
population: ['all', 'small', 'medium', 'large'],
language: ['all', 'indo-european', 'asian', 'other']
};
restoreFilterState();
filterButtons.forEach(button => {
button.addEventListener('click', function() {
const filterType = this.getAttribute('data-type');
let filterValue;
if (filterType === 'region') {
filterValue = this.getAttribute('data-region');
} else if (filterType === 'population') {
filterValue = this.getAttribute('data-population');
} else if (filterType === 'language') {
filterValue = this.getAttribute('data-language');
}
if (filterValue === 'all') {
if (this.classList.contains('active')) {
document.querySelectorAll(`.filter-btn[data-type="${filterType}"]`)
.forEach(btn => btn.classList.add('active'));
filterState[filterType] = [];
document.querySelectorAll(`.filter-btn[data-type="${filterType}"]`).forEach(btn => {
if (filterType === 'region') {
filterState[filterType].push(btn.getAttribute('data-region'));
} else if (filterType === 'population') {
filterState[filterType].push(btn.getAttribute('data-population'));
} else if (filterType === 'language') {
filterState[filterType].push(btn.getAttribute('data-language'));
}
});
} else {
document.querySelectorAll(`.filter-btn[data-type="${filterType}"]`)
.forEach(btn => btn.classList.remove('active'));
filterState[filterType] = [];
}
} else {
this.classList.toggle('active');
const allButton = document.querySelector(`.filter-btn[data-type="${filterType}"][data-${filterType}="all"]`);
const typeButtons = document.querySelectorAll(`.filter-btn[data-type="${filterType}"]:not([data-${filterType}="all"])`);
const allActive = Array.from(typeButtons).every(btn => btn.classList.contains('active'));
if (allActive) {
allButton.classList.add('active');
} else {
allButton.classList.remove('active');
}
filterState[filterType] = [];
document.querySelectorAll(`.filter-btn[data-type="${filterType}"].active`).forEach(btn => {
if (filterType === 'region') {
filterState[filterType].push(btn.getAttribute('data-region'));
} else if (filterType === 'population') {
filterState[filterType].push(btn.getAttribute('data-population'));
} else if (filterType === 'language') {
filterState[filterType].push(btn.getAttribute('data-language'));
}
});
}
saveFilterState();
applyFilters();
});
});
function saveFilterState() {
localStorage.setItem('countryFilters', JSON.stringify(filterState));
}
function restoreFilterState() {
const savedState = localStorage.getItem('countryFilters');
if (savedState) {
filterState = JSON.parse(savedState);
filterButtons.forEach(button => {
const type = button.getAttribute('data-type');
let value;
if (type === 'region') {
value = button.getAttribute('data-region');
} else if (type === 'population') {
value = button.getAttribute('data-population');
} else if (type === 'language') {
value = button.getAttribute('data-language');
}
if (filterState[type].includes(value)) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
});
applyFilters();
}
}
function applyFilters() {
countryCards.forEach(card => {
const cardRegion = card.getAttribute('data-region');
const cardPopulation = card.getAttribute('data-population');
const cardLanguage = card.getAttribute('data-language');
const matchesRegion = filterState.region.includes('all') || filterState.region.includes(cardRegion);
const matchesPopulation = filterState.population.includes('all') || filterState.population.includes(cardPopulation);
const matchesLanguage = filterState.language.includes('all') || filterState.language.includes(cardLanguage);
if (matchesRegion && matchesPopulation && matchesLanguage) {
card.classList.remove('hidden');
} else {
card.classList.add('hidden');
}
});
}
const resetButton = document.createElement('button');
resetButton.textContent = '필터 초기화';
resetButton.classList.add('reset-filters-btn');
document.querySelector('.filter-container').appendChild(resetButton);
resetButton.addEventListener('click', function() {
filterButtons.forEach(button => button.classList.add('active'));
filterState = {
region: ['all', 'JAPAC', 'AMR', 'EMEA'],
population: ['all', 'small', 'medium', 'large'],
language: ['all', 'indo-european', 'asian', 'other']
};
saveFilterState();
applyFilters();
});
});
이 코드에서 구현한 필터 상태 관리 기능은 크게 세 가지다.
- saveFilterState() 함수는 현재 필터 상태를 localStorage에 JSON 문자열 형태로 저장한다.
필터 버튼을 클릭할 때마다 이 함수를 호출하여 최신 상태를 저장한다. - restoreFilterState() 함수는 페이지 로드 시 localStorage에서 저장된 필터 상태를 불러와 적용한다.
저장된 상태가 있으면 filterState 객체를 업데이트하고, 버튼 UI와 카드 표시 여부를 그에 맞게 조정한다. - 필터 초기화 버튼을 추가하여 사용자가 모든 필터를 초기 상태(모두 활성화)로 쉽게 되돌릴 수 있게 했다.
이는 여러 필터를 조작하다가 빠르게 원래 상태로 돌아가고 싶을 때 유용하다.
기존 코드와 달리, 다중 필터 타입을 지원하기 위해 필터 유형(type)에 따라 올바른 데이터 속성에서 값을 가져오는 로직이 추가되었다.
이는 버튼의 data-region, data-population, data-language 속성과 카드의 동일한 속성들을 매핑하기 위한 것이다.
localStorage는 브라우저에 데이터를 영구적으로 저장하는 Web Storage API로, 사용자가 브라우저를 닫았다가 다시 열어도 데이터가 유지된다.
이를 활용하면 사용자 설정을 세션 간에도 보존할 수 있어 사용자 경험이 크게 향상된다.
필터 상태 관리는 특히 복잡한 필터링 시스템이나 사용자가 자주 방문하는 웹 애플리케이션에서 중요한 기능이다.
사용자의 선호도를 기억하고 반영함으로써 더 개인화된 경험을 제공할 수 있다.
데이터 필터링은 많은 양의 정보를 효과적으로 관리하고 사용자에게 필요한 데이터만 표시할 수 있는 강력한 기능이다.
JavaScript와 데이터 속성(data-*)을 활용하면 복잡한 필터링 시스템도 비교적 쉽게 구현할 수 있으며, 사용자 경험을 크게 향상시킬 수 있다.
이 글에서 소개한 방법들을 활용하여 자신만의 데이터 필터링 시스템을 구축해보자.