🖥Frontend/Nuxt.JS

Nuxt new fetch 사용해보며 dev.to 클론 빌드

코너(Corner) 2022. 12. 19.
반응형

Nuxt new fetch 튜토리얼 번역 1탄

Nuxt new fetch로 dev.to 클론 빌드

Nuxt 및 Dev API를 사용하여 lazy loading, placeholders, caching, 그리고 최신 뉴모픽 디자인 UI를 사용하여 엄청나게 빠른 앱을 빌드해 보겠습니다.

데모 보기 / 소스

이 문서는 릴리즈 v2.12에 도입된 새로운 Nuxt fetch기능의 사용 사례와 놀라운 기능을 보여주고 자신의 프로젝트에 그 기능을 적용하는 방법을 보여주기 위한 것입니다. 새로운 fetch기술에 대한 심층적인 기술 분석 및 세부 정보는 Krutie Patel의 기사에서 확인할 수 있습니다.

fetch훅(hook)을 사용하여 dev.to 클론을 빌드하는 방법에 대한 대략적인 개요는 다음과 같습니다.

  • fetchState클라이언트 측에서 데이터를 가져오는 동안 placeholders를 표시하는데 사용
  • 이미 방문한 페이지에서 API 요청을 효율적으로 캐싱하기 위해 keep-aliveactivated 훅을 사용합니다.
  • this.$fetch()fetch 훅을 재사용합니다.
  • fetchOnServer값을 설정하여 서버 측에서 데이터를 렌더링해야 하는지 여부를 설정합니다.
  • fetch 훅에서 오류를 처리할 방법을 찾습니다.
목차 >

개발 API

2019년 9월에 DEV는 사용자 및 기타 리소스 데이터에 접근하는데 사용할 수 있는 공개API를 열었습니다.

아직 베타 버전이므로 향후 변경되거나 일부 기능이 예상대로 작동하지 않을 수 있습니다.

DEV 클론을 생성하기 위해 다음과 같은 API 엔드포인트에 관심이 있습니다.

💡 API와 Endpoint의 차이 한 줄 정리

API가 두 시스템(어플리케이션)이 상호작용할 수 있게 하는 프로토콜의 총집합이라면,

Endpoint는 API가 서버에서 리소스에 접근할 수 있도록 가능하게 하는 URL이라 할 수 있다.

  • getArticles : tag, state, top, username으로 필터링되고 page라는 매개 변수로 페이지 지정된 항목들에 접근합니다
  • getArticleById : 항목 컨텐츠에 접근하기 위함
  • getUser : 사용자 데이터에 접근
  • getCommentsByArticledId : 항목과 관련된 댓글 가져오기

간단하고 DEV API와의 통신을 위해 기본 JavaScript Fetch API를 사용합니다.

프로젝트 설정

숙련된 개발자라면 이 부분을 건너뛰고 바로 본론으로 들어갈 수 있습니다.

Node와 npm이 설치되어 있는지 확인하세요. create-nuxt-app 프로젝트를 초기화하는 데 사용할 것 이므로 터미널에 다음 명령어를 입력하세요.

npx create-nuxt-app nuxt-dev-to-clone
# leave the default answers for each question

image-20221219140627684

cd nuxt-dev-to-clone으로 경로로 이동한 뒤, npm run dev로 프로젝트를 실행하세요. Nuxt 앱이 http://loclahost:3000에서 실행 됩니다.

필요한 패키지를 설치하고 다음에 앱을 빌드하는 방법에 대해 알아보겠습니다.

CSS 스타일

스타일링을 위해 가장 일반적인 CSS 전처리기 Sass/SCSS를 사용하고 Vue.js Scoped CSS 기능을 활용하여 구성 요소 스타일을 캡슐화합니다. Nuxt에서 Sass/SCSS를 사용하려면 다음을 실행합니다.

yarn add sass sass-loader@10 -D 
# ||
npm install -D sass sass-loader@10

또한 각 파일에서 명령문을 사용할 필요없이 모든 Vue 파일에서 SCSS 변수에 정의된 디자인 토큰을 사용하는 데 도움이 되는 @nuxtjs/style-resources모듈을 @import하여 사용할 것입니다.

yarn add @nuxtjs/style-resources

이제 nuxt.config.js에서 이 코드를 추가하여 사용하도록 지정합니다.

buildModules: ['@nuxtjs/style-resources']

buildModulesmodules vs buildModules문서에서 자세히 알아볼 수 있습니다, 이 모듈에 대한 자세한내용은 여기서 확인하세요.

스타일 파일 토큰을 SCSS 변수로 정의하고, 추가하여 로드하도록 추가 해줍니다.

~/assets/styles/tokens.scss, @nuxtjs/style-resources

nuxt.config.js

styleResources: {
    scss: ['~/assets/styles/tokens.scss']
}

우리의 스타일 토큰 파일은 이제 모든 Vue의 컴포넌트에 SCSS 변수를 통해 사용할 수 있습니다.

UI 디자인

기존의 DEV 디자인과 레이아웃을 그대로 복사하는 것은 다소 지루할 것이므로 조금 테스트해보는 것이 좋겠습니다. 새로운 UI 트렌드인 뉴모피즘에 대해 들어보시지 않았다면 여기에서 자세한 내용을 읽어보시길 권유합니다.

우리는 Dribble shots을 찾을 수 있지만 여전히 뉴모피즘 스타일 인터페이스로 구축된 실제 웹 앱의 몇 가지 예일 뿐이므로 CSS와 Vue.js로 다시 만들 수 있는 기회를 놓칠 수 없습니다.

이 애플리케이션의 스타일링 측면을 자세히 설명하지는 않겠지만 관심이 있는 경우 CSS Tricks에서 뉴모피즘 및 CSS에 대한 이 멋진 글을 읽을 수 있습니다.

SVG 아이콘

SVG 아이콘의 경우 @nuxt/svg를 사용할 수 있습니다. 이 모듈을 사용하면 SVG 소스를 한 곳에 보관하고 많은 SVG 코드로 Vue 템플릿 마크업을 흐리지 않으면서 .svg확장자 파일을 인라인으로 가져올 수 있습니다.

yarn add @nuxtjs/svg -D

nuxt.config.js

buildModules: ['@nuxtjs/svg', '@nuxtjs/style-resources']

종속성

프론트엔드 앱을 빠르고 간단하게 유지하기 위해 Vue.js 핵심 두 종속성만 사용하겠습니다.

두 개의 파일을 생성하여 Nuxt 플러그인으로 추가합니다.

yarn add vue-content-placeholders
yarn add vue-observe-visibility

/plugins/vue-observe-visibility.client.js

import Vue from 'vue'
import VueObserveVisibility from 'vue-observe-visibility'

Vue.use(VueObserveVisibility)

/plugins/vue-placeholders.js

import Vue from 'vue'
import VueContentPlaceholders from 'vue-content-placeholders'

Vue.use(VueContentPlaceholders)

그리고 nuxt.config.js에 추가합니다.

plugins: [
  '~/plugins/vue-placeholders.js',
  '~/plugins/vue-observe-visibility.client.js'
]

애플리케이션 개발

이제 마침내 Nuxt와 new Fetch로 구동되는 DEV 클론 개발을 시작할 수 있습니다.

URL 구조

간단한 앱의 DEV URL 구조를 모방해 보겠습니다. 페이지폴더는 다음과 같아야합니다.

├── index.vue
├── t
│   └── _tag.vue
├── top.vue
└── _username
    ├── _article.vue
    └── index.vue

image-20221219143157920

2개의 정적 페이지가 있습니다.

  • index.vue : Nuxt에 대한 최신 항목들이 나열됩니다.
  • top.vue : 작년 기간 동안 인기 있었던 게시물

나머지 앱 URL의 경우 편리한 Nuxt 파일 기반 다이나믹 페이징(동적 경로) 기능을 사용하여 이러한 파일 구조를 생성하여 필요한 페이지를 스캐폴딩(scaffold)합니다.

  • _username/index.vue : 자신이 게시한 글 목록이 있는 사용자 프로필 페이지
  • _username/_article.vue : 여기에서 게시글, 작성자 프로필 및 댓글이 렌더링됩니다.
  • t/_tag.vue : DEV에 존재하는 모든 태그별 최고 게시글 목록

아주 간단합니다.

keep-aliveactivated 훅을 사용한 캐싱 요청

new fetch 기능의 가장 멋진 기능 중 하나는 이미 방문한 페이지를 불러올 때 fetch를 저장할 명령어 keep-alive와 함께 작동하는 기능입니다.

이것을 layouts/default.vue 레이아웃 vue 컴포넌트에 적용해봅니다.

<template>
    <nuxt keep-alive />
</template>

fetch의 명령문을 사용하면 첫 번째 페이지 방문시에만 발동되며 Nuxt는 렌더링된 컴포넌트를 메모리에 저장하고 이후 방문할 때마다 캐시에서 재사용됩니다.

또한 Nuxt는 캐시하려는 컴포넌트의 수를 설정할 수 있는 keep-alive속성과 캐시의 TTL(Time to Live)을 제어할 수 있는 훅을 통해 세밀한 제어를 제공합니다.

$fetch를 페이지 컴포넌트에서 사용

fetch 기능 자체에 대해 살펴봅니다.

localhost:3000 현재 최종 결과에서 볼 수 있듯이 기본적으로 동일한 코드를 재사용하는 3개의 페이지 컴포넌트가 있습니다. 바로 index.vue, top.vue, t/_tag.vue 페이지 컴포넌트입니다. 단순히 글 미리보기 카드 목록을 렌더링합니다.

pages/index.vue

<template>
    <div class="page-wrapper">
        <div class="article-cards-wrapper">
            <article-card-block
                v-for="(article) in articles"
                :key="article.id"
                :article="article"
                class="article-card-block"
            />
        </div>
    </div>
</template>

<script>
import Vue from 'vue'
import ArticleCardBlock from '~/components/ArticleCardBlock.vue'

export default Vue.extend({
    name: 'IndexPage',
    components: {
        ArticleCardBlock
    },
    data() {
        return {
            currentPage: 1,
            articles: []
        }
    },
    async fetch() {
        const articles = await fetch(
            `https://dev.to/api/articles?tag=nuxt&state=rising&page=${this.currentPage}`
        ).then(res => res.json())

        this.articles = this.articles.concat(articles)
    }
})
</script>

코드가 있는데, 스크립트 코드에 async fetch() { ... } 함수를 주의깊게 살펴봅시다.

/articles에서는 API가 이해하는 쿼리 매개변수를 사용하여 DEV 엔드포인트에 요청합니다. fetch 훅을 DEV API에 요청을 보낸 다음 res.json()를 응답 받습니다.

또한 new fetch 훅은 Vuex 스토어 액션을 전달하거나 상태를 저장하기 위한 커밋하는 데만 사용되지 않습니다. 이제 this 컨텍스트에 접근할 수 있으며 컴포넌트의 데이터를 직접 변형할 수 있습니다. 매우 중요한 기능이며, 이전 문서에서 fetch에 대한 자세한 내용을 읽을 수 있습니다.

이제 ArticleCardBlock컴포넌트에서 Props를 전달받고 article데이터를 멋잇게 컴포넌트에 렌더링하도록 UI 스타일링을 시작합니다.

components/ArticleCardBlock.vue

<template>
  <nuxt-link
    :to="{ name: 'username-article', params: { username: article.user.username, article: article.id } }"
    tag="article"
  >
    <div class="image-wrapper">
      <img
        v-if="article.cover_image"
        :src="article.cover_image"
        :alt="article.title"
      />
      <img v-else :src="article.social_image" :alt="article.title" />
    </div>
    <div class="content">
      <nuxt-link
        :to="{name: 'username-article', params: { username: article.user.username, article: article.id } }"
      >
        <h1>{{ article.title }}</h1>
      </nuxt-link>
      <div class="tags">
        <nuxt-link
          v-for="tag in article.tag_list"
          :key="tag"
          :to="{ name: 't-tag', params: { tag } }"
          class="tag"
        >
          #{{ tag }}
        </nuxt-link>
      </div>
      <div class="meta">
        <div class="scl">
          <span>
            <heart-icon />
            {{ article.positive_reactions_count }}
          </span>
          <span>
            <comments-icon />
            {{ article.comments_count }}
          </span>
        </div>
        <time>{{ article.readable_publish_date }}</time>
      </div>
    </div>
  </nuxt-link>
</template>

<script>
  import HeartIcon from '@/assets/icons/heart.svg?inline'
  import CommentsIcon from '@/assets/icons/comments.svg?inline'

  export default {
    components: {
      HeartIcon,
      CommentsIcon
    },
    props: {
      article: {
        type: Object,
        default: null
      }
    }
  }
</script>

this.$fetch()fetch재사용

이미 DEV에서 가져온 게시글 목록을 표시해야 하지만 이 API를 충분히 활용하고 있지 않은 것 같습니다. 게시글 목록에 lazy loading을 추가하고 이 API에서 제공하는 페이징 처리 변수를 사용합니다. 페이지 하단으로 스크롤 할 떄마다 새로운 기사 청크 데이터를 가져와서 렌더링하게 될 것입니다.

다음 페이지를 가져올 시기를 효율적으로 감지하려면 Intersection Observer API를 사용하는 것이 좋습니다. 이를 위해 우리는 기본적으로 이전에 설치 된 vue-oberserve-visibility Vue 플러그인을 사용하고 페이지에서 요소가 표시되거나 숨겨지는 시기를 감지합니다. 이 플러그인은 모든 요소에 명령문을 사용할 수 있는 가능성을 제공하므로 마지막 <article-card-block> 컴포넌트에 v-observe-visibility를 추가해보겠습니다.

index.vue

<template>
    <div class="page-wrapper">
        <div class="article-cards-wrapper">
            <article-card-block
                v-for="(article, i) in articles"
                :key="article.id"
                v-observe-visibility="i === articles.length - 1 ? lazyLoadArticles : false"
                :article="article"
                class="article-card-block"
            />
        </div>
    </div>
</template>

methods:안에 아래 함수 코드를 적용해 줍니다.

lazyLoadArticles(isVisible) {
  if (isVisible) {
    if (this.currentPage < 5) {
      this.currentPage++
      this.$fetch()
    }
  }
}

그리고 여기서 우리는 new fetch의 능력을 알 수 있습니다. $fetch 함수로 재사용하고, lazy loading이 될 때 마다 다음 페이지를 가져올 수 있습니다.

$fetchState로 placeholders 적용하기

이전 코드를 이미 적용했고 index.vue, top.vuet/_tag.vue 페이지 컴포넌트 간에 클라이언트 측 탐색을 시도한 경우 API 요청이 완료되기를 기다리는 동안 빈 페이지가 잠시 표시되는 것을 느꼈을 겁니다. 이것은 의도된 동작이며 페이지 탐색 전에 asyncDatafetch 훅은 다릅니다.

$fetchState.pending 훅이 현명하게 제공한 덕분에 fetch이 플래그를 사용하여 가져오기가 클라이언트 측에서 호출될 때 placeholders를 표시할 수 있습니다. vue-content-placeholders플러그인이 사용됩니다.

index.vue를 다시 수정합니다.

<template>
    <div class="page-wrapper">
        <template v-if="$fetchState.pending">
            <div class="article-cards-wrapper">
                <content-placeholders v-for="p in 30" :key="p" rounded class="article-card-block">
                    <content-placeholders-img />
                    <content-placeholders-text :lines="3" />
                </content-placeholders>
            </div>
        </template>
        <template v-else-if="$fetchState.error">
            <p>{{ $fetchState.error.message }}</p>
        </template>
        <template v-else>
            <div class="article-cards-wrapper">
                <article-card-block v-for="(article, i) in articles" :key="article.id" v-observe-visibility="
                  i === articles.length - 1 ? lazyLoadArticles : false
                " :article="article" class="article-card-block" />
            </div>
        </template>
    </div>
</template>

우리는 vue-content-placeholders<article-card-block> 컴포넌트 디자인을 모방하고 소스 코드에서 볼 수 있듯이 fetch훅을 사용하는 거의 모든 컴포넌트에서 사용되므로 코드의 해당 부분에 더 이상 주의를 기울이지 않아도 됩니다.

fetch다른 컴포넌트에서 사용

이건 아마도 new fetch 훅의 가장 흥미로운 기능일 것입니다. 이제 *SSR에서 데이터 손상에 대한 걱정 없이 모든 Vue 컴포넌트에서 fetch 훅을 사용할 수 있습니다. *

이것은 비동기 API 호출 및 컴포넌트를 조회하는 방법에 대한 골칫거리가 훨씬 적어졌다는 것에 큰 의미가 있습니다.

이 좋아진 기능을 살펴보기 위해 _username/_article.vue 페이지 컴포넌트로 이동해서 코드를 작성합니다.

<template>
  <div class="page-wrapper">
    <div class="article-content-wrapper">
      <article-block class="article-block" />
      <div class="aside-username-wrapper">
        <aside-username-block class="aside-username-block" />
      </div>
    </div>
    <comments-block class="comments-block" />
  </div>
</template>

<script>
import ArticleBlock from '@/components/blocks/ArticleBlock'
import CommentsBlock from '@/components/blocks/CommentsBlock'
import AsideUsernameBlock from '@/components/blocks/AsideUsernameBlock'

export default {
  components: {
    ArticleBlock,
    CommentsBlock,
    AsideUsernameBlock
  }
}
</script>

components/ ArticleBlock.vue,CommentsBlock.vue,AsideUsernameBlock.vueGithub 소스코드에 공유 하겠습니다.

위 코드에서 여기서는 데이터 가져오기를 전혀 볼 수 없으며 <article-block />, <aside-username-block />, commnets-blocks /> 의 3가지 컴포넌트로 구성된 템플릿 레이아웃만 볼 수 있습니다. 그리고 각 컴포넌트에는 자체 fetch 훅이 있습니다.

이전 fetch 또는 현재 asyncData 이전 버전에서는 다른 DEV 엔드포인트에 대한 세가지 요청을 모두 수행한 다음 각 컴포넌트에 소품으로 전달해야 합니다. 그러나 이제 이러한 컴포넌트가 완전히 캡슐화 되었습니다.

페이지 컴포넌트에서 사용하는 것처럼 fetch를 사용합니다. <article-block />

async fetch() {
    const article = await fetch(
      `https://dev.to/api/articles/${this.$route.params.article}`
    ).then((res) => res.json())

    if (article.id && article.user.username === this.$route.params.username) {
      this.article = article
      this.$store.commit('SET_CURRENT_ARTICLE', this.article)
    } else {
      // set status code on server
      if (process.server) {
        this.$nuxt.context.res.statusCode = 404
      }
      throw new Error('Article not found')
    }
},

이제 캐싱에 대한 목차에서 fetch TTL 관리에 사용할 수 있는 activated 훅이 있다고 언급한 것을 기억해야 합니다. activated 훅의 사용법의 예 :

<script>
export default {
    ...
    activated() {
        if (this.$fetchState.timestamp <= Date.now() - 60000) {
          this.$fetch()
        }
    },
}
</script>

이 코드를 사용하면 마지막 가져오기가 60초 이상 전이면 가져오기를 다시 호출합니다. 이 기간 내의 다른 모든 요청은 캐시됩니다.

컴포넌트에서 호출 되는 또 다른 fetch 기능의 사용법도 있습니다. 댓글은 사용자가 생성하고 관련이 없거나 스팸일 수 있기 때문에 서버 측에서 이 콘텐츠를 렌더링하고 싶지 않을 수도 있습니다. 이 콘텐츠 블록에는 SEO가 필요하지도 않습니다. 이제 fetchOnServer의 도움으로 다음과 같은 제어를 할 수 있습니다. <comments-block>

<script>
export default {
    // ...
    async fetch() {
        this.comments = await fetch(
        `https://dev.to/api/comments?a_id=${this.$route.params.article}`
        ).then((res) => res.json())
    },
    fetchOnServer: false
}
</script>

에러 핸들링

마지막으로 언급해야할 것은 에러 처리입니다. 위에서 에러 처리를 사용한 것을 이미 보았을 거지만, 이 중요한 주제에 대하여 자세히 살펴봅니다.

fetch컴포넌트 레벨에서 처리되며, 서버 사이드 렌더링을 할 때 부모(가상) dom 트리는 컴포넌트를 렌더링 할 때 이미 렌더링되어 있으므로 $nuxt.error(..)를 호출하여 변경할 수 없으며, 대신에 컴포넌트 레벨에 오류를 처리해야 합니다.

$fetchState.error 훅에서 오류가 발생하면 설정 fetch 되므로 템플릿에서 오류 메시지를 표시하는 데 사용할 수 있습니다.

layouts/error.vue를 생성하고 아래와 같이 코드를 작성합니다.

<template>
  <div class=“page-wrapper”>
    <template v-if="$fetchState.pending">
      <!— placeholders goes here —>
    </template>
    <template v-else-if=“$fetchState.error">
      <p>{{ $fetchState.error.message }}</p>
    </template>
    <template v-else>
      <!— fetched content goes here —>
    </template>
  </div>
</template>

그런 다음 fetch 훅에서 정의된 작성자에 해당하는 글을 찾지 못하면 오류를 발생시킵니다.

async fetch() {
  const article = await fetch(
    `https://dev.to/api/articles/${this.$route.params.article}`
  ).then((res) => res.json())

  if (article.id && article.user.username === this.$route.params.username) {
    this.article = article
  } else {
    // set status code on server
    if (process.server) {
      this.$nuxt.context.res.statusCode = 404
    }
    throw new Error('Article not found')
  }
}

올바른 SEO를 위해 SSR HTTP 상태 코드 this.$nuxt.context.res.statusCode = 404를 사용하고, process.server 일 때 담아줍니다.

결론

이 글에는 Nuxt의 새로운 fetch훅을 살펴보고 직접 사용하여 기본 DEV 콘텐츠 기능 및 구조로 앱을 만들었습니다. 자신만의 dev.to 버전을 구축할 수 있는 영감을 얻으셨길 바랍니다.

더 나은 예제 또는 기능, 코드를 보려면
github
에서 확인하는 것을 잊지마세요!

[nuxtjs.org 튜토리얼 번역의 참고 원본 문서](

반응형

댓글