Select2 연동 드롭다운 중복 선택 방지 구현 가이드

웹 애플리케이션, 특히 복잡한 데이터 입력 폼이나 관리자 페이지를 개발하다 보면 필연적으로 마주치는 시나리오가 있습니다. 바로 여러 개의 드롭다운 메뉴가 서로의 선택에 따라 동적으로 반응해야 하는 경우입니다. 예를 들어, 하나의 프로젝트에 '주 담당자', '부 담당자', '검수자'를 동일한 직원 목록에서 할당해야 한다고 상상해 보십시오. 사용자가 '주 담당자'로 '이영희'를 선택했다면, '부 담당자'와 '검수자' 목록에서는 '이영희'를 더 이상 선택할 수 없도록 막아야 데이터의 무결성이 보장되고 사용자의 실수를 미연에 방지할 수 있습니다. 이는 좋은 사용자 경험(UX)의 기본입니다.

이러한 고급 드롭다운 기능을 구현하기 위해 전 세계 수많은 개발자들이 jQuery 기반의 Select2 라이브러리를 애용합니다. Select2는 평범한 HTML <select> 태그를 강력한 검색 기능, 태깅, 무한 스크롤, 원격 데이터 로딩 등이 가능한 현대적인 UI 컴포넌트로 변신시켜 줍니다. 하지만 그 강력함만큼이나 여러 인스턴스를 연동하여 동적으로 옵션을 제어하는 것은 처음 접하는 개발자에게는 상당한 혼란을 안겨줄 수 있습니다.

많은 개발자들이 Select2를 초기화할 때 사용한 원본 데이터 배열(Array)의 속성(예: disabled: true)을 변경하면 Select2의 옵션도 마법처럼 갱신될 것이라 기대합니다. 하지만 안타깝게도 Select2는 초기화 시점에 DOM을 생성한 이후에는 원본 데이터 소스의 변경을 자동으로 감지하지 않습니다. 이로 인해 좌절감을 느끼고 비효율적인 방법을 시도하며 시간을 허비하는 경우가 많습니다.

이 글에서는 풀스택 개발자의 경험을 바탕으로, 하나의 Select2에서 항목을 선택했을 때 다른 연관된 Select2들의 동일 항목을 동적으로 비활성화(disable)하는 가장 효율적이고 안정적인 방법을 A부터 Z까지 심도 있게 파헤쳐 보겠습니다. 흔히 저지르는 실수부터 시작해 올바른 접근 방식의 원리, 실제 프로젝트에 바로 적용 가능한 완성형 코드까지 단계별로 상세히 안내할 것입니다.

1. 문제의 본질: 왜 Select2 연동은 생각보다 까다로울까?

본격적인 해결책을 탐구하기 전에, 이 문제가 왜 단순한 JavaScript DOM 조작과 차원이 다른지, 그리고 어떤 함정들이 숨어있는지 명확하게 이해하는 것이 중요합니다. 문제의 본질을 알아야 올바른 해결책의 가치를 제대로 이해할 수 있습니다.

대표적인 사용자 시나리오

우리가 해결하려는 문제는 다양한 형태로 나타납니다.

  • 역할 배정 시스템: 여러 사용자에게 '1순위 책임자', '2순위 책임자', '3순위 책임자'를 배정해야 합니다. '1순위'로 A를 배정했다면, '2순위'와 '3순위'에서는 A가 선택 불가능해야 합니다.
  • 상품 옵션 선택: 의류 쇼핑몰에서 '상의 색상'과 '하의 색상'을 선택할 때, 한정판 색상 세트의 경우 '상의'로 '네온 레드'를 선택하면 '하의' 목록에서는 '네온 레드'를 선택할 수 없도록 해야 할 수 있습니다.
  • 출발지와 도착지 선택: 항공권이나 기차표 예매 시스템에서 출발지로 '서울'을 선택했다면, 도착지 목록에서 '서울'은 의미가 없으므로 비활성화하거나 목록에서 숨기는 것이 사용자 편의성을 높입니다.
  • 태그(Tag) 선택기: 게시물에 '메인 카테고리'와 '서브 카테고리'를 선택할 때, 동일한 카테고리를 중복으로 지정하지 못하도록 막아야 합니다.

이 모든 시나리오는 결국 '동일한 데이터 소스를 공유하는 여러 선택지에서 중복 선택을 방지한다'는 하나의 공통된 목표를 가집니다.

개발자들이 흔히 빠지는 함정: 잘못된 접근법들

이 문제에 직면한 개발자들이 가장 먼저 시도하지만 결국 실패로 돌아가는 대표적인 방법 두 가지를 분석해 보겠습니다.

잘못된 접근 1: 원본 데이터 소스 객체 수정

가장 직관적이고 순진한 생각은 Select2를 초기화할 때 사용했던 JavaScript 배열이나 객체를 직접 수정하는 것입니다. 코드로 보면 다음과 같습니다.


// 1. 초기 데이터 정의
const userList = [
    { id: 'user1', text: '김철수' },
    { id: 'user2', text: '이영희' },
    { id: 'user3', text: '박민준' }
];

// 2. 두 개의 Select2를 동일한 데이터로 초기화
$('#primary-manager, #secondary-manager').select2({
    data: userList
});

// 3. 첫 번째 Select2에서 '김철수'를 선택한 후...
$('#primary-manager').on('change', function() {
    const selectedId = $(this).val(); // 'user1'

    // 원본 데이터(userList)에서 해당 ID를 가진 객체를 찾아 disabled 속성을 추가
    const targetUser = userList.find(user => user.id === selectedId);
    if (targetUser) {
        targetUser.disabled = true; // 이 코드가 #secondary-manager에 반영될까?
    }
    
    // ❌ 결론: 아무 일도 일어나지 않는다.
});
작동하지 않는 이유: Select2는 .select2({ data: ... })가 호출되는 시점에, 전달된 userList 배열을 읽기 전용으로 사용하여 필요한 HTML <option> 태그들을 단 한 번 생성하고 DOM에 삽입합니다. 그 이후, JavaScript 변수 userList의 내용이 어떻게 바뀌든 이미 생성된 DOM 요소들과의 연결고리는 끊어진 상태입니다. Select2는 이 변수를 지속적으로 감시(watching)하거나 데이터 바인딩을 유지하지 않습니다. 즉, 초기화는 일회성 스냅샷일 뿐입니다.

잘못된 접근 2: 'destroy' 후 재초기화

첫 번째 방법이 실패한 후, 일부 개발자들은 더 극단적인 방법을 시도합니다. 바로 Select2 인스턴스를 완전히 파괴(destroy)하고, 변경된 데이터로 다시 생성하는 것입니다.


$('#primary-manager').on('change', function() {
    const selectedId = $(this).val();

    // 부 담당자 Select2를 대상으로...
    const secondarySelect = $('#secondary-manager');

    // 기존 데이터를 복사하고, 선택된 옵션을 비활성화 처리한 새 데이터 생성
    let newData = userList.map(user => ({
        ...user,
        disabled: user.id === selectedId
    }));

    // 💣 Select2를 파괴하고!
    secondarySelect.select2('destroy');
    // ✨ 새로운 데이터로 다시 초기화!
    secondarySelect.select2({ data: newData });
});

이 방법은 놀랍게도 기능적으로는 동작합니다. 하지만 이는 마치 작은 벌레를 잡기 위해 집 전체를 부수고 새로 짓는 것과 같은 매우 비효율적이고 위험한 접근법입니다. 심각한 부작용은 다음과 같습니다.

단점 상세 설명
심각한 성능 저하 destroy와 재초기화 과정은 내부적으로 많은 DOM 조작과 이벤트 리스너 해제/재등록을 수행합니다. 옵션이 수십 개만 되어도 사용자가 인지할 수 있는 지연이나 브라우저의 부하를 유발하며, 옵션이 수백 개 이상인 경우 심각한 성능 문제로 이어집니다.
사용자 경험(UX) 저해 Select2가 파괴되었다가 다시 렌더링되는 찰나의 순간에 화면이 깜빡이거나(flickering), 스타일이 잠시 깨졌다가 적용되는 현상이 발생할 수 있습니다. 이는 사용자에게 애플리케이션이 불안정하다는 인상을 줍니다.
상태 유지의 어려움 만약 부 담당자 Select2가 이미 어떤 값을 선택하고 있었거나, 검색창에 무언가를 입력 중이었다면, 재초기화 과정에서 그 모든 상태가 소실됩니다. 이를 유지하려면 상태를 별도로 저장했다가 복원하는 복잡한 추가 코드가 필요해져 코드의 복잡도가 기하급수적으로 증가합니다.

결론적으로, destroy 후 재초기화 방식은 기능 구현에만 급급한 임시방편일 뿐, 안정적이고 전문적인 프로덕션 레벨의 해결책이라고 절대 볼 수 없습니다. 우리는 더 우아하고 효율적인 방법을 찾아야 합니다.

2. 올바른 해법: 이벤트 리스너와 DOM 직접 제어의 아름다운 조화

가장 효율적이고 올바른 방법은 Select2가 생성한 실제 HTML <option> 요소의 disabled 속성을 직접 제어하는 것입니다. Select2는 화려한 UI 뒤에 표준 <select><option> 태그 구조를 그대로 유지하고 있습니다. 따라서, 우리는 jQuery를 사용해 이 DOM 요소들을 직접 조작할 수 있고, Select2는 이 변경 사항을 자신의 UI에 즉시 반영합니다. 이것이 핵심 원리입니다.

문제는 '언제' 이 조작을 실행할 것인가입니다. 이를 위해 Select2가 친절하게 제공하는 전용 이벤트 시스템을 적극적으로 활용해야 합니다.

핵심 원리 1: 올바른 이벤트의 선택 (`change` vs `select2:select`)

jQuery에 익숙한 개발자라면 드롭다운의 값 변경을 감지할 때 자연스럽게 .on('change', function() { ... })를 떠올릴 것입니다. 대부분의 경우 이는 문제가 없지만, Select2와 함께 사용할 때는 더 나은 선택지가 있습니다.

Select2는 자체적인 이벤트 시스템을 가지고 있으며, 사용자가 옵션을 선택하는 순간 select2:select 이벤트를 발생시킵니다. 물론 표준 change 이벤트도 연달아 발생하지만, select2:select 이벤트를 사용하는 것에는 다음과 같은 명확한 이점이 있습니다.

  • 정확한 시점: select2:select는 Select2가 선택과 관련된 모든 내부 처리를 완료한 직후에 발생하므로 타이밍 관련 오류를 방지할 수 있습니다.
  • 풍부한 정보: 이벤트 객체(e)의 params.data 속성을 통해 선택된 항목의 원본 데이터(ID, text 등)를 손쉽게 얻을 수 있어 추가적인 탐색 없이 로직을 처리할 수 있습니다.
  • 명확성: 코드만 보아도 "이것은 Select2의 고유한 액션을 처리하는 로직"임을 명확하게 알 수 있습니다.
따라서, 특별한 이유가 없는 한 Select2의 고유한 사용자 액션(선택, 해제, 열기, 닫기 등)을 감지할 때는 반드시 Select2가 제공하는 네임스페이스 이벤트(select2:*)를 사용하는 것이 안전하고 권장되는 모범 사례입니다.

핵심 원리 2: 상태 복원을 위한 '이전 값' 기억하기

단순히 새로 선택된 값을 다른 드롭다운에서 비활성화하는 것만으로는 완벽한 기능을 구현할 수 없습니다. 사용자 경험을 한 단계 끌어올리려면, 사용자가 선택을 변경했을 때의 시나리오를 고려해야 합니다.

예를 들어, '주 담당자'로 처음에는 '김철수'를 선택했습니다. 그러면 '부 담당자' 목록에서는 '김철수'가 비활성화됩니다. 잠시 후 사용자가 마음을 바꿔 '주 담당자'를 '박민준'으로 변경했습니다. 이때 '박민준'이 '부 담당자' 목록에서 새롭게 비활성화되는 것은 당연합니다. 하지만 동시에, 이전에 비활성화했던 '김철수'는 다시 선택 가능하도록 활성화시켜주어야 합니다. 그렇지 않으면 비활성화된 옵션들만 계속 누적될 것입니다.

이를 구현하기 위해 우리는 각 Select2의 '변경 전' 값을 어딘가에 저장해 둘 필요가 있습니다. 이 '과거'를 기억하는 간단한 메커니즘이 우리 코드의 완성도를 크게 높여줄 것입니다.

3. 단계별 실전 구현: 코드로 배우는 Select2 옵션 비활성화

이제 이론을 바탕으로 실제 동작하는 코드를 한 단계씩 작성하며 원리를 체화해 보겠습니다.

1단계: 기본 HTML 구조 및 라이브러리 로드

가장 먼저 jQuery와 Select2 라이브러리를 로드하고, 우리가 제어할 두 개의 <select> 요소를 포함하는 기본 HTML 문서를 준비합니다. 공용 CDN을 사용하는 것이 가장 간편합니다.


<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Select2 연동형 옵션 비활성화 예제</title>
    <!-- jQuery -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <!-- Select2 CSS -->
    <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
    <!-- Select2 JS -->
    <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; padding: 2rem; max-width: 600px; margin: 0 auto; }
        .form-group { margin-bottom: 1.5rem; }
        label { display: block; margin-bottom: 0.5rem; font-weight: bold; font-size: 1rem; }
        .select-control { width: 100%; }
    </style>
</head>
<body>

    <h1>담당자 배정</h1>

    <div class="form-group">
        <label for="primary-manager">주 담당자</label>
        <select id="primary-manager" class="manager-select select-control">
            <option value="">선택하세요</option>
            <option value="user1">김철수</option>
            <option value="user2">이영희</option>
            <option value="user3">박민준</option>
            <option value="user4">최지우</option>
        </select>
    </div>

    <div class="form-group">
        <label for="secondary-manager">부 담당자</label>
        <select id="secondary-manager" class="manager-select select-control">
            <option value="">선택하세요</option>
            <option value="user1">김철수</option>
            <option value="user2">이영희</option>
            <option value="user3">박민준</option>
            <option value="user4">최지우</option>
        </select>
    </div>

    <script>
        // 여기에 우리의 마법 같은 JavaScript 코드가 작성됩니다.
    </script>

</body>
</html>

중요한 점은 두 <select> 요소에 동일한 manager-select 클래스를 부여한 것입니다. 이를 통해 jQuery로 두 요소를 한 번에 선택하고 동일한 로직을 적용하기 용이해집니다.

2단계: Select2 초기화 및 '이전 값' 저장을 위한 준비

이제 <script> 태그 안에 JavaScript 코드를 작성합니다. Select2를 초기화하고, '이전 값'을 추적하기 위한 변수와 로직을 추가합니다.


$(document).ready(function() {
    // 1. 모든 manager-select 클래스에 Select2를 적용합니다.
    $('.manager-select').select2({
        placeholder: "담당자를 선택하세요",
        allowClear: true // 선택 해제 기능 활성화
    });

    // 2. 각 select 요소의 이전 선택값을 저장하기 위한 객체를 선언합니다.
    let previousValues = {};

    // 3. 드롭다운이 열릴 때('select2:opening'), 현재 값을 '이전 값'으로 저장합니다.
    $('.manager-select').on('select2:opening', function (e) {
        const selectId = $(this).attr('id');
        previousValues[selectId] = $(this).val();
    });

    // ... 4. 여기에 메인 로직이 들어갑니다 ...
});

여기서 아주 영리한 트릭을 사용했습니다. 바로 select2:opening 이벤트입니다. 이 이벤트는 사용자가 드롭다운을 클릭하여 목록을 열기 직전에 발생합니다. 바로 이 시점에 현재 선택된 값을 previousValues 객체에 저장해두면, 나중에 사용자가 새로운 값을 선택하여 select2:select 이벤트가 발생했을 때, 방금 저장해 둔 값을 '변경 전 값'으로 완벽하게 활용할 수 있습니다.

3단계: 메인 로직 작성 (`select2:select` 이벤트 핸들러)

드디어 이 가이드의 심장부입니다. 사용자가 새로운 옵션을 선택했을 때 다른 드롭다운의 옵션을 제어하는 로직을 작성합니다.


// 4. 사용자가 새로운 옵션을 선택했을 때 실행될 메인 로직
$('.manager-select').on('select2:select', function(e) {
    // 현재 이벤트가 발생한 select 요소
    const currentSelect = $(this);
    // 현재 select의 ID (예: 'primary-manager')
    const selectId = currentSelect.attr('id');
    // 새로 선택된 값 (예: 'user2')
    const newValue = currentSelect.val();
    // 이전에 선택됐었던 값 (예: 'user1'), select2:opening 이벤트에서 저장됨
    const oldValue = previousValues[selectId];

    // 현재 select를 제외한 다른 모든 manager-select를 순회
    $('.manager-select').not(currentSelect).each(function() {
        const otherSelect = $(this);

        // [핵심 로직 1] 새로 선택된 값(newValue)에 해당하는 옵션을 찾아 비활성화
        if (newValue) {
            otherSelect.find('option[value="' + newValue + '"]').prop('disabled', true);
        }

        // [핵심 로직 2] 이전에 선택됐던 값(oldValue)에 해당하는 옵션은 다시 활성화
        if (oldValue) {
            otherSelect.find('option[value="' + oldValue + '"]').prop('disabled', false);
        }
    });

    // [중요!] DOM의 변경사항을 Select2 UI에 즉시 반영하도록 강제 트리거
    $('.manager-select').trigger('change.select2');
});

코드 상세 해설:

  1. 이벤트 핸들링: 모든 .manager-select 요소에 select2:select 이벤트 리스너를 바인딩합니다. 어떤 드롭다운에서든 선택이 일어나면 이 함수가 실행됩니다.
  2. 컨텍스트 변수 선언: currentSelect, selectId, newValue, oldValue 변수를 선언하여 코드의 가독성을 높이고 현재 상태를 명확히 합니다.
  3. 타겟 루프: .not(currentSelect)를 사용하여 현재 이벤트가 발생한 요소를 제외한 나머지 모든 연동 드롭다운을 대상으로 로직을 실행합니다. 이 덕분에 코드가 3개 이상의 드롭다운에도 자동으로 확장 적용됩니다.
  4. 옵션 비활성화: otherSelect.find('option[value="..."]')로 다른 드롭다운 내부의 특정 옵션을 정확히 찾아냅니다. 그리고 .prop('disabled', true)를 사용하여 해당 옵션을 비활성화합니다.
    jQuery Tipdisabled, checked, selected와 같은 boolean 속성을 제어할 때는 .attr() 대신 .prop()을 사용하는 것이 더 정확하고 권장되는 방법입니다.
  5. 옵션 재활성화: oldValue가 존재할 경우, 즉 이전에 선택된 값이 있었을 경우, 동일한 방식으로 해당 옵션을 찾아 .prop('disabled', false)로 설정하여 다시 선택 가능하게 만듭니다. 이것이 UX를 완성하는 중요한 디테일입니다.
  6. UI 갱신 트리거: 이 부분이 가장 중요하고 많이 놓치는 부분입니다. JavaScript로 <option>disabled 속성을 변경했더라도, Select2는 이 변경을 자동으로 인지하지 못할 수 있습니다. .trigger('change.select2')를 호출하여 Select2에게 "내부 데이터가 변경되었으니, 너의 UI를 다시 그려라"고 알려주는 신호를 보내야 합니다. 이 코드가 없으면 비활성화 상태가 다음 번에 드롭다운을 열 때까지 반영되지 않는 문제가 발생할 수 있습니다.

4. 최종 보스: 심화 과정 및 프로덕션 레벨 코드

위의 코드는 기본적인 시나리오를 완벽하게 처리합니다. 하지만 실제 프로젝트에서는 더 복잡한 요구사항과 엣지 케이스를 마주하게 됩니다. 진정한 전문가가 되기 위해 몇 가지 시나리오를 더 다뤄보겠습니다.

시나리오 1: '선택 해제' 처리하기 (`allowClear` 옵션)

우리는 초기화 시 allowClear: true 옵션을 주어 사용자가 선택을 해제할 수 있는 'x' 버튼을 제공했습니다. 사용자가 선택을 해제했을 때도 비활성화되었던 옵션이 다른 곳에서 다시 활성화되어야 합니다. 이를 위해 select2:unselect 이벤트를 처리해야 합니다.


// 선택 해제 시 이벤트 핸들러 추가
$('.manager-select').on('select2:unselect', function (e) {
    // 선택 해제된 값은 이벤트 파라미터에서 얻어옵니다.
    const unselectedVal = e.params.data.id; 

    if (unselectedVal) {
        // 다른 모든 select에서 해당 값을 다시 활성화
        $('.manager-select').not(this)
            .find('option[value="' + unselectedVal + '"]')
            .prop('disabled', false);

        // UI 갱신 트리거
        $('.manager-select').trigger('change.select2');
    }
});

select2:unselect 이벤트 핸들러는 e.params.data.id를 통해 해제된 옵션의 값을 쉽게 얻을 수 있습니다. 이 값을 가지고 다른 모든 드롭다운에서 해당 옵션을 찾아 disabled 속성을 제거해주면 완벽하게 동작합니다.

시나리오 2: 페이지 로드 시 '초기값' 처리하기

사용자가 처음부터 데이터를 가지고 있는 수정 페이지에 진입했다고 가정해 봅시다. 페이지가 로드될 때 '주 담당자'에는 '김철수'가, '부 담당자'에는 '이영희'가 이미 선택된 상태로 폼이 그려져야 합니다. 이때는 페이지 로드 직후에 한 번 비활성화 로직을 실행해주어야 초기 상태의 정합성이 맞게 됩니다.

이를 위해 우리는 기존 로직들을 재사용 가능한 함수로 만드는 리팩토링을 진행할 수 있습니다.

최종 완성 코드: 리팩토링을 통한 우아한 마무리

지금까지 논의된 모든 내용(선택, 변경, 해제, 초기값 처리)을 종합하여, 가장 안정적이고 확장 가능한 최종 코드를 아래에 제시합니다. 이 코드는 개별 이벤트 핸들러를 하나로 통합하고, 모든 상태 동기화를 담당하는 중앙 함수를 만들어 코드의 중복을 제거하고 유지보수성을 극대화했습니다.


<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Select2 연동 최종 완성 예제</title>
    <!-- 라이브러리 로드 부분은 위와 동일 -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
    <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
    <style>
        body { font-family: sans-serif; padding: 2rem; max-width: 600px; margin: auto; }
        .form-group { margin-bottom: 1.5rem; }
        label { display: block; margin-bottom: 0.5rem; font-weight: bold; }
        .select-control { width: 100%; }
        .info-box { background-color: #f0f8ff; border-left: 5px solid #1e90ff; padding: 15px; margin-top: 20px; }
    </style>
</head>
<body>

    <h1>프로젝트 팀원 배정 (3개 연동)</h1>
    <p>각 역할에 중복되지 않는 팀원을 배정하세요.</p>

    <div class="form-group">
        <label for="role-pm">프로젝트 매니저(PM)</label>
        <select id="role-pm" class="team-select select-control">
            <option value="">팀원 선택</option>
            <option value="user1" selected>김철수</option> <!-- 초기 선택값 -->
            <option value="user2">이영희</option>
            <option value="user3">박민준</option>
            <option value="user4">최지우</option>
            <option value="user5">정다혜</option>
        </select>
    </div>

    <div class="form-group">
        <label for="role-dev">메인 개발자</label>
        <select id="role-dev" class="team-select select-control">
            <option value="">팀원 선택</option>
            <option value="user1">김철수</option>
            <option value="user2">이영희</option>
            <option value="user3">박민준</option>
            <option value="user4">최지우</option>
            <option value="user5">정다혜</option>
        </select>
    </div>

    <div class="form-group">
        <label for="role-design">UI/UX 디자이너</label>
        <select id="role-design" class="team-select select-control">
            <option value="">팀원 선택</option>
            <option value="user1">김철수</option>
            <option value="user2">이영희</option>
            <option value="user3">박민준</option>
            <option value="user4" selected>최지우</option> <!-- 초기 선택값 -->
            <option value="user5">정다혜</option>
        </select>
    </div>

<script>
$(document).ready(function() {

    // 모든 team-select에 Select2 적용
    $('.team-select').select2({
        placeholder: '팀원을 선택하세요',
        allowClear: true
    });

    /**
     * 모든 Select2의 옵션 상태를 동기화하는 중앙 제어 함수
     */
    function syncTeamSelects() {
        const selectedValues = [];
        
        // 1. 현재 선택된 모든 값들을 수집 (빈 값 제외)
        $('.team-select').each(function() {
            const selectedValue = $(this).val();
            if (selectedValue) {
                selectedValues.push(selectedValue);
            }
        });

        // 2. 모든 select를 다시 순회하며 옵션 상태를 업데이트
        $('.team-select').each(function() {
            const currentSelect = $(this);
            const currentValue = currentSelect.val();

            // 현재 select의 모든 옵션을 순회
            currentSelect.find('option').each(function() {
                const option = $(this);
                const optionValue = option.val();

                // 옵션 값이 유효하고, 다른 select에서 이미 선택된 값이며, 현재 select의 값과는 다른 경우
                if (optionValue && selectedValues.includes(optionValue) && optionValue !== currentValue) {
                    option.prop('disabled', true); // 비활성화
                } else {
                    option.prop('disabled', false); // 활성화
                }
            });
        });
        
        // 3. 변경사항 UI 반영을 위해 Select2에 갱신 신호 전송
        $('.team-select').trigger('change.select2');
    }

    // 값 변경(선택, 해제 모두 포함)이 일어날 때마다 동기화 함수 호출
    $('.team-select').on('change', function() {
        syncTeamSelects();
    });

    // 페이지가 처음 로드될 때, 초기 선택값에 대한 동기화를 위해 함수를 한 번 실행
    syncTeamSelects();

});
</script>

    <div class="info-box">
        <h4>핵심 요약</h4>
        <p>이 최종 코드에서는 개별 이벤트(`select2:select`, `select2:unselect`)를 모두 처리하는 대신, 상태 변경을 포괄적으로 감지하는 표준 <strong>`change` 이벤트</strong>를 사용했습니다. 그리고 모든 동기화 로직을 <strong>`syncTeamSelects()`</strong>라는 하나의 함수로 통합하여 구조를 더욱 단순하고 명확하게 만들었습니다. 이 방식은 3개, 4개, 혹은 그 이상의 Select2가 연동되는 복잡한 경우에도 매우 효과적이며 유지보수가 용이한 최상의 아키텍처입니다.</p>
    </div>

</body>
</html>

결론적으로, 연동형 Select2의 옵션 비활성화 기능은 라이브러리의 내부 데이터 구조를 직접 건드리는 위험하고 비효율적인 방식이 아니라, Select2가 생성한 표준 DOM(<option>) 요소를 직접 제어하고, Select2 전용 이벤트(또는 표준 `change` 이벤트)를 활용하여 그 제어 시점을 정확히 포착하는 것이 핵심입니다. 이 원리를 이해하고 최종 코드와 같은 견고한 패턴을 적용한다면, 어떤 복잡한 폼 시나리오에서도 사용자 친화적이고 버그 없는 안정적인 UI를 자신 있게 구축할 수 있을 것입니다.

Post a Comment