[사이드 프로젝트] 59Mins - 2편

김영훈·2022년 8월 17일
1

Side Project

목록 보기
2/4
post-thumbnail

사이드 프로젝트 소개


⭐️59mins⭐️

직장인들의 고민인 회의 시간을 보다 효율적으로 사용으로 운용하고자 만들어진 제품입니다.


작업 내용


1. API 수정 요청

마이크로서비스처럼 백엔드가 짜여있었는데 상황에 따라선 화면을 그리는데에 이론상 API를 100번 이상 호출해야되는 경우도 있고 미팅을 최초 등록할때는 그냥 텍스트 저장만 하는데에 20번 이상 호출해야하고 게다가 서버에는 트랜잭션도 되어 있지 않았다.

graphql이였으면 모르겠지만 rest API에선 꽤나 부담스러웠고 백엔드 분들에게 수정 요청을 드리게 되었다.

2. Timer

  1. 타이머는 지정된 미팅 시간 (최대 60분)부터 카운트
  2. 지정된 시간 초과시 초과된 시간 출력
  3. 중간에 종료시 이어서 카운트

Websocket을 이용하여 Realtime 구현이 목표였지만 여러가지 이유로 어렵다고 판단되어 1초에 한번 Polling을 하는 형태로 구현되어 있었다.

1초 단위로 언제 끝날거라는 보장 없이 Polling을 하는게 맞나? 라는 의문이 들었다.
구현하는게 어렵다기보단 비용이나 1초 마다 요청을 보내야하는 부담 때문에 팀원들을 설득하여 1분 단위로 보내기로 변경하였다.


  useEffect(() => {
    (async () => {
      if (minutes < 60) {
        if (progress > minutes || progress === null) {
          await patchProgressTime(minutes + 1);
        }
      }
      if (minutes > 60) {
        await patchProgressTime(-minutes);
      }
    })();
  }, [minutes, progress]);


3. SWR & SSR

이전 글에서 언급한대로 Next를 사용하였지만 해당 스펙을 사용할만한 이유를 납득하는 코드는 없었다. SSG도 없고 최근 추가된 ISR을 사용한 것도 아니었다.
아무래도 정보가 프라이빗한 페이지고 유저 경험상 SSR로 데이터를 불러와 처리했으면 하는 페이지들이 있어서 적용하였다

그중 한페이지인 미팅 진행 중 페이지이다.
현재 구현된 코드에서 개인적으로 개선되었으면 하는 부분들이다.

  1. CSR로 작업하여 위에 언급된 타이머에 진행시간이 잠깐 변수 초기 값인 0으로 표기가 되는 현상이 있음
  2. 미팅 전, 미팅 완료인 미팅엔 접근 불가 처리가 안되어 있고 잘못된 url로 접속시 에러 페이지도 없음

사실 모두 잠깐 보이는 화면이라서 어찌보면 CSR로 처리를 해도 되었을테지만 SSR이 더 좋은 선택인 것 같아서 적용하게 되었다.

/api/meeting/meeting.ts

import useSWR from "swr";
import { baseURL } from "..";

import { Cookies } from "react-cookie";
const cookies = new Cookies();

export const meetSWR = (id) => {
  const { data: meetData, mutate: meetMutate } = useSWR(
    id ? `${baseURL}/api/meet/?meet_id=${id}` : null,
    (url) =>
      fetch(url, {
        headers: {
          Authorization: cookies.get("Authorization"),
        },
      }).then((res) => res.json()),
    { revalidateOnFocus: false }
  );

  return { meetData };
};

/page/meeting/[id].tsx


const MeetPage: NextPage<{ meetData: MeetData; id: string }> = ({
  meetData,
  id,
}: {
  meetData: MeetData;
  id: string;
}) => {
  return (
    <SWRConfig
      value={{
        fallback: {
          [`${baseURL}/api/meetall/?meet_id=${id}`]: meetData,
        },
      }}
    >
      <Meeting />
    </SWRConfig>
  );
};


export const getServerSideProps = async function ({
  req,
  params: { id },
}: any) {
  const session = await getSession({ req });
  const token = req.cookies["Authorization"];

  let permanent: boolean = false;
  let destination: string;
  const isAuth = session && token && id

  if (isAuth) {
      const meetData = await fetch(`${baseURL}/api/meetall/?meet_id=${id}`, {
        headers: {
          Authorization: token,
          Accept: "application/json",
        },
      }).then((res) => res.json());

      const { meet_status, meet_id } = meetData;

      if (!meet_id || meet_status !== "y") {
        // 접근 불가 redirect 
        destination = !meet_id
          ? "/404"
          : meet_status === "p"
          ? `/setting/${meet_id}`
          : `/minutes/${meet_id}`;

        return returnDestination(permanent, destination);
      } else {
        return {
          props: { meetData, id },
        };
      }
  } else {
    destination = "/login";
    return returnDestination(permanent, destination);
  }
};

export default MeetPage;

meetData를 가져와서 meet_id가 없다면 존재하지 않는 미팅으로 판단하였고
meet_status가 y(진행 중)이 아니라면 해당 상태에 맞게 redirect를 시킬 수 있게 SSR 작업을 하였다.


4. Form

react-hook-form의 사용

react-hook-form이 패키지에 추가되었는데 직접 구현을 해놓으려고 하셨던 것 같은 코드로 판단이 되었다.

다만 아직 완성이 안되어 있었고 너무 추상적인 형태라서 아예 만들어진 형태에 강제하는게 더 빠른 작업이 가능할 것 같아서 react-hook-form을 사용하기로 하였다.


 const {
    register,
    handleSubmit,
    getValues,
    setValue,
    watch,
    control,
    formState: { isSubmitting },
  } = useForm({
    mode: "onChange",
    shouldFocusError: false,
  });


  useEffect(() => {
    if (meetAllData) {
      setValue("agendaForm", meetAllData.agenda);
    }
  }, [setCursor, setValue]);


 const { fields } = useFieldArray({
    control,
    name: "agendaForm",
  });

return(
    <Body>
      <Controller
        name={`agendaForm.discussion`}
        control={control}
        render={({ field }) => (
          <>
            <TextArea
              {...field}
              name={`agendaForm.discussion`}
              style={{ padding: 0 }}
              value={field.value}
              control={control}
              ref={null}
              placeholder="논의할 내용에 대해 작성해주세요."
            />
          </>
        )}
      />
    </Body>
)

회의록 실시간 저장

회의록은 실시간으로 저장이 되어야 했다.
react-hook-form의 watch 기능을 사용하여서 내용이 변경이 되었을 경우 저장할 수 있도록 하였다.


const onPatchAgenda = async (data: IAgenda, id: string) =>
    await axios.patch(`/api/agenda/${id}/`, data, { headers });

  useEffect(() => {
    let timer;

    const subscription = watch((value) => {
      const { agendaForm } = value;

      const { decisions, discussion, action: actions } = agendaForm[cursor];
      const prevData = meetAllData.agenda[cursor];
      const {
        decisions: prevDecisions,
        discussion: prevDiscussion,
        action: prevActions,
      } = prevData;
      
     const isAgendaChanged = decisions !== prevDecisions || discussion !== prevDiscussion

      if (isAgendaChanged) {
        clearTimeout(timer);
        timer = setTimeout(async () => {
          await onPatchAgenda({ discussion, decisions }, prevData.agenda_id);
        }, 200);
      }
    });
    
    return () => {
      clearTimeout(timer);
      subscription.unsubscribe();
    };
  }, [watch, cursor]);

5. SEO, GA 작업

SEO는 로그인 페이지 말고는 노출되어지기가 쉽지 않은 형태였다. 그래서 우선 공통적인 SEO만 적용하기로 하였고 팀원들이 GA를 붙여보고 싶다라는 이야기가 나와서 GA까지 작업을 하게 되었다.

SEO


//app.tsx

const DEFAULT_SEO = {
  title: "59mins",
  description:
    "59mins는 직장 내 효율적인 회의를 돕는 웹 서비스입니다. 59분 이내로 아젠다 별 타이머를 세팅한 후 논의, 결정, 액션 아이템을 정리할 수 있는 양식에 따라 회의록을 작성할 수 있습니다. 미팅 도중에 타이머가 적절한 논의 시간과 결정 시간을 안내해주어 보다 효율적으로 시간을 관리할 수 있도록 도와드립니다. 회의가 종료된 후에는 동료들과 자가진단을 통해 회의 시간을 회고하고, 저장된 회의록을 조회 및 관리할 수 있습니다.",
  canonical: "https://59mins.net",
  openGraph: {
    type: "website",
    locale: "ko_KR",
    url: "https://59mins.net",
    title: "59mins",
    description:
      "59mins는 직장 내 효율적인 회의를 돕는 웹 서비스입니다. 59분 이내로 아젠다 별 타이머를 세팅한 후 논의, 결정, 액션 아이템을 정리할 수 있는 양식에 따라 회의록을 작성할 수 있습니다. 미팅 도중에 타이머가 적절한 논의 시간과 결정 시간을 안내해주어 보다 효율적으로 시간을 관리할 수 있도록 도와드립니다. 회의가 종료된 후에는 동료들과 자가진단을 통해 회의 시간을 회고하고, 저장된 회의록을 조회 및 관리할 수 있습니다.",
    site_name: "59mins",
    images: [
      {
        url: "https://59mins.net/image/Kakao_Share_Thumbnail.png",
        width: 96,
        height: 96,
      },
    ],
  },
  twitter: {
    handle: "@handle",
    site: "https://59mins.net",
    cardType: "summary_large_image",
  },
};

GA

//_document.tsx

<Head>
  ....
  <meta name="google-site-verification" content="" />
  <script async src="https://www.googletagmanager.com/gtag/js?id=G-" />
  <script
    dangerouslySetInnerHTML={{
      __html: `
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'G-', {
      page_path: window.location.pathname,
    });
    `,
    }}
  />
</Head>

배포


기능, 디자인 검수가 끝나고 프로젝트를 배포하기로 결정하게 되었다.
백엔드분들과 어떤 형태로 배포를 할지 논의가 되었는데 선택지는 두가지였다.

  1. EC2에 프론트 & 백엔드 배포
  2. 둘이 따로 배포

Next로 만들었기 떄문에 프론트는 Vercel에 배포를 하고 싶었다. CI/CD, 이미지 최적화, 성능 모니터링등의 기능 게다가 무료라는게 매력적으로 느껴졌다.

그래서 백엔드분들께 따로 배포하자고 말씀드렸고 프론트는 Vercel에 백엔드는 EC2에 배포하게 되었다.

백엔드를 배포하면서 서브도메인 & SSL이슈 때문에 조금 시간이 지체되었지만 문제 없이 배포할 수 있었다.

처음 들었던 이야기와는 달리 프로젝트를 CSS 제외하고 새로 다시 만든 것 같다...
우선 1차적인 목표였던 배포를 마무리 지을 수 있어서 뿌듯했다.



배포 후 이야기는 다음 포스팅에서..

profile
개인적인 기록.

0개의 댓글