jQuery / 동적 셀렉트(select) 만드는 방법
동적 셀렉트는 앞의 select에서 고른 값에 따라 뒤의 select의 option 목록을 바꾸는 구성입니다. jQuery에서는 상위 select의 change를 감지하고, 해당 데이터로 하위 목록을 비운 뒤 다시 채우면 됩니다. 목록만 바꾸지 말고 이전 하위 선택값도 초기화해야 서로 맞지 않는 값이 남지 않습니다.
데이터와 두 select의 역할 정하기
예제는 Group A·B와 각각에 속한 Item 목록을 메모리에 준비합니다. 서버 요청이나 별도 플러그인은 사용하지 않습니다. 그룹의 value가 a면 a 그룹 배열을 읽습니다. 표시명 label과 실제 value를 분리해 화면 문구를 바꾸더라도 제출용 식별자는 유지할 수 있게 했습니다.
각 select에는 연결된 label과 고유 id를, 폼에서 보낼 값에는 name을 둡니다. select 요소의 required는 빈 value를 고르지 않도록 검사하는 데 쓰며, 하위 목록이 없으면 하위 select를 disabled로 둡니다. 비활성화된 폼 항목은 제출 데이터에서 빠지므로 서버에서는 값이 없거나 조합이 잘못된 경우도 별도로 확인해야 합니다.
초기 상태의 빈 option은 실수로 첫 항목을 확정하지 않도록 하는 안내입니다. 빈 값을 실제 유효한 항목의 식별자로 쓰면 안내와 구분할 수 없으므로, 선택 가능한 실제 데이터에는 빈 값이 아닌 식별자를 사용합니다.
그룹에 따라 하위 목록을 바꾸는 전체 예제
UTF-8의 dependent-select-demo.html로 저장하고 엽니다. 인터넷에서 jQuery 전체 파일 4.0.0을 읽으며 서버나 빌드는 필요 없습니다. Windows Edge에서 4.0.0과 3.7.1 각각으로 키보드 선택, 값 초기화, 빈·알 수 없는 그룹, 데이터 확인과 반복 초기화를 검증했습니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dependent select controls</title>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<style>
body { font: 16px/1.6 sans-serif; max-width: 720px; margin: 20px; }
label { display: block; margin-top: 12px; }
select, button { font: inherit; max-width: 100%; margin: 6px 0; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<form id="chooser">
<label for="group">Group</label>
<select id="group" name="group" required>
<option value="">Choose a group</option>
<option value="a">Group A</option>
<option value="b">Group B</option>
<option value="empty">Group without items</option>
</select>
<label for="item">Item</label>
<select id="item" name="item" required disabled><option value="">Choose a group first</option></select>
<button type="submit">Inspect selection</button>
<button type="button" id="restore">Reset</button>
</form>
<pre id="result" aria-live="polite"></pre>
<p id="submission" role="status">Nothing inspected.</p>
<script>
jQuery(function ($) {
const choices = new Map([
["a", [{value: "a1", label: "A-1"}, {value: "a2", label: "A < B & C"}]],
["b", [{value: "b1", label: "B-1"}, {value: "b2", label: "B-2"}]],
["empty", []]
]);
const $group = $("#group"), $item = $("#item"), form = document.querySelector("#chooser");
let changes = 0;
function render() {
$("#result").text("group=" + ($group.val() || "(none)") + "\nitem=" + ($item.val() || "(none)") +
"\noptions=" + Math.max(0, $item[0].options.length - 1) + "; disabled=" + $item.prop("disabled") + "\nchanges=" + changes);
}
function rebuildItem() {
const items = choices.get($group.val()) || [];
const prompt = items.length ? "Choose an item" : ($group.val() ? "No available items" : "Choose a group first");
$item.empty().append(new Option(prompt, ""));
for (const item of items) $item.append(new Option(item.label, item.value));
$item.val("").prop("disabled", items.length === 0);
$("#submission").text("Nothing inspected."); render();
}
$group.on("change", function () { changes++; rebuildItem(); });
$item.on("change", function () { changes++; $("#submission").text("Nothing inspected."); render(); });
$(form).on("submit", function (event) {
event.preventDefault();
const items = choices.get($group.val()) || [];
if (!items.some(item => item.value === $item.val())) {
$("#submission").text("Choose a group with an available item."); return;
}
$("#submission").text("Valid pair: " + JSON.stringify(Object.fromEntries(new FormData(form))));
});
$("#restore").on("click", function () { form.reset(); changes = 0; rebuildItem(); });
rebuildItem();
});
</script>
</body>
</html>
처음에는 그룹과 항목을 선택하지 않았고, 안내를 제외한 실제 option은 0개이며 하위 선택은 비활성화되어 있습니다.
group=(none)
item=(none)
options=0; disabled=true
changes=0
- Group A를 선택합니다. 하위에는 Choose an item, A-1, A < B & C가 표시됩니다. 실제 항목 수는 2입니다.
- A-1을 골라 Inspect selection을 누릅니다. Valid pair: {"group":"a","item":"a1"}처럼 식별자 쌍이 표시됩니다. 외부로 전송하지는 않습니다.
- Group B로 바꿉니다. 목록은 B-1·B-2가 되고 하위 값은 빈 안내로 돌아갑니다. a1이 남지 않아야 합니다.
- 하위를 고르지 않고 Inspect selection을 누르면 required 검사가 제출 이벤트보다 먼저 막습니다. 안내 문구는 브라우저와 언어에 따라 다를 수 있습니다.
- Group without items를 고르면 하위가 비활성화되고 No available items가 표시됩니다. Inspect selection은 유효한 조합을 선택하라는 문구를 보여줍니다.
- Reset으로 초기 상태를 복원합니다. 그룹·항목을 키보드 방향키로 고르는 경우에도 change 처리와 결과가 일치하는지 확인합니다.
changes는 이 예제에서 change 핸들러가 실행된 횟수입니다. option 수, 클릭 수나 실제 사용자 수가 아닙니다. 초기 목록 생성과 Reset에서는 0으로 두며, 상위를 코드로 변경해 change를 발생시켜도 증가합니다.
목록 재생성과 change를 이해하기
.on("change", ...)은 이 예제에서 이미 존재하는 두 select에 연결합니다. 상위에서 값을 읽고 Map에서 배열을 찾습니다. 없는 키는 빈 배열로 처리해 정의되지 않은 값을 반복하려는 오류를 막습니다. 하위 .empty() 후 안내와 새 항목을 추가하고, .val("")로 이전 선택을 지웁니다. 이 순서 덕분에 여러 번 A·B를 바꿔도 option이 누적되지 않습니다.
항목은 new Option(label, value)으로 만듭니다. 문자열을 option HTML에 이어 붙이지 않으므로 A < B & C 같은 표시명도 문자로 들어갑니다. 표시명과 value를 같은 것으로 취급하지 않고, 선택 후 읽는 값은 실제 식별자인 a1·a2 등입니다.
.val()로 상위 값을 설정하는 것만으로 change가 자동 발생하지는 않습니다. 같은 페이지의 다른 기능이 Group B를 선택하게 하려면 jQuery 초기화가 끝난 뒤 다음을 실행합니다. 하위 목록 재생성까지 같은 경로로 처리합니다.
$("#group").val("b").trigger("change");
직접 rebuildItem을 호출하는 방법도 있지만, 해당 함수는 예제의 ready 함수 내부에 있으므로 외부 전역 함수처럼 호출할 수 없습니다. 다른 코드와 연결할 때는 이벤트로 전달하거나 필요한 범위에 공개 API를 설계합니다. select 자체를 통째로 바꾸는 동적 화면이라면 기존 요소에 연결한 이벤트도 새 요소로 옮겨야 하며, 유지되는 부모에서 위임하는 방식도 검토합니다.
Reset은 form.reset()으로 기본 선택을 복원한 뒤 하위 목록을 다시 계산합니다. form.reset()만으로 JavaScript가 생성한 option 목록까지 원래대로 재구성되지는 않습니다. 버튼 id는 restore로 두어 reset이라는 이름이 폼의 reset 메서드를 가리는 충돌도 피했습니다.
유효한 조합과 실제 서비스 적용
Inspect selection은 기본 제출을 막고, 선택된 item 값이 현재 그룹 배열에 실제로 포함되는지 확인합니다. required는 빈 선택을 다루지만, 빈 그룹에서 하위가 disabled이면 그 항목은 기본 검사를 받지 않습니다. 그래서 단순히 form이 유효하다는 것과 업무상 유효한 그룹·항목 쌍인 것을 구분했습니다.
예제의 FormData는 name이 있는 현재 폼 항목으로 구성합니다. 화면에서 고른 label 대신 value가 들어가며 disabled인 하위 항목은 빠집니다. 이 검사는 브라우저 안의 실습일 뿐, 실제 서버에 보낼 때는 허용되는 그룹과 항목의 관계를 서버 데이터로 다시 확인해야 합니다.
세 단계 선택으로 확장할 때는 첫 단계 변경 시 둘째뿐 아니라 셋째 목록·값도 함께 초기화합니다. 둘째가 유효한 상태가 된 뒤 셋째를 채워야 이전 선택이 남지 않습니다. 자주 쓰는 조합을 미리 선택하려면 상위 목록을 만든 다음 상위를 설정하고, 그 하위를 생성한 뒤 유효한 하위 값을 선택하는 순서를 지킵니다.
서버에서 비동기로 목록을 받는 구성은 별도 처리가 필요합니다. 요청 중에는 이전 값을 지우고 로딩·실패·빈 결과를 구분하며, A 요청 뒤 B로 바꿨을 때 늦게 온 A 응답이 B 목록을 덮지 않게 취소나 요청 식별을 사용합니다. 위 예제에는 서버 통신과 그 동시성 처리가 포함되어 있지 않습니다.
목록이 바뀌지 않으면 실제 id와 value, 로딩 순서와 Console 오류를 확인합니다. 초기 선택을 복원해도 잘못된 값이 남는다면 option 재생성과 .val()의 실행 순서를 확인합니다. label 연결, 모바일 기본 선택 UI와 긴 표시명도 점검하고 자동으로 포커스를 옮겨 다음 입력을 강요하지 않습니다.




