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

빌드노트

data-enabled="false"인데 조건문은 참으로 들어간다

data-enabled="false"는 불리언이 아니라 문자열이다. 새 브라우저 문서에서 false·true·빈 값·누락을 비교하고, "true" 명시 비교와 잘못된 값을 거절하는 파서 중 어떤 계약을 쓸지 정한다.

data-enabled="false"를 붙였는데 Boolean(el.dataset.enabled)가 참이라면, 읽은 값의 타입부터 확인해야 한다. dataset.enabled가 돌려주는 것은 불리언 false가 아니라 문자열 "false"다. 소문자 "true"일 때만 기능을 켜려면 el.dataset.enabled === "true"로 비교한다.

MDN의 dataset 문서 (새 탭에서 열림)는 data-* 속성을 문자열 맵인 DOMStringMap으로 제공한다고 설명한다. 속성 이름이 enabled라고 해서 불리언으로 변환하지 않는다. JavaScript의 ToBoolean 규칙 (새 탭에서 열림)에서 빈 문자열은 거짓이고, 비어 있지 않은 문자열은 참이다. "false"라는 단어의 뜻을 해석하는 과정은 없다.

false·true·빈 값·누락을 같이 읽기

이 글을 위해 Windows 11, headless Chromium 147.0.7727.15의 새 임시 컨텍스트에서 합성 요소를 비교했다. 네트워크를 끈 about:blank 문서에서 실행했으며, 실제 서비스 설정을 읽거나 변경하지 않았다.

직접 비교하려면 브라우저의 새 빈 탭에서 개발자 도구 Console을 열고 다음 코드를 한 번 실행한다. 화면에 붙이지 않은 요소를 메모리에 만들기 때문에 서버나 HTML 파일은 필요 없다. 각 행은 id | dataset 값 | typeof | Boolean(값) | 값 === "true" | 속성 존재 순서다.

const sample = document.createElement("section");
sample.innerHTML = `
  <div id="off" data-enabled="false"></div>
  <div id="on" data-enabled="true"></div>
  <div id="empty" data-enabled=""></div>
  <div id="missing"></div>
  <div id="bare" data-enabled></div>
`;
const rows = [...sample.children].map(el => {
  const value = el.dataset.enabled;
  return [el.id, value === undefined ? "<missing>" : JSON.stringify(value),
    typeof value, Boolean(value), value === "true", el.hasAttribute("data-enabled")];
});
console.log(rows.map(row => row.join(" | ")).join("\n"));

실제 브라우저에서 얻은 출력은 다음과 같다. <missing>은 누락된 값을 보이게 하려고 코드에서 붙인 표시다. 실제 값은 문자열 "<missing>"이 아니라 undefined다.

off | "false" | string | true | false | true
on | "true" | string | true | true | true
empty | "" | string | false | false | true
missing | <missing> | undefined | false | false | false
bare | "" | string | false | false | true

off와 on은 Boolean만으로 구분할 수 없었다. 둘 다 비어 있지 않은 문자열이기 때문이다. 별도로 if (value)의 분기 진입도 기록했는데, 이 두 행에서 모두 안으로 들어갔다. Boolean을 빼고 dataset.enabled를 그대로 조건에 넣어도 문제가 남는 이유다.

반대로 empty와 missing은 모두 거짓으로 변환되지만 원래 값과 타입은 다르다. 값을 생략해 data-enabled만 쓴 bare도 빈 문자열이었다. 속성이 있다는 사실과 값이 켜짐을 뜻한다는 판단은 따로 정해야 한다. HTML 표준의 dataset 정의 (새 탭에서 열림)는 속성값을 문자열로 읽는 인터페이스를 규정한다. 값 없이 적었다고 dataset.enabled가 불리언 true가 되는 것은 아니다.

켜짐으로 인정할 문자열을 정한다

계약이 “정확히 "true"만 켜짐이고 나머지는 꺼짐”이라면 앞의 명시 비교 한 줄이면 된다. 이 방식은 "false"뿐 아니라 빈 값, 누락, 오타도 모두 꺼짐으로 처리한다. 실제로 "0", "TRUE", 앞뒤에 공백이 있는 " true ", 오타 "flase"를 추가로 넣자 Boolean은 네 값 모두 참이었고 === "true"는 모두 거짓이었다.

이 중 "TRUE"나 " true "도 받아야 하는지는 값을 만드는 쪽과 합의할 일이다. 이번 비교는 대소문자와 공백을 그대로 구분했다. 먼저 trim()이나 소문자 변환을 넣으면 허용하는 입력이 달라지므로, 의도한 규칙인지 정한 뒤 적용한다.

오타를 조용히 꺼짐으로 처리하면 안 되는 설정도 있다. 예를 들어 “속성 누락은 기본값인 꺼짐, 속성이 있으면 "true" 또는 "false"만 허용”한다면 다음처럼 읽는 지점에서 검증한다. 아래 함수도 같은 브라우저에서 실행했다.

function parseEnabled(value) {
  if (value === undefined) return false;
  if (value === "true") return true;
  if (value === "false") return false;
  throw new TypeError('data-enabled must be "true" or "false"');
}

앞의 Console 입력을 실행한 상태에서 이 함수를 정의하고 parseEnabled(sample.querySelector("#off").dataset.enabled)로 호출하면 false가 나온다. #on은 true, #missing은 false였다. #empty와 #bare는 TypeError가 났다. 추가로 비교한 "0", "TRUE", " true ", "flase"도 같은 오류로 거절됐다. 빈 속성을 정상적인 꺼짐으로 받아야 한다면 이 함수의 계약부터 바꿔야 한다.

반대로 “속성의 존재 자체가 켜짐”이라는 계약을 택했다면 el.hasAttribute("data-enabled")로 읽는다. 앞 출력의 마지막 열처럼 빈 값도 켜짐이 된다. 다만 data-enabled="false" 역시 속성이 있으므로 켜짐이다. 이 계약을 쓰면서 "false"를 비활성 표시로 출력하는 코드를 섞어서는 안 된다.

쓰는 쪽에 false를 넣어도 다시 읽으면 문자열이다

읽는 코드를 고쳤는데 같은 문제가 돌아오면 값을 쓰는 곳도 확인한다. 별도 새 요소에 el.dataset.enabled = false를 실행하자 HTML은 data-enabled="false", 다시 읽은 값은 string 타입의 "false"였다. Boolean은 여전히 참이었다. MDN의 값 설정 설명 (새 탭에서 열림)처럼 대입할 때도 속성값은 문자열로 변환된다.

자기 코드에서는 기능을 켜는 조건 바로 앞에서 원래 값과 typeof를 함께 읽는다. 소문자 "true"인데 명시 비교가 거짓이라면 공백이나 다른 철자, 다른 요소를 읽는지 확인한다. 값과 비교 결과가 맞는데 기능이 켜진다면, 그 결과를 실제 분기에서 사용하는지와 뒤에서 값을 덮어쓰는 코드를 확인한다. 누락이 뜻하는 기본값도 읽는 코드와 HTML을 만드는 코드에서 같아야 한다.

직접 확인한 범위는 Chromium 한 버전의 네이티브 dataset 읽기·대입과 위 파서다. Firefox·Safari·모바일, 프레임워크나 라이브러리가 별도로 변환한 값, 실제 서버 템플릿의 출력은 실행하지 않았다. 그런 코드에서는 변환을 거친 결과와 원래 el.dataset.enabled를 나란히 읽어 어느 단계에서 타입이 바뀌는지 확인해야 한다.

← 전체 글 보기