Alpine.js : 종속형 Select Box 구현
Your new, lightweight, JavaScript framework.
Alpine.js를 활용한 종속형 Select Box 구현
이번 시간에는 Alpine.js를 사용하여 3개의 Select Box를 만들고, 첫 번째 Select Box의 값에 따라 두 번째 Select Box의 옵션이 바뀌고, 두 번째 Select Box의 값에 따라 세 번째 Select Box의 옵션이 바뀌는 기능을 구현해 보겠습니다. 또한, 첫 번째 Select Box의 값이 변경되면 두 번째와 세 번째 Select Box는 초기화되도록 만들어 보겠습니다.
핵심 개념:
- Alpine.js: 간단하고 강력한 JavaScript 프레임워크로, HTML에 직접 JavaScript 동작을 추가할 수 있습니다.
- x-data: Alpine.js 컴포넌트의 데이터를 정의합니다. 이 데이터는 HTML 요소와 상호작용할 수 있습니다.
- x-model: HTML 요소의 값과 Alpine.js 데이터 속성을 양방향으로 바인딩합니다. Select Box에서 선택된 값이 자동으로 JavaScript 변수에 저장되고, JavaScript 변수의 값이 바뀌면 Select Box의 선택된 값도 바뀝니다.
- x-for: JavaScript 배열의 각 항목에 대해 HTML 요소를 반복하여 생성합니다. Select Box의 옵션들을 동적으로 생성하는 데 사용됩니다.
- x-on:change: 특정 HTML 이벤트(여기서는 change 이벤트, 즉 Select Box의 값이 변경될 때 발생)가 발생했을 때 실행할 JavaScript 함수를 정의합니다.
HTML 구조:
먼저 기본적인 HTML 구조를 만들어 보겠습니다. 3개의 <select> 태그를 감싸는 <div>를 만들고, Alpine.js를 사용할 것이므로 x-data 속성을 추가합니다.
실행 결과:
코드를 HTML 파일에 저장하고 웹 브라우저로 열면, 다음과 같은 동작을 확인할 수 있습니다.
- 첫 번째 Select Box에서 "과일"을 선택하면, 두 번째 Select Box에는 "사과", "바나나", "오렌지" 옵션이 나타납니다.
- 두 번째 Select Box에서 "사과"를 선택하면, 세 번째 Select Box에는 "빨간 사과", "푸른 사과" 옵션이 나타납니다.
- 다시 첫 번째 Select Box에서 "채소"를 선택하면, 두 번째와 세 번째 Select Box는 "선택하세요"로 초기화되고, 두 번째 Select Box에는 "당근", "브로콜리", "오이" 옵션이 나타납니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alpine.js 종속형 Select Box</title>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body>
<div x-data="{
categories: [
{ value: 'fruit', label: '과일' },
{ value: 'vegetable', label: '채소' },
{ value: 'drink', label: '음료' }
],
subCategories: {
'fruit': [
{ value: 'apple', label: '사과' },
{ value: 'banana', label: '바나나' },
{ value: 'orange', label: '오렌지' }
],
'vegetable': [
{ value: 'carrot', label: '당근' },
{ value: 'broccoli', label: '브로콜리' },
{ value: 'cucumber', label: '오이' }
],
'drink': [
{ value: 'water', label: '물' },
{ value: 'juice', label: '주스' },
{ value: 'soda', label: '탄산음료' }
]
},
items: {
'apple': ['빨간 사과', '푸른 사과'],
'banana': ['달콤한 바나나', '작은 바나나'],
'orange': ['신선한 오렌지', '제주 오렌지'],
'carrot': ['단단한 당근', '미니 당근'],
'broccoli': ['유기농 브로콜리', '냉동 브로콜리'],
'cucumber': ['아삭한 오이', '백오이'],
'water': ['생수', '탄산수'],
'juice': ['오렌지 주스', '사과 주스'],
'soda': ['콜라', '사이다']
},
selectedCategory: '',
selectedSubCategory: '',
selectedItem: '',
filteredSubCategories: [],
filteredItems: []
}">
<div>
<label for="category">카테고리:</label>
<select id="category" x-model="selectedCategory" @change="selectedSubCategory = ''; selectedItem = ''; filteredSubCategories = subCategories[selectedCategory] || []; filteredItems = []">
<option value="">선택하세요</option>
<template x-for="category in categories" :key="category.value">
<option :value="category.value" x-text="category.label"></option>
</template>
</select>
</div>
<div style="margin-top: 10px;">
<label for="subCategory">세부 카테고리:</label>
<select id="subCategory" x-model="selectedSubCategory" @change="selectedItem = ''; filteredItems = items[selectedSubCategory] || []">
<option value="">선택하세요</option>
<template x-for="subCategory in filteredSubCategories" :key="subCategory.value">
<option :value="subCategory.value" x-text="subCategory.label"></option>
</template>
</select>
</div>
<div style="margin-top: 10px;">
<label for="item">품목:</label>
<select id="item" x-model="selectedItem">
<option value="">선택하세요</option>
<template x-for="item in filteredItems" :key="item">
<option :value="item" x-text="item"></option>
</template>
</select>
</div>
<div style="margin-top: 20px;">
선택된 카테고리: <span x-text="selectedCategory"></span><br>
선택된 세부 카테고리: <span x-text="selectedSubCategory"></span><br>
선택된 품목: <span x-text="selectedItem"></span>
</div>
</div>
</body>
</html>
개발자를 위한 추가 설명:
- 데이터의 구조화: categories, subCategories, items와 같이 데이터를 JavaScript 객체나 배열로 구조화하는 것은 웹 개발에서 매우 중요합니다. 이렇게 하면 데이터를 체계적으로 관리하고, 필요에 따라 쉽게 접근하고 사용할 수 있습니다.
- 이벤트 핸들링: @change와 같이 특정 이벤트가 발생했을 때 원하는 동작을 실행하도록 하는 것은 사용자 인터랙션을 처리하는 기본적인 방법입니다.
- 양방향 바인딩 (x-model): x-model을 사용하면 HTML 요소의 값과 JavaScript 변수를 연결하여, 한쪽이 변경되면 다른 쪽도 자동으로 업데이트됩니다. 이는 사용자 입력과 애플리케이션 상태를 동기화하는 데 매우 편리합니다.
- 조건부 렌더링 (Implicit in x-for with filtered data): x-for를 사용하여 배열의 내용에 따라 HTML 요소를 동적으로 생성하는 것은 매우 강력한 기능입니다. 여기서는 filteredSubCategories와 filteredItems를 사용하여 조건에 맞는 옵션들만 보여지도록 구현했습니다.
웹 서비스를 호출하는 방식으로 변경해 보겠습니다. 이번에는 서버에서 데이터를 비동기적으로 가져와서 Select Box의 옵션을 채우게 됩니다.
핵심 변경 사항:
- 데이터 구조 변경: 더 이상 categories, subCategories, items를 미리 정의하지 않습니다. 대신 서버에서 받아온 데이터를 저장할 변수를 사용합니다.
- API 호출 함수: 각 Select Box의 데이터를 가져오는 JavaScript 함수를 만듭니다. Workspace API를 사용하여 서버에 요청을 보냅니다.
- 로딩 및 에러 처리: API 호출이 완료될 때까지 로딩 상태를 표시하고, 에러가 발생했을 경우 사용자에게 알립니다.
- x-init 활용: Alpine.js 컴포넌트가 초기화될 때 첫 번째 Select Box의 데이터를 가져오도록 x-init을 사용합니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alpine.js 웹 서비스 호출 종속형 Select Box</title>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body>
<div x-data="{
categories: [],
subCategories: [],
items: [],
selectedCategory: '',
selectedSubCategory: '',
selectedItem: '',
categoryLoading: false,
subCategoryLoading: false,
itemLoading: false,
categoryError: null,
subCategoryError: null,
itemError: null,
// 첫 번째 Select Box 데이터 가져오기
loadCategories() {
this.categoryLoading = true;
this.categoryError = null;
fetch('/api/categories') // 실제 API 엔드포인트로 변경해야 합니다.
.then(response => {
if (!response.ok) {
throw new Error('카테고리 데이터를 불러오는 데 실패했습니다.');
}
return response.json();
})
.then(data => {
this.categories = data;
this.categoryLoading = false;
})
.catch(error => {
this.categoryError = error.message;
this.categoryLoading = false;
});
},
// 두 번째 Select Box 데이터 가져오기
loadSubCategories() {
if (!this.selectedCategory) {
this.subCategories = [];
return;
}
this.subCategoryLoading = true;
this.subCategoryError = null;
this.subCategories = []; // 이전 데이터 초기화
this.selectedSubCategory = ''; // 두 번째 Select Box 초기화
this.selectedItem = ''; // 세 번째 Select Box 초기화
this.items = []; // 세 번째 Select Box 데이터 초기화
this.itemError = null;
fetch(`/api/subcategories?category=${this.selectedCategory}`) // 실제 API 엔드포인트로 변경해야 합니다.
.then(response => {
if (!response.ok) {
throw new Error('세부 카테고리 데이터를 불러오는 데 실패했습니다.');
}
return response.json();
})
.then(data => {
this.subCategories = data;
this.subCategoryLoading = false;
})
.catch(error => {
this.subCategoryError = error.message;
this.subCategoryLoading = false;
});
},
// 세 번째 Select Box 데이터 가져오기
loadItems() {
if (!this.selectedSubCategory) {
this.items = [];
return;
}
this.itemLoading = true;
this.itemError = null;
this.items = []; // 이전 데이터 초기화
this.selectedItem = ''; // 세 번째 Select Box 초기화
fetch(`/api/items?subcategory=${this.selectedSubCategory}`) // 실제 API 엔드포인트로 변경해야 합니다.
.then(response => {
if (!response.ok) {
throw new Error('품목 데이터를 불러오는 데 실패했습니다.');
}
return response.json();
})
.then(data => {
this.items = data;
this.itemLoading = false;
})
.catch(error => {
this.itemError = error.message;
this.itemLoading = false;
});
},
// 컴포넌트 초기화 시 첫 번째 Select Box 데이터 로드
init() {
this.loadCategories();
}
}">
<div>
<label for="category">카테고리:</label>
<select id="category" x-model="selectedCategory" @change="loadSubCategories()" :disabled="categoryLoading">
<option value="">선택하세요</option>
<template x-for="category in categories" :key="category.value">
<option :value="category.value" x-text="category.label"></option>
</template>
</select>
<template x-if="categoryLoading">
<span>로딩 중...</span>
</template>
<template x-if="categoryError">
<span style="color: red;" x-text="categoryError"></span>
</template>
</div>
<div style="margin-top: 10px;">
<label for="subCategory">세부 카테고리:</label>
<select id="subCategory" x-model="selectedSubCategory" @change="loadItems()" :disabled="subCategoryLoading || !selectedCategory">
<option value="">선택하세요</option>
<template x-for="subCategory in subCategories" :key="subCategory.value">
<option :value="subCategory.value" x-text="subCategory.label"></option>
</template>
</select>
<template x-if="subCategoryLoading">
<span>로딩 중...</span>
</template>
<template x-if="subCategoryError">
<span style="color: red;" x-text="subCategoryError"></span>
</template>
</div>
<div style="margin-top: 10px;">
<label for="item">품목:</label>
<select id="item" x-model="selectedItem" :disabled="itemLoading || !selectedSubCategory">
<option value="">선택하세요</option>
<template x-for="item in items" :key="item">
<option :value="item" x-text="item"></option>
</template>
</select>
<template x-if="itemLoading">
<span>로딩 중...</span>
</template>
<template x-if="itemError">
<span style="color: red;" x-text="itemError"></span>
</template>
</div>
<div style="margin-top: 20px;">
선택된 카테고리: <span x-text="selectedCategory"></span><br>
선택된 세부 카테고리: <span x-text="selectedSubCategory"></span><br>
선택된 품목: <span x-text="selectedItem"></span>
</div>
</div>
</body>
</html>
코드 변경 상세 설명:
- 데이터 속성 추가:
- categoryLoading, subCategoryLoading, itemLoading: API 호출 중임을 나타내는 불리언 값입니다.
- categoryError, subCategoryError, itemError: API 호출 실패 시 에러 메시지를 저장하는 변수입니다.
- loadCategories() 함수:
- this.categoryLoading = true;: 로딩 상태를 true로 설정합니다.
- Workspace('/api/categories'): /api/categories 엔드포인트로 GET 요청을 보냅니다. 주의: 실제 웹 서비스의 API 엔드포인트로 변경해야 합니다.
- .then(...): API 호출이 성공하면 응답을 처리합니다.
- response.ok: HTTP 상태 코드가 200번대인지 확인합니다.
- response.json(): 응답 데이터를 JSON 형식으로 파싱합니다.
- this.categories = data;: 파싱된 카테고리 데이터를 categories 배열에 저장합니다.
- this.categoryLoading = false;: 로딩 상태를 false로 설정합니다.
- .catch(...): API 호출이 실패하면 에러를 처리합니다.
- this.categoryError = error.message;: 에러 메시지를 categoryError 변수에 저장합니다.
- this.categoryLoading = false;: 로딩 상태를 false로 설정합니다.
- loadSubCategories() 함수:
- 첫 번째 Select Box의 값이 선택되지 않았으면 세부 카테고리 목록을 비우고 함수를 종료합니다.
- 로딩 상태 및 에러를 초기화하고, 이전 선택 값과 데이터를 초기화합니다.
- Workspace(/api/subcategories?category=${this.selectedCategory}): /api/subcategories 엔드포인트로 GET 요청을 보내면서, 선택된 카테고리 값을 쿼리 파라미터로 전달합니다. 주의: 실제 API 엔드포인트로 변경해야 합니다.
- 응답 처리 방식은 loadCategories()와 유사합니다.
- loadItems() 함수:
- 두 번째 Select Box의 값이 선택되지 않았으면 품목 목록을 비우고 함수를 종료합니다.
- 로딩 상태 및 에러를 초기화하고, 이전 선택 값을 초기화합니다.
- Workspace(/api/items?subcategory=${this.selectedSubCategory}): /api/items 엔드포인트로 GET 요청을 보내면서, 선택된 세부 카테고리 값을 쿼리 파라미터로 전달합니다. 주의: 실제 API 엔드포인트로 변경해야 합니다.
- 응답 처리 방식은 loadCategories()와 유사합니다.
- init() 함수:
- Alpine.js 컴포넌트가 초기화될 때 자동으로 실행되는 함수입니다. 여기서 loadCategories()를 호출하여 첫 번째 Select Box의 데이터를 가져옵니다.
- HTML 수정:
- 첫 번째 Select Box의 @change 이벤트 핸들러를 loadSubCategories()로 변경합니다.
- 두 번째 Select Box의 @change 이벤트 핸들러를 loadItems()로 변경합니다.
- 각 Select Box에 :disabled 속성을 추가하여 데이터 로딩 중이거나 이전 Select Box가 선택되지 않았을 때 비활성화합니다.
- 로딩 중 상태와 에러 메시지를 표시하는 부분을 추가했습니다. <template x-if="categoryLoading">...</template>와 같은 형태로 구현했습니다.
// /api/categories 응답 예시
[
{ "value": "fruit", "label": "과일" },
{ "value": "vegetable", "label": "채소" },
{ "value": "drink", "label": "음료" }
]
// /api/subcategories?category=fruit 응답 예시
[
{ "value": "apple", "label": "사과" },
{ "value": "banana", "label": "바나나" },
{ "value": "orange", "label": "오렌지" }
]
// /api/items?subcategory=apple 응답 예시
[
"빨간 사과",
"푸른 사과"
]
이제 이 HTML 파일을 웹 브라우저로 열면, 페이지가 로드될 때 /api/categories를 호출하여 첫 번째 Select Box의 옵션을 채웁니다. 첫 번째 Select Box의 값을 변경하면 /api/subcategories를 호출하여 두 번째 Select Box의 옵션을 채우고, 두 번째 Select Box의 값을 변경하면 /api/items를 호출하여 세 번째 Select Box의 옵션을 채우는 방식으로 동작하게 됩니다.
