본문 바로가기
Insight/회고

[Liven 2.0] 포토카드 - S3 리소스 랜덤 배정

by 싯벨트 2025. 3. 6.
728x90

배경

티켓 등을 담을 수 있는 콜렉션 모델에 포토카드 타입을 추가하여 팬들에게 아티스트와 함께 하는 순간을 기억할 수 있는 포토카드를 주고자 했습니다. 촬영했던 공연의 순간들을 2초 정도의 움짤로 만들어서 선물상자를 열면 티켓과 매칭하여 랜덤 배정된 포토카드가 티켓의 커버로 들어가는 기능을 구현했습니다.

해당 공연은 페스티벌처럼 여러 날짜에 거쳐 다양한 아티스트가 출연했기 때문에 포토카드 이벤트를 위해 티켓 구매자에게 선호 아티스트에 대한 설문을 진행했었습니다. 움짤의 제작과 S3 업로드를 완료한 뒤, 진행했던 기능을 살펴보겠습니다.

돌아보며

이전에 짰던 코드들을 다시 살펴보니, 함수들의 책임 분리가 잘 되지 않았던 게 가장 눈에 띄었습니다. 읽기가 좀 어렵더군요. 단일책임원칙을 준수하는 것의 중요성을 다시금 깨닫게 되었습니다. 딕셔너리 형태의 객체로 인한 for 문의 사용, S3, SNS 등의 기능들을 좀 더 세분화해서 구성하면 좋았을 것 같습니다.

기능

해당 기능은 이벤트로 기획했던 사항입니다. 그렇기 때문에 어울리는 컨텍스트에 저장하는 것이 아니라, admin에서 다양한 도메인은 가지고와서 처리를 했으며, 해당 함수의 호출을 통해 S3의 리소스 랜덤 배정 및 카카오톡 알림 발송까지 이루어지도록 구성했습니다.

선호도 기반 s3 버킷에 있는 영상을 랜덤 배정

  1. manager.query로 sql 문을 짜고, 설문 관련 데이터 추출
  2. lodash groupBy 메서드로 event → artist 그룹화 및 for…in 문으로 딕셔너리 형태의 객체 사용
  3. 저장한 리소스 랜덤 배정
    • ListObjectsCommand 호출
    • 반환값에서 첫 번째(파일 없는 prefix) 제거 후 랜덤 섞기

티켓 업데이트를 위한 SNS 토픽에 메시지 게시

  1.  AWS SNS 주제에 게시

카카오톡 알림을 발송

  1. 카카오 알림 SNS 주제에 게시

코드

manager.query 사용

<> 안에 추출되는 데이터의 타입을 지정하고, 쿼리는 가독성을 위해 따로 빼줬습니다. 만약 쿼리에 파라미터가 들어가야 한다면 쿼리 뒤에 콤마를 찍고 [] 안에 인수를 넣어줍니다.

const samples = await datasource.manager.query<SampleInterface[]>(EXTRACT_SAMPLE_QUERY, [
    someParam,
]);

lodash groupBy 메서드

groupBy() 메서드 대상을 기준으로 그룹화하여 딕셔너리 형태의 객체로 만듭니다. 연속으로 그룹화를 할 경우 아래처럼 for…in을 통해 키값을 사용할 수도 있고, 키 벨류를 보다 명확하게 표현하기 위해 Object.entries를 사용할 수도 있습니다.

const firstGroupedSample = _.groupBy(samples, "firstKey");

for (const firstKey in firstGroupedSample) {
    const secondGroupedSample = _.groupBy(firstGroupedSample[firstKey], "secondKey");

    for (const secondKey in secondGroupedSample) {
    //...
    }
}
const firstGroupedSample = _.groupBy(samples, "firstKey");

for (const [firstKey, firstValue] of Object.entries(firstGroupedSample)) {
    const secondGroupedSample = _.groupBy(firstValue, "secondKey");

    for (const [secondKey, secondValue] of Object.entries(secondGroupedSample)) {
        // ...
    }
}

ListObjectsCommand 를 통해 리소스 가져오기

S3에서 리소스 리스트를 가져오면 첫 번째 요소는 “bucket/sample/” 처럼 Prefix 만 있는 형태입니다. 대응되는 리소스가 없으므로 첫 번째 요소를 제거한 뒤 랜덤으로 섞어줍니다. 이때 lodash의 shuffle을 활용하거나, Array.sort(() => Math.random() - 0.5) 를 활용할 수 있습니다.

export class S3Service {
    private s3Client;
    constructor() {
        this.s3Client = new S3Client({ region: process.env.REGION });
    }

    async getShuffledList(bucket: string, prefix: string): Promise<string[]> {
        const input: ListObjectsCommandInput = {
            Bucket: bucket,
            Prefix: prefix,
        };
        const response = await this.s3Client.send(new ListObjectsCommand(input));

        const s3UriList =
            response.Contents?.map(
                (obj) => `https://${bucket}.${process.env.REGION}.amazonaws.com/${obj.Key}`
            ) || [];
        s3UriList.shift();

        return _.shuffle(s3UriList);
    }
}