홈페이지의 목적에 문의가 있다면 연락 버튼을 쉽게 찾게 만드는 일은 중요하다. 그렇다고 모든 순간에 큰 버튼이 가장 먼저 보이면 좋은 것은 아니다. 작은 화면에서 본문을 읽을 때는 그 버튼이 필요한 정보의 끝부분을 가릴 수 있다. 나는 연락하기와 읽기를 같은 화면에서 함께 성립시키고 싶었다.

공통 문의바의 실제 소스에는 아래로 이동할 때 잠시 숨기고, 멈추면 다시 나타나게 하는 동작이 있다. 위로 돌아가면 숨김을 풀고, 다른 팝업이나 조작부와 겹치는 상황은 별도의 상태로 다룬다. 이 모듈에서는 그중 읽는 동안 비켜나는 핵심을 떼어내 새로 정리했다.

문의 버튼이 중요한 것과 항상 보여야 하는 것은 다르다

나는 방문자가 내용을 이해한 뒤 다음 행동을 할 수 있게 만들고 싶다. 버튼이 계속 눈에 띄더라도 본문을 가리거나 팝업 조작을 방해하면 설명과 행동이 서로 부딪힌다. 특히 모바일에서는 고정 영역이 차지하는 높이가 상대적으로 크다. 문의를 강조하는 방식이 읽기의 비용을 높이고 있는지 먼저 살폈다.

한편 버튼을 완전히 없애면 연락하고 싶은 순간에 다시 찾아야 한다. 그래서 계속 보이거나 영원히 사라지는 두 선택 사이에, 이동하는 동안 비켜나고 멈춘 뒤 돌아오는 상태를 두었다. 이는 문의율이 올라갔다고 주장하는 장치가 아니다. 읽는 상황과 연락하는 상황을 구별한 인터페이스 선택이다.

스크롤 방향과 정지 시간을 하나의 상태로 읽는다

실제 공통 원본은 아래로 움직이는 경우를 감지하고 재등장 타이머를 갱신한다. 조금 위로 돌아오는 경우에는 숨김을 해제한다. 모바일과 PC의 대기 시간을 설정으로 나눌 수 있고, 휠과 터치 이동도 보완 입력으로 다룬다. 단순히 스크롤 이벤트가 한 번 왔다고 버튼을 지우는 구조보다 상태를 설명하기 쉽다.

나는 작은 움직임도 고려한다. 숨기는 임계값과 위로 돌아올 때의 임계값을 구별하면 손 떨림처럼 작은 이동에 버튼이 계속 튀는 일을 줄일 수 있다. 재등장 타이머는 다음 이동 때 갱신하되, 작은 이동 때문에 타이머만 지워지고 버튼이 돌아오지 않는 상태는 만들지 않아야 한다. 멈췄다는 판단도 코드의 선택이다.

키보드로 버튼을 찾는 사람을 숨겨두지 않는다

화면 밖으로 움직인 버튼은 문서에서 사라진 버튼과 다르다. 키보드 포커스가 그 안으로 들어갈 수 있다면 실제로 보이게 해야 한다. 새로 정리한 예시에는 focusin이 발생할 때 숨김을 풀고, 버튼 안에 초점이 있는 동안 스크롤로 다시 가리지 않는 조건을 넣었다.

움직임을 줄이는 설정을 사용하는 사람에게는 전환 애니메이션을 줄인다. 문의바의 높이를 본문 아래 여백에 반영하는 것도 별도 문제다. 숨김과 재등장을 잘 구현해도 마지막 문장이 바 아래에 계속 가려지면 끝까지 읽을 수 없다. 이 기능을 쓸 때는 본문과 고정 UI의 관계까지 확인해야 한다.

핵심 동작을 별도 모듈로 붙이고 떼어낸다

다음 JavaScript는 기존 고객별 연락 설정과 사이트 클래스를 제외하고 새로 작성한 작은 구현이다. 같은 요소에 다시 연결할 때 이전 리스너를 정리하고, 반환한 함수로 타이머와 이벤트를 해제할 수 있게 했다. 단일 페이지 앱에서 페이지를 다시 그리거나 설정을 바꾸는 경우에도 이전 동작이 중복되지 않도록 하기 위한 것이다.

430밀리초는 이 예시의 시작값일 뿐 모든 홈페이지의 정답이 아니다. 자료를 읽는 길이, 문의바 크기, 실제 스크롤 방식에 맞게 정한다. 이 핵심형은 팝업이나 VR과의 충돌 검사, 손가락 입력 보완, 실제 문의 전송을 포함하지 않는다. 그 기능이 필요하면 각각의 조건을 연결하고 확인한다.

코드와 구현 살펴보기

JavaScript 모듈 · 읽기 상태에 맞춰 문의바를 비켜나게 하기

const mounted = new WeakMap();

export function attachReadingContactBar(bar, {delay=430, view=window}={}) {
  mounted.get(bar)?.();
  let lastY=view.scrollY, timer;
  const show=()=>bar.classList.remove('is-reading-hidden');
  const scheduleShow=()=>{
    view.clearTimeout(timer);
    timer=view.setTimeout(show,delay);
  };
  const onScroll=()=>{
    const y=view.scrollY;
    if(bar.contains(bar.ownerDocument.activeElement))show();
    else if(y>lastY+2)bar.classList.add('is-reading-hidden');
    else if(y<lastY-8)show();
    lastY=y;
    scheduleShow();
  };
  const onFocus=()=>{view.clearTimeout(timer);show();};
  view.addEventListener('scroll',onScroll,{passive:true});
  bar.addEventListener('focusin',onFocus);
  const destroy=()=>{
    view.clearTimeout(timer);
    view.removeEventListener('scroll',onScroll);
    bar.removeEventListener('focusin',onFocus);
    show();
    if(mounted.get(bar)===destroy)mounted.delete(bar);
  };
  mounted.set(bar,destroy);
  return destroy;
}

CSS와 본문의 여백을 함께 정한다

이 모듈은 문의바에 is-reading-hidden 클래스를 붙이므로 디자인은 별도의 CSS에서 정한다. 아래 예시에는 고정 위치와 전환, 안전 영역을 포함했다. 높이는 실제 버튼 구성과 글자 크기에 따라 달라지므로 숫자 하나를 복사한 뒤 모든 상황이 해결됐다고 판단하지 않는다.

이 홈페이지의 실제 연락 방법은 이메일 하나다. 다른 사이트에서 쓰던 전화나 수신자를 공통 코드에 넣어 재사용하지 않는다. 읽기 동작을 재사용하는 것과 연락처를 재사용하는 것은 서로 다른 문제다. 실제 전송 기능이 없다면 버튼을 누르는 행동을 문의가 발송됐다고 설명하지 않는다.

코드와 구현 살펴보기

CSS · 움직임과 본문 아래 공간을 따로 설정

.reading-bar {
  position: fixed; left: 1rem; right: 1rem; bottom: 1rem;
  padding-bottom: env(safe-area-inset-bottom);
  transition: transform 180ms ease;
}
.reading-bar.is-reading-hidden { transform: translateY(150%); }
main { padding-bottom: 7rem; }
@media (prefers-reduced-motion: reduce) {
  .reading-bar { transition: none; }
}

이 페이지에서 실행하는 구현

앞에서 설명한 문의바 함수를 이 프레임에서 직접 실행한다. 프레임 내부를 아래로 읽으면 숨김과 다시 표시하는 동작을 살펴볼 수 있다. 버튼은 예시 상태만 바꾸며 실제 문의를 전송하지 않는다.

배경은 이 홈페이지에 사용한 이미지를 식별 정보 없이 재사용했다. 코드 동작 검사와 실제 HTTP·API 전달은 확인했지만 새 PC·모바일 실화면과 이 프레임의 브라우저 상호작용을 직접 검수한 결과는 아직 없다.

기존 재사용 함수가 실행되는 설명용 예시

내가 동작 확인에서 보는 순서

소스의 조건을 읽는 것과 실제 브라우저에서 보는 것은 다르다. 새로운 작은 구현은 상태 전환과 재연결·해제 동작을 코드로 확인할 수 있지만, 실제 모바일 스크롤과 화면 읽기, 버튼의 가림은 적용 환경에서 확인해야 한다. 지금 이 모듈의 PC·모바일 실화면 검수를 마쳤다고 말하지 않는다.

전환 시간을 줄이거나 버튼을 더 빨리 돌아오게 하는 일도 목적에 맞춰 판단한다. 버튼이 잠깐 숨었다는 이유만으로 문의가 늘었다고 결론내리지 않는다. 나는 읽기를 방해하지 않고 필요한 순간에는 연락 길이 보이는지를 먼저 확인한다.

  1. 아래로 이동하면 잠시 숨고, 이동이 멈춘 뒤 돌아오는가.
  2. 조금 위로 돌아올 때 버튼이 찾기 쉬운 상태로 바뀌는가.
  3. 작은 추가 이동 후에도 재등장 타이머가 살아 있는가.
  4. 키보드 초점이 들어오면 버튼이 보이고 다시 숨지 않는가.
  5. 같은 요소에 재연결해도 한 번의 동작만 실행되는가.
  6. 기능을 해제하면 타이머와 이벤트가 남지 않는가.
  7. 마지막 본문과 팝업 조작부가 실제 화면에서 가리지 않는가.