Article List 화면 - API 호출 및 버그 수정

손대중·2022년 8월 12일
0

이제 Article List 화면에 GET /article 로 가져온 실제 데이터를 적용해보자.

그전에 먼저 실제 사용할 데이터를 만들어야겠다.

실제 사용할 메뉴 & 글 데이터 추가

현재 이 블로그 사이트 - velog 에 작성한 시리즈 & 글을 수동으로.... 추가했다.

혹시나 velog.io 에서 제공하는 Restful API 가 있나 찾아봤는데, 없는듯?? 내가 못찾은 걸 수도 있고...

여튼, 먼저 "프로그래머스 문제 풀이", "블로그 직접 만들기 대작전" 두개의 메뉴를 생성했다.

그리고 글을 하나하나... 수동으로... 생성함.

어차피 velog 나 만들고 있는 blog 나 markdown 기반으로 작성한 거기 때문에 글 생성 자체는 어렵지 않았음. (뭐 애초부터 velog 글 옮길 생각이었기 때문에 markdown editor 를 사용한 거긴 하지.)

velog 글 수정 화면으로 이동본문 markdown 복사블로그 editor 에 붙여넣고 저장

여튼, 지금 velog 에 올린 글 모두 저쪽으로 복사 완료했다.

아직 GET /article 호출 전이기 때문에 글들은 미노출이지만, 메뉴들은 잘 노출되는 것 확인했다.

이제 ArticleList.svelte, ArticleListItem.svelteGET /article 을 호출하고 가져온 데이터를 화면에 노출하는 로직을 추가하자. (ArticleList.svelte, ArticleListItem.svelte 에 대해서는 이전 글 참조하삼)

ArticleList.svelte

ArticleList.svelteGET /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 /> 가 삭제되고 새로 만들어지는게 아니라 재활용된다.


  1. "프로그래머스 문제 풀이" 메뉴 포커싱
    • 데이터 length 가 10 개라 <ArticleListItem /> 가 10개 생성 & 렌더링
  2. "블로그 직접 만들기 대작전" 메뉴 포커싱
    • 데이터 length 가 20 개임
      • 기존에 존재한 10개의 <ArticleListItem /> 에 새로운 article 데이터가 전달됨
      • 나머지 데이터에 대해, <ArticleListItem /> 가 10개 생성 & 렌더링
  3. 다시 "프로그래머스 문제 풀이" 메뉴 포커싱
    • 데이터 length 가 10 개임
      • 기존에 존재한 10개의 <ArticleListItem /> 에 새로운 article 데이터가 전달됨
      • 나머지 <ArticleListItem /> 는 삭제됨

기존에 존재하는 컴포넌트에 데이터만 바뀌는 경우가 존재하기 때문에, 데이터 변화 감지를 위해 $ {...} 안에서 세팅함 ㅎㅎ

scroll 버그 수정

지금까지 개발했을때 기대되로 잘 동작하는 것 확인했다.

다만 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>
    ...

지금까지 개발한 부분 잘 동작하는 것 확인했다 ㅎㅎ

이제는 글 상세 화면을 만들어야 할때... 언제 하나...

0개의 댓글