Svelte - store 와 api

손대중·2022년 5월 4일
1

Svelte 는 자체적으로 상태 관리 수단 - store 를 제공한다.

svelte/store 에 대한 자세한 내용은 아래 링크들을 참조하숑.

내가 svelte/store 를 쓰려는 이유는?

사실 그냥 blog 화면만 만든다고 하면 크게 필요는 없을 듯한데 메뉴 생성하는 admin 화면도 만들다 보니 메뉴 생성 후 화면을 업데이트 해야 할 필요가 있다.

뭐 상태 관리 수단은 대부분의 플젝에서 필요하니까 겸사 겸사 익힐겸도 해서 한번 써 봄.

일단 svelte/store 를 쓰기 전에, 데이터를 받아올 수 있도록 REST API 를 사용하는 함수을 구현하자.

REST API 를 사용하는 함수

api/api.js 를 생성하고 fetch 를 호출하는 fetchWrap 함수를 작성하자. (어떤 api 를 호출하든 fetchWrap 함수를 호출하도록......)

const address = 'http://132.226.16.67:8080';

const getQueryString = query => {
    let str = '';

    try {
        for (let key in query) {
            if (str.length > 0) {
                str += '&';
            }

            str += (key + '=' + query[key]);
        }
    } catch (e) {
        console.error(e);
        str = '';
    }

    return str;
};

const fetchWrap = async (options) => {
    const { url, method, query, body, headers } = options;

    try {
        return (await fetch(`${address + url}?${getQueryString(query)}`, {
            method,
            headers: {
                'Content-Type': 'application/json',
                ...headers
            },
            body: JSON.stringify(body)
        })).json();
    } catch (e) {
        console.error(e);
    }
};

fetchWrap 에서는 api 호출에 필요한 옵션들을 파라미터로 받을 수 있다.

  • url
    • 말그대로 url - address 과 조합하여 호출함
  • method
    • HTTP Request Method - GET, POST, PATCH, DELETE
  • query
    • GET 방식일때 url 뒤에 붙는 query - address + url 과 조합하여 호출함
    • 최종 결과물은 string 이지만 편의를 위해 객체로 받고 getQueryString 함수에서 string 으로 변환
  • body
    • HTTP Request Body - REST API 가 요구하는 형식대로
    • 마찬가지로 string 으로 보내야 하기 때문에 JSON.stringify 를 호출해 string 으로 변환
  • header
    • 그 외 특수한 header 값이 필요한 경우를 위해

fetchWrap 아래에 바로 메뉴 관련 API 들을 호출하는 함수들을 작성했고, 해당 함수들은 다른 Component 에서 써야 하기 때문에 export 로 뺐다.

api/menu.js 같은 파일로 따로 정의 할수도 있지만... 귀찮으니 일단은 한꺼번에 작성

export const getMenu = async query => {
    return await fetchWrap({
        url: '/menu',
        method: 'GET',
        query
    });
};

export const postMenu = async body => {
    return await fetchWrap({
        url: '/menu',
        method: 'POST',
        body
    });
};

export const patchMenu = async body => {
    return await fetchWrap({
        url: '/menu',
        method: 'PATCH',
        body
    });
};

export const deleteMenu = async body => {
    return await fetchWrap({
        url: '/menu',
        method: 'DELETE',
        body
    });
}; 

호출해보니 잘된다.

이제 메뉴 데이터의 상태 관리를 할 수 있는 코드를 작성하자.

메뉴 데이터 상태 관리 (svelte/store)

store/menu.js 파일안에 메뉴 데이터 상태 관리 코드를 작성하자. (svelte/store 에 대한 자세한 사용법은 맨 위의 링크 참조)

해당 파일에서는 getMenu 를 호출하여 메뉴 데이터를 가져오고 store 에 set 하는 로직만 들어가도록 하자.

또한 외부 Component 에서는 메뉴 데이터 참조, 변화 감지, 업데이트 요청 등만 할 수 있도록 subscribe, reset 함수만 제공한다.

import { writable } from 'svelte/store';
import { getMenu } from '../api/api.js';

const { subscribe, set } = writable([]);

const reset = async () => {
    const response = await getMenu();
    if (response && response.data) {
        set(response.data);
    }
};

export default menu = { subscribe, reset };

svelte/store 에서 제공하는 writable 를 호출하면 store 가 생성되고 store 에 접근할 수 있는 set, update, subscribe 함수를 제공한다.

reset 함수에서는 getMenu 호출 후 set 을 통해 메뉴 데이터를 업데이트하고, 메뉴 데이터가 업데이트 되면 subscribe 함수를 통해 메뉴 데이터 업데이트 소식을 알린다.

subscribe 사용법을 작성하기 전에, 메뉴 데이터를 tree 구조로 만들자. 서버에서 주는 메뉴 데이터는 1차원 배열 형태이기 때문에 tree 구조로 변경해서 사용할 필요가 있음.

const getMenuTree = data => {
    const result = [];

    data.map(target => {
        // 부모 걸기
        if (target.parent) {
            target.parent = data.filter(t => t._id === target.parent)[0];
        } else {
            result.push(target);
        }

        // 자식들 걸기
        if (target.children?.length > 0) {
            target.children = data.filter(t => target.children.includes(t._id));
        }
    });

    return result;
};

const reset = async () => {
    const response = await getMenu();
    if (response && response.data) {
        set(getMenuTree(response.data));
    }
};

추가로 메뉴 데이터는 언제 init 해야 할까?

menu/menu.js 안에서 바로 해도 되긴 하는데, 아예 SPA 가 처음 시작할때 init 하는 지점을 만들어서 거기서 init 을 해보자.

  • App.svelte

    • SPA 시작점으로 init 이 필요한 로직은 여기에 두자
    import { initStore } from './store/store.js';
    
    onMount(() => {
        initStore();
    });
  • store/store.js

    • 모든 store 들의 init 지점 - 뭐 사실 필요 없을 것 같긴 한데 걍 넣음
    import menu from './menu.js';
    
    export const initStore = () => {
        menu.reset();
    };

이제 AdminMenu.svelte 에 메뉴 데이터 관련 로직을 추가하면서 이 메뉴 데이터를 어떻게 참조하고 구독하는지 알아보자.

AdminMenu.svelte

store/menu.js 를 import 하면 메뉴 데이터를 두가지 방법으로 참조할 수 있다.

첫번째는 subscribe 함수 호출, 두번째는 svelte 에서 자동으로 생성하는 참조변수 - $menu 를 사용해서 데이터를 알 수 있다.

둘 중에 하나만 써도 되고 둘 다 써도 된다. 다만 $menu<script> 태그 안에서 쓸 경우에는 데이터 변화 감지를 못하므로 <script> 태그 안에서 데이터 변화를 감지하려면 subscribe 를 써야 한다.

	import menu from '../../store/menu.js';

    .....
    
	const init = value => {
		if (value === 'parent') {
        	// $menu 를 따로 정의하지 않고 그냥 사용 - 해당 시점의 데이터
			parents = [{list: $menu, select: null}];
			parents.initTime = (new Date()).getTime();
		}

		if (value === 'info') {
			name = '';
		}
	};

	// 메뉴 데이터 변화를 감지하기 위해 subscribe 사용
    // value 가 메뉴 데이터임
	menu.subscribe(value => {
		init('parent');
	});

참고로 관찰 결과 최초에는 subscribe 이 두번 호출된다. (맨 처음 초기화시 호출 - [], 메뉴 데이터 가져온 이후에 한번 - [......])

어쨌든 AdminMenu.svelte 에서는 자동으로 메뉴 데이터 세팅을 하게 된다.

추가로 메뉴 데이터 생성 버튼 눌렀을때 실제로 postMenu 를 호출하고, 메뉴 생성 성공시 데이터를 reset 하는 코드를 추가하자.

	import { postMenu } from '../../api/api.js';
    
	const addMenu = async () => {
		if (!name) {
			alert('메뉴명 입력하숑~');
			return;
		}

		let parent = null;

		for (let i = parents.length - 1; i >= 0; i--) {
			if (!!parents[i].select?._id) {
				parent = parents[i].select._id;
			}
		}
		
        // 실제 메뉴 생성 - api 호출
		const response = await postMenu({name, parent});

		if (response?.success) {
			alert(`${name} 메뉴 생성에 성공하였습니다.`);
			
            // 메뉴 생성 이후 메뉴 데이터 다시 가져옴
			menu.reset();
			name = '';
		} else {
			alert(`${name} 메뉴 생성에 실패하였습니다.`);
		}
	};

menu.reset() 호출하면 메뉴 데이터가 업데이트 되고 subscribe 함수 - 정확히는 subscribe 함수로 등록한 listener 함수가 호출되고 화면을 초기화한다.

글쓰기가 힘들다. ㅜㅜ
AdminMenu.svelte 의 완성은 담 글에서 하자.

0개의 댓글