순수 JavaScript로 구현하는 AJAX 기반 지역 연동 선택 기능

데이터베이스 설계부터 프론트엔드 라이브러리 구현까지, 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를 깊이 이해하는 데 도움이 됩니다.

태그: AJAX JavaScript Servlet JSON DOM

6월 1일 10:32에 게시됨