Blitz.js는 Next.js에서 포크되어 탄생한 React 프레임워크입니다. 이번 튜토리얼에서는 작업 데이터를 Upstash에 저장하는 Blitz.js 기반의 할 일(To-Do) 애플리케이션을 직접 만들어 보겠습니다. 그럼 지금 바로 시작해 볼까요?
환경 설정
시작하려면 먼저 컴퓨터에 Blitz.js를 설치해야 합니다.
NPM:
npm install -g blitz --legacy-peer-deps
Yarn:
yarn global add blitz
blitz new 명령어로 새 Blitz.js 앱을 생성한 뒤, 해당 디렉터리로 이동합니다.
blitz new blitzjs-todo && cd blitzjs-todo
좋습니다. 이제 웹사이트 스타일링을 위해 TailwindCSS를 설치하겠습니다.
blitz install tailwind
마지막으로 Upstash API 호출을 손쉽게 처리할 수 있도록 Upstash JS SDK를 설치합니다.
NPM:
npm i @upstash/redis
Yarn:
yarn i @upstash/redis
여기까지 완료했다면 blitz dev 명령어를 실행해 모든 것이 정상적으로 동작하는지 확인해 보세요. 계정을 만들고 로그인도 시도해 보세요. 모든 과정을 올바르게 따라왔다면 아래 화면과 같이 표시될 것입니다.

또한 파일 구조도 아래와 같아야 합니다.

Upstash 콘솔에서 UPSTASH_REDIS_REST_URL과 UPSTASH_REDIS_REST_TOKEN 값을 복사해 .env 파일에 붙여넣습니다. 다음과 같은 형태가 됩니다.
# This env file should be checked into source control
# This is the place for default values for all environments
# Values in `.env.local` and `.env.production` will override these values
UPSTASH_REDIS_REST_URL=YOUR_URL_HERE
UPSTASH_REDIS_REST_TOKEN=YOUR_TOKEN_HERE
이것으로 Blitz.js 애플리케이션 설정이 모두 끝났습니다! 이제 할 일 목록 기능을 직접 구현해 보겠습니다.
기능 구현하기
Blitz.js에는 사용자 인증 기능이 기본으로 내장되어 있습니다! 이를 활용하면 사용자별 개인 할 일 목록을 만들 수 있습니다.
먼저 /lib/redis.ts 파일에서 Upstash JS SDK를 초기화합니다.
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
export default redis;
할 일 목록에 접근하려면 3개의 서로 다른 API 라우트가 필요합니다.
app/api 디렉터리로 이동해 getall.ts라는 파일을 생성하고, 아래 코드를 붙여넣으세요.
import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";
export const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
const session = await getSession(req, res);
if (!session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` });
} else {
await redis
.lrange(String(session.userId), 0, 100)
.then((data) => res.status(200).json({ data: data, success: true }))
.catch((error) => res.status(500).json({ error: error }));
}
};
export default handler;
이 API 라우트가 동작하는 방식을 단계별로 살펴보겠습니다. 먼저 해당 라우트로 요청이 들어오면, 사용자가 로그인 상태인지 검증합니다. 로그인하지 않은 사용자라면 "Not Authorized(권한 없음)" 응답을 반환합니다. 반대로 로그인한 사용자라면 Upstash Redis 데이터베이스에서 현재 목록에 저장된 모든 할 일 항목을 가져옵니다. 이 요청은 대략 100개까지의 할 일을 불러옵니다.
Q: 잠깐, 그럼 맨 처음 할 일을 추가하려면 어떻게 해야 하죠?
A: 좋은 질문입니다! 바로 다음에서 알아보겠습니다!
이번에는 app/api 안에 add.ts라는 새 파일을 만들고 아래 코드를 붙여넣으세요.
import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";
const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
const session = await getSession(req, res);
if (req.method !== "POST" || !req.body.data || !session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` });
} else {
let todo = encodeURI(req.body.data);
await redis
.lpush(String(session.userId), todo)
.then(() => res.status(200).json({ success: true }))
.catch(() => res.status(500).json({ error: "Error adding data." }));
}
};
export default handler;
이 API 라우트는 앞서 만든 것과 상당히 비슷하지만, 5번째 줄에 더 많은 검증 로직이 추가된 점이 다릅니다. 이 요청은 GET이 아닌 POST 요청이기 때문입니다. 여기서는 세 가지 사항을 확인합니다. 첫째, 요청이 실제로 POST인지 확인합니다. 둘째, req.body.data에 JSON 또는 텍스트가 존재하는지 확인합니다. 마지막으로 사용자가 로그인 상태인지 확인합니다. 이 검증들을 모두 통과하면 할 일 항목을 Upstash의 Redis 리스트에 저장(push)할 수 있습니다. 처리 중 오류가 발생하면 .catch를 통해 500 에러를 반환합니다.
마지막으로 필요한 라우트는 할 일을 삭제하는 기능입니다. 작업을 완료했다면 당연히 목록에서 지워야겠죠! app/api/remove.ts에 마지막 API 라우트를 추가합니다. 아래 코드를 파일에 복사하세요.
import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";
const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
const session = await getSession(req, res);
if (req.method !== "POST" || !req.body.data || !session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` });
} else {
let todo = encodeURI(req.body.data);
await redis
.lrem(String(session.userId), 1, todo)
.then(() => res.status(200).json({ success: true }))
.catch(() => res.status(500).json({ error: "Error removing data." }));
}
};
export default handler;
앞선 코드와 매우 유사하다는 것을 눈치채셨나요? 이 라우트는 add API 라우트와 거의 동일합니다. 가장 큰 차이점은 항목을 추가하는 LPUSH 대신, Redis에서 항목을 제거하는 LREM을 사용한다는 점입니다.
프론트엔드 구축하기
먼저 app/pages/index.js 파일의 내용을 모두 삭제하고, 할 일 목록 UI를 한 단계씩 작성해 보겠습니다.
파일 최상단에 아래 임포트 문을 붙여넣으세요.
import { Link, BlitzPage, useMutation, Routes, getAntiCSRFToken } from "blitz";
import { useRef, useEffect, useState, Suspense } from "react";
import Layout from "app/core/layouts/Layout";
import { useCurrentUser } from "app/core/hooks/useCurrentUser";
import logout from "app/auth/mutations/logout";
React Hooks를 사용해 할 일 목록의 핵심 기능을 구현합니다. 목록의 주요 기능들을 작성해 보겠습니다.
const Main = () => {
const todoRef = useRef<HTMLInputElement>(null)
const [todos, setTodos] = useState([])
const currentUser = useCurrentUser()
const [logoutMutation] = useMutation(logout)
const handleAddTodo = async (e) => {
e.preventDefault()
const antiCSRFToken = await getAntiCSRFToken()
const response = await fetch("/api/add", {
method: "POST",
headers: {
"Content-Type": "application/json",
"anti-csrf": antiCSRFToken,
},
body: JSON.stringify({ data: todoRef.current?.value }),
})
const data = await response.json()
if (data.success) {
todoRef.current!.value = ""
fetchTodos()
}
}
const handleRemoveTodo = async (id) => {
const antiCSRFToken = await getAntiCSRFToken()
const response = await fetch("/api/remove", {
method: "POST",
headers: {
"Content-Type": "application/json",
"anti-csrf": antiCSRFToken,
},
body: JSON.stringify({ data: id }),
})
const data = await response.json()
if (data.success) {
fetchTodos()
}
}
const fetchTodos = async () => {
const antiCSRFToken = await getAntiCSRFToken()
const response = await fetch("/api/getall", {
method: "GET",
headers: {
"anti-csrf": antiCSRFToken,
},
})
const res = await response.json()
setTodos(res.data)
}
useEffect(() => {
fetchTodos()
}, [])
if (currentUser) {
return (
<>
<button
className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400 mb-3"
onClick={async () => {
await logoutMutation()
}}
>
Logout
</button>
<div>
User id: <code>{currentUser.id}</code>
<br />
User email: <code>{currentUser.email}</code>
</div>
<form className="mt-2" onSubmit={handleAddTodo}>
<p>add a todo:</p>
<input
ref={todoRef}
className="w-full border-black border-2 focus:outline-none text-center"
/>
</form>
<div className="flex flex-col gap-2 mt-4 bg-gray-300 rounded-md">
{(todos as string[]).map((todo: string, index: number) => (
<div className="flex items-center p-3 rounded-md bg-gray-300" key={index}>
<button
onClick={() => handleRemoveTodo(todo)}
className="flex items-center mr-4 justify-center w-5 h-5 rounded-[0.25rem] border border-solid border-gray-500 shadow-sm hover:bg-gray-700"
></button>
<span>{todo}</span>
</div>
))}
</div>
</>
)
} else {
return (
<div className="flex flex-col gap-4 text-center">
<Link href={Routes.SignupPage()}>
<a className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400">
<strong>Sign Up</strong>
</a>
</Link>
<Link href={Routes.LoginPage()}>
<a className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400">
<strong>Login</strong>
</a>
</Link>
</div>
)
}
}
<Main/> 컴포넌트가 바로 이 애플리케이션의 핵심입니다. 코드를 자세히 살펴보면 어떻게 동작하는지 알 수 있습니다. 컴포넌트 최상단에서는 애플리케이션의 상태(state)를 초기화합니다. 또한 나중에 '새 할 일' 입력창에서 사용할 ref도 선언합니다. 그리고 antiCSRFToken 사용에도 주목해 보세요! Blitz.js는 악의적인 공격자가 사이트에 피해를 주는 것을 막기 위해, API 라우트를 호출할 때마다 이 토큰을 함께 전송하도록 요구합니다. 보안 차원에서 꽤 유용한 기능이라고 생각합니다!
웹사이트에서 데이터를 처리하기 위해 세 가지 주요 함수를 사용합니다. 바로 다음과 같습니다.
handleAddTodohandleRemoveTodofetchTodos
페이지가 로드되자마자 fetchTodos를 호출해 사용자가 아직 완료하지 않은 모든 할 일을 불러옵니다. 사용자가 할 일을 추가하거나 삭제할 때에도 fetchTodos를 다시 호출하여 변경 사항이 화면에 즉시 반영되도록 합니다!
로그인하지 않은 사용자는 이 페이지를 보기 전에 먼저 로그인하라는 안내를 받게 됩니다.
아직 웹사이트에 세션이 없다면 회원가입을 하거나 로그인할 수 있습니다. 계정 없이는 할 일을 저장할 수 없으며, 모든 API 라우트는 AntiCSRFToken을 통한 인증을 필수로 요구한다는 점을 기억하세요!
그런데 잠깐, 아직 중요한 단계가 하나 남았습니다! 페이지를 export해야 합니다.
const Home: BlitzPage = () => {
return (
<div className="flex flex-col min-h-screen items-center justify-center">
<main>
<div className="my-4">
<Suspense fallback="Loading...">
<Main />
</Suspense>
</div>
</main>
</div>
);
};
Home.suppressFirstRenderFlicker = true;
Home.getLayout = (page) => <Layout title="Home">{page}</Layout>;
export default Home;
위 코드에서 볼 수 있듯이, Blitz.js는 Next.js와 약간 다른 방식을 사용하지만 핵심 원리는 동일합니다. 앞서 임포트한 Suspense를 활용해 앱이 로딩 중임을 사용자에게 보여주고, 로딩이 완료되면 그때 <Main/> 컴포넌트를 화면에 표시합니다!
변경 사항을 확인하려면 콘솔에서 아래 명령어를 다시 실행하고, 브라우저에서 앱에 접속하세요.
blitz dev
안내대로 따라오셨다면, 로그인 후 몇 개의 할 일을 추가했을 때 애플리케이션이 대략 아래 화면처럼 보일 것입니다.

할 일 옆의 체크박스를 클릭하면 해당 항목을 삭제할 수 있습니다. 바로 removeTodo 함수가 담당하는 기능입니다 😉.
축하합니다!
이 글을 통해 새로운 것을 배우셨기를 바랍니다. 이미 알고 계신 내용이었다면 실력을 되새기는 좋은 복습이 되었을 겁니다! Blitz.js는 Next.js와는 다른 방향으로 발전해 나가고 있어, 미래에는 완전히 다른 프레임워크가 될 수도 있습니다. 최신 소식은 공식 웹사이트에서 확인하실 수 있습니다.
프로젝트 소스: GitHub 링크
동작 데모: 데모 링크
피드백이 있으신가요? Twitter에서 @upstash를 팔로우하고 Discord 서버에 참여해 보세요!