Xlera8

SvelteKit에서 데이터 캐싱

My 이전 게시물 웹 개발을 위한 훌륭한 도구인 SvelteKit에 대한 광범위한 개요였습니다. 이 게시물은 우리가 그곳에서 수행한 작업을 분기하고 모든 개발자가 가장 좋아하는 주제에 대해 자세히 설명합니다. 캐싱. 따라서 아직 읽지 않았다면 내 마지막 게시물을 읽어보십시오. 이 게시물의 코드 GitHub에서 사용 가능, 만큼 잘 라이브 데모.

이 게시물은 데이터 처리에 관한 것입니다. 기본 제공 SvelteKit 기능을 사용하여 페이지의 쿼리 문자열을 수정하고 페이지의 로더를 다시 트리거하는 기본적인 검색 기능을 추가합니다. 그러나 (가상) 데이터베이스를 다시 쿼리하는 대신 일부 캐싱을 추가하여 이전 검색을 다시 검색(또는 뒤로 버튼 사용)하면 캐시에서 이전에 검색된 데이터를 빠르게 표시할 것입니다. 캐시된 데이터가 유효한 상태로 유지되는 시간을 제어하는 ​​방법과 더 중요한 것은 캐시된 모든 값을 수동으로 무효화하는 방법을 살펴보겠습니다. 금상첨화로 캐시를 제거하면서 변형 후 현재 화면의 클라이언트 측 데이터를 수동으로 업데이트하는 방법을 살펴보겠습니다.

이것은 우리가 더 어려운 주제를 다루고 있기 때문에 내가 일반적으로 쓰는 대부분의 것보다 더 길고 어려운 게시물이 될 것입니다. 이 게시물은 기본적으로 다음과 같은 인기 있는 데이터 유틸리티의 공통 기능을 구현하는 방법을 보여줍니다. 반응 쿼리; 그러나 외부 라이브러리를 가져오는 대신 웹 플랫폼과 SvelteKit 기능만 사용할 것입니다.

불행히도 웹 플랫폼의 기능은 약간 낮은 수준이므로 익숙한 것보다 조금 더 많은 작업을 수행할 것입니다. 장점은 번들 크기를 훌륭하고 작게 유지하는 데 도움이 되는 외부 라이브러리가 필요하지 않다는 것입니다. 합당한 이유가 없는 한 제가 보여드릴 방법을 사용하지 마세요. 캐싱은 잘못되기 쉽고 보시다시피 약간의 복잡성으로 인해 애플리케이션 코드가 생성됩니다. 데이터 저장소가 빠르고 UI가 괜찮아서 SvelteKit이 주어진 페이지에 필요한 데이터를 항상 요청할 수 있기를 바랍니다. 그렇다면 그대로 두십시오. 단순함을 즐기십시오. 하지만 이 게시물에서는 이러한 상황이 더 이상 발생하지 않는 경우에 대한 몇 가지 트릭을 보여줍니다.

반응 쿼리에 대해 말하자면, 방금 출시되었습니다 Svelte를 위해! 따라서 수동 캐싱 기술에 의존하는 경우 많이, 해당 프로젝트를 확인하고 도움이 될 수 있는지 확인하십시오.

설정

시작하기 전에 다음을 약간 변경해 보겠습니다. 우리가 전에 가지고 있던 코드. 이것은 다른 SvelteKit 기능을 볼 수 있는 변명을 제공하고 더 중요한 것은 성공을 위한 준비를 갖추게 할 것입니다.

먼저 로더에서 데이터 로드를 +page.server.jsAPI 경로. 우리는 +server.js 에있는 파일을 routes/api/todos를 추가한 다음 GET 기능. 즉, 이제 기본 GET 동사를 사용하여 가져올 수 있습니다. /api/todos 길. 이전과 동일한 데이터 로드 코드를 추가합니다.

import { json } from "@sveltejs/kit";
import { getTodos } from "$lib/data/todoData"; export async function GET({ url, setHeaders, request }) { const search = url.searchParams.get("search") || ""; const todos = await getTodos(search); return json(todos);
}

다음으로, 우리가 가지고 있던 페이지 로더를 가지고 간단히 파일 이름을 바꾸겠습니다. +page.server.js+page.js (또는 .ts TypeScript를 사용하도록 프로젝트를 스캐폴딩한 경우). 이렇게 하면 로더가 서버 로더가 아닌 "범용" 로더로 변경됩니다. SvelteKit 문서 차이점을 설명하다., 그러나 범용 로더는 서버와 클라이언트 모두에서 실행됩니다. 우리에게 한 가지 장점은 fetch 새 엔드포인트에 대한 호출은 브라우저의 네이티브 fetch 기능. 표준 HTTP 캐싱을 조금 더 추가하겠지만 지금은 끝점을 호출하기만 하면 됩니다.

export async function load({ fetch, url, setHeaders }) { const search = url.searchParams.get("search") || ""; const resp = await fetch(`/api/todos?search=${encodeURIComponent(search)}`); const todos = await resp.json(); return { todos, };
}

이제 간단한 양식을 추가해 보겠습니다. /list 페이지 :

<div class="search-form"> <form action="/ko/list"> <label>Search</label> <input autofocus name="search" /> </form>
</div>

예, 양식은 일반 페이지 로더를 직접 대상으로 지정할 수 있습니다. 이제 검색창에 검색어를 추가할 수 있습니다. 엔터 버튼, "검색" 용어가 URL의 쿼리 문자열에 추가되어 로더를 다시 실행하고 할 일 항목을 검색합니다.

검색 폼

또한 우리의 지연을 증가시키자 todoData.js 에있는 파일을 /lib/data. 이렇게 하면 이 게시물을 통해 작업할 때 데이터가 언제 캐시되고 캐시되지 않는지 쉽게 확인할 수 있습니다.

export const wait = async amount => new Promise(res => setTimeout(res, amount ?? 500));

이 게시물의 전체 코드는 GitHub의 모든 것, 참조해야 하는 경우.

기본 캐싱

캐싱을 추가하여 시작해 보겠습니다. /api/todos 끝점. 우리는 우리로 돌아갈 것입니다. +server.js 파일을 만들고 첫 번째 캐시 제어 헤더를 추가합니다.

setHeaders({ "cache-control": "max-age=60",
});

...전체 함수가 다음과 같이 표시됩니다.

export async function GET({ url, setHeaders, request }) { const search = url.searchParams.get("search") || ""; setHeaders({ "cache-control": "max-age=60", }); const todos = await getTodos(search); return json(todos);
}

곧 수동 무효화에 대해 살펴보겠지만 이 기능은 이러한 API 호출을 60초 동안 캐시하는 것입니다. 이것을 원하는대로 설정하십시오.사용 사례에 따라 stale-while-revalidate 살펴볼 가치가 있습니다.

마찬가지로 쿼리가 캐싱됩니다.

DevTools의 캐시.

주의 사항 당신을 확인 체크 해제 개발 도구에서 캐싱을 비활성화하는 확인란.

앱의 초기 탐색이 목록 페이지인 경우 해당 검색 결과는 내부적으로 SvelteKit에 캐시되므로 해당 검색으로 돌아올 때 DevTools에 아무 것도 표시되지 않을 것임을 기억하십시오.

캐시되는 항목 및 위치

앱의 첫 번째 서버 렌더링 로드( /list 페이지)를 서버에서 가져옵니다. SvelteKit은 이 데이터를 직렬화하여 클라이언트로 보냅니다. 또한 다음을 관찰합니다. Cache-Control 머리글 응답에서 캐시 창(put 예제에서 60초로 설정) 내에서 해당 엔드포인트 호출에 대해 이 캐시된 데이터를 사용하는 것을 알게 됩니다.

초기 로드 후 페이지에서 검색을 시작하면 브라우저에서 /api/todos 목록. 이미 검색한 항목(최근 60초 이내)을 검색하면 응답이 캐시되므로 즉시 로드되어야 합니다.

이 접근 방식에서 특히 멋진 점은 이것이 브라우저의 기본 캐싱을 ​​통해 캐싱되기 때문에 이러한 호출은 페이지를 다시 로드하더라도(우리가 살펴볼 캐시 무효화를 관리하는 방법에 따라) 캐시를 계속할 수 있다는 것입니다( 마지막 60초 이내에 수행한 경우에도 항상 엔드포인트를 새로 호출하는 초기 서버 측 로드).

분명히 데이터는 언제든지 변경될 수 있으므로 이 캐시를 수동으로 제거하는 방법이 필요합니다. 다음에 살펴보겠습니다.

캐시 무효화

현재 데이터는 60초 동안 캐시됩니다. 무슨 일이 있어도 XNUMX분 후에 데이터 저장소에서 새로운 데이터를 가져옵니다. 더 짧거나 더 긴 기간을 원할 수 있지만 일부 데이터를 변경하고 캐시를 즉시 지워 다음 쿼리가 최신 상태가 되도록 하려면 어떻게 해야 할까요? 새 주소로 보내는 URL에 검색어 무효화 값을 추가하여 이 문제를 해결할 것입니다. /todos 엔드 포인트.

이 캐시 무효화 값을 쿠키에 저장해 보겠습니다. 해당 값은 서버에서 설정할 수 있지만 여전히 클라이언트에서 읽을 수 있습니다. 몇 가지 샘플 코드를 살펴보겠습니다.

우리는 만들 수 있습니다 +layout.server.js 우리의 루트에 있는 파일 routes 폴더. 이는 응용 프로그램 시작 시 실행되며 초기 쿠키 값을 설정하기에 완벽한 위치입니다.

export function load({ cookies, isDataRequest }) { const initialRequest = !isDataRequest; const cacheValue = initialRequest ? +new Date() : cookies.get("todos-cache"); if (initialRequest) { cookies.set("todos-cache", cacheValue, { path: "/", httpOnly: false }); } return { todosCacheBust: cacheValue, };
}

당신은 isDataRequest 값. 클라이언트 코드가 호출될 때마다 레이아웃이 다시 실행된다는 점을 기억하세요. invalidate(), 또는 서버 작업을 실행할 때마다(기본 동작을 끄지 않는다고 가정). isDataRequest 재실행을 나타내므로 다음과 같은 경우에만 쿠키를 설정합니다. false; 그렇지 않으면 이미 있는 것을 함께 보냅니다.

XNUMXD덴탈의 httpOnly: false 플래그도 중요합니다. 이렇게 하면 클라이언트 코드에서 이러한 쿠키 값을 읽을 수 있습니다. document.cookie. 이것은 일반적으로 보안 문제이지만 우리의 경우 캐시 또는 캐시 버스트를 허용하는 의미 없는 숫자입니다.

캐시 값 읽기

우리의 범용 로더는 /todos 끝점. 이것은 서버나 클라이언트에서 실행되며 우리가 어디에 있든 방금 설정한 캐시 값을 읽어야 합니다. 우리가 서버에 있다면 비교적 쉽습니다. 호출할 수 있습니다. await parent() 상위 레이아웃에서 데이터를 가져옵니다. 그러나 클라이언트에서는 일부 전체 코드를 사용하여 구문 분석해야 합니다. document.cookie:

export function getCookieLookup() { if (typeof document !== "object") { return {}; } return document.cookie.split("; ").reduce((lookup, v) => { const parts = v.split("="); lookup[parts[0]] = parts[1]; return lookup; }, {});
} const getCurrentCookieValue = name => { const cookies = getCookieLookup(); return cookies[name] ?? "";
};

다행히 한 번만 필요합니다.

캐시 값 보내기

하지만 이제 우리는 보내다 우리에게 이 가치 /todos 엔드 포인트.

import { getCurrentCookieValue } from "$lib/util/cookieUtils"; export async function load({ fetch, parent, url, setHeaders }) { const parentData = await parent(); const cacheBust = getCurrentCookieValue("todos-cache") || parentData.todosCacheBust; const search = url.searchParams.get("search") || ""; const resp = await fetch(`/api/todos?search=${encodeURIComponent(search)}&cache=${cacheBust}`); const todos = await resp.json(); return { todos, };
}

getCurrentCookieValue('todos-cache') 우리가 클라이언트에 있는지 확인하고(문서 유형을 확인하여) 클라이언트에 있으면 아무 것도 반환하지 않습니다. 이 시점에서 우리는 우리가 서버에 있다는 것을 알게 됩니다. 그런 다음 레이아웃의 값을 사용합니다.

캐시 무효화

그러나 방법 필요할 때 캐시 무효화 값을 실제로 업데이트합니까? 쿠키에 저장되기 때문에 모든 서버 작업에서 다음과 같이 호출할 수 있습니다.

cookies.set("todos-cache", cacheValue, { path: "/", httpOnly: false });

구현

여기서부터는 모두 내리막길입니다. 우리는 열심히 일했습니다. 우리는 우리에게 필요한 다양한 웹 플랫폼 프리미티브와 그 프리미티브가 어디로 가는지 다루었습니다. 이제 재미를 느끼고 응용 프로그램 코드를 작성하여 모두 함께 연결해 보겠습니다.

잠시 후에 명확해질 이유 때문에 먼저 편집 기능을 추가해 보겠습니다. /list 페이지. 각 할 일에 대해 이 두 번째 테이블 행을 추가합니다.

import { enhance } from "$app/forms";
<tr> <td colspan="4"> <form use:enhance method="post" action="?/editTodo"> <input name="id" value="{t.id}" type="hidden" /> <input name="title" value="{t.title}" /> <button>Save</button> </form> </td>
</tr>

그리고 물론 우리는 양식 작업을 추가해야 합니다. /list 페이지. 작업은 들어갈 수만 있습니다. .server 페이지를 추가하겠습니다. +page.server.js 우리의 /list 폴더. (예, +page.server.js 파일 옆에 공존할 수 있습니다. +page.js 파일.)

import { getTodo, updateTodo, wait } from "$lib/data/todoData"; export const actions = { async editTodo({ request, cookies }) { const formData = await request.formData(); const id = formData.get("id"); const newTitle = formData.get("title"); await wait(250); updateTodo(id, newTitle); cookies.set("todos-cache", +new Date(), { path: "/", httpOnly: false }); },
};

양식 데이터를 가져오고, 강제로 지연하고, 할 일을 업데이트하고, 가장 중요한 것은 캐시 버스트 쿠키를 지우는 것입니다.

한번 해봅시다. 페이지를 다시 로드한 다음 할 일 항목 중 하나를 편집합니다. 잠시 후 테이블 값 업데이트가 표시되어야 합니다. DevToold의 네트워크 탭을 보면 /todos 새 데이터를 반환하는 끝점. 간단하고 기본적으로 작동합니다.

데이터 저장

즉각적인 업데이트

할 일 항목을 업데이트한 후에 발생하는 가져오기를 피하고 대신 수정된 항목을 화면에서 바로 업데이트하려면 어떻게 해야 할까요?

이것은 단순히 성능의 문제가 아닙니다. "게시물"을 검색한 다음 목록의 할 일 항목에서 "게시물"이라는 단어를 제거하면 더 이상 해당 페이지의 검색 결과에 없기 때문에 편집 후 목록에서 사라집니다. 종료 할 일에 대한 세련된 애니메이션으로 UX를 개선할 수 있지만 지원 해당 페이지의 로드 기능을 다시 실행하지만 여전히 캐시를 지우고 사용자가 편집 내용을 볼 수 있도록 수정된 할 일을 업데이트합니다. SvelteKit이 이를 가능하게 합니다. 방법을 살펴보겠습니다!

먼저 로더를 약간 변경해 보겠습니다. 할 일 항목을 반환하는 대신 쓰기 가능한 저장소 할 일이 포함되어 있습니다.

return { todos: writable(todos),
};

이전에 우리는 data 우리가 소유하지 않고 업데이트할 수 없는 소품. 그러나 Svelte를 사용하면 자체 저장소에서 데이터를 반환할 수 있습니다(우리가 범용 로더를 사용한다고 가정). 우리는 우리의 /list 페이지.

대신 이것 :

{#each todos as t}

...이 작업을 수행해야 하는 이유 todos 그 자체가 이제 상점입니다.:

{#each $todos as t}

이제 데이터가 이전처럼 로드됩니다. 하지만 그때부터 todos 쓰기 가능한 저장소이므로 업데이트할 수 있습니다.

먼저 우리에게 기능을 제공합시다. use:enhance 속성:

<form use:enhance={executeSave} on:submit={runInvalidate} method="post" action="?/editTodo"
>

이것은 제출 전에 실행됩니다. 다음에 작성해 보겠습니다.

function executeSave({ data }) { const id = data.get("id"); const title = data.get("title"); return async () => { todos.update(list => list.map(todo => { if (todo.id == id) { return Object.assign({}, todo, { title }); } else { return todo; } }) ); };
}

이 기능은 data 양식 데이터가 있는 객체. 우리 return 실행할 비동기 함수 시간 내에 편집이 완료되었습니다. 문서 이 모든 것을 설명하지만 이렇게 함으로써 로더를 다시 실행해야 하는 SvelteKit의 기본 양식 처리를 차단합니다. 이것이 바로 우리가 원하는 것입니다! (문서에서 설명하는 것처럼 기본 동작을 쉽게 되돌릴 수 있습니다.)

우리는 지금 전화 update 우리에 todos 상점이기 때문에 배열. 그게 다야. 할 일 항목을 편집한 후 변경 사항이 즉시 표시되고 캐시가 지워집니다(이전과 마찬가지로 새 쿠키 값을 editTodo 양식 행동). 따라서 검색한 다음 이 페이지로 다시 이동하면 로더에서 최신 데이터를 가져와 업데이트된 모든 업데이트된 할 일 항목을 올바르게 제외합니다.

즉시 업데이트 코드 GitHub에서 사용 가능.

더 깊은 파기

루트 레이아웃뿐만 아니라 모든 서버 로드 기능(또는 서버 작업)에서 쿠키를 설정할 수 있습니다. 따라서 일부 데이터가 단일 레이아웃 또는 단일 페이지에서만 사용되는 경우 해당 쿠키 값을 설정할 수 있습니다. 더군다나 당신이라면 지원 화면 데이터를 수동으로 업데이트하고 대신 로더가 변형 후 다시 실행되기를 원하는 트릭을 수행하면 확인 없이 해당 로드 함수에서 항상 새 쿠키 값을 설정할 수 있습니다. isDataRequest. 처음에 설정한 다음 서버 작업을 실행할 때마다 페이지 레이아웃이 자동으로 무효화되고 로더를 다시 호출하여 범용 로더가 호출되기 전에 캐시 버스트 문자열을 다시 설정합니다.

재로드 함수 작성

마지막 기능인 새로고침 버튼을 만들어 마무리하겠습니다. 사용자에게 캐시를 지우고 현재 쿼리를 다시 로드하는 버튼을 제공하겠습니다.

더트 심플 폼 액션을 추가할 것입니다:

async reloadTodos({ cookies }) { cookies.set('todos-cache', +new Date(), { path: '/', httpOnly: false });
},

실제 프로젝트에서는 동일한 코드를 복사/붙여넣기하여 여러 위치에서 동일한 방식으로 동일한 쿠키를 설정하지 않을 수 있지만 이 게시물에서는 단순성과 가독성을 위해 최적화할 것입니다.

이제 게시할 양식을 만들어 보겠습니다.

<form method="POST" action="?/reloadTodos" use:enhance> <button>Reload todos</button>
</form>

작동합니다!

새로 고침 후 UI.

이 작업을 완료하고 계속 진행할 수 있지만 이 솔루션을 약간 개선해 보겠습니다. 특히 페이지에 대한 피드백을 제공하여 사용자에게 새로고침이 진행되고 있음을 알려줍시다. 또한 기본적으로 SvelteKit 작업은 무효화됩니다. 모두. 현재 페이지 계층 구조의 모든 레이아웃, 페이지 등이 다시 로드됩니다. 무효화하거나 다시 로드할 필요가 없는 루트 레이아웃에 한 번 로드된 일부 데이터가 있을 수 있습니다.

그래서, 조금 집중하고 이 함수를 호출할 때만 할 일을 다시 로드해 봅시다.

먼저 향상시킬 함수를 전달해 보겠습니다.

<form method="POST" action="?/reloadTodos" use:enhance={reloadTodos}>
import { enhance } from "$app/forms";
import { invalidate } from "$app/navigation"; let reloading = false;
const reloadTodos = () => { reloading = true; return async () => { invalidate("reload:todos").then(() => { reloading = false; }); };
};

우리는 새로운 설정 reloading 변수 true스타트 이 행동의. 그런 다음 모든 것을 무효화하는 기본 동작을 재정의하기 위해 다음을 반환합니다. async 기능. 이 기능은 서버 작업이 완료되면 실행됩니다(단지 새 쿠키를 설정함).

이것 없이 async 함수가 반환되면 SvelteKit은 모든 것을 무효화합니다. 우리가 이 함수를 제공하고 있기 때문에 아무 것도 무효화하지 않으므로 무엇을 다시 로드할지 알려주는 것은 우리에게 달려 있습니다. 우리는 이것을 invalidate 기능. 의 값으로 호출합니다. reload:todos. 이 함수는 무효화가 완료되면 해결되는 약속을 반환합니다. reloading 뒤로 false.

마지막으로 로더를 이 새로운 것과 동기화해야 합니다. reload:todos 무효화 값. 우리는 로더에서 depends 기능:

export async function load({ fetch, url, setHeaders, depends }) { depends('reload:todos'); // rest is the same

그게 다야. dependsinvalidate 매우 유용한 기능입니다. 멋진 것은 invalidate 우리가 한 것처럼 우리가 제공하는 임의의 값을 취하지 않습니다. 또한 SvelteKit이 추적할 URL을 제공하고 해당 URL에 의존하는 모든 로더를 무효화할 수 있습니다. 이를 위해 다음 호출을 건너뛸 수 있는지 궁금하신가요? depends 무효화 /api/todos 엔드포인트를 모두 사용할 수 있지만 다음을 제공해야 합니다. 정확한 다음을 포함한 URL search 용어(및 캐시 값). 따라서 다음과 같이 현재 검색에 대한 URL을 함께 입력하거나 경로 이름을 일치시킬 수 있습니다.

invalidate(url => url.pathname == "/api/todos");

개인적으로 나는 다음을 사용하는 솔루션을 찾습니다. depends 더 명확하고 간단합니다. 하지만 봐 문서 물론 더 많은 정보를 얻고 스스로 결정하십시오.

새로고침 버튼이 작동하는 것을 보고 싶다면 해당 코드는 다음 위치에 있습니다. 저장소의 이 지점.

이별의 생각

이것은 긴 게시물 이었지만 압도적이지 않기를 바랍니다. 우리는 SvelteKit을 사용할 때 데이터를 캐시할 수 있는 다양한 방법을 연구했습니다. 이 중 대부분은 웹 플랫폼 프리미티브를 사용하여 올바른 캐시 및 쿠키 값을 추가하는 문제였으며, 이에 대한 지식은 SvelteKit을 넘어 일반적으로 웹 개발에 도움이 될 것입니다.

또한 이것은 당신이 절대적으로 항상 필요하지 않습니다. 이러한 종류의 고급 기능은 다음과 같은 경우에만 사용해야 합니다. 실제로 그것들이 필요하다. 데이터 저장소가 데이터를 빠르고 효율적으로 제공하고 어떤 종류의 확장 문제도 처리하지 않는 경우 여기서 언급한 작업을 수행하면서 불필요하게 복잡하게 애플리케이션 코드를 팽창시키는 것은 의미가 없습니다.

항상 그렇듯이 명확하고 깨끗하며 간단한 코드를 작성하고 필요할 때 최적화하십시오. 이 게시물의 목적은 진정으로 필요할 때 이러한 최적화 도구를 제공하는 것이었습니다. 나는 당신이 그것을 즐겼기를 바랍니다!

우리와 함께 채팅

안녕하세요! 어떻게 도와 드릴까요?