junyeokk
Blog
React Ecosystem·2024. 11. 20

@woowa-babble/random-nickname

익명 채팅, 게스트 댓글, 임시 사용자 등 회원가입 없이 사용자를 식별해야 하는 상황은 생각보다 자주 등장한다. 가장 단순한 방법은 "User1234" 같은 랜덤 문자열을 생성하는 것이지만, 이런 닉네임은 사용자 간 구분이 어렵고 기억에도 남지 않는다.

이 문제를 해결하는 접근법 중 하나가 형용사 + 명사 조합의 랜덤 닉네임 생성이다. "행복한 고양이", "용감한 사자" 같은 형태인데, 읽기 쉽고 기억하기도 좋다. @woowa-babble/random-nickname은 한국어 형용사-명사 조합으로 이런 닉네임을 만들어주는 라이브러리다.


랜덤 닉네임 생성의 설계 선택지

닉네임을 자동 생성하는 방법은 여러 가지가 있다.

UUID / 난수 기반

typescript
const nickname = `user-${crypto.randomUUID().slice(0, 8)}`;
// "user-a3f2b1c9"

충돌 확률이 극히 낮고 구현이 간단하지만, 사람이 읽을 수 없다. 채팅방에서 "user-a3f2b1c9님이 입장했습니다"는 아무 의미도 전달하지 못한다.

단어 조합 기반 (Adjective + Noun)

typescript
const adjectives = ['행복한', '용감한', '조용한', '빛나는'];
const nouns = ['고양이', '사자', '독수리', '돌고래'];

const nickname = adjectives[Math.floor(Math.random() * adjectives.length)]
  + ' '
  + nouns[Math.floor(Math.random() * nouns.length)];
// "용감한 독수리"

읽기 쉽고 기억에 남지만, 단어 목록의 크기에 따라 충돌 확률이 달라진다. 형용사 100개 × 명사 100개면 조합은 10,000개뿐이다. 동시 접속자가 많은 서비스에서는 중복이 발생할 수 있다.

단어 조합 + 숫자 접미사

typescript
const suffix = Math.floor(Math.random() * 1000);
const nickname = `용감한 독수리${suffix}`;
// "용감한 독수리427"

숫자를 붙이면 충돌 확률이 크게 줄어든다. 대부분의 실용적인 시나리오에서 이 정도면 충분하다.

@woowa-babble/random-nickname은 두 번째 방식, 즉 한국어 형용사 + 명사 조합으로 닉네임을 생성한다. 우아한형제들(배달의민족) 내부 해커톤 프로젝트에서 만들어진 라이브러리로, 한국어 서비스에 특화되어 있다.


설치와 기본 사용법

bash
npm install @woowa-babble/random-nickname
typescript
import { getRandomNickname } from '@woowa-babble/random-nickname';

const nickname = getRandomNickname();
// "행복한 고양이", "용감한 사자" 등

API가 극도로 단순하다. getRandomNickname() 하나만 호출하면 된다. 내부적으로 미리 정의된 한국어 형용사 배열과 명사 배열에서 각각 하나씩 랜덤으로 뽑아 조합한다.

닉네임 형식 옵션

라이브러리는 닉네임의 형식을 지정하는 옵션을 제공한다.

typescript
import { getRandomNickname, ADJECTIVES, NOUNS } from '@woowa-babble/random-nickname';

// 기본: "형용사 명사"
const basic = getRandomNickname('animals');
// 동물 카테고리에서 생성

// 랜덤 타입 지정
const defaultNick = getRandomNickname();
// 전체 카테고리에서 랜덤

카테고리를 지정하면 특정 테마의 명사만 사용할 수 있다. 채팅방 테마에 맞춰서 동물 이름만 나오게 하거나, 음식 이름만 나오게 하는 식의 커스터마이징이 가능하다.


내부 구조

라이브러리의 핵심 로직은 간단하다.

typescript
// 실제 구현을 단순화한 형태
const ADJECTIVES = ['행복한', '슬픈', '용감한', '조용한', '빛나는', ...];
const NOUNS = {
  animals: ['고양이', '강아지', '토끼', '사자', ...],
  foods: ['떡볶이', '김치', '비빔밥', ...],
  // ...
};

function getRandomNickname(type?: string): string {
  const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
  const nounList = type ? NOUNS[type] : Object.values(NOUNS).flat();
  const noun = nounList[Math.floor(Math.random() * nounList.length)];
  return `${adjective} ${noun}`;
}

Math.random()을 사용하므로 암호학적으로 안전한 난수는 아니다. 보안이 중요한 식별자로는 적합하지 않고, 순수하게 표시용 닉네임으로만 사용해야 한다.

충돌 확률

형용사 N개, 명사 M개일 때 가능한 조합 수는 N × M이다. 라이브러리에 포함된 단어 수에 따라 달라지지만, 일반적으로 수천~수만 개의 조합이 가능하다.

동시 접속자 100명 기준으로 생일 문제(Birthday Problem)를 적용하면:

충돌 확률 ≈ 1 - e^(-n² / (2 * combinations))

조합이 10,000개이고 동시 접속자가 100명이면 충돌 확률은 약 39%로 꽤 높다. 따라서 닉네임을 유일한 식별자로 사용하면 안 된다. IP 주소, 세션 ID 등 별도의 고유 식별자와 함께 사용해야 한다.


실전 패턴: IP 기반 닉네임 고정

랜덤 닉네임의 한 가지 문제는 매번 새로운 닉네임이 생성된다는 것이다. 채팅방에서 같은 사용자가 새로고침할 때마다 다른 이름으로 표시되면 대화 흐름을 따라가기 어렵다.

이를 해결하는 패턴은 고유 키(IP, 세션 등)에 닉네임을 매핑하고 캐시하는 것이다.

typescript
충돌 확률 ≈ 1 - e^(-n² / (2 * combinations))

이 패턴의 핵심은 캐시의 TTL(Time To Live)이다. TTL이 너무 짧으면 같은 사용자가 자주 이름이 바뀌고, 너무 길면 캐시 메모리를 많이 차지한다. 익명 채팅에서는 24시간 정도가 적당한 선택이다.

Redis를 캐시로 사용하면 서버가 재시작되어도 닉네임이 유지된다는 장점이 있다.

typescript
@Injectable()
class ChatService {
  constructor(private readonly cacheService: CacheService) {}

  async getOrCreateNickname(clientKey: string): Promise<string> {
    // 1. 캐시에서 기존 닉네임 조회
    const existing = await this.cacheService.get(`nickname:${clientKey}`);
    if (existing) {
      return existing;
    }

    // 2. 없으면 새로 생성하고 캐시에 저장
    const nickname = getRandomNickname();
    await this.cacheService.set(
      `nickname:${clientKey}`,
      nickname,
      60 * 60 * 24  // 24시간 TTL
    );
    return nickname;
  }
}

직접 구현 vs 라이브러리 사용

이 라이브러리의 코드량은 매우 적다. 직접 구현해도 100줄이면 충분하다. 그렇다면 왜 라이브러리를 쓸까?

라이브러리 사용이 유리한 경우:

  • 한국어 단어 목록을 직접 큐레이션하기 귀찮을 때
  • 비속어, 부적절한 단어가 섞이지 않은 검증된 목록이 필요할 때
  • 빠르게 프로토타이핑해야 할 때

직접 구현이 유리한 경우:

  • 서비스 테마에 맞는 커스텀 단어 목록이 필요할 때
  • 닉네임 형식을 세밀하게 제어해야 할 때 (3단어 조합, 숫자 접미사 등)
  • 다국어 지원이 필요할 때

직접 구현 예시

typescript
// Redis에 저장하는 경우
const redisKey = `chat:nickname:${clientIp}`;
const cached = await redis.get(redisKey);

if (cached) {
  return cached;
}

const nickname = getRandomNickname();
await redis.set(redisKey, nickname, 'EX', 86400); // 24시간
return nickname;

generateUnique 메서드는 이미 사용 중인 닉네임과 중복되지 않도록 재시도한다. 재시도 횟수를 제한해서 무한 루프를 방지하고, 모든 조합이 소진되면 숫자 접미사로 폴백한다.


유사 라이브러리 비교

영어권에서는 비슷한 목적의 라이브러리가 여러 개 있다.

라이브러리언어형식특징
@woowa-babble/random-nickname한국어형용사 + 명사한국어 특화, 카테고리 지원
unique-names-generator영어형용사 + 동물/색상커스텀 딕셔너리, 구분자 설정
random-words영어랜덤 단어 N개단순 단어 생성, 닉네임 특화 아님
human-id영어형용사 + 명사UUID 대안, 기억하기 쉬운 ID
typescript
class NicknameGenerator {
  private adjectives = ['행복한', '용감한', '조용한', '빛나는', '따뜻한'];
  private nouns = ['고양이', '강아지', '토끼', '펭귄', '수달'];

  generate(): string {
    const adj = this.pick(this.adjectives);
    const noun = this.pick(this.nouns);
    return `${adj} ${noun}`;
  }

  generateUnique(existing: Set<string>, maxRetries = 100): string {
    for (let i = 0; i < maxRetries; i++) {
      const nickname = this.generate();
      if (!existing.has(nickname)) {
        return nickname;
      }
    }
    // 모든 조합이 소진되면 숫자 접미사 추가
    return `${this.generate()}${Math.floor(Math.random() * 1000)}`;
  }

  private pick<T>(arr: T[]): T {
    return arr[Math.floor(Math.random() * arr.length)];
  }
}

한국어 서비스에서는 한글 닉네임이 훨씬 자연스럽기 때문에 @woowa-babble/random-nickname이 좋은 선택이다. 영어 서비스라면 unique-names-generator가 딕셔너리 커스터마이징이 더 유연하다.


주의사항

  1. 닉네임을 고유 식별자로 사용하지 마라. 충돌이 발생할 수 있으므로 반드시 별도의 고유 ID와 함께 사용해야 한다.
  2. 서버사이드에서 생성하라. 클라이언트에서 닉네임을 생성하면 사용자가 원하는 닉네임으로 조작할 수 있다. 익명성이 중요한 경우 서버에서 생성하고 클라이언트에 전달하는 방식이 안전하다.
  3. 캐시 전략을 고려하라. 같은 사용자에게 일관된 닉네임을 보여주려면 세션이나 IP 기반으로 닉네임을 캐시해야 한다.
  4. 단어 목록의 적절성을 검토하라. 직접 구현할 경우 비속어나 부적절한 단어가 포함되지 않았는지 반드시 확인해야 한다.

정리

  • 익명 사용자 식별에는 "형용사 + 명사" 조합이 UUID보다 읽기 쉽고 기억에 남는다
  • 닉네임은 표시용이지 고유 식별자가 아니다 — 반드시 세션 ID나 IP 등 별도 식별자와 함께 사용해야 한다
  • 동시 접속자가 많으면 충돌 확률이 높아지므로, 서버사이드 생성 + 캐시(Redis 등) 전략이 필수다

관련 문서