Nest.js를 사용하여 짧은 링크 서비스 구축하기
Takashi Yamamoto
Infrastructure Engineer · Leapcell

짧은 링크란 무엇인가?
인터넷 서핑을 하다 보면 t.cn/A67x8Y
또는 bit.ly/3z4aBc
와 같은 링크를 자주 보셨을 것입니다. 이 링크들은 보통 매우 짧으며, 클릭하면 원래 도메인 이름과 완전히 다른 페이지로 리디렉션됩니다.
이러한 링크들을 짧은 링크라고 합니다.
짧은 링크가 필요한 이유는?
짧은 링크는 트위터 초기, 엄격한 글자 수 제한이 있을 때 생겨났습니다. 당시의 주요 목적은 글자를 절약하고 링크가 귀중한 공간을 차지하는 것을 방지하는 것이었습니다.
짧은 링크가 더 널리 퍼지면서, 링크 자체의 "짧음"은 덜 중요해졌습니다. 더 많은 용도가 개발되었습니다:
- 데이터 분석: 짧은 링크 서비스는 종종 클릭 통계를 제공하여 사용자가 클릭 수와 링크의 확산 효과를 쉽게 이해할 수 있도록 합니다.
- 차단 우회: Safari 및 특정 이메일 클라이언트와 같은 일부 소프트웨어는 링크 추적에 매우 엄격하여 추적 매개변수를 자동으로 제거합니다. 짧은 링크를 사용하면 이 문제를 피할 수 있습니다.
- 미관: 짧은 링크는 깔끔하고 간결하여 소셜 미디어 및 문자 메시지와 같은 일반 텍스트 시나리오에서 공유하기에 적합하며 서식을 단순화합니다.
짧은 링크는 어떻게 작동하는가?
짧은 링크 과정은 두 단계로 나뉩니다:
짧은 링크 생성:
- 사용자가 긴 URL을 서비스에 제출합니다.
- 서비스는 이를 위한 고유 식별자(예:
aK8xLq
)를 생성합니다. - 서비스는 이 "코드"와 원래 긴 URL 간의 매핑을 데이터베이스에 저장합니다.
- 식별자가 사용자에게 반환됩니다.
짧은 링크에서 원본 링크로 리디렉션:
- 누군가가
https://short.url/aK8xLq
를 클릭하면, 브라우저는 서버로 요청을 보냅니다. - 서버는 URL 경로에서 식별자
aK8xLq
를 구문 분석합니다. - 서버는 이 식별자에 대한 데이터베이스를 쿼리하여 해당 원본 긴 URL을 찾습니다.
- 서버는 브라우저에 리디렉션 상태 코드(301/302)를 반환하고 응답 헤더의
Location
필드에 원본 긴 URL을 포함합니다. - 이 응답을 받은 브라우저는
Location
필드에 지정된 긴 URL로 자동으로 리디렉션됩니다.
식별자는 어떻게 생성되는가?
해시 알고리즘이 일반적으로 생성에 사용됩니다.
- 해시 생성: 긴 URL 자체 또는 긴 URL에 "salt"(난수 문자열)를 추가한 것을 MD5 또는 SHA1과 같은 해시 알고리즘의 입력으로 사용하여 다이제스트를 생성합니다.
- 세그먼트 자르기: 이전 단계에서 문자열을 생성합니다. 이를 세그먼트(예: 처음 6자)로 취하여 짧은 코드로 사용할 수 있습니다.
- 충돌 처리: 두 개의 서로 다른 긴 URL이 동일한 짧은 코드를 생성할 수 있습니다. 데이터베이스에 짧은 코드를 저장하기 전에 이미 존재하는지 확인해야 합니다. 존재하는 경우 다른 세그먼트를 사용하거나 짧은 코드를 다시 생성할 수 있습니다.
자신만의 짧은 링크 서비스 구축
짧은 링크 서비스는 다음과 같은 두 가지 주요 기능 모듈로 구성됩니다:
- Nest.js
- PostgreSQL (데이터베이스로)
1. 프로젝트 초기화
Nest.js CLI를 설치합니다:
npm i -g @nestjs/cli
CLI를 사용하여 새 프로젝트를 생성합니다:
nest new url-shortener
그러면 url-shortener
라는 새 폴더가 생성되고 필요한 모든 종속성이 설치됩니다. 코딩을 시작하기 위해 이 디렉터리를 편집기에서 엽니다.
2. PostgreSQL 데이터베이스 연결
다음으로 PostgreSQL 데이터베이스를 통합할 것입니다. 공식 권장 사항에 따라 TypeORM을 ORM으로 사용할 것입니다. ORM의 역할은 데이터베이스를 코드에 통합하는 것입니다.
종속성 설치
npm install @nestjs/typeorm typeorm pg # PostgreSQL 통합용 npm install @nestjs/config class-validator class-transformer # 인터페이스 매개변수 유효성 검사용
데이터베이스 설정
단계를 단순화하기 위해 데이터베이스를 로컬에 설치하고 빌드하지는 않을 것입니다. 대신 온라인 데이터베이스를 요청할 것입니다.
Leapcell에서 클릭 한 번으로 무료 데이터베이스를 얻을 수 있습니다.
웹사이트에서 계정을 등록한 후 "데이터베이스 생성"을 클릭합니다.
데이터베이스 이름을 입력하고 배포 지역을 선택하여 PostgreSQL 데이터베이스를 생성합니다.
새 페이지에서 데이터베이스에 연결하는 데 필요한 정보를 볼 수 있습니다. 하단에는 웹페이지에서 직접 데이터베이스를 읽고 수정할 수 있는 제어판이 있습니다.
데이터베이스 연결 구성
src/app.module.ts
파일을 열고 TypeOrmModule
을 가져옵니다.
Leapcell에서 얻은 데이터베이스 자격 증명을 사용하여 연결 정보를 채웁니다. ssl
이 true
로 설정되어야 함을 유의하십시오. 그렇지 않으면 연결이 실패합니다.
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'postgres', host: 'your_postgres_host', port: 5432, username: 'your_postgres_username', // PostgreSQL 사용자 이름으로 바꾸세요 password: 'your_postgres_password', // PostgreSQL 비밀번호로 바꾸세요 database: 'your_postgres_db', // 데이터베이스 이름으로 바꾸세요 schema: 'myschema', entities: [__dirname + '/**/*.entity{.ts,.js}'], synchronize: true, // 개발 시 true로 설정하면 DB 구조를 자동으로 동기화합니다. ssl: true, // Leapcell과 같은 서비스에 필요합니다. }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
짧은 링크 모듈 생성
다음으로 짧은 링크를 관리하는 모듈을 생성합니다.
Nest CLI를 사용하여 필요한 파일을 빠르게 생성할 수 있습니다:
nest generate resource short-link
이후 데이터베이스와 연결하기 위해 ShortLink
엔티티 파일을 생성해야 합니다. src/short-link
디렉터리에 short-link.entity.ts
라는 파일을 생성합니다:
// src/short-link/short-link.entity.ts import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, Index } from 'typeorm'; @Entity() export class ShortLink { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) @Index() // 쿼리 속도를 높이기 위해 shortCode에 인덱스를 생성합니다. shortCode: string; @Column({ type: 'text' }) longUrl: string; @CreateDateColumn() createdAt: Date; }
다음으로 DTO(Data Transfer Object)를 생성합니다. DTO는 들어오는 요청 데이터를 유효성 검사하는 데 사용되어 유효한 형식의 URL을 받았는지 확인합니다.
// src/short-link/dto/create-short-link.dto.ts import { IsUrl } from 'class-validator'; export class CreateShortLinkDto { @IsUrl({}, { message: '유효한 URL을 제공해 주세요.' }) url: string; }
이제 데이터베이스에 연결해야 합니다.
src/short-link/short-link.module.ts
를 열고 TypeOrmModule.forFeature([ShortLink])
를 가져와 ShortLinkModule
에 TypeOrmModule
을 등록합니다.
// src/short-link/short-link.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ShortLinkController } from './short-link.controller'; import { ShortLinkService } from './short-link.service'; import { ShortLink } from './short-link.entity'; @Module({ imports: [TypeOrmModule.forFeature([ShortLink])], controllers: [ShortLinkController], providers: [ShortLinkService], }) export class ShortLinkModule {}
Leapcell의 데이터베이스 상세 페이지로 이동하여 웹 편집기에서 다음 명령을 실행하여 해당 테이블을 생성합니다.
CREATE TABLE "short_link" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "shortCode" VARCHAR(255) NOT NULL UNIQUE, "longUrl" TEXT NOT NULL, "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX "IDX_short_code" ON "short_link" ("shortCode");
짧은 링크 서비스 생성
ShortLinkService
는 짧은 링크와 관련된 모든 비즈니스 로직을 처리하는 역할을 합니다. src/short-link/short-link.service.ts
를 열고 다음 코드를 추가합니다.
// src/short-link/short-link.service.ts import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ShortLink } from './short-link.entity'; // 가져오기 경로 수정됨 import * as crypto from 'crypto'; @Injectable() export class ShortLinkService { constructor( @InjectRepository(ShortLink) private readonly shortLinkRepository: Repository<ShortLink> ) {} // shortCode로 링크 찾기 async findOneByCode(shortCode: string): Promise<ShortLink | null> { return this.shortLinkRepository.findOneBy({ shortCode }); } // 새 짧은 링크 생성 async create(longUrl: string): Promise<ShortLink> { // 긴 링크가 이미 존재하는지 확인; 그렇다면 중복을 피하기 위해 반환합니다. const existingLink = await this.shortLinkRepository.findOneBy({ longUrl }); if (existingLink) { return existingLink; } // 고유한 shortCode 생성 const shortCode = await this.generateUniqueShortCode(longUrl); // 데이터베이스에 저장 const newLink = this.shortLinkRepository.create({ longUrl, shortCode, }); return this.shortLinkRepository.save(newLink); } /** * 해시된 짧은 코드를 생성하고 충돌을 처리합니다. * @param longUrl 원본 URL */ private async generateUniqueShortCode(longUrl: string): Promise<string> { const HASH_LENGTH = 7; // 원하는 짧은 코드 길이 정의 let attempt = 0; // 시도 횟수 // 무한 루프를 방지하기 위해 최대 시도 횟수 설정 while (attempt < 10) { // Salted hash: 각 시도마다 다른 해시를 보장하기 위해 시도 횟수를 "salt"로 추가 const salt = attempt > 0 ? String(attempt) : ''; const hash = crypto .createHash('sha256') .update(longUrl + salt) .digest('base64url') // URL 안전 문자를 위해 base64url 사용 .substring(0, HASH_LENGTH); const linkExists = await this.findOneByCode(hash); if (!linkExists) { // 해시 코드가 데이터베이스에 존재하지 않으면 고유한 것이므로 반환할 수 있습니다. return hash; } // 존재하는 경우(충돌 발생), 시도 횟수를 증가시키고 루프가 계속됩니다. attempt++; } // 여러 번의 시도 후에도 고유 코드를 찾을 수 없는 경우 오류를 발생시킵니다. throw new InternalServerErrorException( '고유한 짧은 링크를 생성할 수 없습니다. 다시 시도해 주세요.' ); } }
짧은 링크 컨트롤러 생성
컨트롤러는 HTTP 요청을 처리하고, 서비스를 호출하고, 응답을 반환하는 역할을 합니다. src/short-link/short-link.controller.ts
를 열고 다음 코드를 추가합니다.
// src/short-link/short-link.controller.ts import { Controller, Get, Post, Body, Param, Res, NotFoundException } from '@nestjs/common'; import { Response } from 'express'; import { ShortLinkService } from './short-link.service'; import { CreateShortLinkDto } from './dto/create-short-link.dto'; @Controller() export class ShortLinkController { constructor( private readonly shortLinkService: ShortLinkService, ) {} @Post('shorten') async createShortLink(@Body() createShortLinkDto: CreateShortLinkDto) { const link = await this.shortLinkService.create(createShortLinkDto.url); return { shortCode: link.shortCode, }; } @Get(':shortCode') async redirect(@Param('shortCode') shortCode: string, @Res() res: Response) { const link = await this.shortLinkService.findOneByCode(shortCode); if (!link) { throw new NotFoundException('짧은 링크를 찾을 수 없습니다.'); } // 리디렉션 수행 return res.redirect(301, link.longUrl); } }
프로젝트 시작
src/main.ts
에서 DTO 유효성 검사를 활성화합니다:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); // 이 줄을 추가하세요 await app.listen(3000); } bootstrap();
프로젝트를 시작하려면 터미널에서 다음 명령을 실행합니다.
npm run start:dev
콘솔에서 다음 명령을 실행하여 짧은 링크를 생성합니다.
curl -X POST http://localhost:3000/shorten \ -H "Content-Type: application/json" \ -d '{"url": "https://www.google.com/search?q=nestjs+url+shortener"}' # 응답: {"shortCode":"some-hash"}
짧은 링크에 액세스합니다.
이전 단계에서 반환된 shortCode
를 사용하여 전체 URL http://localhost:3000/some-hash
를 구성하고 브라우저에서 엽니다. Google 검색 페이지로 자동 리디렉션됩니다.
방문 횟수, 방문자 IP 주소 등을 기록하는 등 이 짧은 링크 서비스를 계속 향상시킬 수 있습니다.
짧은 링크 서비스 온라인 배포
이제 이 서비스를 온라인에 배포하여 짧은 링크를 인터넷에 공유하려면 어떻게 해야 할지 궁금할 것입니다.
데이터베이스를 만드는 데 사용했던 Leapcell을 기억하시나요? Leapcell은 데이터베이스 생성 외에도 Nest.js를 포함하여 다양한 언어와 프레임워크로 프로젝트를 호스팅할 수 있는 웹 앱 호스팅 플랫폼입니다.
아래 단계를 따르십시오:
- 프로젝트를 GitHub으로 푸시합니다. 단계는 GitHub 공식 문서를 참조하십시오. Leapcell은 나중에 GitHub 리포지토리에서 코드를 가져올 것입니다.
- Leapcell 페이지에서 "서비스 생성"을 클릭합니다.
- Nest.js 리포를 선택하면 Leapcell이 필요한 구성을 자동 완성하는 것을 볼 수 있습니다.
- 하단의 "제출"을 클릭하여 배포합니다. 배포는 신속하게 완료되고 배포 홈페이지로 돌아갑니다. 여기서 Leapcell이 도메인을 제공하는 것을 알 수 있습니다. 이것은 짧은 링크 서비스의 온라인 주소입니다.
이제 짧은 링크 서비스가 라이브이며 인터넷에서 모든 사람이 짧은 링크에 액세스할 수 있습니다.