김 양의 멋따라 개발따기
변수 선언하지 않고 JS에서 HTML 요소 사용할 수 있다? 본문
1. 문제 상황
<button class="w-[150px] h-[40px] bg-indigo-950 mt-20 mb-20 hover:bg-slate-300 active:bg-slate-300 text-white" type="button" id="faculty_btn">페컬티정보조회</button>
faculty_btn.addEventListener("click", ()=>{
//... 동작
})
faculty_btn이 변수로 선언하지 않고 스크립트에서 동작하고 있는 걸 발견함.
상황이 신기해서 찾아봄
2. 문제 설명
1) 메커니즘: 왜 되는가
브라우저는 문서를 파싱할 때, id(그리고 일부 태그의 name)가 있는 요소들을 **window 객체의 “이름 있는 프로퍼티(named properties)”**로 노출합니다. 그래서 id="faculty_btn" 이 있으면 window.faculty_btn이 생기고, 일반 스크립트에선 전역 식별자처럼 faculty_btn으로 바로 접근이 됩니다. 이를 **“Named access on the Window object”**라고 합니다. HTML Living Standard+2MDN 웹 문서+2
- 모든 HTML 요소의 id 값은 전역으로 노출됩니다. (예: window.faculty_btn) MDN 웹 문서
- name 속성은 일부 태그(form, iframe, img, object, embed)에 한해 전역으로 노출됩니다. (예: <form name="f"> → window.f) MDN 웹 문서
즉, 지금 코드가 “변수 선언 없이” 돌아간 건 브라우저가 자동으로 window.faculty_btn을 제공했기 때문이에요.
2) 언제 되지 않나 (깨지는 조건들)
이 동작은 “레거시 편의 기능”이라 상황에 따라 바로 깨질 수 있습니다.
- ES 모듈(<script type="module">): 모듈의 최상위 스코프는 모듈 스코프이므로, **맨바닥 식별자 faculty_btn은 ReferenceError**가 납니다. (대신 window.faculty_btn은 존재할 수 있지만, 맨바닥 식별자로는 접근 안 됨) eslint.org
- 파싱 순서: 스크립트가 요소 보다 먼저 실행되면 그 시점엔 아직 window.faculty_btn이 없으므로 실패합니다. (DOM 생성 후에만 생김)
- 이름 충돌: 전역의 다른 속성/함수 이름과 겹치면(예: open, name 등) 헷갈리거나 접근 자체가 달라질 수 있습니다. (이 때문에 사양도 “의존하지 말라”고 경고) HTML Living Standard
- 중복 id(유효하지 않은 HTML): 같은 id가 여러 개면, 사양상 그 이름으로 접근하면 **단일 요소가 아닌 HTMLCollection**이 반환될 수 있어 코드가 깨질 수 있습니다. HTML Living Standard
3) 이건 ‘변수’가 아니다 (미묘한 차이)
faculty_btn은 let/const/var로 만든 변수 바인딩이 아니라, window의 동적 프로퍼티에 더 가깝습니다.
- 예시:즉, 맨바닥 식별자 해석 규칙과 window 프로퍼티가 섞여 동작하기 때문에, 유지보수/도구(린터·번들러) 관점에서 취약해집니다. 사양도 “이 방식에 의존하면 깨지기 쉽다(brittle)”고 못박고 있어요. HTML Living Standard
-
// HTML 어딘가에 <button id="faculty_btn"></button> 존재 console.log(window.faculty_btn === document.getElementById('faculty_btn')); // true let faculty_btn = 123; console.log(faculty_btn); // 123 (이제는 '변수'가 우선) console.log(window.faculty_btn); // <button ...> (여전히 window엔 요소가 존재)
4) 보안 측면(DOM Clobbering)
이 기능을 악용해, 공격자가 특정 id/name으로 요소를 삽입해 전역 변수/함수를 덮어쓰게(clobber) 만드는 기법이 있습니다. 예컨대 어떤 코드가 전역 config를 기대할 때, <a id="config" href=...> 같은 마크업으로 흐름을 교란할 수 있습니다. 그래서 보안 가이드들도 이 기능에 의존하지 말라고 권고해요. OWASP Cheat Sheet Series+1
5) 실무 권장 패턴 (당장 적용 팁)
- 항상 명시적으로 선택
document.addEventListener("DOMContentLoaded", () => { const facultyBtn = document.getElementById("faculty_btn"); // ✅ facultyBtn?.addEventListener("click", () => { /* ... */ }); });
- 헷갈리는 전역 이름 피하기
open, name, status 같은 전역과 겹치는 id를 피하고, 변수명도 openBtn, nameInput처럼 구체적으로. - 중복 id 금지
유효한 HTML 유지(한 문서에 같은 id는 1개). - 모듈을 쓰면 더 안전
점진적으로 <script type="module">로 옮기면 “맨바닥 식별자 = 전역” 패턴이 차단되어 버그가 줄어듭니다. (필요 시 window.faculty_btn처럼 명시적으로 접근) eslint.org
핵심 요약
- 왜 되나? id가 자동으로 window의 이름 있는 프로퍼티로 노출되기 때문. HTML Living Standard+1
- 하지만 모듈/파싱순서/이름충돌/중복id 등에서 쉽게 깨지고, 보안 리스크도 있다. HTML Living Standard+1
- 그래서 실전에서는 항상 getElementById/querySelector로 변수 선언해서 쓰는 게 정석. MDN 웹 문서
'TIL' 카테고리의 다른 글
| JavaScript !! (느낌표 두 개) 완전 정리: 왜 쓰고, 언제 위험할까? (0) | 2026.01.19 |
|---|---|
| 2.0 + 4.4 + 6.2 + 2.4 = 15.000000000000002 이 되는 자바스크립트의 부동소수점 오차 (0) | 2025.11.21 |
| JavaScript - Pinch Zoom 보완하기 (4) | 2025.07.24 |
| 집에 가고 싶을 때 퇴근 타이머 만들기 (2) | 2025.07.11 |
| Index DB 사용해보기 (2) | 2025.06.19 |