스파르타 주관 해커톤대회 회고 - 2

버들·2023년 10월 2일
0

Experience

목록 보기
4/5
post-thumbnail

이전 해커톤 회고 편의 기획 - 개발 편입니당
해당 링크로 1편을 보실 수 있으셔요.

2편입니다

저번 포스트에서는 항해커톤에 대한 총체적인 부분을 다뤘다면, 이번에는 기획 - 개발 등을 통해서 과제가 어떻게 진행되었는지 다룰려고 한다.

앞선 포스트에서 호랑에듀의 교육서비스에서 학생용 LMS를 만든다면 어떻게 될까를 초점을 두고 서비스 기획 및 개발을 하는 과제를 수행한다는 이야기를 했었다.

당시에 호랑에듀 측에서는 단순하게 학습자 위주의 LMS 를 기획 / 개발을 요구했기에 이 넓고도 난해한 주제를 풀어나가는 형식으로 일주일이 흘렀고, 그 부분을 자세하게 또한 내가 정말 좋은 경험을 했던 부분을 끄적여가보도록 하겠다!

기획

일단 기획이 가장 걱정이 컸다.
원래 같았으면 총 다섯명 인원을 뽑을 때, PM을 모집하고 싶었지만 생각해보니 개발자들이 가득한 항해커톤이었기에 개발자는 고사하고 디자이너 또한 모래사장에서 바늘 찾기 만큼 모시기가 어려웠다.
그래도 좋은 팀원들을 빠르게 모았고, 빠르게 모은 만큼 여러 레퍼런스를 찾는데 투자하면 어느정도 해결될 문제라고 생각이 들었던 차라 크게 문제가 될 것이라고 생각하지는 않았다.

그래서 팀원들끼리 이런 식으로 LMS 기획 아이템 및 디자인 레퍼런스를 따오기로 디스코드 상에서 정하고 회의 시간에 모여서 이야기 하는 형식으로 진행이 되었다.
아, 그리고 이번 해커톤은 밤샘데이(토요일~일요일 아침) 제외하고 평일은 일정을 소화할 수 있는 최소 시간에만 모이는 기간이었기에 직장인들이 많은 우리 팀은 저녁 아홉시 부터 회의를 진행 할 수 있었다.

첫 번째 회의 (인사 및 자기소개 그리고 짧은 기획)

첫번째 회의에서는 팀원들끼리 서로 자기소개를 하는 등, 인사를 곁들인 아이스브레이킹 시간을 갖게 되었다.
그리고 이제 팀원이 백엔드 2 프론트엔드 2 인프라 1로 나누어졌으니 자신들이 어떠한 역할을 수행하면 될지, 또한 그 임무들을 각각 기간을 얼마나 나눠서 해결할 것인지 간단하게 예상되는 스코프도 짜보았다. 아무래도 해커톤은 기간내에 할 수 있는 만큼 적절하게 분배해서 개발까지 진행을 해야되기에 기획의 범위가 큰 부분을 차지할 것이라는 생각에 걱정이 조금 앞서긴했다. 아니 조금 많이

어쨋든 첫날이기도 하고 무언가 사전 조사를 해야된다는 생각에 앞서 말했듯이 LMS에 대해서, 그리고 디자이너가 없기에 LMS에 어울리는 디자인 레퍼런스를 조사하는 것으로 마무리 하였다.

두 번째 회의

두 번째 회의 부터 무언가 진전이 보이기 시작했다. 새로 알게 된 아이데이션 기법을 활용하는 계기가 되었던 하루였으며, 예기치 못한 팀원의 방출이 있었던 하루이기도 했다.

팀원의 방출

스파르타 측에서 종합했던 팀 명단에서 백엔드 개발자분의 이름이 올라오지 않았다. 우리는 주최 측이 미스를 범해 명단에 올리지 못했던 것으로 생각했지만 결과는 달랐다.
문의한 것의 답변은 그 분께서 무언가 조건이 달성되지 않은 수료자였는데 어떻게 알았는지 참가하게된 케이스라서 최종 검토에서 걸러지게 된 것이다.
이로써 팀원은 백엔드 1 프론트엔드 2 인프라 1 이 되버렸다.. ㅎ

테오의 스프린트 - 아이데이션

이제 팀의 유일한 백엔드 개발자가 되신 연석님이 모두가 조사해온 레퍼런스를 figma 내에서 figzam 이라는 페이지에서 아이데이션을 하여서 기획을 하는 것을 추천해주셨다.

이 방법은 이전에 테오의 스프린트에서 프로젝트를 진행할 때 기획자 없이 좋은 아이디어를 뽑아내는 방법으로 정말 좋은 방법이라서 팀원들에게 공유도 할 겸 이 방법으로 진행하고 싶다고 하신 것이 계기가 되었다.

이렇게 피그잼 파일에서 십자로 중요도와 개발속도를 기준으로 나누게 되었는데, 나는 처음에 이걸 보고 SWOT (Strengths(강점), Weaknesses(약점), Opportunities(기회), Threats(위협)) 방법이 떠올랐는데, 마찬가지로 위로 갈 수록, 오른쪽으로 갈 수록 긍정적인 요소인 것이다.

그래서 각자 생각한 이 LMS 서비스에서 어떠한 부분이 꼭 필요한지 생각하고 그것을 포스트잇에 기능을 적었는데 해당 기능들이 얼마나 중요하며, 이것을 개발하는데 있어서 얼마나 오래걸리는지 판별하는 시간을 가지게 되었다. 아래와 같은 기능들이 나오게 되었다.

그리고 이 기능들을 중요도와 개발속도에 따라서 정리를 한 것이 아래이다.
그리고 이 기능들을 처음에 다 한번에 개발 할 순 없으니, 투표등을 해서 우선적으로 개발할 기능들을 5~6개 고르게 되었다.

그리고 이렇게 아래와 같이 추리게 되었고...

위의 내용에 관해 해당되는 디자인 레퍼런스를 찾아 붙이기 시작했다.

이것 또한 투표로 한 기능에 한 디자인 예시를 골라서 페이지 및 컴포넌트 디자인의 예시로 삼고 최대한 우리 컨셉에 맞게 적용하여 피그마에서 그리게 되었다.

이런식으로 뭔가 기획 + 디자인 또한 진행하다보니 꽤나 수월하게 척척 맞춰지는 것 같았다. 다음에도 사이드 프로젝트를 진행할 시에 이런 아이데이션 방법을 써보면 좋을 것 같다는 생각이 들어서 꽤나 큰 수확이 있었던 회의였다.

그래서 이렇게 정해진 기획에 각 포지션 별 스택을 정할 수 있게 되었고, 우리 프론트 같은 경우에는 배우려는 의지를 반영해 Nextjs (page routing), typescript, react-query, scss 를 사용하여 LMS 페이지를 제작하게 되었다.

개발

수요일부터 프론트엔드는 개발에 들어가게 되었다. 물론 중간중간에 기획도 많이 변경 및 추가 되면서 이와 같은 부분과 얼라인하면서 개발을 진행하였다.

배포는 인프라 개발자 분이 미리 네이버 클라우드와 쿠버네티스로 CI/CD를 구축해주셔서 main 브랜치에 push가 가면 배포가 되는 형식이었다.

그래서 온전히 개발과 퍼블리싱에 집중을 할 수 있었다 :)

나머지 프론트엔드 팀원 분(진영님)과 이야기를 나눠 역할을 분담을 하게 되었다.
Next에 대해 조금 더 실무 경험이 있는 내가 간략하게 레이아웃 및 폴더구조를 생성해 놓고, 그 후에 페이지 단위로 역할을 나누기로 했었다.

진영님이 피그마로 디자인을 진행하실 수 있다고 하시며 빠르게 멋진 디자인을 만들어 놓으셔서, 정말 많은 부분을 느꼈다. 퍼블리셔라고 다 피그마를 잘 사용하는 것은 아니여서, 따로 학습하신 것이라고 하셔서 나 또한, 프론트엔드 개발자로써 좀더 디자이너 분들의 툴을 깊게 이해하면 많이 유용하겠다고 느끼게 되었다.

암튼 어느정도 레이아웃을 만들고 페이지별로 개발 파트를 나누어 개발에 착수하였다.
결과물은 이렇게 나온다..!

LMS이면서 저학년 친구들에게 보급될 예정이다보니까 최대한 그들의 눈높이에서 자신이 하고 있는 부분을 이해하기 쉽게 풀어내야하는 것이 관건이었다.
그래서 어린아이들이 요즘 관심있는, 그리고 친숙해할 멘트와 주제들을 활용한 LMS를 개발하였다.

문제 직면

물론 빠르게 팀원들을 모으고 주제도 정하고 개발까지 착착 진행, 팀원들은 뭐하나라도 더 좋은 방법이나 아이디어를 찾으려고 열심히 노력하는 모습을 보이면서 재밌게 프로젝트를 진행해서 순탄할 줄만 알았지만 역시나 기술적으로도, 협업적으로도 문제가 조금씩 보이기 시작했다.

Nextjs와 Scss.module

NextJS는 프레임워크이다 보니까 폴더 및 파일 이름 또한 Next가 정해준 것을 따라야지 적용이 가능하다.
회사에서는 NextJS + Styled-Components 조합을 쓰다가 여기서는 SCSS 를 사용해보았는데, 역시 기본적으로 사용되는 스타일시트이다보니 Next에서는 규칙을 설계해 놓았었다.

이렇게 styles라는 폴더 내에 css 및 scss 파일을 모아두는 것을 장려하고, globals.scss = 전역으로 뿌릴 scss 파일, 그리고 각 컴포넌트 별로 module이라는 이름이 가운데에 들어간 scss파일을 만들고 해당 컴포넌트에만 쓰게 하는 것을 요하는 것 같았다.

왜 그렇게 생각했냐면, 여기 똑같이 사용되는 컴포넌트가 하나 있고 이것을 2개의 페이지에서 사용하게 된다.

// pages/mypage/current/index.tsx

import StudyingProgressPieComponent from "@/components/statistics/StudyingProgressPie2";

export default function Current() {
  /**true: 수강완료 강의 탭, false (default): 수강중인 강의 탭 이 활성화 */
  const [isLectureToggle, setIsLectureToggle] = useState(false);

  const { data } = lmsPageGetApi.useGetMagnitudeData();

  const lectureToggleHandler = () => {
    setIsLectureToggle(!isLectureToggle);
  };

  return (
    <main className={styles.totalTemplate}>
      <section className={styles.topContainer}>
        {/* 내 학습 진도현황 */}
        <div className={styles.studyingProgressContainer}>
          <StudyingProgressPieComponent />
        </div>
// pages/mypage/home/index.tsx

import StudyingProgressPieComponent from "@/components/statistics/StudyingProgressPie";

export default function Home() {
  const { data } = lmsPageGetApi.useGetMainData();
  console.log(data);

  return (
    <main className={styles.totalTemplate}>
      <section className={styles.topSpanContainer}>
        <span className={styles.hello}>👋 반가워</span>
        <span className={styles.randomComment}>
          펭구야, 오늘도 즐겁게 코딩해보자!
        </span>
      </section>
      <section className={styles.topContainer}>
        <div className={styles.weeklyStudyContainer}>
          <span>이번 주 학습 시간</span>
          ...
        </div>
        <section className={styles.studyingProgressContainer}>
          <StudyingProgressPieComponent magnitude={data?.magnitude} />
        </section>
        </article>
      </section>
    </main>

그리고 StudyingProgressPieComponent 는 그래프 라이브러리를 사용하여 만든 학생들의 학습 진행 현황 컴포넌트이다.

import { ResponsivePie } from "@nivo/pie";
import { useRouter } from "next/router";
import styles2 from "@/styles/Home.module.scss";
import Link from "next/link";
import { IoIosArrowForward } from "react-icons/io";
import { HiOutlineArrowSmRight } from "react-icons/hi";
import { LuMonitorPlay } from "react-icons/lu";
import { MagnitudeProps } from "@/interface/home";

/**progressPie 원본 컴포넌트 */
interface ValueProps {
  percentage: number;
}

const StudyingProgressPie = ({ percentage }: ValueProps) => {
  const data = [
    {
      id: "studyingProgress",
      label: "studyingProgress",
      value: percentage / 100,
    },
    {
      id: "empty",
      label: "empty",
      value: 1 - percentage / 100,
    },
  ];

  return (
    <>
      <ResponsivePie
        data={data}
        /* margin={{ top: 43, right: 400, bottom: 43, left: 0 }} */
        startAngle={-8}
        innerRadius={0.75}
        cornerRadius={45}
        colors={["#51c741", "#b0f1ad99"]}
        fit={true}
        enableArcLinkLabels={false}
        enableArcLabels={false}
        arcLabelsRadiusOffset={0.55}
        isInteractive={false}
        legends={[]}
      />
    </>
  );
};

/**내 학습 진행 현황 */
const StudyingProgressPieComponent = ({ magnitude }: MagnitudeProps) => {
  const router = useRouter();

  const calcPercentage = () => {
    const completeVideoCount = magnitude?.completeVideoCount as number;
    const totalVideoCount = magnitude?.totalVideoCount as number;
    return Math.floor((completeVideoCount / totalVideoCount) * 100);
  };

  return (
    <>
      <div className={styles2.spaceBetweenTemplate}>
        <span>학습 진행률</span>

        <Link href={"/mypage/current"} className={styles2.moreInfo}>
          자세히 보기 <IoIosArrowForward />
        </Link>
      </div>
      <div className={styles2.graphContainer}>
        <div className={styles2.leftBox}>
          <span>
            <LuMonitorPlay /> 진행중인 강의
          </span>
          <span>{magnitude?.progressVideoTitle}</span>
          <p>
            {magnitude?.completeVideoCount}
            <span className={styles2.gray}>/{magnitude?.totalVideoCount}</span>
          </p>
          <button>
            <span style={{ display: "flex" }}>바로 학습</span>{" "}
            <i>
              <HiOutlineArrowSmRight />
            </i>
          </button>
        </div>
        <div className={styles2.studyingProgressPie}>
          <div className={styles2.percentage}>{calcPercentage()}%</div>
          <StudyingProgressPie percentage={calcPercentage()} />
        </div>
      </div>
    </>
  );
};

export default StudyingProgressPieComponent;

아 그리고 깜박했는데 이 SCSS 의 className을 불러내려면 import styles2 from "@/styles/Home.module.scss"; 이렇게 데이터 이름을 지정하여 부르고 <span className={styles2.gray}> 이렇게 객체 접근을 통해 스타일을 꺼내 쓸 수 있다. 이것도 Next에서 그런것이다.

아무튼 원래 시도하려했던 것은, 이 컴포넌트 만의 scss 파일을 만들어서 적용시킨 후 다른 페이지에서도 무난히 사용할 수 있게 하려했으나, 이상하게 페이지내에서 스타일이 불러와지지 않았다..

그리고 조금 위를 보면 페이지 별로 module 파일을 만들어놓았는데, 이걸로 불러서 공통으로 사용하면 다른 페이지에서는 해당 모듈이 적용이 되지 않아서 골치가 꽤나 아팠다..

그래서 우선 결과물을 만들어서 내야되는 입장이기에 해당 컴포넌트를 페이지별로 상이하게 찢어서 module 또한 그렇게 분리하여 사용하게 되었는데, 내가 설정을 잘못해서 이런건지 분명 Next에서도 이렇게 불편하게 쓰라고 이러한 규칙을 만들지는 않았을 것이라 해서 추후 한번 더 찾아볼 생각이다.

아래는 scss 적용하기 위한 Next.config.js 이다.

/** @type {import('next').NextConfig} */
const path = require('path');

const nextConfig = {
  reactStrictMode: false,
  transpilePackages: ["@nivo"], experimental: { esmExternals: "loose", },
  images: { unoptimized: true },

   sassOptions: {
    includePaths: [path.join(__dirname, 'styles')],
  },
   
    async redirects() {
    return [
      {
        source: '/',
        destination: '/mypage/home',
        permanent: true,
      },
    ]
  },

}

module.exports = nextConfig

Next랑 안친한듯한 Nivo chart

처음으로 데이터 시각화 라이브러리를 프로젝트에 도입하게 된 것 같다. 진영님은 이 Nivo chart를 통해서 데이터 시각화를 해본 경험이 있다고 하셔서 해당 라이브러리를 선택했던 것 같다. 심지어 커스텀도 편하다고 하니, 선택하지 않을 이유가 없었다.

이 라이브러리를 마치 개구리 해부하듯이 뜯어보느라 커스텀에도 시간이 꽤나 들었지만, 어느정도 서비스에 맞게 수정하는 것은 가능했다. 하지만 문제는 아래와 같았다.

Lazy-loading & ssr: false

Next에서 바로 Nivo로 만든 차트 컴포넌트를 써보려고 불러오면, 에러가 난다.
(아쉽게도 당시에 캡쳐는 못했는데, 아마 무슨 babel module 이렇게 된 것 같기도 했다. 왜냐면 import 쓰지말고 require를 써야됬었고, 그걸 babel.rc? 이 파일에서 설정을 해라고 깃헙에서 그래서 시도해봤었기 때문이다.)

그래서 우리와 같은 고민을 가지고 있던 사람들의 이야기를 github 내의 커뮤니티에서 찾을 수 있었는데, 바로 Lazy-loading을 사용하라는 것이었다.

import dynamic from "next/dynamic";

const WeeklyStudyingTimeGraph = dynamic(
  () => import("@/components/statistics/WeeklyStudyingTimeGraph"),
  { ssr: false }
);

const StudyingProgressPie = dynamic(
  () => import("@/components/statistics/StudyingProgressPie"),
  { ssr: false }
);

const MyResponsivePie = dynamic(
  () => import("@/components/statistics/AttendanceJandi"),
  { ssr: false }
);
const MyResponsiveRadar = dynamic(
  () => import("@/components/statistics/CodingMBTI"),
  { ssr: false }
);

이런 식으로 말이다.
아마 이렇게해서 되는 이유가, Next 자체에서 CSR를 사용한다 하더라도 먼저 서버에서 데이터를 다운받아 가져오려고 시도를 한다는 로직으로 이루어져있다고 들었는데, 그 과정에서 Nivo 측에서 이 부분을 헤아리지 못해서 바로 렌더될 시에 결여된 채로 다운되기에 에러가 나는 것으로 개인적으로 추측한다.
그래서 이 Nivo 컴포넌트가 다 다운되기 전까지 렌더링을 늦추는 기법을 사용하는게 아닐까 싶다.

그런데 이러면 ok? nono

lazy-loading을 난무하게 사용했으며, Nivo 자체가 조금 렌더링이 뎌딘 친구이다 보니까 엄~청 오래걸린다 초기 렌더링이.

프론트엔드 개발자로써 이것은 용납할 수 없었다 ㅋㅋㅋ. 무언가 애들이 봤을때에도 너무 지루하게 버벅여서 애초에 학습관리를 쳐다보지 않을 것만 같았다.

그래서 정말 열심히, 회사에서 내가 물어볼 사람이 없어, 그렇다고 질문방에 올려서 기다리기도 시간이 아까워 혼자서 뭔가를 해결하기 위해 구글링했던 것 처럼 열심히 해답을 찾아봤고 찾아냈다.

/** @type {import('next').NextConfig} */
const path = require('path');

const nextConfig = {
  reactStrictMode: false,
  transpilePackages: ["@nivo"], experimental: { esmExternals: "loose", },
  images: { unoptimized: true },

   sassOptions: {
    includePaths: [path.join(__dirname, 'styles')],
  },
   
    async redirects() {
    return [
      {
        source: '/',
        destination: '/mypage/home',
        permanent: true,
      },
    ]
  },

}

module.exports = nextConfig

이렇게 next.config.js에서 시험적인 요소를 사용하여 조절하면 된다고 한다.
그래도 이걸 쓰니까 왜 되는건지 궁금해서 chat gpt에게 물어보니까 다음과 같이 답하더라

  • transpilePackages: ["@nivo"]: 이 설정은 Next.js에서 특정 패키지를 트랜스파일하는 패턴을 나타냅니다. 여기서 @nivo 패키지가 트랜스파일될 것으로 지정되어 있으며, 이렇게 하면 Next.js가 해당 패키지를 트랜스파일하여 브라우저에서 실행 가능한 코드로 변환합니다. 이것은 특정 패키지가 서버 측 렌더링과 호환되지 않는 경우에 유용합니다.

  • experimental: { esmExternals: "loose" }: 이 설정은 실험적인 기능인 "esmExternals"를 활성화합니다. "esmExternals"는 ECMAScript 모듈 (ESM) 형식의 외부 종속성 (예: 패키지 또는 모듈)을 처리하는 방법을 설정합니다. "loose"로 설정하면 ESM 외부 종속성을 더 효율적으로 처리하도록 돕습니다.

결론은 @nivo 패키지들을 next에서 호환가능하게 트랜스파일링한다는 것 같다. 그래도 앞으로는 웬만하면 next에서 @nivo는 안건들이는걸로.. ㅎ

profile
태어난 김에 많은 경험을 하려고 아등바등 애쓰는 프론트엔드 개발자

0개의 댓글