토스트 메시지 구성요소 빌드

적응형과 액세스 가능한 토스트 메시지 구성요소를 빌드하는 방법에 관한 기본적인 개요입니다.

이 게시물에서는 토스트 메시지 구성요소를 빌드하는 방법을 공유하고자 합니다. 데모 사용해 보기

데모

동영상을 선호한다면 이 게시물의 YouTube 버전을 참조하세요.

개요

토스트 메시지는 사용자를 위한 비대화형, 수동, 비동기 짧은 메시지입니다. 일반적으로 사용자에게 작업 결과를 알리기 위한 인터페이스 의견 패턴으로 사용됩니다.

상호작용

토스트 메시지는 알림, 알림, 메시지와는 다릅니다. 대화형이 아니므로 닫거나 유지하기 위한 것이 아니기 때문입니다. 알림은 더 중요한 정보, 상호작용이 필요한 동기식 메시지 또는 페이지 수준이 아닌 시스템 수준 메시지를 위한 것입니다. 토스트 메시지는 다른 알림 전략보다 수동적입니다.

마크업

<output> 요소는 스크린 리더에 알려주므로 토스트 메시지에 적합합니다. 올바른 HTML은 JavaScript 및 CSS로 향상할 수 있는 안전한 기반을 제공하며, 많은 JavaScript가 사용됩니다.

토스트

<output class="gui-toast">Item added to cart</output>

role="status"를 추가하여 더 포괄할 수 있습니다. 이는 브라우저가 사양에 따라 <output> 요소에 암시적 역할을 부여하지 않는 경우 대체를 제공합니다.

<output role="status" class="gui-toast">Item added to cart</output>

토스트 메시지 컨테이너

토스트 메시지는 한 번에 두 개 이상 표시할 수 있습니다. 여러 토스트 메시지를 조정하기 위해 컨테이너가 사용됩니다 이 컨테이너는 화면에서 토스트 메시지 위치도 처리합니다.

<section class="gui-toast-group">
  <output role="status">Wizard Rose added to cart</output>
  <output role="status">Self Watering Pot added to cart</output>
</section>

레이아웃

토스트 메시지를 표시 영역의 inset-block-end에 고정하기로 했고, 토스트 메시지가 더 추가되면 해당 화면 가장자리에서 토스트 메시지가 겹쳐집니다.

GUI 컨테이너

토스트 메시지 컨테이너는 토스트 메시지 표시를 위한 모든 레이아웃 작업을 실행합니다. 표시 영역에 대한 fixed이며 논리 속성 inset를 사용하여 고정할 가장자리와 동일한 block-end 가장자리의 padding를 약간 지정합니다.

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

DevTools 상자 크기와 .gui-toast-container 요소 위에 패딩이 오버레이된 스크린샷

토스트 컨테이너는 표시 영역 내에 자체적으로 배치하는 것 외에도 토스트 메시지를 정렬하고 배포할 수 있는 그리드 컨테이너입니다. 항목은 justify-content를 사용하여 그룹으로 중앙에 배치되고 justify-items를 사용하여 개별적으로 중앙에 배치됩니다. 토스트가 닿지 않도록 gap를 약간 넣으세요.

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

토스트 메시지 그룹에 CSS 그리드 오버레이가 있는 스크린샷. 이번에는 토스트 하위 요소 사이의 공간과 간격을 강조 표시합니다.

GUI 토스트

개별 토스트에는 padding, border-radius로 더 부드러운 모서리, 모바일 및 데스크톱 크기를 조정하는 데 도움이 되는 min() 함수가 있습니다. 다음 CSS의 반응형 크기는 토스트 메시지가 표시 영역 또는 25ch의 90% 를 초과하여 확장되는 것을 방지합니다.

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

패딩과 테두리 반경이 표시된 단일 .gui-toast 요소의 스크린샷

스타일

레이아웃 및 위치 지정이 설정된 상태에서 사용자 설정 및 상호작용에 맞게 조정하는 데 도움이 되는 CSS를 추가합니다.

토스트 메시지 컨테이너

토스트 메시지는 대화형이 아닙니다. 토스트 메시지를 탭하거나 스와이프해도 아무 작업도 실행되지 않지만 현재 포인터 이벤트를 사용합니다. 다음 CSS를 사용하여 토스트 메시지가 클릭을 도용하지 않도록 합니다.

.gui-toast-group {
  pointer-events: none;
}

GUI 토스트

맞춤 속성, HSL, 환경설정 미디어 쿼리를 사용하여 토스트 메시지에 밝은 색 또는 어두운 적응형 테마를 부여합니다.

.gui-toast {
  --_bg-lightness: 90%;

  color: black;
  background: hsl(0 0% var(--_bg-lightness) / 90%);
}

@media (prefers-color-scheme: dark) {
  .gui-toast {
    color: white;
    --_bg-lightness: 20%;
  }
}

애니메이션

새 토스트 메시지는 화면에 들어올 때 애니메이션과 함께 표시되어야 합니다. 감소된 모션을 수용하려면 기본적으로 translate 값을 0로 설정하지만 모션 환경설정 미디어 쿼리에서 모션 값을 길이로 업데이트합니다 . 모든 사용자에게 애니메이션이 제공되지만, 토스트가 특정 거리를 이동하는 것은 일부 사용자만이 있습니다.

다음은 토스트 애니메이션에 사용된 키프레임입니다. CSS가 토스트의 진입, 대기, 이탈을 모두 하나의 애니메이션으로 제어합니다.

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

그런 다음 토스트 요소가 변수를 설정하고 키프레임을 조정합니다.

.gui-toast {
  --_duration: 3s;
  --_travel-distance: 0;

  will-change: transform;
  animation: 
    fade-in .3s ease,
    slide-in .3s ease,
    fade-out .3s ease var(--_duration);
}

@media (prefers-reduced-motion: no-preference) {
  .gui-toast {
    --_travel-distance: 5vh;
  }
}

JavaScript

스타일과 스크린 리더에 액세스할 수 있는 HTML이 준비된 상태에서 사용자 이벤트를 기반으로 토스트 메시지 생성, 추가, 삭제를 조정하려면 JavaScript가 필요합니다. 토스트 구성요소의 개발자 환경은 다음과 같이 최소화되고 쉽게 시작할 수 있어야 합니다.

import Toast from './toast.js'

Toast('My first toast')

토스트 메시지 그룹 및 토스트 메시지 만들기

토스트 메시지 모듈이 JavaScript에서 로드될 때 토스트 메시지 컨테이너를 만들어 페이지에 추가해야 합니다. body 앞에 요소를 추가하기로 했습니다. 이렇게 하면 컨테이너가 모든 본문 요소의 컨테이너 위에 있으므로 z-index 스태킹 문제가 발생할 가능성이 낮아집니다.

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

head 태그와 body 태그 사이에 있는 토스트 그룹의 스크린샷

init() 함수는 모듈 내부적으로 호출되어 요소를 Toaster로 보관합니다.

const Toaster = init()

토스트 HTML 요소는 createToast() 함수를 사용하여 생성됩니다. 이 함수는 토스트 메시지에 관한 텍스트가 필요하고, <output> 요소를 만들고, 일부 클래스와 속성으로 장식하고, 텍스트를 설정하고, 노드를 반환합니다.

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

하나 이상의 토스트 메시지 관리

이제 JavaScript가 토스트 메시지를 포함하는 컨테이너를 문서에 추가하며 이제 생성된 토스트 메시지를 추가할 수 있습니다. addToast() 함수는 하나 이상의 토스트 메시지를 처리합니다. 먼저 토스트 메시지 수와 모션이 정상인지 확인한 다음 이 정보를 사용하여 토스트 메시지를 추가하거나 멋진 애니메이션을 실행하여 다른 토스트 메시지가 새 토스트를 위한 '공간을 확보'합니다.

const addToast = toast => {
  const { matches:motionOK } = window.matchMedia(
    '(prefers-reduced-motion: no-preference)'
  )

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

첫 번째 토스트 메시지를 추가할 때 Toaster.appendChild(toast)는 CSS 애니메이션을 트리거하는 페이지에 토스트 메시지를 추가합니다(애니메이션을 입히고 3s 대기, 애니메이션 종료). 기존 토스트 메시지가 있을 때 flipToast()폴 루이스플립이라는 기법을 사용하여 호출됩니다. 새 토스트 메시지 추가 전후의 컨테이너 위치 차이를 계산하는 개념입니다. 토스터의 현재 위치와 위치를 표시한 다음 토스터가 있는 위치에서 원래 위치로 애니메이션 처리한다고 생각하면 됩니다.

const flipToast = toast => {
  // FIRST
  const first = Toaster.offsetHeight

  // add new child to change container size
  Toaster.appendChild(toast)

  // LAST
  const last = Toaster.offsetHeight

  // INVERT
  const invert = last - first

  // PLAY
  const animation = Toaster.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
  ], {
    duration: 150,
    easing: 'ease-out',
  })
}

CSS 그리드는 레이아웃을 해제합니다. 새 토스트 메시지가 추가되면 그리드가 시작 부분에 토스트를 배치하고 다른 토스트와 간격을 둡니다. 한편 웹 애니메이션을 사용하여 컨테이너를 이전 위치에서 애니메이션 처리합니다.

모든 JavaScript 통합

Toast('my first toast')가 호출되면 토스트 메시지가 생성되어 페이지에 추가되고(새 토스트를 수용하도록 컨테이너가 애니메이션 처리된 경우도 있음) 프로미스가 반환되고 생성된 토스트 메시지가 프로미스 해결을 위한 CSS 애니메이션 완료 (키 3개 애니메이션)에 감시됩니다.

const Toast = text => {
  let toast = createToast(text)
  addToast(toast)

  return new Promise(async (resolve, reject) => {
    await Promise.allSettled(
      toast.getAnimations().map(animation => 
        animation.finished
      )
    )
    Toaster.removeChild(toast)
    resolve() 
  })
}

이 코드의 혼동을 야기하는 부분은 Promise.allSettled() 함수와 toast.getAnimations() 매핑에 있습니다. 토스트 메시지에 여러 개의 키프레임 애니메이션을 사용했으므로 모든 키프레임이 완료되었는지 확실히 알 수 있도록 각각 JavaScript에서 요청되고 완료 시 관찰되는 각 finished 프로미스에서 요청해야 합니다. allSettled는 이 작업을 실행하며 모든 Promise가 충족되면 완성된 것으로 확인됩니다. await Promise.allSettled()를 사용하면 다음 코드 줄에서 확실하게 요소를 삭제하고 토스트 메시지 수명 주기가 완료되었다고 가정할 수 있습니다. 마지막으로 resolve()를 호출하면 토스트 메시지가 표시된 후 개발자가 정리하거나 다른 작업을 실행할 수 있도록 높은 수준의 토스트 메시지 프로미스가 충족됩니다.

export default Toast

마지막으로 다른 스크립트를 가져와서 사용할 수 있도록 모듈에서 Toast 함수를 내보냅니다.

토스트 구성요소 사용

토스트 메시지 또는 토스트의 개발자 환경을 사용하려면 Toast 함수를 가져오고 메시지 문자열로 호출하면 됩니다.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

개발자가 토스트 메시지가 표시된 후에 정리 작업 등의 작업을 실행하려는 경우 async와 await를 사용하면 됩니다.

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

결론

이제 제가 어떻게 했는지 알았으니 어떻게 되세요?‽ 🙂

접근 방식을 다양화하고 웹에서 빌드하는 모든 방법을 알아보겠습니다. 데모를 만들고 링크를 트윗해 주세요. 그러면 아래의 커뮤니티 리믹스 섹션에 추가하겠습니다.

커뮤니티 리믹스