{22-23년}코듀온(Codueon) - 회고

SANGHYUN KIM·2023년 2월 22일
1

프로젝트 회고록

목록 보기
4/4
post-thumbnail

배포링크
깃헙링크

0. 프로젝트전 나의 상태

메인 프로젝트가 끝난 이후의 잠깐 붕 떠있었는데 그 시간이 불안했다.
올바른 사유일 지는 모르나 불안함을 해결하면서 취업준비를 하고 싶었다.
또한, 결제 기능 및 실시간 기능이 요구사항이라서 끌렸다.

1. 주요 기능구현 및 애로사항

A. ⚙️CI/CD(부제: From SSG to SSR)

SSG 배포를 위한 AWS S3 + CloudFront 과정

주제는 코딩 과외 매칭 플랫폼이며 레퍼런스로 잡은 것은 김과외를 참고했다.
해당 페이지에 있는 세부정보까지 검색이 되는 것을 확인했고 SEO가 필요하다고 생각되어 Next.js 이용이 확정되었다. 또한, 팀원 중 한 분이 Next.js 경험이 있어서 처음 시도해보기에 좋은 시기라고 판단되었다.

그 다음, SSG 또는 SSR로 배포할 지 논의를 했으며 아래와 같은 이유로 SSG로 결정했다.(DailyLog:2022-12-30)

  1. 데이터가 수시로 바뀌지 않을 것 같다. 왜냐하면 한 사람당 하나의 과외만 등록할 수 있다.
  2. 클라이언트에서 Fetch를 통해서 데이터를 받아오기에 변경되는 정보에 대응할 수 있다.

결정이 되고 바로 CI/CD pipeline 작성을 하기 시작했고 AWS S3 + CloudFront를 이용하기로 했다. 왜냐하면, 초반부터 CI/CD를 진행함으로써 배포환경을 수시로 확인할 수 있고, 정적 페이지 호스팅을 지원하는 S3와 HTTPS로 변환 및 CloudFront로 빠른 페이지 진입을 할 수 있기 때문이다.

SSG 배포환경에서 문제 인식 및 SSR 전환 인식

문제 없이 잘 되는 것 같았지만 배포환경에서 새로고침 시 메인페이지로 다시 나가는 현상을 발견했다.
처음 마주하는 에러로 상당히 당황했고 여러 문서를 봤고 next.js github의 Discussion도 찾아보면서 두 개의 가설을 세워봤다.

  1. SSG 방식으로 배포가 되었으며 뒤에 html이 붙어서 문제가 있는 것 같다.
  2. 정상적으로 배포는 되었지만 작동방식이 CSR이 합쳐져 있다. index.html을 가져오고 로드하면서 JS가 동작이 되고 이 후 다른 페이지 link가 csr로 적용이되고 페이지 전환이 안되는 것 같아서 새로고침 시 index.html로 돌아오는 것 같다.

먼저 첫번 째 가설의 현상이 맞다는 가정 하에 두 개의 해결 방법을 찾았다.

  1. (실패)lambda function 또는 CI/CD에서 html을 삭제하고 올려준다 또는 next-config에서 trailingSlash를 넣어준다.
  2. (해결)정확한 방법은 모르겠지만 _app.tsx에서 useEffect code를 올려준다.

실질적으로 SSG배포에서 해결한 방식은 2번으로 다음과 같이 적용했다.

//_app.tsx
 const router = useRouter()

 useEffect(() => {
    router.push(window.location.href);
  }, []);

문제는 해결된 것 같았지만 로그인 페이지에서 새로고침을 하면 아래와 같이 OAuth 버튼이 사라지는 것이 확인 되었다.

결국 가설 1과 해결방법은 현재 틀리다고 생각이 되었고, 프로젝트 시작 때 부터 SEO를 의식해왔기에, SSR로 전환이 더 맞다고 판단되었고 팀원들의 동의 하에 SSR로 전환하면서 가설 2의 검증을 같이 시작해봤다.

SSR 배포 전환 시작

두 번째 가설을 검증하고 해결하기 위해서 SSR을 지원하는 AmplifyVercel두 가지 선택지를 고민했고 아래와 같은 이류로 Vercel을 선택했다.(DailyLog:2023-02-19)

  • Next.js는 Vercel이 만들었다. 따라서, 유지보수가 필요한 시점에서 만약 최신 기능을 테스트할 수 있는 가장 안전한 배포 환경을 제공할 거라고 생각했다.
  • Preview와 Production의 배포환경을 따로 확인할 수 있다. Production은 내가 main branch에 있는 실제 배포용을 확인할 수 있고 그 이전에 다른 branch에서의 push가 될 때마다 main으로 넘어가기 전 확인할 수 있는 Preview를 지원한다
  • Amplify의 배포가 느리다는 의견이 Vercel Github에서 나왔었다.
  • 그 외에 장점은 다음 "Choosing the best Next.js hosting platform"에서 많이 확인할 수 있었다,

또한 커스텀 도메인 사용을 Route53을 이용하기로 했는데 이유는 아래와 같다.(DailyLog:2023-02-19)

  • Vercel에서 베포 Domain을 변경할 수 있지만 free plan은 뒤에 vercel.app이 포함되어야 하고 커스텀 도메인은는 돈을 지불하고 변경해야 한다. 그러나 우리팀은 가비아에서 도메인을 이미 구입하였다. 따라서, Route53을 사용하는 것이 추가 금액을 방지할 수 있다.

이 후, SSG에서 보이던 문제가 말끔히 사라졌다.
(SSR 배포 시 _app.tsx에 적어놨던 해결방법은 삭제하고 진행했다)

B. 🔍SEO

SSR로 전환하면서 getServerSideProps를 활용하여 html 요청이 있을 때마다 SEO 데이터를 변경 및 적용할 수 있게 되었다.
다른 사람들의 SEO를 염탐하면서 아래와 같은 패턴으로 작성했다. 이런 패턴을 활용하여 특정 페이지에서만 따로 변경하여 사용했다. (해당 파일 깃헙 링크)

const RepeatedMeta = ({ url }: { url: string }) => {
  return (
    <>
      <meta
        name="description"
        content="코딩 과외 매칭 플랫폼 코듀온, Codueon"
      ></meta>
      {/* Facebook Meta Tags  */}
      <meta property="og:title" content="코듀온" />
      <meta property="og:url" content={url} />
      <meta property="og:type" content="website" />
      <meta property="og:image" content="images/logo.png" />
      <meta
        property="og:description"
        content="코딩 과외 매칭 플랫폼 코듀온, Codueon"
      />
      {/* Twitter Meta Tags  */}
      <meta name="twitter:card" content="images/logo.png" />
      <meta property="twitter:url" content={url} />
      <meta name="twitter:title" content="코듀온" />
      <meta
        name="twitter:description"
        content="코딩 과외 매칭 플랫폼 코듀온, Codueon"
      />
      <meta name="twitter:image" content="/images/logo.png" />
    </>
  );
};

그 결과 아래와 같이 LightHouse를 확인했을 때 SSG에서 보이던 70점대 및 확인 불가능과는 다르게 100점대가 나오는 것을 확인할 수 있었다.

또한, 카톡 공유 시 아래와 같이 잘 표현이 되는 것을 확인할 수 있다.

C. 🏞️이미지와 📜글자 전송(부제: 이미지 압축이 추가로...)

API 명세서의 요구사항 만족하기(데이터 타입이 두 개???)

이번 프로젝트를 진행하면서 이미지 전송을 같이 하게 되었고 multipart/form-data형식이 존재하는 것을 처음 알았다.
BE에서 정해준 요구사항은 아래와 같다.

// data는 application/json 형식으로 명시하여 전달 필요
const formData = {
  data: { 글자 데이터},
  image: File type,
}

처음에는 multipart/form-data로 전부 묶어서 보내면 받는 것이 안 되었고 data의 타입을 application/json을 명시할 필요가 있다고 생각하고 검색했다. 그러나 request의 Content-type에는 하나의 형식만 기재가 가능하다고 BE에서 정해준 요구사항을 어떻게 만족할 지 계속 검색하고 고민했고 이 블로그(React에서 file, json data를 같이 formData에 넣어서 REST API에 post 하기)를 통해서 해결할 수 있었다.

이미지 압축 요구

그 다음 직면한 문제는 이미지 압축이다. BE에서 받아들일 수 있는 이미지 용량이 크지 않다고 하여 고화질 이미지는 압축이 필요했다. 이 부분은 내가 다른 요구사항을 충족하고 있을 때 다른 팀원이 말해줬고 browser-image-compression 라이브러리 사용법을 적용하여 코드를 넘겨줬다.

하나의 이미지를 적용하는 것은 쉬웠기만 Array.prototype.map을 이용하여 최대 3개의 이미지를 압축하는 과정은 쉽지 않았다. 왜냐하면 3개의 이미지를 압축하고 form-data에 넣는 과정보다 form-data 전송하는 것이 먼저 실행이 되었기 때문이다. 의심할만한 부분은 이미지 압축 라이브러리의 비동기적 처리 과정이었고 다음 2번의 시도와 1번의 리팩터링을 통하여 요구사항을 만족했다.

  • 첫 번째 시도는 async await을 활용한 처리였다. 그러나 이 부분 또한 실팼다.
  • 두 번째 시도는 위 사항에서 아이디어가 도저히 생각이 나지 않아서 결국 3초간의 딜레이를 부여하고 통신을 진행했다.
  • 세 번째 시도는 위 3초간의 기다림을 줄일 수 있느냐는 BE측의 의견을 듣고 여러 부분 고민하다가 Promise까지 적용하기로 했고 이를 통해, 3초간의 기다림을 없애고 바로 통신이 가능한 상태로 리팩터링을 완료했다.
// form-data 안에서 문자열은 JSON 형식으로 처리 및 명시 + 이미지 압축 + async await 및 promise까지 적용한 코드
	/** 1. form-data 선언 및 할당 */
	const formData = new FormData();

	/** 2. 문자열 데이터 JSON으로 묶음 처리 */
	const json = JSON.stringify({ /**문자열 데이터를 객체 형태로**/ });
    const blob = new Blob([json], { type: "application/json" });
    formData.append("data", blob);

    /** 3. 프로필 이미지 압축 및 할당 */
    const proImage = profileImg[0];
    const compressImg = await compressImage(proImage);
    let compressFile;

    if (compressImg) {
      compressFile = new File([compressImg], compressImg?.name);
      formData.append("profileImage", compressFile);
    }

    /** 4. 참고 사진(최대 3의 length를 가지는 string[]) 압축 및 할당 */
    if (detailImage.length > 0) {
      const careerImgsBefore = detailImage.map((image: FileList) => image[0]);
      const some = await Promise.all(
        careerImgsBefore.map(async (img: File, idx: number) => {
          const compressImg = await compressImage(img);
          return new File([compressImg!], compressImg?.name!);
        }),
      );
      console.log(some);
      some.map(el => formData.append("careerImage", el));
    }
    mutate(formData);

D. 🔐Refresh 서버 관리 제안(부제: axios interceptor 도입)

이전 프로젝트에서 봤던 여러 글을 통해서 결국 프론트에서 refresh token의 값은 탈취당할 위험이 높으니 아예 refresh token을 서버 쪽에서 관리하여 일정시간마다 갱신을 해주는 것이 어떤 지 의견을 제시했고 BE측도 이를 수락하고 시도해보자고 했다.
로직은 다음과 같다.

  1. 유저가 로그인하면 브라우저는 access token을 갖고 BE는 access와 refresh를 둘 다 저장하고 있는다.
  2. 특정 시간 때 또는 access가 만료가 된 상태라는 것을 인지하면 BE에서 새로운 access를 발급하고 이전 access는 블랙리스트에 기재한다.
  3. 유저가 로그아웃을 하면 BE에서 해당 유저의 refresh와 access정보를 전부 삭제처리 한다.

2번이 작동할 경우 BE는 응답과 함께 header부분에 access token값을 같이 전달해주기에 클라이언트 부분에서도 이를 인지하여 token을 교체해줘야 한다. 이 부분은 axsios.intercepter(공식문서)를 활용하여 코드를 적었다.

이를 활용하여 아래와 같이 instance를 생성해주고 token이 필요한 파일들에서 사용을 해줬다.

import axios from "axios";

const instance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
});

instance.interceptors.response.use(config => {
  if (config.headers.authorization) {
    localStorage.setItem("token", config.headers.authorization);
    console.log("response 작동");
  }
  return config;
});

export default instance;

E. 💳결제 기능(부제: 어? 이거를 전부 BE가?)

프로젝트를 참여한 이유 중 하나가 결제 기능이다. 결제 페이지를 먼저 퍼블리싱 이후 API 명세서에 기재 되어있는 사항을 봤다.

FE측에서 해줘야 할 것이 저 url을 보여주고 사용자가 결제를 완료하면 BE와 통신으로 결제 여부를 갱신한다고 한다.
생각보다 할 게 없었다.(하지만 생각보다 역시나 쉽지 않았다...😅)

먼저 새로운 창을 띄우는 코드를 작성하고 해당 url이 열리게 설정을 하고 결제가 완료되면 새 창의 url값이 변경되는 것을 확인할 수 있어서 이를 통하여 클라이언트에서도 결제 여부를 확인할 수 있게 설정하고 싶었다. 그러나, 새 창의 url값을 언제 확인하고 값을 어떻게 가져오는 지에 대해서 여러 번의 시도를 해봤지만 결국 시간 문제로 결제 확인 버튼을 통해클라이언트도 완료여부를 인지하는 방식으로 진행을 했다.

F. 🚨 유저 action여부에 따른 알림 제안(부제: React-Toastify 및 sweetalert2)

지금까지 유저가 진행한 action의 피드백이 그냥 alert코드 또는 반응 없이 진행했다. 왜냐하면 굳이 필요한 여부를 알지 못 했고 부트램프 진행 시에도 도입여부에 대해 논의를 했으나 필요성에 대한 답안을 하지 못했기 때문이다.
그러나, 원티드 프리온보딩을 들으면서 유저의 action이 성공했는 지, 실패했는 지, 어떤 것이 필요한 지, 등등을 알려주는 것이 더 좋은 사이트라고 들었다. 이를 통해서 유저는 자신 action의 결과를 인지할 수 있다고 한다.

그 다음 날 바로 회의를 해서 action 결과에 대한 피드백이 있으면 좋겠다고 제안을 하게 되었고 React-Toastifysweetalert2 둘 다 사용하게 되었다.

한 번더 확인이 필요한 작업은 sweetalert2를 사용하고 그 외의 작업은 React-Toastify를 통해서 피드백을 표현했다.

2. 아쉬운 점

A. 주석(설명)의 필요성

페이지별 스프린트로 진행이 되었고 각자 작성한 컴포넌트를 하나로 합치는 형식으로 진행했다.
합치는 부분에서 타인의 코드를 대폭 수정, 추가 기능 구현 등 여러 작업을 하면서 느꼈는데 코드 파악을 하는데 필요한 시간이 상당히 많이 든다. 이를 위해서 나중에 누군가와 같이 작업을 할 때 몇몇 사항은 JSDocs로 기재 또는 주석으로 짤막하게 설명하는 부분이 추가 되었으면 더 좋았을 것 같다.

B. Repaint를 인지한 상태에서 코딩

이번 프로젝트를 하면서 다른 스터디를 같이 병행을 하면서 브라우저 렌더링에 대해서 입문을 했다. Paint하는 과정은 필수적이지만 사용되는 자원이 매우 많은 것을 알 수 있었다. Repaint를 유발하는 CSS 속성을 인지하는 상태에서 프로젝트를 한 번 진행해보고 싶다. 즉, 최적화에 대한 지식을 조금이라도 가지고(발만 담근 상태로라도....ㅎㅎ) 프로젝트를 시작하고 싶다.

P.S. 혹시 누군가 이 부분을 본다면 스터디장의 블로그를 링크로 남겨드립니다.

C. 코드 리뷰의 부재(원점 회귀)

코드 리뷰가 거의 진행되지 않았다. 부트램프와 제작당시와는 다르게 이력서 및 포트폴리오 작성, 코딩테스트, 면접 등등 부가 활동을 하다보니 1주일에 할당하는 절대적인 시간이 많이 부족했다. 그렇다보니 PR 올라오는 것을 크게 확인하지 않고 불필요 코드 및 import와 같은 것만 빠르게 체크를 하고 코드 스타일에 대해서 논의를 나눠보지 못 했다. 결과론적으로 여러 가지의 활동을 동시에 욕심내서 하다보니 얻은 것도 있지만 잃은 것이 있다면 코드 리뷰인 것은 확실하다.
또한, 코드리뷰를 하지 않다보니 A와 같이 컴포넌트를 합치는 데도 조금 어려웠던 점이 조금은 있다.

3. 결론

처음 사용하는 stack과 새로운 문제를 많이 만났다. 그리고 여러 강의를 통해서 들은 지식들을 실행에 옮겨보는 과정이었다.

  1. 실행해보는 것이 제일 좋다. 안 될 것 같다고 생각하지 말고 적용해보고 에러를 보자
    • 강의로 들은 내용 실천 포함!
  2. 모르는 것은 Github Discussion을 찾아보는 것이 정말 도움이 된다. 모르면 내가 Discussion을 만들어보자.(실제로 라이브러리 제작자에게 질문을 해봤다 ㅎㅎㅎ)
  3. 역시 어려운 것은 내가 모르는 것이다. 알면 문제해결하기 쉽다
  4. 문서화에 대해서 후반부부터 많이 생각했고 리팩터링 관련 항목은 하나의 issue에 묶어서 관리했다. 그 중 회의록이 없는 것이 많이 아쉬우니 회의록은 그냥 꼭 작성하자.

4. 추후 공부방향?

이제 시작한 개발자 지망생이다. 지금까지 기초이론을 빠르게 추상화 시키고 실습을 통해서 확인을 해봤고 하면서 지금 쯤 기본에 대한 확립이 한번 해줘야할 것 같다.
따라서, "모던 자바스크립트 Deep Dive"와 "러닝 타입스크립트"를 통해서 기초를 다시 굳게 확립할 예정이다.

profile
꾸준히 공부하자

2개의 댓글

comment-user-thumbnail
2023년 2월 27일

프론트엔드 개발자와 백엔드의 협업 과정에서 겪었던 어려움들을 가득 녹여주신 글인 것 같아 너무 재밌게 읽었습니다!
해결하신 기록도 자세히 작성되어 있어 큰 도움이 될 것 같아요 ㅎㅎ!!

만약 해당 프로젝트를 리펙토링 하시게 된다면 라이브러리를 최소화하기부터 해보시는 것도 즐거울 것 같아요 :)
SweetAlert2나 react-hook-form 같이 손이 많이 가지만 직접 구현하며 많은 것을 배우실 수 있을 것 같거든요!

추후 공부 방향에 타입스크립트가 적혀 있어 약간의 팁을 적어보자면 타입 추론을 적극적으로 이용해보시는 것도 하나의 재미가 아닐까 생각이 들어요 ㅎㅎ!!

예를 들어 useState<string>("")useState("") 하는 것들이요!
러닝 타입스크립트를 읽으신 뒤에 이펙티브 타입스크립트 책도 강추 드립니다!!!

앞으로의 상현님이 너무나 기대가 되는 글이었습니다!
고생 많으셨어요 :)

1개의 답글