요즘 IT 업계에서 Redis 데이터베이스 없이는 상상하기 어려울 정도로 Redis는 필수적인 기술이 되었습니다. 2021년 Stackoverflow가 발표한 개발자 설문조사에서 인메모리 데이터베이스인 Redis는 7만 명 이상의 개발자들에게 가장 사랑받는 데이터베이스로 선정되며 큰 인기를 자랑했습니다.
Redis는 인메모리 데이터베이스로서 강력한 성능을 제공하기 때문에 짧은 응답 시간과 최소한의 지연 시간이 필요한 시나리오에 이상적입니다. 하지만 많은 사람들이 Redis의 활용 사례를 캐싱과 메시지 브로커링에 한정된 것으로 오해하고 있습니다. 이번 글에서는 이러한 통념이 왜 잘못되었는지 살펴보고, Redis를 주(primary) 데이터베이스로 활용하는 방법을 알아보겠습니다.
프로젝트 아이디어
우리는 사용자가 온라인 설문조사 형태로 피드백을 남길 수 있는 작은 앱을 만들려고 합니다. 여기서는 한 회사에 대한 고객 피드백을 수집하는 상황을 가정해 보겠습니다.
설명을 단순화하기 위해 앱의 핵심 기능에 집중하겠습니다. 요구 사항은 다음과 같습니다:
- 사용자는 세 가지 질문에 답변할 수 있습니다.
- 저희 제품/서비스에 대해 어떻게 생각하시나요? 1~10점 척도
- 저희를 지인에게 추천하시겠습니까? 예/아니오 (true/false)
- 의견을 자유롭게 남겨주세요... 자유 텍스트
- 사용자는 폼을 제출할 수 있습니다.
- 각 설문 결과는 데이터베이스(해시)에 개별 레코드로 저장됩니다.
- 사용자는 설문 결과를 조회할 수 있습니다.
기술 스택
이 작은 설문조사 앱은 서버리스 아키텍처의 잠재력을 최대한 활용하기에 완벽한 예제입니다. 서버리스는 비용을 최소화하면서 최대 확장성을 보장하며, 다음 기술들로 구현할 수 있습니다:
Next.js
Next.js는 서버 사이드 렌더링, 정적 페이지 생성, 그리고 무엇보다 중요한 API 라우트(API routes) 같은 기능으로 전통적인 React 웹 애플리케이션을 강화하는 오픈소스 개발 프레임워크입니다. 우리는 Next.js를 사용해 앱의 프론트엔드와 API를 모두 구축할 것입니다.
Upstash Redis
Upstash는 놀랍도록 사용하기 쉽고 요청당 매우 저렴한 가격을 제공하는 완전한 서버리스 영구(persistent) Redis 데이터베이스를 제공합니다. Upstash는 기존 Redis 위에 구축되어 Redis의 타의 추종을 불허하는 성능과 디스크 스토리지의 내구성을 결합했으며, 이것이 바로 우리의 유즈 케이스에 완벽하게 부합하는 이유입니다.
프로젝트 설정
- Next.js 앱을 생성합니다:
npx create-next-app survey-app - Upstash 콘솔에서 Upstash Redis 데이터베이스를 생성하고
UPSTASH_REDIS_REST_URL과UPSTASH_REDIS_REST_TOKEN값을 복사합니다.
프로젝트는 두 개의 API 엔드포인트를 가진 싱글 페이지 애플리케이션입니다:
pages/api/submit.js: 설문 응답을 저장합니다.pages/api/results.js: 모든 설문 응답을 조회합니다.
Upstash와 더 쉽게 통신하기 위해 npm install @upstash/redis 명령어로 @upstash/redis npm 패키지를 설치합니다.
코드 작성
새 파일 pages/api/submit.js를 아래와 같이 생성합니다:
// pages/api/submit.js
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: "INSERT_YOUR_URL_HERE",
token: "INSERT_YOUR_TOKEN_HERE",
});
const submitHandler = async (req, res) => {
const body = req.body;
// DB에 삽입할 데이터 준비
const data = {
rating: String(body.rating) || "0",
recommendation: String(body.recommendation) || "false",
comment: String(body.comment) || "",
};
// 설문 응답을 저장할 랜덤 id 생성
const id =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
// Upstash redis에 데이터 삽입
try {
// 설문 데이터 저장
await redis.hset(`entries:${id}`, data);
// 나중에 조회할 수 있도록 설문 id 저장
await redis.sadd("entries", `entries:${id}`);
} catch (error) {
console.error("Failed to insert data into redis", error);
return res.status(500).json({
success: false,
message: "Failed to insert data into redis",
});
}
return res.status(200).json({
success: true,
message: "Data inserted successfully",
});
};
export default submitHandler;여기서 우리는 세 가지 작업을 수행합니다:
- 요청 본문에서 설문 데이터를 가져와 Redis에 맞게 준비합니다.
- 설문 응답을 해시(hash) 형태로 Redis에 삽입합니다.
- 설문 응답의 id를 셋(set)에 추가합니다.
설문 응답을 위한 해시를 생성한 후, 추가로 그 id를 셋에 넣는 이유가 궁금하실 수 있습니다. 이 단계는 나중에 Redis에서 데이터를 다시 조회할 때 매우 중요합니다. Redis는 키-값(key-value) 스토어로 동작하기 때문에 SQL 데이터베이스와 달리, 데이터가 저장된 정확한 키를 지정하지 않으면 데이터를 찾을 수 있도록 설계되어 있지 않습니다. SQL에서는 SELECT * FROM SurveyResults; 같은 쿼리가 가능하지만, Redis에서는 다른 방법을 사용해야 합니다. 그래서 우리는 셋을 생성하고 설문 결과 엔트리들의 모든 Redis 키를 그 셋에 추가합니다. 이후 모든 설문 응답을 조회하고 싶을 때는 셋에서 해당 키들을 찾아보면 됩니다.
이제 코딩으로 돌아가 실제로 어떻게 구현되는지 살펴보겠습니다.
새 파일 pages/api/results.js를 아래와 같이 생성합니다:
// pages/api/results.js
import { Redis } from "@upstash/redis";
const resultsHandler = async (req, res) => {
// redis에서 데이터 조회
const redis = new Redis({
url: "INSERT_YOUR_URL_HERE",
token: "INSERT_YOUR_TOKEN_HERE",
});
try {
// 셋 안의 모든 엔트리 찾기
const entries = await redis.smembers("entries");
// id/키로 모든 설문 응답 가져오기
// 여러 쿼리를 한 번에 실행하기 위해 Upstash는 pipeline 명령어를 지원합니다.
// 이렇게 하면 여러 쿼리를 한 번에 실행하고 단일 호출로 결과를 받을 수 있습니다.
const p = redis.pipeline();
entries.forEach((id) => {
p.hgetall(id);
});
const results = await p.exec();
return res.status(200).json({
success: true,
message: "Data retrieved successfully",
data: results,
});
} catch (error) {
console.error("Failed to retrieve data from redis", error);
return res.status(500).json({
success: false,
message: "Failed to retrieve data from redis",
});
}
};
export default resultsHandler;이제 백엔드가 동작하므로, 프론트엔드를 완성하여 앱을 마무리하겠습니다.
새 파일 pages/index.js를 아래와 같이 생성합니다:
// pages/index.js
import Head from "next/head";
import Image from "next/image";
import styles from "../styles/Home.module.css";
export default function Home() {
const handleSubmit = async (e) => {
e.preventDefault();
const form = e.target;
const data = {
rating: form.rating.value,
recommendation: form.recommendation.value,
comment: form.comment.value,
};
// 백엔드로 데이터 전송
await fetch("/api/submit", {
body: JSON.stringify(data),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
method: "POST",
});
alert("Thank you for your feedback!");
};
const RatingOption = ({ value }) => (
<div>
<input type="radio" name="rating" value={value} required />{" "}
<label>{value}</label>
</div>
);
return (
<div className={styles.container} onSubmit={handleSubmit}>
<form>
<div>
<label>How do you feel about our products/services?</label>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((value) => (
<RatingOption key={value} value={value} />
))}
</div>
<div>
<label>Would you recommend us to your colleagues?</label>
<div>
<input type="radio" name="recommendation" value="true" required />{" "}
<label>Yes</label>
</div>
<div>
<input type="radio" name="recommendation" value="false" required />{" "}
<label>No</label>
</div>
</div>
<div>
<label>Please share your thoughts... (Optional)</label>
<textarea
name="comment"
placeholder="This is what I liked most/this is what you can improve..."
></textarea>
</div>
<input type="submit" />
</form>
</div>
);
}스타일이 적용되도록 styles/Home.styles.css의 내용을 다음으로 교체합니다:
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.container form > div {
padding: 20px;
display: flex;
flex-direction: column;
align-items: stretch;
}
.container form > div > label {
margin-bottom: 10px;
}이제 설문 응답을 받을 준비가 되었습니다! 하지만 아직 끝나지 않았습니다. 설문 결과를 프론트엔드에 표시하는 기능도 구현해야 합니다.
새 파일 pages/results.js를 아래와 같이 생성합니다:
import { useEffect, useState } from "react";
import styles from "../styles/Results.module.css";
export default function Results() {
const [surveyData, setSurveyData] = useState([]);
useEffect(() => {
fetch("/api/results")
.then((res) => res.json())
.then((response) => setSurveyData(response.data));
}, []);
return (
<div className={styles.container}>
{" "}
{surveyData.map((data) => (
<div key={data.id}>
<p>
<strong> Rating: </strong> {data.rating}{" "}
</p>{" "}
<p>
<strong> Recommendation: </strong> {data.recommendation}{" "}
</p>{" "}
<p>
<strong> Comment: </strong> {data.comment}{" "}
</p>{" "}
</div>
))}{" "}
</div>
);
}마지막으로 styles/Results.module.css 파일을 생성하고 다음 내용을 입력합니다:
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
min-height: 100vh;
margin: 50px 0;
}
.container > div {
background: rgba(0, 0, 0, 0.05);
border-radius: 10px;
padding: 15px;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.container p {
margin: 0;
}이제 localhost:3000/results에서 모든 설문 응답의 전체 목록을 확인할 수 있습니다.
애플리케이션의 전체 소스 코드는 GitHub 저장소 upstash-survey-app에서 확인할 수 있습니다.
마무리
이번 글에서는 폼 입력을 처리하고 작성된 폼을 Upstash 서버리스 Redis에 저장하는 Next.js 풀스택 웹 애플리케이션을 개발했습니다. Redis를 주 데이터베이스로 활용하는 방법과, 다른 데이터베이스(예: SQL)에서 Redis로 전환할 때 필요한 설계상의 변화도 함께 살펴보았습니다.
간편하게 설정할 수 있는 서버리스 Redis 데이터베이스 덕분에 Upstash는 클라우드에 폼 데이터를 저장하는 과정을 놀랄 만큼 쉽게 만들어 주었습니다.
이 글이 Redis에 대한 이해를 높이고, Upstash Redis에 대한 감을 잡으며, 새로운 데이터 저장 방식으로 여러분의 애플리케이션을 구축하는 데 도움이 되기를 바랍니다.