Nuxt new fetch 사용해보며 dev.to 클론 빌드
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-alive
및activated
훅을 사용합니다. 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
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']
buildModules
는modules 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 핵심 두 종속성만 사용하겠습니다.
- Guillaume Chau - vue-observe-visibility는 IntersectionObserver를 사용하여 뷰 포트에서 요소를 효과적으로 ㄱ마지하고 지연 로딩을 트리거합니다.
- vue-content-placeholders는 콘텐츠를 가져오는 동안 UI요소에 대한 멋진 애니메이션 placeholders를 보여줍니다.
두 개의 파일을 생성하여 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
2개의 정적 페이지가 있습니다.
index.vue
: Nuxt에 대한 최신 항목들이 나열됩니다.top.vue
: 작년 기간 동안 인기 있었던 게시물
나머지 앱 URL의 경우 편리한 Nuxt 파일 기반 다이나믹 페이징(동적 경로) 기능을 사용하여 이러한 파일 구조를 생성하여 필요한 페이지를 스캐폴딩(scaffold)합니다.
_username/index.vue
: 자신이 게시한 글 목록이 있는 사용자 프로필 페이지_username/_article.vue
: 여기에서 게시글, 작성자 프로필 및 댓글이 렌더링됩니다.t/_tag.vue
: DEV에 존재하는 모든 태그별 최고 게시글 목록
아주 간단합니다.
keep-alive
및 activated
훅을 사용한 캐싱 요청
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.vue
및 t/_tag.vue
페이지 컴포넌트 간에 클라이언트 측 탐색을 시도한 경우 API 요청이 완료되기를 기다리는 동안 빈 페이지가 잠시 표시되는 것을 느꼈을 겁니다. 이것은 의도된 동작이며 페이지 탐색 전에 asyncData
와 fetch
훅은 다릅니다.
$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.vue
는 Github 소스코드에 공유 하겠습니다.
위 코드에서 여기서는 데이터 가져오기를 전혀 볼 수 없으며 <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 버전을 구축할 수 있는 영감을 얻으셨길 바랍니다.
더 나은 예제 또는 기능, 코드를 보려면
에서 확인하는 것을 잊지마세요!
[nuxtjs.org 튜토리얼 번역의 참고 원본 문서](
'🖥Frontend > Nuxt.JS' 카테고리의 다른 글
Nuxt3로 전환하게 된 이유 (0) | 2022.12.21 |
---|---|
eslint, prettier, tailwindcss Install guide for Nuxt3 release (0) | 2022.12.20 |
VueJS/NuxtJS 스토어에서 $i18n 사용에 관해 (0) | 2022.11.27 |
NuxtJS - Error 페이지 처리하기 (0) | 2022.10.23 |
Nuxt.JS - Routing (0) | 2022.10.23 |
댓글