FingerPrintJS로 조회수와 좋아요 기능 만들기 Thumbnail

쿠키 대체? FingerprintJS

12 min read
구현Fingerprint

조회수 좋아요 기능 구현하기

조회수와 좋아요 기능은 블로그에 있어서 쉽게 볼 수 있는 요소이다. 티스토리도 공감이라는 이름으로 존재하고, 벨로그도 이 기능이 있다. 벨로그의 경우에는 내가 좋아요한 포스트로 모아볼 수 있어야하기 때문에 유저 인증이 필수적인 서비스이다.

하지만 나는 일개 개인 블로그이기 때문에 내 블로그에 좋아요 누르려고 회원가입을 하는 사람은 결코 없을 것이다. 그렇다고 누구나 여러번 누를 수 있게 하면 좋아요 수치가 무의미해지는 것 같다고 생각했다.

그리고 조회수, 조회수를 처음에는 간단하게 블로그 데이터를 불러오는 GET 메서드 호출할 때마다 viewCount를 올리는 방식으로 구현했다. 하지만 이 경우에는 새로고침을 할 때마다 조회수가 오르기 때문에 이것도 의미없는 데이터가 되어갔다.

썸네일썸네일

어떻게 고유하게 유저를 식별하지?

어떻게하면 내 블로그에 방문한 유저를 고유하게 식별해서, 조회수나 좋아요 기능을 지원할 수 있을까라고 고민을 했다. 생각한 건 아래와 같다.

  1. 쿠키를 사용하기 다른 서비스들에서도 유저 통계를 집계하기 위해서 쿠키를 많이 사용한다고 한다. 내 블로그에 들어왔을 때 쿠키를 생성해서 유저들에게 나눠주고 다음에 접속했을 때의 대응을 할 수 있게 하는 방식이다.

하지만 한 가지 걸린 점은 특정 국가에서는 통계 목적으로 사용하는 쿠키는 사용자에게 고지 후 사용해야한다는 점이었다. 가끔 특정 웹사이트에 들어가면 팝업 창 형식으로 더 나은 서비스를 제공하기 위해 쿠키를 허용해주세요 뭐 이런 내용의 confirm 창을 본 적이 있다. 나는 항상 거절을 눌러왔기 때문에 쿠키를 사용하는 방식은 뒷전으로 두고 다른 방법을 알아봤다.

  1. 그냥 새로고침해도 조회수 늘게 하기 내가 처음에 구현한 방법이다. 이 방법으로 하면 부정확할지라도 어떤 글이 인기있는지 정도의 추세는 볼 수 있기 때문에 이렇게 구현했었다.

  2. FingerPrintJS 사용하기 이건 이번에 알아보면서 처음 배운 개념이었다. fingerprint라는 단어로부터 유추할 수 있듯 브라우저의 지문이라고 생각하면 쉽다. 물론 실제 지문처럼 겹칠 일이 현저하게 낮은 것은 아니지만, 사용자 브라우저의 고유한 특성을 조합해서 식별 가능한 문자열을 만든다는 점에서 흥미로웠다.

FingerPrint로 고유하게 식별해보자

fingerprint 기술을 쉽게 확인할 수 있는 데모사이트가 있다. 여기서 나의 고유 브라우저 지문을 확인할 수 있다. 새로고침을 해도 핑거프린트는 같기 때문에 내가 몇번 방문했는지도 기록해서 보여준다.

이거다!

이걸 사용해서 사용자를 고유하게 식별해서(고유하지 않더라도 짧은 시간 내의 중복은 쉽게 막을 수 있다.) 조회수를 올리거나 좋아요를 받아보자.

기능 구현하기

좋아요와 조회수 스키마 정의하기

좋아요와 조회수는 fingerprint를 통해 고유하게 식별하고, postId에 의존적이라는 성격 상 이름만 다르지 코드 구성이 같다. 아래는 조회수의 스키마이다.

처음에는 조회수와 좋아요 같은 작은 개념에 대해서 컬렉션을 새롭게 만드는 것에 대해 무의식적인 거부감이 있었지만 mongodb는 이런 식의 데이터를 처리하기에 특화되어있고, 성능상의 오버헤드는 크지 않다고 알게 되어 이렇게 구현했다.

import { Schema, model, models } from 'mongoose';

const viewSchema = new Schema(
  {
    postId: {
      type: Schema.Types.ObjectId,
      ref: 'Post',
      required: true,
    },
    fingerprint: {
      type: String,
      required: true,
    },
    timestamp: {
      type: Date,
      default: Date.now,
    },
  },
  { timestamps: true }
);

viewSchema.index({ postId: 1, fingerprint: 1 }, { unique: true });

const View = models.View || model('View', viewSchema);
export default View;

API 구현하기

이제 조회수를 증가시키는 API를 만들어보자. 조회수 컬렉션에 문서를 추가하는 개념이기 때문에 POST 메서드를 사용해서 만들었다.

이 코드에서 재밌는 점은 fingerprint를 커스텀 헤더로 받았다는 점이다. 처음에는 body로 받았었다. 하지만 일종의 인증 개념으로 사용하는 것이기 때문에 헤더로 받는게 더 좋을 것 같았고, nextjs에서 미들웨어를 쓸 수 있는지는 잘 모르지만 추후 미들웨어로 만들면 더 편하게 사용 가능할 것이라고 생각이 들었다.

만약 미들웨어로 만들때 fingerprint를 body로 받으면 좀 귀찮아질 것이라는 생각이 들었다. X-Fingerprint라는 헤더 항목으로 받아서 유저의 fingerprint를 요청으로 받아와 기록하기 위해서 이렇게 구현했다.

export const POST = async (request: Request) => {
  const { postId } = await request.json();
  const fingerprint = request.headers.get('X-Fingerprint') || '';

  if (!postId) {
    return new Response('postId가 필요합니다.', { status: 400 });
  }
  if (!fingerprint) {
    return new Response('Fingerprint가 필요합니다.', { status: 400 });
  }

  const existingLike = await View.findOne({ postId, fingerprint });
  if (existingLike) {
    const viewCount = await View.countDocuments({ postId });
    return Response.json(
      { message: '이미 조회한 유저입니다.', viewCount },
      { status: 203 }
    );
  } else {
    const result = await View.create({
      postId,
      fingerprint,
    });

    const viewCount = await View.countDocuments({ postId });

    if (result) {
      return Response.json(
        { message: '조회수 증가 성공', viewCount },
        { status: 200 }
      );
    } else {
      return Response.json(
        { message: '조회수 증가 실패', viewCount },
        { status: 500 }
      );
    }
  }
};

FingerPrint 생성하기 (feat. zustand)

fingerprint를 쉽게 만들어주는 @fingerprintjs/fingerprintjs를 사용했다.

간편하게 핑거프린트를 생성해주는 라이브러리이다.

이렇게 생성한 핑거프린트는 유저가 여러개의 글을 읽는 경우 매번 생성되는 것은 리소스 낭비라고 판단했다. 그래서 한번 생성하면 전역상태로 관리하고, 유저가 다시 들어와도 사용할 수 있도록 localStorage에 저장되는 persist 미들웨어도 사용하기로 결정했다.

interface FingerprintState {
  fingerprint: string;
  isLoading: boolean;
  error: string | null;
  initialize: () => Promise<void>;
}

const useFingerprintStore = create(
  persist<FingerprintState>(
    (set, get) => ({
      fingerprint: '',
      isLoading: true,
      error: null,
      initialize: async () => {
        if (get().fingerprint) {
          set({ isLoading: false });
          return;
        }

        try {
          const fp = await FingerprintJS.load();
          const result = await fp.get();
          const visitorId = result.visitorId;

          set({
            fingerprint: visitorId,
            isLoading: false,
          });
        } catch (error) {
          set({
            error: error instanceof Error ? error.message : '알 수 없는 오류',
            isLoading: false,
          });
          console.error('Fingerprint 생성 오류:', error);
        }
      },
    }),
    {
      name: 'fingerprint-storage',
    }
  )
);

이렇게 만든 전역 상태 store 코드를 사용하는 useFingerprint 커스텀 훅도 하나 더 만들어서, 캡슐화하고 외부에서 사용하는 것들만 노출시키는 방법을 사용했다.

fingerprint가 필요하면 아래처럼 쉽게 꺼내 쓸 수 있게 된 것이다.

const { fingerprint } = useFingerprint()

이렇게 하고 나서는 프론트엔드에서 글을 조회할 때, 조회수을 올리는 요청을 fingerprint와 함께 보내고, 서버에서는 중복 조회인지를 체크하고 조회수를 올리거나 유지하는 등의 행동을 취한다.

좋아요 기능은

좋아요 기능은 조회수와 대부분 코드가 같다. 다른 점은 좋아요는 delete 메서드를 만들어 좋아요 취소하는 API를 만들어둔 점과 프론트에서 처리하는 방식이 다르다는 점이다. 그 외에 fingerprint를 헤더로 받고 체킹하는 과정은 조회수와 같다.

완성된 모습

완성본완성본

다음에는 댓글도 커스텀으로 만들어봐야겠다고 생각했다.

📌 Table of Contents

0
추천 글