원문 캡처 · codedge.de
원문 캡처 · codedge.de

코드지(Codedge)의 사이트 운영자는 개인 사이트에서 자바스크립트 기능 대부분을 없애고 HTML과 CSS로 대체했다. 연락처 양식, 사이트 검색, 모바일 메뉴, 이미지 지연 로딩, 읽기 진행 표시를 구현한 방법과 브라우저 지원이 필요한 기능의 한계를 소개한다.

운영자는 친구의 컴퓨터에서 피싱 공격이 발생한 뒤, 읽지 않은 코드를 실행하는 상황을 경계하게 됐다고 설명한다. 공격에 사용된 것은 브라우저의 자바스크립트가 아니라 Node.js였지만, 원격 서버에서 코드를 가져와 실행한다는 점에서 웹사이트의 제3자 <script> 태그도 비슷한 위험을 안고 있다고 봤다. NoScript로 자바스크립트를 차단해 보니 외부 추적 도구에 의존하거나 콘텐츠를 불러오지 못하는 사이트가 많았고, 자기 사이트를 방문하는 사람에게 같은 불편을 주지 않으려 기능을 바꿨다.

연락처 양식은 정적 사이트용으로 직접 만든 백엔드인 staticform을 사용한다. 브라우저에서는 HTML5 속성과 입력 필드 유형으로 유효성을 확인하고, 제출 뒤에는 백엔드가 다시 검사해 성공 또는 오류 페이지로 보낸다.

사이트 검색은 검색 색인 파일과 자바스크립트를 제공하는 대신, 검색어를 DuckDuckGo에 보내고 숨김 필드로 자기 도메인 결과만 찾도록 구성했다. 운영자는 DuckDuckGo가 검색어를 추적하거나 IP 주소를 저장하거나 사용자 프로필을 만들지 않는다는 점을 선택 이유로 들었다.

모바일 메뉴에는 HTML의 <details> 요소를 활용할 수 있다. <summary>를 누르면 메뉴가 열리고, 브라우저가 열린 상태를 관리한다. 다른 방법으로는 주요 브라우저에서 사용할 수 있는 Popover API가 있다. 다만 <details>와 배치 방식이 다르고 Esc 키로 닫는 동작에도 차이가 있다.

이미지 지연 로딩은 loading="lazy"로 이미지가 화면 가까이에 왔을 때 불러오도록 하고, decoding="async"로 이미지 디코딩이 나머지 페이지 렌더링을 막지 않게 할 수 있다. height와 width를 지정하면 이미지가 불러와지는 동안 브라우저가 공간을 확보해 레이아웃이 움직이는 것을 줄인다.

글을 읽는 동안 진행도를 표시하는 막대는 CSS 스크롤 기반 애니메이션으로 구현했다. 다만 이 기능은 모든 브라우저가 지원하지 않는다. 원문은 당시 Firefox가 지원하지 않아 해당 브라우저에서는 막대가 표시되지 않는다고 설명한다. 운영자는 이를 큰 문제로 보지 않는다고 덧붙였다.

사이트에는 방문자 수를 세는 작은 스크립트가 하나 남아 있다. 운영자는 개인정보를 고려한 분석 도구인 GoatCounter를 사용하며, 쿠키를 쓰거나 방문자를 개별 추적하지 않는다고 설명한다. 스크립트는 자기 서버에서 제공해 제3자 코드를 실행하지 않고, 페이지 조회 정보만 GoatCounter에 전송한다고 밝혔다.

부록: 원문 예시

text
1<input id="email" name="email-address" type="email" required />
text
1<form action="https://duckduckgo.com" method="POST"> 2    <input type="text" name="q" placeholder="Search this site..." required> 3    <input type="hidden" name="sites" value="www.codedge.de"> 4    <button type="submit">Search</button> 5</form>
text
1<button popovertarget="menu" aria-label="menu">[OPEN ICON]</button> 2<nav id="menu" popover> 3  <a href="/menu-item-1">Menu item 1</a> 4  <a href="/menu-item-2">Menu item 2</a> 5  <a href="/menu-item-3">Menu item 3</a> 6</nav>
text
1<img class="img" alt="..." src="/posts/00020/cover.jpg" 2     loading="lazy" decoding="async" 3     height="1080" width="1920">