비동기 웹 통신 구현: AJAX 기본 원리부터 JSON 처리 및 동적 페이지네이션까지

1. AJAX의 핵심 개념과 비동기 처리 모델

AJAX(Asynchronous JavaScript And XML)는 단일 기술이 아닌 기존 웹 표준을 조합해 페이지 전체를 새로 고치지 않고도 특정 영역의 데이터만 동적으로 갱신하는 개발 패러다임입니다. 전통적인 HTTP 요청-응답 모델은 서버 처리 완료 시 전체 HTML 문서를 다시 렌더링해야 했으나, 비동기 통신을 도입하면 브라우저가 백그라운드에서 서버와 최소한의 데이터를 교환합니다. 이로 인해 동기 방식에서 발생하는 UI 블록 현상이 제거되고, 사용자 경험이 크게 향상됩니다.

1.1 기본 API를 이용한 서버 통신 (`XMLHttpRequest`)

브라우저 내장 객체인 XMLHttpRequest는 서버와의 데이터 교환을 직접 제어할 수 있는 핵심 수단입니다. 주요 메서드와 상태 값은 다음과 같이 동작합니다.

  • open(method, url, isAsync): HTTP 메서드(GET/POST), 타겟 URL, 실행 모드(동기/비동기)를 설정합니다.
  • send(payload): 구성된 요청을 네트워크로 전송합니다. POST 방식일 경우 요청 본문을 인자로 전달합니다.
  • onreadystatechange: 통신 상태가 변경될 때마다 호출되는 리스너입니다. readyState가 4(응답 완료)이고 status가 200(HTTP OK)일 때만 responseText를 추출해 DOM에 반영합니다.
// Java Servlet (백엔드 검증 로직)
@WebServlet("/api/account-validator")
public class AccountValidatorServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");

        String inputId = request.getParameter("userId");
        boolean isRegistered = "developer01".equalsIgnoreCase(inputId);
        
        String feedbackHtml = isRegistered ? 
            "이미 등록된 계정입니다." : 
            "사용 가능한 계정입니다.";
            
        response.getWriter().write(feedbackHtml);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        this.doGet(req, resp);
    }
}
<!-- HTML/JavaScript (프론트엔드 호출 로직) -->
<form id="registrationForm" autocomplete="off">
    계정 ID: <input type="text" id="inputAccount">
    <span id="validationFeedback"></span><br>
    비밀번호: <input type="password" id="inputPass"><br>
    <button type="submit">가입하기</button>
</form>

<script>
document.getElementById("inputAccount").addEventListener("blur", function() {
    const accountValue = this.value.trim();
    if (!accountValue) return;

    const xhr = new XMLHttpRequest();
    xhr.open("GET", "/api/account-validator?userId=" + encodeURIComponent(accountValue), true);
    
    xhr.onreadystatechange = function() {
        if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
            document.getElementById("validationFeedback").innerHTML = this.responseText;
        }
    };
    xhr.send();
});
</script>

2. jQuery 기반 비동기 요청 패턴

jQuery는 XMLHttpRequest의繁琐한 설정 과정을 추상화하여 직관적인 메서드 체이닝을 제공합니다.

  • $.get(url, data, success, type) / $.post(url, data, success, type): HTTP 동사 전용 간편 함수입니다.
  • $.ajax(settings): 설정 객체를 통해 timeout, headers, beforeSend, error handling 등 모든 통신 옵션을 세밀하게 제어할 수 있는 범용 인터페이스입니다.
$(document).ready(function() {
    $("#inputAccount").on("focusout", function() {
        const currentVal = $(this).val();
        
        $.ajax({
            url: "/api/account-validator",
            method: "POST",
            data: { userId: currentVal },
            dataType: "html",
            success: function(serverResponse) {
                $("#validationFeedback").html(serverResponse);
            },
            error: function(xhr) {
                console.warn("통신 오류 발생:", xhr.statusText);
                $("#validationFeedback").html("서버 응답 실패");
            }
        });
    });
});

3. 백엔드 데이터 포맷 변환 (Jackson)

클라이언트-서버 간 구조화된 데이터 교환에는 JSON 포맷이 표준으로 사용됩니다. Java 생태계에서는 Jackson 라이브러리가 널리 채택되며, ObjectMapper 인스턴스를 통해 객체와 문자열 간의 직렬화/역직렬화를 수행합니다.

  • writeValueAsString(javaObject): Java Bean 또는 컬렉션을 JSON 텍스트로 변환합니다.
  • readValue(jsonStr, targetType): JSON을 특정 클래스 인스턴스로 복원합니다. 제네릭 컬렉션 복원 시 타입 소거 문제를 해결하기 위해 TypeReference를 반드시 활용해야 합니다.
public class JsonDataProcessor {
    private final ObjectMapper mapper = new ObjectMapper();

    // 단순 객체 변환
    public void serializeProfile() throws JsonProcessingException {
        ProfileDto user = new ProfileDto("김철수", 32);
        String jsonStr = mapper.writeValueAsString(user);
        System.out.println("직렬화 결과: " + jsonStr);
    }

    // 제네릭 List 역직렬화
    public void deserializeUserList(String payload) throws JsonProcessingException {
        TypeReference<List<ProfileDto>> typeRef = new TypeReference<>() {};
        List<ProfileDto> userList = mapper.readValue(payload, typeRef);
        System.out.println("복원된 객체 수: " + userList.size());
    }
}

4. 실무 사례: 검색어 자동 완성 구현

사용자가 입력 필드에 텍스트를 작성할 때 마다 백엔드에 부분 일치 쿼리를 날려 후보군을 실시간으로 표시하는 패턴입니다.

  • 클라이언트: 입력 이벤트 발생 시 공백 여부를 검증합니다. 값이 존재하면 AJAX POST 요청을 보내고, 반환된 JSON 배열을 순회해 <li> 엘리먼트로 조립하여 드롭다운 영역에 주입합니다.
  • 서버: 요청 파라미터를 추출해 Service 계층의 LIKE '%param%' 쿼리를 실행합니다. 결과 List를 Jackson으로 변환 후 클라이언트에 응답합니다.
// 서버 측 컨트롤러
@WebServlet("/api/keyword-suggest")
public class KeywordSuggestController extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
        req.setCharacterEncoding("UTF-8");
        res.setContentType("application/json;charset=UTF-8");
        
        String query = req.getParameter("keyword");
        SearchService service = new SearchServiceImpl();
        List<Keyword> matchedItems = service.findSimilar(query);
        
        String payload = new ObjectMapper().writeValueAsString(matchedItems);
        res.getWriter().write(payload);
    }
}
<!-- 프론트엔드 구현 -->
<input type="text" id="searchInput" placeholder="검색어 입력...">
<div id="suggestionBox" class="dropdown" style="display:none;"></div>

<script src="jquery.min.js"></script>
<script>
$("#searchInput").on("input", function() {
    const term = $(this).val().trim();
    const box = $("#suggestionBox");
    
    if (!term) { box.hide(); return; }

    $.ajax({
        url: "/api/keyword-suggest",
        type: "POST",
        data: { keyword: term },
        dataType: "json",
        success: function(items) {
            let html = "";
            items.forEach(item => html += `<div class="item">${item.keywordName}</div>`);
            box.html(html).show();
        },
        error: () => box.hide()
    });
});
</script>

5. 실무 사례: 스크롤 트리거 무한 로딩 및 클릭식 페이지네이션

화면 하단에 도달할 때 다음 데이터 세트를 자동으로 적재하는 기법과, 전통적인 번호 버튼을 통한 페이지 이동 방식을 결합한 구현입니다.

  • 무한 스크롤 조건식: (현재 스크롤 위치 + 뷰포트 높이 + 임계값) ≥ 문서 전체 높이가 성립하면 다음 페이지 요청을 발동합니다.
  • 요직렬화 방지: isProcessing 플래그를 사용해 전 요청이 완료되기 전 중복 호출을 차단합니다.
  • 버튼 페이지네이션: 외부 UI 라이브러리와 연동해 PageInfo 객체의 총 페이지 수 및 현재 위치를 반영하고, 클릭 이벤트로 currentPage를 수정해 AJAX를 재실행합니다.
<script>
let isProcessing = false;
let currentPage = 1;
const itemsPerPage = 15;

function loadNewsBatch(pageNum) {
    isProcessing = true;
    $(".spinner").show();
    
    $.post("/api/news-loader", { page: pageNum, limit: itemsPerPage }, function(response) {
        const articles = response.data;
        if (articles.length === 0) {
            $(".loader-msg").text("더 이상 데이터가 없습니다.");
            $(".spinner").hide();
            return;
        }

        let markup = "";
        articles.forEach(news => {
            markup += `<article class="news-card"><h3>${news.title}</h3></article>`;
        });
        $("#newsContainer").append(markup);
        $(".spinner").hide();
        isProcessing = false;
    });
}

// 스크롤 이벤트 리스너
$(window).on("scroll", function() {
    const viewportHeight = $(window).height();
    const scrollPos = $(window).scrollTop();
    const docHeight = $(document).height();
    const threshold = 50;

    if ((scrollPos + viewportHeight + threshold) >= docHeight && !isProcessing) {
        currentPage++;
        loadNewsBatch(currentPage);
    }
});
</script>
// 서버 측 페이지네이션 처리 컨트롤러
@WebServlet("/api/news-loader")
public class NewsPaginationServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
        req.setCharacterEncoding("UTF-8");
        res.setContentType("application/json;charset=UTF-8");
        
        int page = Integer.parseInt(req.getParameter("page"));
        int limit = Integer.parseInt(req.getParameter("limit"));
        
        NewsService svc = new NewsServiceImpl();
        Page<NewsEntity> pageResult = svc.fetchByOffset(page, limit);
        PageInfo<NewsEntity> pageInfo = new PageInfo<>(pageResult);
        
        // 응답 객체 구성
        Map<String, Object> wrapper = new HashMap<>();
        wrapper.put("data", pageResult.getResult());
        wrapper.put("hasNext", pageInfo.isHasNextPage());
        wrapper.put("currentPage", pageInfo.getPageNum());
        
        String jsonPayload = new ObjectMapper().writeValueAsString(wrapper);
        res.getWriter().write(jsonPayload);
    }
}

태그: XmlHttpRequest JSON-Serialization Jackson-API Servlet-Async Infinite-Scroll

8월 26일 16:45에 게시됨