0️⃣ 서비스 소개
칸반보드 프로젝트는 개발 작업을 효율적으로 추적하고 관리하기 위한 도구로서, Trello와 유사한 기능을 제공합니다. 프로젝트에는 주요 기능으로 메시지 알림(SSE) 및 LexoRank를 사용한 칼럼 및 카드 정렬이 포함되어 있습니다.
⚡ Github_Kanbanbord | 💿 시연영상
1️⃣ 기술 스택
Nest.js
Bootstrap
MySQL
SSE
LexoRank
2️⃣ 핵심 기능

Num | 기능 | 기술 | 비고 |
1 | 백엔드 | Nest.js | 서버 개발을 위한 효율적이고 확장 가능한 구조 제공 |
2 | 프론트 | HTML , CSS (Bootstrap ) | 사용자 인터페이스 개발 |
3 | 데이터 | MySQL | 데이터베이스로 사용자 정보 및 대화기록 저장 |
4 | 메시지 알림 | SSE | 클라이언트에게 단방향 메시지 정도만 전달하기 때문에 SSE의 단점이 부각되지 않겠다고 생각 |
5 | 칼럼 및 카드 정렬 | LexoRank | 정렬기준 속성값을 생성하여 사잇값을 주는 방식으로 선택 |
3️⃣ 개발 내용
3.1 메시지 알림 (SSE)
- 기능 설명: 클라이언트에게 단방향 메시지를 실시간으로 전달하여 업무 현황 및 알림을 제공합니다.
- SSE 활용 이유: 프로젝트의 특성상 단방향 메시지가 주로 필요하며, 복잡한 웹소켓을 도입하지 않고도 간편하게 실시간 업데이트를 제공하기 위해 SSE를 활용했습니다.
// sse.controller.ts import { Controller, Param, Sse } from "@nestjs/common"; import { SseService } from "./sse.service"; @Controller("sse") export class SseController { constructor(private readonly sseService: SseService) {} @Sse(":userId") sendClientAlarm(@Param("userId") userId: string) { return this.sseService.sendClientAlarm(+userId); } }
// sse.service.ts import { Injectable, MessageEvent } from "@nestjs/common"; import { Observable, Subject, filter, map } from "rxjs"; @Injectable() export class SseService { private users$: Subject<any> = new Subject(); private observer = this.users$.asObservable(); emitCardChangeEvent(userId: number, message: string) { this.users$.next({ id: userId, message: message }); } sendClientAlarm(userId: number): Observable<any> { return this.observer.pipe( filter((user) => user.id === userId), map((user) => { return { data: { message: user.message, }, } as MessageEvent; }), ); } }
3.2 칼럼 및 카드 정렬 (LexoRank)
- 기능 설명: 칸반보드의 칼럼과 카드를 정렬하기 위해 LexoRank를 활용했습니다. LexoRank는 사전식 순서를 기반으로 한 카드 정렬을 가능케 하는 알고리즘입니다.
- LexoRank 활용 이유: 사용자들이 직관적으로 칼럼과 카드를 정렬하고 이동할 수 있도록 하기 위해 LexoRank를 도입했습니다. 이를 통해 정확한 사잇값을 생성하여 정렬 기준을 제공하고자 했습니다.
// LexoRank Test import { Injectable } from '@nestjs/common'; import { LexoRank } from 'lexorank'; @Injectable() export class AppService { insert(_data: string) { throw new Error('Method not implemented.'); } items: { data: string; lexo: LexoRank }[] = [ { data: 'item1', lexo: null }, { data: 'item2', lexo: null }, { data: 'item3', lexo: null }, { data: 'item4', lexo: null }, { data: 'item5', lexo: null }, { data: 'item6', lexo: null }, { data: 'item7', lexo: null }, ]; constructor() { let lexoRank = LexoRank.middle(); this.items.map((item) => { item.lexo = lexoRank; lexoRank = lexoRank.genPrev(); }); } //! lexo 값 출력 find() { const newItems = this.items .sort((a, b) => { return a.lexo.compareTo(b.lexo); }) .map((item) => { return { data: item.data, lexo: item.lexo.toString() }; }); return newItems; } //! 위치 이동 move(id: number, where: number) { let newLexo: LexoRank; if (where >= this.items.length) { newLexo = this.items[this.items.length - 1].lexo.genNext(); } else if (where <= 0) { newLexo = this.items[0].lexo.genPrev(); } else { newLexo = this.items[where].lexo.between(this.items[where + 1].lexo); } this.items[id].lexo = newLexo; return true; } }
4️⃣ 성장 경험
LexoRank의 한계점과 해결방법
LEXORANK는 목록이나 순서가 자주 변경되는 경우에도 효과적으로 순서를 부여하는 알고리즘 중 하나입니다. 그러나 이 알고리즘이 가끔 한계에 부딪힐 수 있습니다.
특히, 새로운 아이템을 추가하거나 순서를 변경할 때 이전의 순서 정보가 누적되어 부정확한 결과를 가져올 수 있습니다. 이 문제를 해결하기 위해 이동할 때마다 초기화하는 방법을 사용할 수 있습니다. 이것은 일종의 "갱신" 작업으로 생각할 수 있습니다.
LEXORANK를 사용할 때 자주 발생하는 문제 중 하나는 목록이 동적으로 변할 때 이전 순서 정보가 누적되어 예상치 못한 결과를 초래할 수 있다는 것입니다. 이를 방지하기 위해 주기적으로 또는 특정 이벤트가 발생할 때마다 순서를 초기화하여 새로운 아이템이나 변경된 순서에 대해 정확한 순서를 유지할 수 있습니다.
이를 위한 간단한 절차는 다음과 같습니다:
- 모든 순서 정보 초기화 이동할 때마다 순서를 초기화하려면 기존의 순서 정보를 완전히 초기화해야 합니다. 저장된 순서 데이터를 삭제하거나 리셋합니다.
- 새로운 순서 정보 생성 초기화 후, 새로운 아이템이나 변경된 순서에 대한 정보를 다시 생성합니다. LEXORANK 알고리즘을 다시 적용하거나 필요에 따라 다른 알고리즘을 사용할 수 있습니다.
- 순서 정보 저장: 새로운 순서 정보를 저장하여 나중에 참조할 수 있도록 합니다.
이러한 초기화 과정을 얼마나 자주 수행할지는 애플리케이션의 특성과 요구사항에 따라 다릅니다. 목록이 자주 변경되는 경우 더 자주 초기화해야 할 수 있습니다. 이 방법을 통해 목록의 정확한 순서를 유지하면서도 알고리즘의 성능을 최적화 할 수 있습니다.