데이터베이스 설계부터 프론트엔드 라이브러리 구현까지, jQuery 없이 순수 JavaScript로 AJAX 요청을 처리하는 방법을 살펴봅니다. 특히 상위 지역 선택 시 하위 지역이 자동으로 갱신되는 연동 선택 기능을 중심으로 설명합니다.
데이터베이스 스키마 설계
계층형 지역 데이터를 저장하기 위해 자기 참조 관계를 가진 테이블을 설계합니다. 상위 지역 코드(pcode)가 NULL이면 최상위 지역(도/광역시)으로 간주합니다.
-- 학생 정보 테이블
CREATE TABLE student_info (
sid BIGINT AUTO_INCREMENT PRIMARY KEY,
sname VARCHAR(255),
sage INT,
saddr VARCHAR(255)
);
-- 지역 정보 테이블 (계층 구조)
CREATE TABLE region_data (
rid BIGINT PRIMARY KEY AUTO_INCREMENT,
rcode VARCHAR(255),
rname VARCHAR(255),
parent_code VARCHAR(255)
);
-- 샘플 데이터 삽입
INSERT INTO region_data VALUES (NULL, 'A01', '경기도', NULL);
INSERT INTO region_data VALUES (NULL, 'A02', '강원도', NULL);
INSERT INTO region_data VALUES (NULL, 'A03', '수원시', 'A01');
INSERT INTO region_data VALUES (NULL, 'A04', '성남시', 'A01');
INSERT INTO region_data VALUES (NULL, 'A05', '춘천시', 'A02');
INSERT INTO region_data VALUES (NULL, 'A06', '원주시', 'A02');
INSERT INTO region_data VALUES (NULL, 'A07', '경상남도', NULL);
INSERT INTO region_data VALUES (NULL, 'A08', '창원시', 'A07');
커스텀 DOM 조작 라이브러리 구현
jQuery의 핵심 기능을 모방하여 가벼운 유틸리티 라이브러리를 작성합니다. 선택자 조회, 이벤트 바인딩, AJAX 통신 기능을 포함합니다.
function MiniQuery(selector) {
// DOM 요소 저장용 클로저 변수
let selectedElement = null;
if (typeof selector === 'string') {
// ID 선택자 처리
if (selector.charAt(0) === '#') {
selectedElement = document.getElementById(selector.slice(1));
return new MiniQuery();
}
// 클래스 선택자 확장 가능
if (selector.charAt(0) === '.') {
selectedElement = document.getElementsByClassName(selector.slice(1))[0];
return new MiniQuery();
}
}
// DOMContentLoaded 이벤트 핸들러 등록
if (typeof selector === 'function') {
document.addEventListener('DOMContentLoaded', selector);
}
// HTML 콘텐츠 조작
this.content = function(htmlString) {
if (selectedElement) {
selectedElement.innerHTML = htmlString;
}
return this;
};
// 클릭 이벤트 바인딩
this.onClick = function(callback) {
if (selectedElement) {
selectedElement.addEventListener('click', callback);
}
return this;
};
// 변경 이벤트 바인딩 (select 요소용)
this.onChange = function(callback) {
if (selectedElement) {
selectedElement.addEventListener('change', callback);
}
return this;
};
// 값 조회/설정
this.fieldValue = function(newValue) {
if (selectedElement) {
if (arguments.length === 0) {
return selectedElement.value;
}
selectedElement.value = newValue;
}
return this;
};
// 정적 AJAX 메서드
MiniQuery.ajaxRequest = function(config) {
const httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = function() {
if (httpRequest.readyState !== 4) return;
if (httpRequest.status === 200) {
const parsedData = JSON.parse(httpRequest.responseText);
config.onSuccess(parsedData);
} else {
console.error('요청 실패:', httpRequest.status);
if (config.onError) config.onError(httpRequest.status);
}
};
const methodType = config.method.toUpperCase();
const isAsync = config.async !== false;
if (methodType === 'POST') {
httpRequest.open('POST', config.endpoint, isAsync);
httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
httpRequest.send(config.payload);
} else if (methodType === 'GET') {
const fullUrl = config.payload
? `${config.endpoint}?${config.payload}`
: config.endpoint;
httpRequest.open('GET', fullUrl, isAsync);
httpRequest.send();
}
};
}
// 별칭 설정
window.$$ = MiniQuery;
백엔드 서블릿 구현
Java Servlet을 활용하여 계층형 지역 데이터를 JSON 형식으로 제공하는 API를 구성합니다. 요청 파라미터에 따라 최상위 지역 또는 특정 상위 코드의 하위 지역을 반환합니다.
package com.example.region.api;
import com.alibaba.fastjson.JSON;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@WebServlet("/api/regions")
public class RegionQueryServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
// 인코딩 설정
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=UTF-8");
String parentFilter = req.getParameter("parent");
String querySql;
// 동적 쿼리 구성
if (parentFilter == null || parentFilter.isEmpty()) {
querySql = "SELECT rid, rcode, rname, parent_code FROM region_data WHERE parent_code IS NULL";
} else {
querySql = "SELECT rid, rcode, rname, parent_code FROM region_data WHERE parent_code = ?";
}
List<Map<String, Object>> regionList = new ArrayList<>();
try (Connection conn = DataSourceProvider.getConnection();
PreparedStatement pstmt = conn.prepareStatement(querySql)) {
if (parentFilter != null && !parentFilter.isEmpty()) {
pstmt.setString(1, parentFilter);
}
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
Map<String, Object> region = new HashMap<>();
region.put("regionId", rs.getLong("rid"));
region.put("regionCode", rs.getString("rcode"));
region.put("regionName", rs.getString("rname"));
region.put("parentRegion", rs.getString("parent_code"));
regionList.add(region);
}
} catch (SQLException ex) {
throw new RuntimeException("데이터베이스 조회 실패: " + ex.getMessage());
}
// JSON 응답 출력
PrintWriter responseWriter = resp.getWriter();
responseWriter.print(JSON.toJSONString(regionList));
}
}
프론트엔드 연동 구현
구현한 라이브러리와 백엔드 API를 연결하여 실제 지역 연동 선택 기능을 완성합니다.
<!-- HTML 구조 -->
<select id="province-selector">
<option value="">도/광역시 선택</option>
</select>
<select id="city-selector">
<option value="">시/구/군 선택</option>
</select>
<script>
// 페이지 로드 시 최상위 지역 조회
$$(function() {
loadRegions(null, 'province-selector');
// 상위 지역 변경 시 하위 지역 갱신
$$('#province-selector').onChange(function() {
const selectedCode = this.value;
const cityDropdown = document.getElementById('city-selector');
// 하위 선택 초기화
cityDropdown.innerHTML = '<option value="">시/구/군 선택</option>';
if (selectedCode) {
loadRegions(selectedCode, 'city-selector');
}
});
});
function loadRegions(parentCode, targetElementId) {
const requestParams = parentCode ? `parent=${encodeURIComponent(parentCode)}` : '';
MiniQuery.ajaxRequest({
method: 'GET',
endpoint: '/api/regions',
payload: requestParams,
async: true,
onSuccess: function(regionData) {
const dropdown = document.getElementById(targetElementId);
regionData.forEach(function(item) {
const optionElement = document.createElement('option');
optionElement.value = item.regionCode;
optionElement.textContent = item.regionName;
dropdown.appendChild(optionElement);
});
},
onError: function(statusCode) {
console.error('지역 데이터 로드 실패:', statusCode);
}
});
}
</script>
확장 고려사항
기본 구현을 바탕으로 다음 기능들을 추가로 고려할 수 있습니다:
- Promise 기반 비동기 처리: 콜백 지옥 방지를 위한 async/await 지원
- 요청 캐싱: 반복되는 동일 지역 조회 시 메모리 캐싱 적용
- 로딩 상태 표시: AJAX 요청 중 시각적 피드백 제공
- 에러 재시도 로직: 네트워크 불안정 상황 대응
이러한 접근 방식은 외부 라이브러리 의존성을 최소화하고, 브라우저의 기본 API를 깊이 이해하는 데 도움이 됩니다.