Codelab: 스토리 구성요소 빌드

이 Codelab에서는 Instagram 스토리와 같은 환경을 빌드하는 방법을 알아봅니다. 있습니다. HTML, CSS, JavaScript를 차례로 선택합니다.

블로그 게시물 스토리 구성요소 빌드하기를 확인해 보세요. 을 참조하세요.

설정

  1. 수정할 리믹스를 클릭하여 프로젝트를 수정할 수 있도록 합니다.
  2. app/index.html를 엽니다.

HTML

항상 의미론적 HTML을 사용하고 싶습니다. 친구마다 이야기가 여러 개 있을 수 있기 때문에, 저는 친구들과 이야기할 때 이야기를 나눌 수 있는 친구당 <section>개 요소, 스토리당 <article>개 요소 그래도 처음부터 시작해 봅시다. 먼저, 컨테이너를 사용할 수 있는 스토리 구성요소를 만듭니다.

<body><div> 요소를 추가합니다.

<div class="stories">

</div>

몇 가지 <section> 요소를 추가하여 친구를 나타냅니다.

<div class="stories">
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
</div>

몇 가지 <article> 요소를 추가하여 뉴스를 나타냅니다.

<div class="stories">
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/480/841);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/481/840);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/481/841);"></article>
  </section>
  <section class="user">
    <article class="story" style="--bg: url(https://picsum.photos/482/840);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/482/843);"></article>
    <article class="story" style="--bg: url(https://picsum.photos/482/844);"></article>
  </section>
</div>
  • 스토리 프로토타입을 제작할 때 이미지 서비스 (picsum.com)를 사용하고 있습니다.
  • <article>style 속성은 자리표시자 로드의 일부입니다. 이 기법에 대해서는 다음 섹션에서 자세히 알아볼 것입니다.

CSS

스타일리시한 콘텐츠 그 뼈를 사람들이 할 수 있는 것으로 바꾸자 제공할 수 있습니다. 오늘은 모바일 중심으로 진행하겠습니다.

.stories

<div class="stories"> 컨테이너에는 가로 스크롤 컨테이너가 필요합니다. 이를 위해 다음과 같은 방법을 사용할 수 있습니다.

  • 컨테이너를 그리드로 만들기
  • 각 하위 요소가 행 트랙을 채우도록 설정
  • 각 하위 요소의 너비를 휴대기기 표시 영역의 너비로 만들기

그리드가 이전 열 오른쪽에 새로운 100vw 너비 열을 계속 배치합니다. 첫 번째는 마크업의 모든 HTML 요소를 배치하는 것입니다.

전체 너비 레이아웃을 보여주는 그리드 시각 효과와 함께 열리는 Chrome과 DevTools
가로 스크롤러를 만드는 그리드 열 오버플로를 보여주는 Chrome DevTools

app/css/index.css 하단에 다음 CSS를 추가합니다.

.stories {
  display: grid;
  grid: 1fr / auto-flow 100%;
  gap: 1ch;
}

이제 표시 영역 너머로 콘텐츠가 확장되었으므로 어떻게 처리할지 살펴보겠습니다 강조표시된 코드 줄을 .stories 규칙 세트에 추가합니다.

.stories {
  display: grid;
  grid: 1fr / auto-flow 100%;
  gap: 1ch;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior: contain;
  touch-action: pan-x;
}

가로 스크롤을 원하므로 overflow-x를 다음과 같이 설정합니다. auto입니다. 사용자가 스크롤하면 구성 요소가 다음 스토리에 부드럽게 배치되도록 하고, scroll-snap-type: x mandatory를 사용하겠습니다. 자세히 알아보기 CSS 스크롤 스냅 포인트의 CSS overscroll-behavior와 섹션을 만들 수 있습니다

상위 컨테이너와 하위 컨테이너가 모두 스크롤 스냅에 동의해야 하므로 이제 해보겠습니다 app/css/index.css 하단에 다음 코드를 추가합니다.

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

앱이 아직 작동하지 않지만 아래 동영상은 다음과 같은 경우에 어떻게 작동하는지 보여줍니다. scroll-snap-type이(가) 사용 설정 및 사용 중지되었습니다. 이를 사용 설정하면 각 가로 방향은 스크롤하면 다음 스토리로 스냅됩니다. 비활성화하면 브라우저에서 기본 스크롤 동작입니다.

이렇게 하면 친구를 스크롤할 수 있지만 문제가 있습니다. 해결할 수 있는 이야기가 될 수 있습니다.

.user

.user 섹션에 하위 스토리를 랭글링하는 레이아웃을 만들어 보겠습니다. 요소를 제자리에 삽입합니다. 이 문제를 해결하기 위해 유용한 스택 트릭을 사용하겠습니다. 기본적으로 행과 열이 동일한 그리드를 갖는 1x1 그리드를 만듭니다. [story]의 별칭이고 각 스토리 그리드 항목은 이 공간의 소유권을 주장하려고 합니다. 스택이 생성됩니다.

강조표시된 코드를 .user 규칙 세트에 추가합니다.

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: grid;
  grid: [story] 1fr / [story] 1fr;
}

app/css/index.css 하단에 다음 규칙 집합을 추가합니다.

.story {
  grid-area: story;
}

이제 절대 위치 지정, 부동 소수점 수 또는 요소가 흐름에서 벗어나도 여전히 흐름에 있습니다. 게다가 코드가 거의 전혀 없는 듯합니다. 이것 좀 봐! 자세한 내용은 동영상과 블로그 게시물에서 확인하세요.

.story

이제 스토리 항목 자체의 스타일을 지정하기만 하면 됩니다.

앞서 각 <article> 요소의 style 속성이 자리표시자 로드 기법:

<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>

CSS의 background-image 속성을 사용하여 다음을 지정합니다. 2개 이상의 배경 이미지가 필요합니다. 이를 순서대로 배치하여 사용자가 사진이 상단에 있고 로드가 완료되면 자동으로 표시됩니다. 받는사람 사용 설정하려면 이미지 URL을 맞춤 속성 (--bg)에 넣어서 를 사용하여 로드 자리표시자로 층을 이룹니다.

먼저 그라데이션을 배경 이미지로 대체하도록 .story 규칙 세트를 업데이트해 보겠습니다. 표시됩니다. 강조표시된 코드를 .story 규칙 세트에 추가합니다.

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}

background-sizecover로 설정하면 이미지가 표시 영역만 채우는 것을 의미합니다. 배경 이미지 2개 정의 중 를 사용하면 로딩 Tombstone이라고 하는 깔끔한 CSS 웹 트릭을 가져올 수 있습니다.

  • 배경 이미지 1 (var(--bg))은 HTML에서 인라인으로 전달한 URL입니다.
  • 배경 이미지 2 (linear-gradient(to top, lch(98 0 0), lch(90 0 0))은 그라데이션임) URL이 로드되는 동안 표시되도록

이미지 다운로드가 완료되면 CSS에서 자동으로 그라데이션을 이미지로 바꿉니다.

다음으로, 몇몇 동작을 제거하기 위해 CSS를 추가하겠습니다. 따라서 브라우저가 더 빠르게 움직일 수 있습니다. 강조표시된 코드를 .story 규칙 세트에 추가합니다.

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));

  user-select: none;
  touch-action: manipulation;
}
  • user-select: none는 사용자가 실수로 텍스트를 선택하는 것을 방지합니다.
  • touch-action: manipulation는 브라우저에 이러한 상호작용이 터치 이벤트로 취급해야 하며, 따라서 터치 이벤트로 처리하여 브라우저가 URL을 클릭할지 여부를 결정하세요

마지막으로, 스토리 간 전환에 애니메이션을 적용하는 작은 CSS를 추가해 보겠습니다. 강조 표시된 코드를 .story 규칙 세트에 추가합니다.

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));

  user-select: none;
  touch-action: manipulation;

  transition: opacity .3s cubic-bezier(0.4, 0.0, 1, 1);

  &.seen {
    opacity: 0;
    pointer-events: none;
  }
}

.seen 클래스가 이탈이 필요한 스토리에 추가됩니다. 맞춤 이징 함수 (cubic-bezier(0.4, 0.0, 1,1))를 사용할 수 있습니다. Material Design의 Easing에서 가이드 (가속 이징 섹션으로 스크롤)를 참조하세요.

관심이 있으시다면 pointer-events: none 정말 어려웠죠. 이 것이 유일하게 단점이 있었습니다. 필요한 이유는 .seen.story 요소가 가 상단에 위치하며 보이지 않더라도 탭을 받습니다. 먼저 pointer-eventsnone로 바꾸면 유리 이야기를 창문으로 바꾸고 사용자 상호작용이 더 많아집니다. 적절한 균형점도 있고 관리하기도 어렵지 않습니다. 살펴보겠습니다. z-index에서 여러 데이터를 처리하지 않습니다. 좋습니다. 멈추지 마세요.

자바스크립트

사용자가 스토리 구성요소의 상호작용은 매우 간단합니다. 앞으로 이동하려면 오른쪽을 누르고, 돌아가려면 왼쪽을 탭합니다. 사용자는 일반적으로 많은 노력을 하고 있습니다. 그래도 Google에서 많이 처리하겠습니다.

설정

먼저 최대한 많은 정보를 계산하고 저장해 보겠습니다. 다음 코드를 app/js/index.js에 추가합니다.

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

JavaScript의 첫 번째 줄은 기본 HTML에 대한 참조를 가져와 저장합니다. 요소 루트에 복사합니다. 다음 행에서는 요소의 가운데 위치를 계산하므로 탭할 때 앞으로 이동할지 뒤로 이동할지 결정할 수 있습니다.

다음으로 로직과 관련된 상태가 있는 작은 객체를 만듭니다. 이 현재 뉴스에만 관심이 있습니다. HTML 마크업에서는 첫 번째 친구와 그 친구의 최근 스토리를 가져가서 게임에 액세스하세요. 강조 표시된 코드 추가 app/js/index.js에 추가합니다.

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

리스너

이제 사용자 이벤트를 수신 대기하고 전달하기 위한 로직이 충분합니다.

먼저 스토리 컨테이너에서 'click' 이벤트를 수신 대기합니다. 강조표시된 코드를 app/js/index.js에 추가합니다.

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

stories.addEventListener('click', e => {
  if (e.target.nodeName !== 'ARTICLE')
    return

  navigateStories(
    e.clientX > median
      ? 'next'
      : 'prev')
})

클릭이 발생하고 <article> 요소에 있지 않은 경우 Google에서는 보석을 설정하고 아무 작업도 하지 않습니다. 기사인 경우 마우스 또는 손가락의 가로 위치를 clientX 아직 navigateStories를 구현하지는 않았지만 가야 할 방향을 지정합니다. 사용자 위치가 중앙값보다 크면 next로 이동해야 합니다. 그렇지 않으면 prev (이전)

키보드

이제 키보드 누르기를 들어보겠습니다. 아래쪽 화살표를 누르면 next에게 전송합니다. 위쪽 화살표이면 prev로 이동합니다.

강조표시된 코드를 app/js/index.js에 추가합니다.

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

stories.addEventListener('click', e => {
  if (e.target.nodeName !== 'ARTICLE')
    return

  navigateStories(
    e.clientX > median
      ? 'next'
      : 'prev')
})

document.addEventListener('keydown', ({key}) => {
  if (key !== 'ArrowDown' || key !== 'ArrowUp')
    navigateStories(
      key === 'ArrowDown'
        ? 'next'
        : 'prev')
})

스토리 탐색

스토리의 고유한 비즈니스 로직과 스토리가 된 UX를 다룰 시간입니다. 잘 알려져 있습니다. 버벅거리고 까다롭게 보이긴 하지만, 조금만 더 해 보면 꽤 이해하기 쉽습니다.

우선, 스크롤 해야 볼 수 있는 페이지로 스크롤할지 여부를 결정하는 데 도움이 되는 스토리 표시/숨기기 HTML이 우리가 일하는 곳이기 때문에 친구 (사용자) 또는 스토리 (이야기)의 존재 여부에 대해 쿼리합니다.

이러한 변수는 '주어진 스토리 x, '다음'을 하는 등의 질문에 답하는 데 도움이 됩니다. 같은 친구의 다른 이야기로 이동할까요, 아니면 다른 친구의 이야기로 이동할까요?" 나무를 사용해 부모와 자녀들에게 다가가고 있어요

app/js/index.js 하단에 다음 코드를 추가합니다.

const navigateStories = direction => {
  const story = state.current_story
  const lastItemInUserStory = story.parentNode.firstElementChild
  const firstItemInUserStory = story.parentNode.lastElementChild
  const hasNextUserStory = story.parentElement.nextElementSibling
  const hasPrevUserStory = story.parentElement.previousElementSibling
}

다음은 가능한 한 자연어에 가까운 비즈니스 로직 목표입니다.

  • 탭 처리 방식 결정 <ph type="x-smartling-placeholder">
      </ph>
    • 다음/이전 뉴스가 있는 경우 해당 뉴스를 표시합니다.
    • 친구의 마지막/첫 번째 이야기인 경우 새 친구를 보여줍니다.
    • 해당 방향으로 진행할 스토리가 없는 경우 아무 조치도 취하지 않습니다.
  • 새로운 현재 스토리를 state에 저장합니다.

강조표시된 코드를 navigateStories 함수에 추가합니다.

const navigateStories = direction => {
  const story = state.current_story
  const lastItemInUserStory = story.parentNode.firstElementChild
  const firstItemInUserStory = story.parentNode.lastElementChild
  const hasNextUserStory = story.parentElement.nextElementSibling
  const hasPrevUserStory = story.parentElement.previousElementSibling

  if (direction === 'next') {
    if (lastItemInUserStory === story && !hasNextUserStory)
      return
    else if (lastItemInUserStory === story && hasNextUserStory) {
      state.current_story = story.parentElement.nextElementSibling.lastElementChild
      story.parentElement.nextElementSibling.scrollIntoView({
        behavior: 'smooth'
      })
    }
    else {
      story.classList.add('seen')
      state.current_story = story.previousElementSibling
    }
  }
  else if(direction === 'prev') {
    if (firstItemInUserStory === story && !hasPrevUserStory)
      return
    else if (firstItemInUserStory === story && hasPrevUserStory) {
      state.current_story = story.parentElement.previousElementSibling.firstElementChild
      story.parentElement.previousElementSibling.scrollIntoView({
        behavior: 'smooth'
      })
    }
    else {
      story.nextElementSibling.classList.remove('seen')
      state.current_story = story.nextElementSibling
    }
  }
}

사용해 보기

  • 사이트를 미리 보려면 앱 보기를 누릅니다. 그런 다음 전체 화면 전체 화면입니다.

결론

지금까지 구성 요소와 관련된 요구 사항을 모두 살펴봤습니다. 자유롭게 빌드하거나 데이터로 구동하고, 일반적으로 직접 만들어 보세요!