jQuery / 체크박스 모두 선택, 모두 선택 해제 만들기

체크박스를 모두 선택하거나 해제하려면 대상 집합에 .prop("checked", true) 또는 .prop("checked", false)를 적용합니다. 전체 선택용 체크박스와 개별 항목을 함께 사용한다면 개별 항목의 변경 때도 전체 선택 상태를 다시 계산해야 합니다. 일부만 선택됐을 때는 indeterminate 속성으로 부분 선택을 표시합니다.

전체 선택의 대상과 상태 정하기

.prop()은 체크박스의 현재 checked 상태를 읽고 바꿀 때 사용합니다. HTML checked 속성을 다루는 .attr()은 현재 사용자의 선택을 관리하는 수단으로 삼지 않습니다. true·false는 문자열이 아닌 불리언 값으로 전달합니다.

이 예제의 전체 선택 범위는 #items 안의 item 클래스가 있는 체크박스 중 활성 항목입니다. 전체 선택용 #all은 이 집합에 들어가지 않습니다. disabled 항목과 비활성 fieldset에서 상속받아 선택할 수 없는 항목은 :enabled로 제외합니다. CSS로 숨겼다는 이유만으로 제외하지는 않습니다.

활성 항목이 한 개 이상이고 전부 선택됐으면 전체 선택을 체크합니다. 일부만 선택됐으면 checked=false, indeterminate=true입니다. 아무것도 선택하지 않았으면 두 값 모두 false이며, 대상이 0개면 전체 선택도 비활성화합니다. 0개 중 0개가 같다는 이유로 ‘전부 선택’ 처리하지 않습니다.

한 파일로 실행하는 전체 예제

전체 코드를 UTF-8의 checkbox-demo.html로 저장한 뒤 브라우저로 엽니다. 인터넷으로 공식 jQuery 파일 4.0.0을 읽습니다. 서버나 패키지 설치 없이 실행할 수 있으며, Windows Edge에서 jQuery 3.7.1·4.0.0으로 주요 동작을 확인했습니다.

Alpha는 선택, Beta는 미선택으로 시작합니다. Locked는 선택돼 있어도 비활성 항목이라 전체 선택과 개수 계산에서 제외합니다. 전체 선택 컨트롤에는 name을 주지 않았으므로 개별 선택값처럼 폼에 전송되지 않습니다.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Select all eligible checkboxes</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    body { font: 16px/1.6 sans-serif; max-width: 760px; margin: 20px; }
    label { display: block; padding: 6px; }
    button { font: inherit; margin: 4px; }
    fieldset { margin: 12px 0; }
    pre { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <form id="choices">
    <label><input type="checkbox" id="all"> Select all available items</label>
    <fieldset id="items"><legend>Items</legend></fieldset>
    <button type="button" id="add">Add item</button>
    <button type="button" id="clear">Remove items</button>
    <button type="button" id="reset">Reset example</button>
    <button type="submit">Inspect submission</button>
  </form>
  <pre id="result" aria-live="polite"></pre>
  <pre id="submission"></pre>
  <script>
    $(function () {
      const $form = $("#choices"), $all = $("#all");
      let next = 1, itemChanges = 0;
      function available() { return $("#items input.item:checkbox:enabled"); }
      function sync() {
        const $items = available(), total = $items.length;
        const count = $items.filter(":checked").length;
        $all.prop({ checked: total > 0 && count === total,
          indeterminate: count > 0 && count < total, disabled: total === 0 });
        const values = $items.filter(":checked").map(function () { return this.value; }).get();
        $("#result").text(`selected=${count}/${total}\nmaster checked=${$all.prop("checked")}\nmaster mixed=${$all.prop("indeterminate")}\nitem changes=${itemChanges}\nvalues=${values.join(",")}`);
      }
      function addItem(value, text, checked, disabled) {
        const label = document.createElement("label"), input = document.createElement("input");
        input.type = "checkbox"; input.name = "item"; input.className = "item";
        input.value = value; input.checked = checked; input.disabled = disabled;
        label.append(input, document.createTextNode(" " + text));
        document.getElementById("items").append(label);
      }
      $all.on("change.selectAllLesson", function () {
        available().prop("checked", this.checked);
        sync(); // Setting checked does not emit change on each item.
      });
      $("#items").on("change.selectAllLesson", "input.item:checkbox", function () {
        itemChanges++; sync();
      });
      $("#add").on("click", function () {
        addItem("extra" + next, "Extra " + next, false, false); next++; sync();
      });
      $("#clear").on("click", function () { $("#items label").remove(); sync(); });
      $("#reset").on("click", function () {
        $("#items label").remove(); $("#items").prop("disabled", false);
        addItem("alpha", "Alpha", true, false);
        addItem("beta", "Beta", false, false);
        addItem("locked", "Locked (unavailable)", true, true);
        next = 1; itemChanges = 0; $("#submission").empty(); sync();
      });
      $form.on("submit", function (event) {
        event.preventDefault();
        const params = new URLSearchParams(new FormData(this));
        $("#submission").text(params.toString() || "(no successful controls)");
      });
      $("#reset").trigger("click");
    });
  </script>
</body>
</html>

초기 결과는 활성 2개 중 1개 선택입니다. 전체 선택 상자는 체크 대신 부분 선택 모양으로 표시됩니다.

selected=1/2
master checked=false
master mixed=true
item changes=0
values=alpha
  1. Select all available items를 누르면 selected=2/2, master checked=true, master mixed=false가 됩니다. 한 번 더 누르면 0/2이며 Locked는 계속 선택된 상태입니다.
  2. Beta만 선택하면 1/2와 master mixed=true입니다. 이어 Alpha도 선택하면 전체 선택이 체크됩니다. 레이블을 누르거나 입력에 포커스를 두고 Space 키로도 바꿀 수 있습니다.
  3. 전부 선택한 상태에서 Add item을 누르면 미선택 Extra 1이 추가돼 2/3과 부분 선택으로 바뀝니다. Extra 1을 선택하면 다시 전체 선택입니다.
  4. Remove items는 모든 항목을 지워 0/0을 만들고 전체 선택을 비활성화합니다. Add item으로 새 항목을 추가하면 전체 선택을 다시 사용할 수 있습니다.
  5. Alpha·Beta를 선택하고 Inspect submission을 누르면 item=alpha&item=beta입니다. Locked는 disabled여서 전송 대상에서 빠집니다. 활성 선택값이 없으면 (no successful controls)로 표시합니다.
  6. Reset example은 초기 세 항목을 새로 만들고 출력과 카운터를 초기화합니다. 서버로 보내지 않으며, 이전에 추가한 항목과 선택 변경을 되돌립니다.

개별 변경과 동적 항목을 동기화하기

전체 선택의 change 이벤트에서는 현재 this.checked를 활성 항목 모두에 적용한 뒤 sync()를 한 번 호출합니다. .prop()으로 값을 대입하는 것만으로 각 항목의 change 이벤트가 발생하지 않습니다. 따라서 전체 선택을 눌러도 item changes는 늘지 않으며, 사용자가 개별 항목을 바꿀 때만 증가합니다.

개별 항목 처리는 유지되는 #items에 위임했습니다. 나중에 추가한 체크박스도 같은 처리기를 쓰므로 항목마다 이벤트를 새로 붙이지 않습니다. available()은 호출할 때마다 현재 DOM을 조회합니다. 처음 선택한 집합을 계속 재사용하면 추가·삭제한 항목이 계산에 반영되지 않을 수 있습니다.

항목을 추가·삭제하는 코드 끝에서도 sync()를 호출합니다. 위임 이벤트가 DOM 변경 자체를 감시하는 것은 아닙니다. 다른 코드가 항목의 disabled나 checked를 직접 변경한다면 그 코드에서도 같은 동기화 함수를 호출하도록 연결해야 합니다. 외부 변경이 자동으로 감지된다고 가정하지 않습니다.

개별 변경에 결제액 계산처럼 추가 처리가 연결돼 있다면 전체 선택에서 그 작업도 명시적으로 실행하도록 설계합니다. 필요한 경우 .trigger("change")를 선택적으로 쓸 수 있지만, 항목마다 전체 개수를 다시 계산하거나 중복 요청을 보내지 않게 합니다. 이 예제는 일괄 변경 후 한 번 동기화하는 방식입니다.

부분 선택·초기화·전송에서 주의할 점

indeterminate는 JavaScript로 설정하는 표시 상태이며 HTML 속성으로 초기화하지 않습니다. checked와 독립적이고 별도의 전송값도 만들지 않습니다. sync()는 두 속성을 모두 지정해 과거 부분 선택 표시가 남지 않게 합니다. 실제 label과 기본 checkbox를 사용하므로 클릭뿐 아니라 기본 키보드 조작도 유지합니다.

Inspect submission은 FormData로 전송될 항목을 읽어 화면에 표시할 뿐 실제 네트워크 요청을 하지 않습니다. checked인 활성 항목만 이름과 값으로 포함됩니다. 비활성 항목을 업무상 반드시 보내야 한다면 브라우저가 자동 전송한다고 생각하지 말고 서버 검증과 별도의 데이터 설계를 검토합니다. 선택 상태를 신뢰해 서버 권한 검사를 생략해서도 안 됩니다.

Reset example은 실습 상태를 다시 만드는 사용자 정의 버튼입니다. 실제 form의 네이티브 reset은 각 항목을 기본값으로 되돌리는 별도 동작이며, 그 동작이 끝난 뒤 전체 선택 표시도 다시 계산해야 합니다. 이 예제 버튼을 type="reset"으로만 바꾸면 동적 항목 정리와 카운터 초기화까지 같은 방식으로 처리되지는 않습니다.

이 방식은 현재 문서에 존재하는 항목을 선택합니다. 서버 페이지네이션으로 아직 불러오지 않은 행까지 전체 선택하는 기능은 별도 서버 상태가 필요합니다. 화면에 그룹이 여러 개라면 각 그룹의 대상과 전체 선택 컨트롤을 분리하고, 필터를 사용한다면 ‘보이는 항목만’인지 ‘그룹의 모든 활성 항목’인지 먼저 정한 뒤 선택자와 설명을 일치시킵니다.