Blog Park - 로그 데이터 적용하기, 로그 팝업 위치 조정하기

박상준

2024년 10월 31일

1

Blog Park
Next.js
캘린더

지금까지 진행 상황

저번에 현재 연도를 기반으로 1년짜리 캘린더를 구현했다. 이제 각 날짜를 클릭했을때 상세 로그 정보를 확인하도록 만들어줘야한다.

로그 불러오기

우선 로그 테이블을 생성해줬다.

comment_count: number | null;
created_at: string;
id: string;
like_count: number | null;
rate: number | null;
updated_at: string | null;
user_id: string | null;

유저가 활동하게 되면 최초에 활동 로그를 생성하고, 이후에는 각 활동에 대한 count값을 바꿔주는 식으로 구상했다. 그리고 rate는 각 점수마다 캘린더 색상을 지정하기 위한 값이다. 그래서 내가 구상한 점수는 포스트 50점, 댓글 40점, 좋아요 10점이다. 전부다 달성했을때 100점에 도달하고 캘린더에서 확인함으로서 블로그 활동 빈도와 정도를 알수가 있다.

구상 이유 : 이렇게 했을때 이 유저가 포스팅을 위주로 하는지, 댓글을 쓰는 유저인지, 그냥 활동량이 적은 유저인지 파악이 가능하기 때문이다.

그럼 이 데이터를 어떻게 불러와야할지 고민해봐야한다.

export default async function CalendarSingleDay({ date }) {
cosnt data = await getLog(date);
...
})

만약 클라이언트 컴포넌트가 아닌 서버 컴포넌트로 구현한다면 이렇게하면 간단하긴 할거다. 단 한개의 로그만 불러오고 로그 여부를 쉽게 관리할 수 있다. 하지만 이렇게 코드를 작성하면 1년 전체 일수만큼의 요청이 간다. 즉 올해는 366일이니 366번의 데이터 요청이 가는것이다. 유저가 몇명 안될때는 상관없겠지만 동시에 100명의 유저가 접속한다면 36600번의 요청이 가는 것이다. 그래서 한번의 요청으로 처리하도록 구현하는게 시간은 걸릴지 몰라도 서버 부하를 줄일 수 있다.

export const getLogById = async ({
  userId,
  year,
}: {
  userId: string;
  year: number;
}) => {
  const { data } = await supabase
    .from('activity_logs')
    .select('*')
    .eq('user_id', userId)
    .gte('created_at', `${year}-01-01`)
    .lte('updated_at', new Date().toISOString());

  return data;
};

우선 해당 연도와 userId를 받아 로그를 조회하는 로직을 작성해준다. 이제 activity_log라는 테이블에서 요청 userId에 해당하는 데이터를 전부 가져온다.

gte, lte는 뭘까? SQL명령어이다. 그래서 created_at이 2024-01-01부터, updated_at이 현재 날짜까지 조회하라는 명령이다.

이제 현재 날짜까지의 로그를 불러온다.

로그 데이터 변환하기

이제 전체 데이터를 불러왔으니 이제 각 날짜에 배정해주면 된다. 하지만 지금 데이터는 배열 형태로 온다. 맘같아선 filter같은걸 사용하면 좋겠지만 activity_log는 객체 형태로 오기 때문에 쉽지가 않다. 만약 배열을 그대로 사용한다면 반복문을 통해서 같은 날짜의 값을 찾아도 된다. 하지만 위에서 고민했듯이 1년 전체에 대한 배열을 반복하는 것은 상당히 비효율적이다. 그래서 배열을 객체 형태로 변환해줄 것이다.

객체로 변환하는 이유 : 날짜를 key값으로 갖는 객체로 변환한다면 대괄호로 접근이 가능하기 때문이다. 반복을 할 이유가 없다.

  const logByDate: Record<string, Log> = log!.reduce((acc, entry) => {
    const date = formatDateTz(entry.created_at!);
    acc[date] = { ...entry };
    return acc;
  }, {} as Record<string, Log>);

reduce를 사용해서 새로운 객체를 만들어주는 것이다. formatDateTz는 지금 데이터가 서버기준인 UTC를 사용하고 있어서 한국 시간대로 변환시켜주기 위한 로직이다. 그래야 서버기준 시간이 아니라 한국 기준으로 사용하기 때문이다. 외국인들은 어쩔수 없다..

이제 변환한 날짜를 key값으로 해당 값은 log를 넣어주면 된다. 객체 이미지 객체를 확인해보면 로그의 날짜별로 로그가 잘 들어가 있다.

{Array.from({ length: totalDay }, (_, index) => {
  // 위에서 만들어진 객체에서 같은 날짜에 있는 프로퍼티값을 props로 전달
  const day = index + 1;
  const dayByYear = getDateFromDay(currentYear, day); // 현재 연도에서 해당 일수의 날짜로 변환
  const formatDate = formatDateTz(dayByYear); // 날짜를 시간 제외한 포멧으로 변경
  const matchLog = logByDate[formatDate];

  return (
    <CalendarSingleDay
      key={day}
      day={day}
      year={currentYear}
      log={matchLog}
      containerRef={containerRef}
    />
  );
})}

이제 객체의 대괄호 접근법을 통해 직접 해당 값에 접근하는 것이다. 그렇게 각 날짜에 맞는 로그를 전달해주는 것이다. 이제 한번에 요청으로 반복문을 사용할 필요없이 데이터를 넣어주고 있다.

로그 팝업 위치

로그를 구현하면서 이런 문제점을 발견했다. 로그 위치 사진 현재 로그 팝업은 각 날짜 칸을 기준으로 absolute와 포지션이 적용되어 있다. 하지만 사진과 같이 특정 위치에서는 이 팝업이 가려지는 문제가 발생했다. 그래서 각 칸마다 위치를 수정해줄 필요가 있다.

  useEffect(() => {
    if (containerRef.current && infoRef.current) {
      const child = infoRef.current.getBoundingClientRect(); //로그 기록 위치
      const parent = containerRef.current.getBoundingClientRect(); // 컨테이너 위치
      const leftPosition = child.left - parent.left; // 컨테이너 기준 로그 태그 좌측 포지션
      const topPosition = child.top - parent.top; // 컨테이너 기준 로그 태그 상단 포지션
      if (leftPosition > parent.width - 130) {
        // 컨테이너 넓이 - 로그 태그 넓이
        setXDirection('left');
      }
      if (topPosition > parent.height - 130) {
        setYDirection('center');
      }
      if (topPosition > parent.height - 100) {
        // 컨테이너 높이 - 로그 태그 높이
        setYDirection('bottom');
      }
    }
  }, [containerRef]);

그래서 ref를 사용해서 위치를 조정해주기로 했다. containerRef는 전체 캘린더를 ref로 지정한 값이고 infoRef는 개별 캘린더를 ref로 지정한 것이다.

워선 클라이언트측에서 뷰포트를 기준으로 컨테이너와 로그 팝업의 위치를 파악한다. 그리고 leftPositiontopPosition을 통해 각 팝업이 컨테이너 내에서 어디에 위치하는지 파악가능하다. 그리고 내가 조절한 위치에 따라 state를 변경시켜줘 각 state에 해당하는 스타일을 지정해주면 된다.

여기에서 130, 100과 같은 값들은 임의로 지정한 값들이다. 조금씩 변경하면서 거슬리지 않도록 위치를 조정한 것이다.

const directionStyle: DirectionStyle = {
  right: 'origin-top-left translate-x-4',
  left: 'origin-top-right right-0 -translate-x-4',
  top: 'top-0',
  bottom: 'bottom-0',
  center: 'top-0 -translate-y-1/2',
};

이렇게 지정한 스타일을 적용시켜주면 된다. 수정한 팝업 이제 같은 위치의 날짜를 클릭해도 가리는 부분없이 잘 나타난다.

마무리

날짜도 많고 날짜에 따른 로그를 어떻게 관리하면 효율적일지 고민을 많이 했다. 아직은 내가 생각한 방법이 최선이다. 하지만 더 효율적인 방법이 있진 않을까 고민을 해봐야겠다. 이제 활동을 한눈에 파악하기 좋은 도넛 차트를 만들것이다. 내 기준에서 도넛차트가 항목이 적은 데이터를 가시성이 좋게 만들기에 최선의 방법이라고 생각한다. 여러 라이브러리가 있지만 svg를 활용해서 직접 만들어 보겠다.

개의 댓글