[JavaScript] 연속된 trigger의 비동기 처리

khj·2025년 12월 28일

JavaScript

목록 보기
3/3
post-thumbnail

JavaScript에서 trigger()를 사용해 이벤트를 강제로 호출하는 경우가 있다.
특히 select 태그가 여러 개 연쇄적으로 동작하는 화면에서 자주 사용된다.

하지만 아래와 같은 문제를 한 번쯤은 겪게 된다.

첫 번째 trigger는 정상 동작하는데
두 번째 trigger는 실행되지 않는 것처럼 보인다.

이 글에서는 해당 현상이 발생하는 정확한 이유와 올바른 해결 방법을 정리한다.


문제 상황

selectA의 값이 변경되면
서버에서 데이터를 받아 selectB의 옵션을 세팅하고
selectB의 값을 기준으로 추가 로직을 수행하는 구조다.

$('#selectA').trigger('change');
$('#selectB').trigger('change');

$('#selectA').on('change', async function () {
  const list = await fetch('/api/options').then(res => res.json());
  setSelectOptions('#selectB', list);
});

$('#selectB').on('change', function () {
  const value = $('#selectB').val();
  console.log(value);
});

이 경우 다음과 같은 현상이 발생한다.

  • selectB의 change 이벤트가 실행되지 않음

  • 또는 실행되지만 값이 undefined로 나옴

원인 분석

핵심 원인 요약

trigger()는 동기 실행이지만
select 옵션 세팅은 비동기다

실제 실행 흐름은 다음과 같다.

  1. #selectA.trigger('change')

  2. 서버 요청 시작 (fetch)

  3. 아직 옵션 세팅이 끝나지 않음

  4. 바로 #selectB.trigger('change') 실행

  5. selectB에는 아직 옵션이 없음

즉, 옵션이 준비되기 전에 change 이벤트가 먼저 실행되는 구조다.

그래서 두 번째 trigger가 동작하지 않는 것처럼 보이게 된다.

잘못된 해결 방법 (비추천)

setTimeout으로 시간 벌기

$('#selectA').trigger('change');

setTimeout(() => {
  $('#selectB').trigger('change');
}, 300);

이 방식은 다음과 같은 문제가 있다.

  • 네트워크 속도에 따라 실패 가능
  • 환경에 따라 동작이 달라짐
  • 코드 의도가 불명확함

근본적인 해결책이 아니다.

해결 방법 1

옵션 세팅이 끝난 뒤에 trigger 호출

옵션 세팅이 완료된 시점에서 다음 이벤트를 호출한다.

$('#selectA').on('change', async function () {
  const list = await fetch('/api/options').then(res => res.json());
  setSelectOptions('#selectB', list);

  $('#selectB').trigger('change');
});

실행 흐름은 다음과 같다.

데이터 로드 완료
→ 옵션 세팅 완료
→ 다음 select 처리

해결 방법 2

옵션 세팅 함수를 Promise로 만들기 (추천)

옵션 세팅 자체를 명확한 비동기 작업으로 만든다.

function setSelectOptions(selector, list) {
  return new Promise(resolve => {
    const $select = $(selector);
    $select.empty();

    list.forEach(item => {
      $select.append(
        `<option value="${item.id}">${item.name}</option>`
      );
    });

    resolve();
  });
}
$('#selectA').on('change', async function () {
  const list = await fetch('/api/options').then(res => res.json());
  await setSelectOptions('#selectB', list);

  $('#selectB').trigger('change');
});

이 방식의 장점은 다음과 같다.

  • 비동기 흐름이 명확함
  • select가 늘어나도 확장 가능
  • 유지보수와 테스트에 유리함

해결 방법 3

trigger 의존 구조 제거 (가장 권장)

이벤트를 로직 실행 수단으로 사용하지 않는다.

❌ 이벤트 체인 구조

A change → trigger B → trigger C

⭕ 데이터 흐름 기반 구조

async function loadSelectB() {
  const list = await fetch('/api/options').then(res => res.json());
  setSelectOptions('#selectB', list);
}

function handleSelectB() {
  const value = $('#selectB').val();
  console.log(value);
}

$('#selectA').on('change', async function () {
  await loadSelectB();
  handleSelectB();
});

이 구조의 특징은 다음과 같다.

  • 이벤트는 사용자 입력 처리용
  • 실제 로직은 함수 단위로 관리
  • 디버깅과 리팩토링이 쉬움

정리

  • trigger()는 이벤트를 흉내낼 뿐 흐름 제어 도구가 아니다

  • select 옵션 세팅은 대부분 비동기 작업이다

  • 연속 trigger 호출은 타이밍 문제를 만든다

핵심 결론

옵션 세팅이 완료된 이후에
다음 로직을 직접 호출해야 한다

select 연쇄 구조에서 발생하는 문제를 제대로 이해하면
불필요한 디버깅 시간을 크게 줄일 수 있다.

profile
Spring, Django 개발 블로그

0개의 댓글