[DISMU] Discord 노래봇의 새로운 기준, AI 노래 플랫폼의 가능성 (프로젝트 소개, 시장 분석)
[DISMU] Discord 노래봇의 새로운 기준, AI 노래 플랫폼의 가능성 (프로젝트 소개, 시장 분석)
[DISMU] WebSocket을 활용한 공용 플레이리스트 구현 (nest, next) [DISMU] WebSocket을 활용한 공용 플레이리스트 구현 (nest, next)👋 소개안녕하세요! 대학생 개발자 주이어입니다!오늘은 글을 진짜 오랜만에
blog.juyear.dev
이전 글 읽으러 가기!
👋 소개
안녕하세요! 대학생 개발자 주이어입니다.
오늘은 한 달 전 즈음에 소개드렸던 DISMU 프로젝트 글을 적어보려고 합니다.
DISMU 프로젝트는 한 달 넘게 내부 테스트를 진행하고 있는데요.
그 내부 테스트를 통한 피드백 반영과 버그 해결 내용을 정리해보려고 합니다.
* 공개 배포는 안 할 예정입니다.
🛠️ 기술 스택
- Frontend : Next.js, ts, html, css
- 기본적인 웹사이트 제작 및 UI 구현 - Backend : NestJS, Python
- NestJS: API 중앙화 및 Next.js와 직접적인 통신
- Python : discord bot 제작 FastAPI 서버 구축 - DataBase : PostgreSQL
- 데이터베이스 설계 및 CRUD 작업 수행 - ORM : Prisma
- NestJS에서 DB를 효율적이고 안전하게 다루기 위해서 사용 - API : Discord Oauth
- discord 계정으로 간편하게 로그인할 수 있도록 사용 - Cloud / Storage : Firebase Storage
- 사용자가 업로드한 노래 및 사진 파일을 저장하기 위해 사용 - Server : Vercel, Railway
- Vercel : 프론트 배포용
- Railway : 백엔드 및 데이터베이스 배포용 - 기타
- Uvicorn : FastAPI 서버 실행에 사용
- FFmpeg : 노래 인코딩 및 처리에 사용
💬 앨범 기능 피드백

내부 테스트를 진행하고 있는 디스코드 서버에서 들어온 피드백입니다.
어떻게 보면 음악 스트리밍 프로그램에서 기본적인 기능일 수 있는데요.
바로 아티스트 앨범 기능이 있으면 좋겠다는 내용이었습니다.
예를 들면 아이유의 정규 5집 LILAC 처럼 말이죠.
저는 아티스트가 이미 업로드한 노래에 대해서 앨범을 만들 수 있는 기능을 계획했습니다.
🎶 앨범 기능 제작
데이터베이스 (Prisma)
앨범 기능을 만들기 위해서는 크게 앨범을 생성하는 기능, 생성된 앨범을 보여주는 페이지 2가지가 필요했습니다.
앨범을 생성하는 기능을 만들기 위해서는 먼저 앨범을 저장할 데이터베이스가 있어야겠죠.
model Album {
id Int @id @default(autoincrement())
title String
artistId String
imgUrl String
createdAt DateTime @default(now())
artistInfo Artist @relation(fields: [artistId], references: [userId])
songList Song[]
}
prisma를 사용하여 위와 같이 앨범 테이블을 생성해주었습니다.
- id : 앨범 고유 아이디
- title : 앨범 제목
- artistId : 앨범을 생성한 아티스트와 연결하기 위한 key
- imgUrl : firebase에 저장된 앨범 썸네일 url
- createdAt : 앨범 생성 일자
- artistInfo : 1 : m 관계 설정
- songList : 앨범에 들어있는 노래들과 연결 (n : m 관계)
API 엔드포인트
그 다음에는 앨범 데이터들을 다룰 API 엔드포인트가 필요합니다.
@Get('getAlbum')
async getAlbum() {
return await this.albumService.getAlbum();
}
@Get('getAlbumById')
async getAlbumById(@Query('id') id: string) {
return await this.albumService.getAlbumById(Number(id));
}
@Get('getAlbumByArtist')
async getAlbumByArtist(@Query('artistId') artistId: string) {
return this.albumService.getAlbumByArtist(artistId);
}
@Post('addAlbum')
async addAlbum(
@Body('title') title: string,
@Body('artistId') artistId: string,
@Body('songs') songs: Song[],
) {
return await this.albumService.addAlbum(title, artistId, songs);
}
@Post('updateAlbum')
async updateAlbum(
@Body('id') id: string,
@Body('title') title: string,
@Body('imgUrl') imgUrl: string,
@Body('songs') songs: Song[],
) {
return await this.albumService.updateAlbum(
Number(id),
title,
imgUrl,
songs,
);
}
코드를 자세히 설명하기 위한 글이 아니기 때문에, 어떤 API 엔드포인트인지만 간단하게 정리해보겠습니다.
- Get / album : 모든 앨범 데이터를 가져옴
- Get / albumById : id를 통해 특정 앨범 데이터를 가져옴
- Get / albumByArtist : 아티스트의 모든 앨범 데이터를 가져옴
- Post / addAlbum : 새로운 앨범 데이터를 생성 및 추가함
- Post / updateAlbum : 기존 앨범 데이터를 수정함
(REST 관점에서 API 네이밍에 get이나 post가 직접 들어가지 않는 것이 좋습니다.)
여기까지 해주면, 앨범을 생성하고 관리하는 코드까지는 완성되었습니다.
이제 이러한 기능들을 실제로 사용자가 서비스 내에서 이용할 수 있도록 페이지 및 UI를 제작해주면 됩니다.
페이지 및 UI 제작

위 사진은 앨범 생성 modal 입니다.
프로필 페이지에 있는 "앨범 생성" 버튼을 클릭하면 위와 같은 창이 열리며,
기존에 업로드 했던 노래를 선택하여 앨범을 만들 수 있습니다.
가장 핵심 중 하나인 앨범을 생성하는 함수에 대해서만 정리해보도록 하겠습니다.
const handleUpload = async () => {
if (!imgFile || !title || !albumSongs) return;
setIsLoading(true);
const id = await fetch("https://discord.com/api/users/@me", {
headers: {
Authorization: `Bearer ${session?.accessToken}`,
},
})
.then((res) => res.json())
.then((data) => data.id);
console.log(id);
먼저 사용자가 앨범 생성에 필요한 데이터 및 정보를 다 입력했는지 확인해줍니다.
그 후 discord api를 사용하여 현재 사용자에 대한 discord user id를 가져와줍니다.(artist id는 discord user id이기 때문입니다.)
const res = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND}/album/addAlbum`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: title,
artistId: id,
songs: albumSongs,
}),
}
);
그 후 addAlbum API를 사용하여 앨범 생성 요청을 보냅니다.
이 때 title, artistId, songs 정보를 전송합니다.
const data = await res.json();
const imgRef = ref(storage, `uploads/album/${data.id}_img`);
await uploadBytes(imgRef, imgFile);
const imgUrl = await getDownloadURL(imgRef);
await fetch(`${process.env.NEXT_PUBLIC_BACKEND}/album/updateAlbum`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id: data.id, title, imgUrl, songs: albumSongs }),
});
setIsLoading(false);
setIsOpen(false);
};
앨범 생성이 완료되었다면, firebase에 img 파일을 업로드하고 해당 img의 url을 받아와 updateAlbum API 요청을 보냅니다.
앨범 생성을 한 이후에 다시 update 요청으로 imgUrl을 등록하는 이유는 앨범의 고유 id를 이용하여 firebase에 저장하기 위해서 입니다. (이후 데이터 및 썸네일 복구를 위해서 이렇게 저장하였습니다.)

다음으로는 사용자들이 앨범을 볼 수 있는 앨범 페이지를 만들어 주었습니다.
앨범 페이지는 노래 페이지, 아티스트 페이지의 UI를 그대로 활용하였습니다.
UI와 관련된 부분이기 때문에 코드 설명은 따로 하지 않도록 하겠습니다.
앨범 재생 기능

앨범 기능을 제작하고 나서 추가적으로 피드백이 들어왔습니다.
바로 앨범 안에 있는 곡을 한 번에 플레이리스트에 추가하는 기능이었는데요.
사실 앨범 제작을 진행하면서 만들어야겠다고 생각했던 기능인데, 다른 기능 제작에 몰입하여 까먹고 있었습니다.
이 기능 같은 경우에, 기존에 만들어두었던 플레이리스트에 추가하는 함수를 활용하면 될 것 같았습니다.
const handleAddQueue = async () => {
const isBot = sessionStorage.getItem("isBot");
if (!isBot) return;
if (!JSON.parse(isBot).isBot) return;
if (!session) return;
let i;
for (i = 0; i < album!.songList.length; i++) {
await addQueue(album!.songList[i], session, setQueue);
}
};
위와 같이 만들어주었습니다.
addQueue 함수는 미리 만들어둔 플레이리스트에 노래를 추가하는 유틸 함수입니다.
처음에는 map을 사용하여 노래가 추가되도록 구현했지만, map의 경우 await가 제대로 적용되지 않았고,
앨범에 있는 노래 순서가 섞여서 플레이리스트에 추가되었습니다.
이를 해결하고자 for문으로 확실히 순서대로 삽입될 수 있도록 만들어 주었습니다.
이제 앨범에 있는 셔플 버튼을 클릭하면, 앨범에 있는 모든 곡이 순서대로 플레이리스트에 추가되어 들을 수 있습니다.
이렇게 앨범과 관련된 피드백을 모두 반영하여 기능을 추가했습니다.
🚨 discord token 버그 발견

내부 테스트를 하다 발견된 discord token 버그입니다.
discord oauth를 통해 로그인을 할 경우, cookies에 discord token이 저장되는데요.
이 token이 만료되거나 문제가 생겨서 발생하는 버그입니다.
이 경우에는 token이 만료되거나 문제가 생겼을 때 재발급을 받도록 설정하면 되는데요.
이러한 token 재발급을 refresh token이라고 합니다.
✅ refresh token으로 해결
기존에는 discord oauth로 로그인하는 부분만 구현했었는데요.
버그를 해결하기 위해 refresh token을 받아와 사용하는 방식으로 수정해주었습니다.
이러한 외부 라이브러리 및 API를 사용하는 경우 관련 정보를 찾기가 쉽지 않은데요.
이럴때 저는 GPT와 공식문서를 확인하는 편입니다.
GPT로 해결되는 경우가 대부분이지만, 가끔 API의 최신 업데이트 사항을 반영하지 못 하거나
제대로된 정보를 알려주지 못 하는 경우가 있어 공식문서도 같이 확인해줍니다.
* discord oauth 공식문서
https://discord.com/developers/docs/topics/oauth2
Discord for Developers
Build games, experiences, and integrations for millions of users on Discord.
discord.com
import NextAuth from "next-auth";
import DiscordProvider from "next-auth/providers/discord";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function refreshAccessToken(token: any) {
try {
const response = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: process.env.DISCORD_CLIENT_ID!,
client_secret: process.env.DISCORD_CLIENT_SECRET!,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
}),
});
const refreshed = await response.json();
if (!response.ok) {
throw refreshed;
}
return {
...token,
accessToken: refreshed.access_token,
accessTokenExpires: Date.now() + refreshed.expires_in * 1000,
refreshToken: refreshed.refresh_token ?? token.refreshToken,
};
} catch (error) {
console.error("Refresh token error:", error);
return {
...token,
error: "RefreshAccessTokenError",
};
}
}
const handler = NextAuth({
providers: [
DiscordProvider({
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
authorization: {
params: {
scope: "identify guilds",
prompt: "consent",
access_type: "offline",
},
},
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
token.refreshToken = account.refresh_token;
token.accessTokenExpires =
Date.now() + Number(account.expires_in!) * 1000;
return token;
}
if (!token.accessTokenExpires) token.accessTokenExpires = 0;
if (!token.refreshToken) return token;
if (Date.now() < token.accessTokenExpires) {
return token;
}
return refreshAccessToken(token);
},
async session({ session, token }) {
session.accessToken = token.accessToken as string;
session.error = token.error;
return session;
},
},
secret: process.env.NEXTAUTH_SECRET,
});
export { handler as GET, handler as POST };
저는 위와 같이 구현을 해주었습니다.
API 사용법과 관련된 부분이기 때문에 자세히 설명할 부분은 없지만,
기존과 다른 점이라면,
- providers에서 prompt와 access_type을 추가 설정
- callback을 이용한 refresh token 활용
- refresh token을 이용한 token 재발급 로직
정도인 것 같습니다.
이렇게 해주면 token 버그를 해결할 수 있게 됩니다.
🎨 로고 제작...?!

DISMU 프로젝트 공개 이후 얼마 지나지 않아 디자인과를 재학 중인 친구가 로고를 제작해주겠다고 했습니다.
안그래도 임시 로고를 사용 중이었기 때문에, 저는 좋다고 했습니다.

그렇게 탄생하게 된 DISMU의 새로운 로고입니다.
기존 로고보다 훨씬 깔끔하고, 진짜 서비스 로고 느낌이 나서 너무 만족스럽습니다.
😊 마무리
오늘 이렇게 내부 테스트를 통한 피드백 반영과 버그 해결에 대해서 정리해보았습니다.
실제 배포 계획은 없지만, 이번 경험을 통해 내부 테스트 및 비공개 테스트가 얼마나 중요한지에 대해서 확인할 수 있었습니다.
만약 실제 배포 이후에 이러한 버그가 발견된다면 서비스 이탈율 증가, 사용자 경험 하락 등 큰 피해가 발생할 수 있습니다.
비록 내부 테스트 이지만, 제가 만든 서비스를 이용해주면서 피드백을 주신 분들께 감사드리며,
지금까지 읽어주신 분들도 감사드립니다.
KYT CODING COMMUNITY Discord 서버에 가입하세요!
Discord에서 KYT CODING COMMUNITY 커뮤니티를 확인하세요. 26명과 어울리며 무료 음성 및 텍스트 채팅을 즐기세요.
discord.com
KYT CODING COMMUNITY 가입하기!
'[Projects] > [DISMU]' 카테고리의 다른 글
| [DISMU] Discord 노래봇의 새로운 기준, AI 노래 플랫폼의 가능성 (프로젝트 소개, 시장 분석) (2) | 2025.10.26 |
|---|---|
| [DISMU] WebSocket을 활용한 공용 플레이리스트 구현 (nest, next) (2) | 2025.10.05 |