Nest.js 단축 링크 서비스에 클릭 추적 추가하기
Emily Parker
Product Engineer · Leapcell

이전 글에서 기본적인 단축 링크 서비스를 구축했습니다.
종합적인 단축 링크 서비스가 되기 위해서는 데이터 분석 기능을 추가하는 것이 필수적입니다. 데이터 분석은 단축 링크 서비스의 핵심 가치 중 하나입니다. 링크 클릭을 추적함으로써 전파 효과, 사용자 프로필 및 기타 정보를 이해할 수 있습니다.
다음으로, 서비스에 클릭 추적 기능을 추가하여 각 클릭 시 시간, IP 주소 및 기기 정보를 기록할 것입니다.
1. 클릭 기록을 위한 데이터베이스 엔티티 생성
먼저 각 클릭 기록을 저장할 새로운 데이터베이스 테이블이 필요합니다. 마찬가지로 TypeORM 엔티티를 생성하여 구조를 정의할 것입니다.
src
디렉토리에 click
이라는 새 폴더를 만들고, 그 안에 click.entity.ts
파일을 만듭니다.
// src/click/click.entity.ts import { ShortLink } from '../short-link/short-link.entity'; import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, } from 'typeorm'; @Entity() export class Click { @PrimaryGeneratedColumn('uuid') id: string; // ShortLink 엔티티와의 다대일(Many-to-One) 관계 설정 // 이는 하나의 단축 링크가 여러 클릭 기록에 해당될 수 있음을 의미합니다. @ManyToOne(() => ShortLink, (shortLink) => shortLink.clicks) @JoinColumn({ name: 'shortLinkId' }) // 데이터베이스에 외래 키 열을 생성합니다. shortLink: ShortLink; @CreateDateColumn() clickedAt: Date; @Column({ type: 'varchar', length: 45 }) // IPv4 또는 IPv6 주소를 저장하기 위함 ipAddress: string; @Column({ type: 'text' }) userAgent: string; }
설명:
@ManyToOne
: 이 데코레이터는Click
과ShortLink
간의 관계를 설정합니다. 많은(Many) 클릭이 하나의(One) 단축 링크와 연결될 수 있습니다.@JoinColumn
: 두 테이블을 데이터베이스 수준에서 연결하는 데 사용되는 외래 키 열의 이름을 지정합니다.
2. ShortLink
엔티티 업데이트
단축 링크에서 모든 클릭 기록을 쿼할 수 있도록, 양방향 관계를 설정하기 위해 short-link.entity.ts
파일도 업데이트해야 합니다.
src/short-link/short-link.entity.ts
를 열고 clicks
속성을 추가합니다.
// src/short-link/short-link.entity.ts import { Click } from '../click/click.entity'; // Click 엔티티 가져오기 import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, Index, OneToMany, } from 'typeorm'; @Entity() export class ShortLink { @PrimaryGeneratedColumn('uuid') id: string; // ... 기타 기존 필드 ... @Column({ unique: true }) @Index() shortCode: string; @Column({ type: 'text' }) longUrl: string; @CreateDateColumn() createdAt: Date; // 새 속성 @OneToMany(() => Click, (click) => click.shortLink) clicks: Click[]; }
설명:
@OneToMany
:ShortLink
에서Click
으로의 일대다(One-to-Many) 관계를 설정합니다. 하나의(One) 단축 링크는 여러(Many) 클릭 기록을 가질 수 있습니다.
3. 클릭 서비스 모듈 및 로직 생성
ShortLink
와 마찬가지로, 관련 로직을 처리하고 코드 모듈성을 유지하기 위해 Click
을 위한 전용 모듈과 서비스를 생성할 것입니다.
필요한 파일을 빠르게 생성하기 위해 Nest CLI를 사용합니다.
nest generate resource click
ClickModule 구성
src/click/click.module.ts
를 열고, TypeOrmModule
을 가져오며 Click
엔티티를 등록합니다.
// src/click/click.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ClickService } from './click.service'; import { Click } from './click.entity'; @Module({ imports: [TypeOrmModule.forFeature([Click])], providers: [ClickService], exports: [ClickService], // 다른 모듈에서 사용할 수 있도록 ClickService를 내보냅니다. }) export class ClickModule {}
ClickService 구현
src/click/click.service.ts
를 열고 클릭 기록을 위한 로직을 작성합니다.
// src/click/click.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Click } from './click.entity'; import { ShortLink } from '../short-link/short-link.entity'; @Injectable() export class ClickService { constructor( @InjectRepository(Click) private readonly clickRepository: Repository<Click> ) {} async recordClick(shortLink: ShortLink, ipAddress: string, userAgent: string): Promise<Click> { const newClick = this.clickRepository.create({ shortLink, ipAddress, userAgent, }); return this.clickRepository.save(newClick); } }
4. 리디렉션 중 클릭 추적 통합
이제 ShortLinkController
의 redirect
메서드에 추적 로직을 통합할 것입니다.
ShortLinkModule
업데이트
먼저 ShortLinkModule
이 ClickModule
을 가져와 ClickService
를 사용할 수 있도록 해야 합니다.
// 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'; import { ClickModule } from '../click/click.module'; // ClickModule 가져오기 @Module({ imports: [TypeOrmModule.forFeature([ShortLink]), ClickModule], // ClickModule 추가 controllers: [ShortLinkController], providers: [ShortLinkService], }) export class ShortLinkModule {}
ShortLinkController
수정
src/short-link/short-link.controller.ts
를 열고, ClickService
를 주입하여 redirect
메서드에서 호출합니다.
// src/short-link/short-link.controller.ts import { Controller, Get, Post, Body, Param, Res, NotFoundException, Req } from '@nestjs/common'; import { Response, Request } from 'express'; import { ShortLinkService } from './short-link.service'; import { CreateShortLinkDto } from './dto/create-short-link.dto'; import { ClickService } from '../click/click.service'; // ClickService 가져오기 @Controller() export class ShortLinkController { constructor( private readonly shortLinkService: ShortLinkService, private readonly clickService: ClickService // ClickService 주입 ) {} // ... createShortLink 메서드는 변경되지 않았습니다 ... @Post('shorten') // ... @Get(':shortCode') async redirect( @Param('shortCode') shortCode: string, @Res() res: Response, @Req() req: Request // Express Request 객체 주입 ) { const link = await this.shortLinkService.findOneByCode(shortCode); if (!link) { throw new NotFoundException('Short link not found.'); } // 사용자의 리디렉션을 지연시키지 않도록, 클릭 기록을 비동기적으로 처리합니다. this.clickService.recordClick(link, req.ip, req.get('user-agent') || ''); // 리디렉션 수행 return res.redirect(301, link.longUrl); } }
주요 변경 사항:
ClickService
를 주입했습니다.redirect
메서드에서@Req()
데코레이터를 통해express
의Request
객체를 가져왔습니다.req
객체에서req.ip
(IP 주소)와req.get('user-agent')
(기기 및 브라우저 정보)를 가져왔습니다.this.clickService.recordClick()
을 호출하여 클릭 기록을 저장했습니다. 참고, 클릭 기록 작업이 리디렉션을 차단하지 않도록await
를 사용하지 않았습니다.
5. 통계 보기
데이터가 기록된 후에는 이를 볼 수 있는 엔드포인트도 필요합니다. ShortLinkController
에 통계 엔드포인트를 추가해 보겠습니다.
// src/short-link/short-link.controller.ts // ... (가져오기 및 생성자) @Controller() export class ShortLinkController { // ... (생성자 및 기타 메서드) // 새 통계 엔드포인트 @Get('stats/:shortCode') async getStats(@Param('shortCode') shortCode: string) { const linkWithClicks = await this.shortLinkService.findOneByCodeWithClicks(shortCode); if (!linkWithClicks) { throw new NotFoundException('Short link not found.'); } return { shortCode: linkWithClicks.shortCode, longUrl: linkWithClicks.longUrl, totalClicks: linkWithClicks.clicks.length, clicks: linkWithClicks.clicks.map((click) => ({ clickedAt: click.clickedAt, ipAddress: click.ipAddress, userAgent: click.userAgent, })), }; } }
이 엔드포인트가 작동하려면 ShortLinkService
에 findOneByCodeWithClicks
메서드도 추가해야 합니다.
src/short-link/short-link.service.ts
를 엽니다.
// src/short-link/short-link.service.ts // ... @Injectable() export class ShortLinkService { constructor( @InjectRepository(ShortLink) private readonly shortLinkRepository: Repository<ShortLink> ) {} // ... (findOneByCode 및 create 메서드) // 관련 클릭을 로드하도록 TypeORM에 지시하는 키 async findOneByCodeWithClicks(shortCode: string): Promise<ShortLink | null> { return this.shortLinkRepository.findOne({ where: { shortCode }, relations: ['clicks'], // 키: TypeORM에 관련 클릭 엔티티를 로드하도록 지시합니다. }); } }
이제 서비스를 다시 시작합니다. 단축 링크를 방문한 후 http://localhost:3000/stats/your-short-code
로 GET
요청을 하면, 아래와 유사한 JSON 응답을 볼 수 있으며, 여기에는 상세한 클릭 기록이 포함됩니다.
{ "shortCode": "some-hash", "longUrl": "https://www.google.com/", "totalClicks": 1, "clicks": [ { "clickedAt": "2025-09-17T23:00:00.123Z", "ipAddress": "::1", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ..." } ] }
이제 단축 링크 서비스에 클릭 추적 및 통계 분석 기능이 추가되었습니다!
프로덕션으로 업데이트
서비스가 이미 온라인 상태이므로, 로컬에서 디버깅한 후 다음 단계는 업데이트를 프로덕션 서버에 배포하는 것입니다.
Leapcell에 배포된 애플리케이션의 경우, 이 단계는 매우 간단합니다. GitHub로 코드를 푸시하기만 하면 Leapcell이 자동으로 최신 코드를 가져와 애플리케이션을 업데이트합니다.
또한 Leapcell 자체에는 강력한 내장 트래픽 분석 기능이 있습니다. 클릭 추적을 직접 구현하지 않더라도 Leapcell 대시보드에 표시되는 데이터는 심층적인 사용자 통찰력을 제공할 수 있습니다.