본문 바로가기
빌드노트 검색

빌드노트

체크박스를 눌렀는데 checked 속성은 그대로다

클릭 뒤에도 남아 있는 checked 속성으로 현재 선택을 판단하면 틀릴 수 있다. 실제 클릭과 reset을 비교하고, checked로 현재값을 읽는 방법과 defaultChecked로 초기화 기준을 정하는 시점을 설명한다.

체크박스를 눌러 해제했는데 getAttribute("checked")는 그대로라면, 현재 선택을 읽는 위치부터 바꿔야 한다. 지금 체크되어 있는지는 input.checked로 읽는다. HTML의 checked 속성과 이를 반영하는 defaultChecked는 기본 선택 상태를 나타낸다. 사용자의 클릭이 이 기본값까지 고치지는 않는다.

MDN의 checked 설명 (새 탭에서 열림)도 현재 상태인 프로퍼티와 기본 상태인 속성을 구분한다. 개발자 도구의 Elements에 checked가 남아 있어도 화면의 체크가 해제된 것은 모순이 아니다. 그 속성을 조건으로 저장할 값을 정하면 사용자 선택과 다른 결과를 만들 수 있다.

같은 폼에서 클릭 전후를 읽었다

이 글을 위해 처음에 선택된 메일 알림과 선택되지 않은 문자 알림을 가진 폼을 만들었다. Windows 11, headless Chromium 147.0.7727.15의 새 임시 컨텍스트에서 두 입력을 실제 포인터 클릭으로 바꾸고 비교했다. 기존 서비스나 저장된 브라우저 상태는 사용하지 않았다.

직접 확인하려면 다음 내용을 UTF-8의 새 demo.html로 저장해 브라우저에서 연다. 서버는 필요 없다. 초기화 버튼만 있으며 외부로 폼을 제출하지 않는다.

<!doctype html>
<meta charset="utf-8">
<title>체크박스 현재값과 기본값</title>
<form id="prefs">
  <label><input id="mail" type="checkbox" name="mail" value="yes" checked>메일 알림</label>
  <label><input id="sms" type="checkbox" name="sms" value="yes">문자 알림</label>
  <button type="reset">초기화</button>
</form>

개발자 도구 Console에서 아래 코드를 한 번 실행한다. 첫 줄 출력의 각 행은 [id, checked, checked 속성값, defaultChecked] 순서다. 두 번째 줄은 그 순간 새로 만든 FormData의 항목이다.

const form = document.querySelector("#prefs");
const boxes = [...form.querySelectorAll('input[type="checkbox"]')];
function read() {
  console.log(JSON.stringify(boxes.map(x =>
    [x.id, x.checked, x.getAttribute("checked"), x.defaultChecked])));
  console.log(JSON.stringify([...new FormData(form)]));
}
read();

이후 메일과 문자 체크박스를 각각 한 번씩 누르고 read()를 실행한다. 다음으로 초기화 버튼을 누르고 다시 read()를 실행한다. 아래는 실제 관찰값에 단계 이름을 붙인 것이다.

처음
[["mail",true,"",true],["sms",false,null,false]]
[["mail","yes"]]
두 입력을 클릭한 뒤
[["mail",false,"",true],["sms",true,null,false]]
[["sms","yes"]]
초기화 버튼을 누른 뒤
[["mail",true,"",true],["sms",false,null,false]]
[["mail","yes"]]

클릭 뒤 메일의 checked는 false, 문자는 true로 바뀌었다. 하지만 속성값과 defaultChecked는 두 입력 모두 그대로였다. 새 FormData에는 현재 선택된 문자만 들어갔다. 이 폼에서는 두 입력 모두 name이 있고 활성 상태이므로, 기본값과 현재값 중 어느 쪽을 따라가는지 비교할 수 있다. 서버로 전송한 결과는 아니다.

출력의 빈 문자열 ""은 값 없이 쓴 checked 속성이 있다는 뜻이고, null은 속성이 없다는 뜻이다. 빈 문자열도 조건문에서는 거짓으로 변환되므로 if (input.getAttribute("checked"))는 기본값 확인에도 부적절하다. 기본 선택 여부는 defaultChecked, 속성의 존재 자체는 hasAttribute("checked"), 현재 선택 여부는 checked로 읽는다.

reset은 되돌릴 기본값을 따로 읽는다

위 실험의 초기화 버튼은 메일 선택, 문자 미선택으로 돌아갔다. 클릭이 바꾼 현재값을 버리고 기본 선택 상태를 적용한 것이다. HTML 표준의 input 초기화 절차 (새 탭에서 열림)는 reset 때 checked 속성의 유무로 선택 상태를 다시 정한다고 규정한다.

스크립트로 현재 체크를 바꾸려는 경우에도 프로퍼티를 쓴다. 예를 들어 앞 예제의 문자 알림을 선택하려면 다음 한 줄이면 된다.

document.querySelector("#sms").checked = true;

이 대입으로 문자 알림의 기본값은 바뀌지 않는다. 별도 새 문서에서 메일은 checked = false, 문자는 checked = true로 바꾼 뒤 form.reset()을 호출했다. 클릭 실험과 마찬가지로 메일 선택, 문자 미선택으로 돌아왔다. 따라서 화면의 선택만 바꿀 것인지, 다음 초기화의 기준도 바꿀 것인지 먼저 정해야 한다.

예를 들어 저장이 성공한 시점의 선택을 이후 취소 버튼의 기준으로 삼기로 했다면, 그때의 현재값을 defaultChecked에 반영할 수 있다. 위 예제가 초기화된 상태에서 두 체크박스를 다시 한 번씩 눌러 메일 해제, 문자 선택으로 만든 다음 실행한다. 아래 코드는 서버 저장을 수행하지 않는다.

for (const box of boxes) {
  box.defaultChecked = box.checked;
}
read();

실험에서는 메일의 checked 속성이 사라지고 문자의 속성이 생겼다. 기본값도 메일 false, 문자 true가 됐다. 두 입력을 다시 눌러 선택을 뒤집은 다음 form.reset()을 호출하자, 이번에는 메일 해제, 문자 선택으로 돌아왔다. reset의 기준은 바꿀 수 있다. 최초 HTML 원문을 별도로 보관해 복원하는 동작은 아니다. MDN의 defaultChecked 설명 (새 탭에서 열림)은 이 프로퍼티가 checked 속성을 반영한다고 설명한다.

다만 사용자가 편집할 때마다 기본값을 덮어쓰면 취소할 기준도 계속 바뀐다. 처음 연 값으로 취소해야 하는 폼이라면 기본값을 유지하고, 저장 후 값으로 취소해야 하는 폼이라면 저장 성공을 확인한 시점에 갱신한다. 저장을 기다리는 동안 추가 편집을 허용한다면, 성공한 요청에 담긴 값과 그때 화면의 값이 같은지도 구분해야 한다.

속성 변경이 처음에는 통할 수도 있다

setAttribute("checked", "")로 체크를 켜 본 적이 있다면 이 구분이 낯설 수 있다. 새 입력에서는 속성 변경이 현재 선택에도 반영될 수 있기 때문이다. HTML 표준은 사용자가 선택을 바꾸거나 스크립트가 checked에 대입한 뒤에는 내부의 dirty checkedness 표시를 켠다. 이 상태에서는 속성을 고쳐도 현재 선택을 덮어쓰지 않는다. 이 표시와 reset 동작은 HTML 표준의 checkedness 규칙 (새 탭에서 열림)에 정의되어 있다.

별도 새 컨텍스트에서 문자 입력에 클릭 전 속성을 추가하자 현재값도 true가 됐다. 이어 클릭으로 해제하고 속성을 제거했다가 다시 추가하자, 현재값은 false인데 기본값은 true인 상태가 됐다. 그 뒤 reset은 문자 입력을 선택했다. 속성을 써서 현재 체크를 조작하면 사용자 편집 여부에 따라 동작이 달라지는 이유다.

자기 코드에서는 선택을 읽는 곳의 getAttribute("checked")를 checked로 바꾸고, 선택을 바꾸는 곳의 setAttribute도 현재값 변경인지 기본값 변경인지에 맞춰 고른다. defaultChecked가 의도와 다르게 달라졌다면 이 프로퍼티에 대입하거나 checked 속성을 추가·제거하는 코드부터 찾는다. 클릭 뒤 속성과 기본값까지 바뀌는 화면이라면, 이번 합성 폼에는 없는 이벤트 핸들러나 요소 재생성도 확인할 대상이다.

직접 실행한 범위는 Chromium 한 버전의 네이티브 체크박스, 기본값 변경, reset과 메모리 안의 FormData 비교다. Firefox·Safari·모바일, 프레임워크가 관리하는 체크박스, 실제 서버 저장과 비동기 저장 중 추가 편집은 검증하지 않았다.

← 전체 글 보기