이제 Article List 화면에 GET /article
로 가져온 실제 데이터를 적용해보자.
그전에 먼저 실제 사용할 데이터를 만들어야겠다.
현재 이 블로그 사이트 - velog 에 작성한 시리즈 & 글을 수동으로.... 추가했다.
혹시나 velog.io 에서 제공하는 Restful API 가 있나 찾아봤는데, 없는듯?? 내가 못찾은 걸 수도 있고...
여튼, 먼저 "프로그래머스 문제 풀이", "블로그 직접 만들기 대작전" 두개의 메뉴를 생성했다.
그리고 글을 하나하나... 수동으로... 생성함.
어차피 velog 나 만들고 있는 blog 나 markdown 기반으로 작성한 거기 때문에 글 생성 자체는 어렵지 않았음. (뭐 애초부터 velog 글 옮길 생각이었기 때문에 markdown editor 를 사용한 거긴 하지.)
velog 글 수정 화면으로 이동 | 본문 markdown 복사 | 블로그 editor 에 붙여넣고 저장 |
---|---|---|
![]() | ![]() | ![]() |
여튼, 지금 velog 에 올린 글 모두 저쪽으로 복사 완료했다.
아직 GET /article
호출 전이기 때문에 글들은 미노출이지만, 메뉴들은 잘 노출되는 것 확인했다.
이제 ArticleList.svelte
, ArticleListItem.svelte
에 GET /article
을 호출하고 가져온 데이터를 화면에 노출하는 로직을 추가하자. (ArticleList.svelte
, ArticleListItem.svelte
에 대해서는 이전 글 참조하삼)
ArticleList.svelte
ArticleList.svelte
에 GET /article
를 호출하고 articles
라는 변수에 결과값을 저장하는 로직을 추가했다.
추가로 ArticleList.svelte
자체가 현재 포커싱된 메뉴에 속한 글 리스트를 보여주는 컴포넌트이기 때문에, API 호출시 현재 포커싱된 메뉴의 _id
을 알아내는 로직도 같이 추가.
<LayoutGrid>
<Cell spanDevices={{ desktop: 12, tablet: 8, phone: 4 }}>
<div><h1>menu > menu > ...</h1></div>
</Cell>
{#each articles as article, i}
<Cell spanDevices={{ desktop: 3, tablet: 4, phone: 4 }}>
<ArticleListItem article={article}></ArticleListItem>
</Cell>
{/each}
</LayoutGrid>
<script>
...
import { getArticle } from '../../api/article.js';
import { location } from 'svelte-spa-router';
import menu from '../../store/menu.js';
let articles = [];
const getMenusByUrl = () => {
const names = decodeURIComponent($location).split('/');
let result = [];
let tempMenus = $menu;
while (names.length > 0) {
const name = names.shift();
for (let i = 0; i < tempMenus.length; i++) {
if (tempMenus[i].name === name) {
result.push(tempMenus[i]);
tempMenus = tempMenus[i].children;
break;
}
}
}
return result;
};
const init = async () => {
let parentMenu = getMenusByUrl().pop();
if (!parentMenu) {
return;
}
const response = await getArticle({ parent: parentMenu?._id });
if (response.success) {
articles = response.data;
dispatch('routeEvent', { name: 'moveTopMain' });
}
};
...
</script>
init()
을 호출하면 현재 포커스 메뉴를 알아내 GET /articles
를 호출해 articles
를 초기화한다.
그럼 init()
은 언제 호출해야 하나?
이 블로그는 url 기반으로 메뉴 포커싱이 바뀔때마다 url 이 바뀐다. 즉, url 이 변경될때마다 호출해주면 됨.
추가로 맨 처음 접속시에는 url listener 가 호출되도 메뉴 데이터가 없기 때문에, 메뉴 데이터 변경될때에도 init()
을 호출하도록 한다. (어차피 GET /menu
는 맨 처음 한번만 호출되기 때문에 해당 listener 도 한번만 호출됨)
<script>
...
menu.subscribe(() => {
init();
});
location.subscribe(() => {
init();
});
</script>
API 호출이랑은 상관없지만, 수정하는김에 모든 부모 메뉴들의 name 을 노출하는 로직도 추가하자.
<LayoutGrid>
<Cell spanDevices={{ desktop: 12, tablet: 8, phone: 4 }}>
<div><h1>{allMenuName}</h1></div>
</Cell>
...
</LayoutGrid>
<script>
...
let allMenuName = '';
const init = async () => {
const parents = getMenusByUrl();
if (parents.length === 0) {
return;
}
allMenuName = parents.reduce((acc, cur) => {
if (acc !== '') {
acc += ' > '
}
return acc + cur.name;
}, '');
...
};
</script>
이제 API 호출로 가져온 데이터를 화면에 노출하도록 ArticleListItem.svelte
를 수정하자.
ArticleListItem.svelte
사실 ArticleListItem.svelte
에는 수정할게 많진 않다.
test 데이터 를 제거하고 건네받은 article 데이터를 사용하도록 하면 됨. (+ 자잘한 버그 수정)
<div class="article-list-item-cell-container">
<div class="article-list-item-cell-image-box">
<img alt="..." src={image} on:error={() => {image = defaultImage}} on:click={moveDetail} />
</div>
<div class="article-list-item-cell-text-box">
<div>
<h3 style="color: greenyellow;" on:click={moveDetail}>{article.title}</h3>
<p on:click={moveDetail}>{contents}{@html '...'}</p>
<span>{created}</span>
</div>
</div>
</div>
<script>
import { location, push } from 'svelte-spa-router';
export let article;
let defaultImage = 'https://static-cdn.jtvnw.net/jtv_user_pictures/dde955e8-5fae-44dc-98db-79b3b14afea2-profile_image-70x70.png';
let image = defaultImage;
let contents = 'Contents....';
let created = '';
const moveDetail = () => {
push(`${decodeURIComponent($location)}/${article._id}`);
};
$: {
image = defaultImage;
contents = 'Contents....';
created = '';
// article.contents 내에서 첫번째 이미지 src 추출해서 image 로 설정
const reg = new RegExp(/(<img[^>]+src\s*=\s*[\"']?([^>\"']+)[\"']?[^>]*>)/, 'i');
const regResult = reg.exec(article.contents);
if (regResult?.length > 1) {
image = regResult[2];
}
// <img>, <h1~5>, <div>, <blockquote>, <table>, <code> 은 그냥 빈스트링으로 replace
// <br>, <li> 는 ' ' 로 replace
// 나머지 태그들 - <~> 전부 빈스트링으로 replace
contents = article.contents
.replace(/(<img[^>]*>)|(<h1[^>]*>.*<\/h1>)|(<h2[^>]*>.*<\/h2>)|(<h3[^>]*>.*<\/h3>)|(<h4[^>]*>.*<\/h4>)|(<h5[^>]*>.*<\/h5>)|(<h6[^>]*>.*<\/h6>)|(<div[^>]*>(?!<div).+?<\/div>)|(<blockquote[^>]*>(?!<blockquote).+?<\/blockquote>)|(<table[^>]*>(?!<table).+?<\/table>)|(<code[^>]*>(?!<code).+?<\/code>)/gi, '')
.replace(/(<br[^>]*>)|(<li[^>]*>)/gi, ' ')
.replace(/<[^>]*>?/gi, '');
const date = new Date(article.created);
created = `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일 ${date.getHours()}시 ${date.getMinutes()}분 ${date.getSeconds()}초`;
};
</script>
...
근데 왜 한번 변환시키는 변수 - image
, contents
, created
는 $ {...}
안에서 세팅할까?
메뉴 포커싱을 왔다 갔다 하면 기존에 렌더링 되어 있는 <ArticleListItem />
가 삭제되고 새로 만들어지는게 아니라 재활용된다.
<ArticleListItem />
가 10개 생성 & 렌더링<ArticleListItem />
에 새로운 article
데이터가 전달됨<ArticleListItem />
가 10개 생성 & 렌더링<ArticleListItem />
에 새로운 article
데이터가 전달됨<ArticleListItem />
는 삭제됨기존에 존재하는 컴포넌트에 데이터만 바뀌는 경우가 존재하기 때문에, 데이터 변화 감지를 위해 $ {...}
안에서 세팅함 ㅎㅎ
지금까지 개발했을때 기대되로 잘 동작하는 것 확인했다.
다만 scroll 관련 bug 가 있는 것으로 확인 (error 는 아님)
메뉴 포커싱 후 리스트 scroll | 다른 메뉴 포커싱 (scroll 이 유지됨...) |
---|---|
![]() | ![]() |
아마 위에서 언급한 컴포넌트 재활용 덕분에 상위 element 의 scrollTop 값이 유지되어 생긴 이슈라 판단됨.
해서 articles
가 바뀌면 상위 컴포넌트에 사용자 정의 이벤트를 전달해서 상위 element 의 scrollTop 을 초기화하는 로직을 추가해야 한다.
근데 문제는 <Router />
는 기존의 방식대로 사용자 정의 이벤트를 전달할 수 없고, svelte-spa-router
에서 가이드한 대로 해줘야 한다. (아래 링크 참조)
https://github.com/ItalyPaleAle/svelte-spa-router/blob/master/Advanced%20Usage.md#routeevent-event
결론적으로 <Router />
에서 사용자 정의 이벤트를 사용하려면 routeEvent
란 이름으로만 가능하다.
ArticleList.svelte
...
<script>
...
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const init = async () => {
...
if (response.success) {
articles = response.data;
dispatch('routeEvent', { name: 'moveTopMain' });
}
};
</script>
Main.svelte
<div class="blog-drawer-container">
<Aside></Aside>
<AppContent class="app-content">
<main class="main-content" bind:this={main}>
<Router {routes} on:routeEvent={onEvent} />
</main>
</AppContent>
</div>
<script>
...
let main;
const onEvent = e => {
const { name } = e.detail;
switch (name) {
case 'moveTopMain':
main.scrollTop = 0;
break;
}
};
</script>
...
지금까지 개발한 부분 잘 동작하는 것 확인했다 ㅎㅎ
이제는 글 상세 화면을 만들어야 할때... 언제 하나...