싸피 2학기 공통 프로젝트(스티치)

김헌규·2024년 8월 21일
2

프롤로그 - 2학기에 접어들며

1학기 관통 프로젝트가 끝난 후 한달 남짓한 방학을 보낸 뒤 7월부터 팀원들을 구하여 공통 프로젝트를 시작하였다. 공통 프로젝트를 하기 앞서 방학 동안 Java 공부만 조금 해놓은 상태여서 공통 프로젝트를 잘할 수 있을지에 대한 고민들이 가득했다.

공통 프로젝트 팀이 만들어지고 나서 어떤 역할을 담당할지에 대한 얘기를 하면서 원래는 백엔드로 취업을 희망하고 있었고 그렇기 때문에 백엔드를 담당하고 싶기는 했지만 팀에 프론트엔드를 한다는 사람도 거의 없었고 프론트엔드도 한번 해보면 나중에 현업에서 프론트 엔드를 담당하는 분들과 협업을 할 때 원활한 소통을 할 수 있겠다고 판단하여 프론트 엔드를 담당한다고 자진해서 말을 하였고 그렇게 프론트 엔드로 공통 프로젝트를 진행하게 되었다.


본문 - 싸피 2학기 공통 프로젝트 (스티치)

🐨STEACH


프로젝트 주제

취약계층 학생들과 봉사활동을 할곳이 마땅히 없는 대학생 및 일반인 분들을 위한 실시간 화상 과외 서비스

개발 기간

7월 2일 ~ 8월 16일

기획 배경

  • 코로나 팬데믹 이후 청소년의 교육 격차가 매우 심각한 상황이며 교육부의 교육 복지는 학교 내 지원 사업이 대부분이므로 방과 후 교육은 사각지대에 위치하고 있다.
  • 만 8~23세 청소년의 16%가 한부모 및 조손 가정이고, 거주지가 반지하 혹은 옥탑 등에 해당하는 가구가 18.2%에 해당한다. 일반 청소년에 비해 취약 계층의 청소년은 방과 후 사교육 경험 비율은 43%에 불과하다.

목적

  • 취약 계층 학생들에게 '공정'한 교육 기회를 부여하여, 교육 불균형을 해소하고 진로를 위한 디딤돌을 제공한다.
  • 소규모 그룹 강의를 제공하여 학생들이 집중적인 교육을 받을 수 있게 하고, 다양한 분야의 교육을 제공하여 학생들이 원하는 진로에 대해 학습할 수 있는 기회를 제공한다.

ERD

목업

사용 기술

  • 백엔드 : Springboot, Redis, MariaDB, mongoDB, Flask, Node.js
  • 프론드엔드 : React, TypeScript, Tailwind.css
  • 인프라 : AmazonEC2, Docker, Jenkins, NGINX

프론트 엔드를 담당하면서 React를 사용했던 이유로는 사용자의 편의성을 위하여 SPA 기반의 홈페이지를 구축하고 싶었으며 또한 React의 특성상 Component를 재사용할 수 있는 부분이 있기에 업무 효율성 및 디버깅 측면에서 이점이 있고 또한 React 커뮤니티의 규모가 크기 때문에 풍부한 라이브러리를 이용할 수 있는 등의 많은 이점이 있기 때문에 React를 도입하여 사용을 했다.

TypeScript의 경우 자바스크립트와 완벽한 호환성을 갖추고 있으며 정적인 타입을 통해 프로그램의 안정성을 높일 수 있기 때문에 도입을 했다.

Tailwind.css를 도입한 이유로는 inline 스타일로 개별적으로 태그 내에다 명시적으로 스타일을 입힐 수 있으며 flex 및 grid를 포함한 반응형 스타일들을 쉽게 설정할 수 있다는 장점이 있어서 도입을 했다.

팀원 정보 및 업무 분담 내역

팀 구성

조시현(팀장 - 백엔드), 주효림(팀원 - 백엔드), 이상철(팀원 - 백엔드), 김호경(팀원 - 인프라, 데이터), 이진송(팀원 - 프론트엔드), 김헌규(팀원 - 프론트엔드)

내가 담당한 업무 내역

  • 메인 페이지 carousel 및 swiper를 이용한 디자인 및 api 통신
  • navBar 디자인 및 회원가입, 로그인, 로그아웃 링크 구현
  • 검색 페이지 구현 및 navBar에 있는 검색 바를 이용한 검색 기능 구현
  • 학생, 선생님 내 강의실 페이지 디자인 및 api 통신(학생 정보 CRUD, 학생 내 수업 선호도, 학생 진로 추천, 학생 커리큘럼 조회, 학생 강의 조회, 학생 강의실 입장, 선생님 정보 CRUD, 선생님 커리큘럼 조회, 선생님 강의 조회, 선생님 강의 수정, 선생님 퀴즈 CRUD)

기능 소개

우선 회원 가입을 할 때에는 위의 사진처럼 학생과 선생님을 나누어 회원가입을 하여 따로 관리를 하였다.

학생

우선 학생으로 회원가입을 하여 로그인을 하면 왼쪽에 있는 사진의 메인 페이지의 모습을 확인할 수 있는데 메인 페이지에서는 요즘 뜨는 강의와 최근 등록된 강의 그리고 광고 등의 carousel 등을 볼 수 있고 페이지 중앙에 있는 과목 아이콘을 통해 과목에 대한 검색 결과를 볼 수 있는 페이지로 이동하거나 navBar에 있는 검색 바에 검색어를 입력하거나 강의 탭을 눌러서 검색 페이지로 이동하게 되면 오른쪽 사진처럼 검색 페이지에서 검색 결과를 확인할 수 있으며 검색 조건으로 과목, 최신순, 인기순, 수강 가능 여부, 검색어 등을 이용해 검색이 가능하도록 구현하였다.

학생은 검색 결과 중 하나를 선택하여 상세 페이지로 들어가게 되면 강의 요일 및 커리큘럼 등을 확인할 수 있고 학생이 수강하기로 결정하였으면 수강 신청을 하여 강의를 수강할 수 있도록 했다.

그 후 학생은 정해진 강의 시간에 맞춰 입장을 하게 되면 학생들은 서로의 화면과 채팅 그리고 화면 공유를 볼 수 없도록 설정하였다.(선생님의 화면 공유는 볼 수 있음.) 그 이유는 아무래도 취약 계층 학생들이 듣는 강의이다 보니 프라이버시 측면을 보호해 주기 위한 정책으로 설정하였기 때문이다.

학생은 선생님의 강의를 듣다가 중간에 선생님이 출제한 퀴즈를 실시간으로 맞추어 점수 및 등수를 확인할 수 있도록 구현하였다. 점수는 퀴즈를 얼마나 정확하고 빨리 맞췄는지에 따라 차등적으로 지급하도록 설정하였다. 이러한 기능을 통해 학생들은 본인이 얼마나 강의를 이해하고 있으며 잘 참여하고 있는지 알게 된다. 퀴즈의 디자인은 카훗이라는 서비스를 참고하여 디자인하였다.

또한 학생이 중간에 졸고 있지는 않는지 실시간으로 감지하여 만약 학생이 졸고 있으면 졸고 있다는 알림을 띄우는 기능도 추가하였다. 이러한 기능은 이미지를 실시간으로 캡처를 하여 AI 서버로 보내게 되면 AI가 이미지를 분석하여 눈을 감고 있다고 판단을 하면 알림을 활성화하는 결과를 반환하게 되어 알림을 출력하게 되는 원리이다.

그렇게 강의가 진행되다가 최종적으로 강의가 종료되면 학생의 퀴즈 점수 및 졸음 감지 등을 통해 계산한 집중도에 대한 정보를 학생별로 계산을 하여 서버에 보내게 되는데 학생이 강의를 들을수록 이러한 데이터가 서버에 쌓이게 되고 이러한 결과들은 학생 내 강의실 페이지의 내 정보 탭에서 나의 수업 선호도와 AI 진로 추천을 위한 데이터로 활용이 되는데 나의 수업 선호도의 경우 모든 과목에 대해 각각 100점 만점을 기준으로 과목별로 어떤 수업에 흥미도가 있는지 서버에서 기존의 강의 데이터들을 계산하여 보내주게 되고 이러한 점수들을 radar chart를 통해 시각적으로 한눈에 파악할 수 있도록 구현하였다. radar chart는 chart.js라는 라이브러리를 이용하였다.

AI 진로 추천 역시 수업 선호도에 사용된 점수들을 활용하여 chat GPT에게 데이터를 제공하여 진로 추천을 받을 수 있도록 하였다.


선생님

선생님 또한 학생처럼 회원가입을 하고 난 후 로그인을 하면 메인 페이지 및 검색 페이지에서 커리큘럼을 검색할 수 있다. 선생님은 navBar의 내 강의실 탭으로 이동하여 왼쪽의 메뉴바에 있는 커리큘럼 생성 버튼을 통해서 커리큘럼 생성을 할 수 있고 그 후 강의 별로 퀴즈를 생성을 하고 학생들의 수강 신청을 기다린 후 강의 날짜에 맞춰 강의를 시작하면 된다.

강의실에서 선생님의 경우 자신의 화면을 포함하여 최대 4명의 학생들의 화면과 공유 화면들을 볼 수 있으며 추가로 선생님이 학생별로 마이크와 화면 공유를 금지 시킬 수 있는 기능을 추가하여 선생님이 어느 정도 통제를 할 수 있도록 페이지를 추가하였다.

선생님은 미리 강의별로 생성해 둔 퀴즈를 학생들에게 출제할 수 있는데 학생들이 퀴즈를 풀고 나면 학생과 똑같이 실시간으로 어떤 학생이 몇 등인지 알려주는 화면을 뜨도록 구현하였다.

그렇게 선생님도 강의를 끝까지 진행한 후 강의 종료를 위해 강의 종료 버튼을 누르게 되면 그 강의에서의 학생들의 통계 결과와 각 학생들의 평균을 계산한 종합적인 통계 점수를 띄우도록 했고 이 모달을 통해 그 수업에서 학생들의 평균적인 데이터를 간단하게 파악할 수 있도록 구현을 했다.

이러한 데이터는 종료할 때 한 번만 볼 수 있도록 하는 것이 아닌 커리큘럼 상세페이지에서 종료된 강의별로 통계 결과를 볼 수 있도록 기능을 추가하였다.

마지막으로 선생님 봉사활동 시간의 인증의 경우는 정부 기관 및 공공 기관과 연계를 하게 될 경우 봉사활동 확인서를 발급을 해줌으로써 대학생 봉사자의 경우 봉사활동 시간을 인증할 수 있도록 봉사활동 양식을 미리 만들어 놓았다.

프로젝트 진행 중 잘한 점

  • Api를 따로 관리하기 위해 Api 폴더를 만들어 관리를 한 점 : 이거는 1학기 관통 프로젝트를 한 후에 다른 분의 프로젝트를 보면서 도입하고자 하는 방식이었는데 axios를 이용한 Api 통신 함수를 한곳에 모아서 관리를 하게 되면 관리하기가 좋을 듯하여 한곳에 모아서 관리를 하였다. 그 결과 이전 프로젝트에 비해서 더 깔끔하게 코드를 관리할 수 있게 되었다.

  • Interface를 따로 관리하기 위해 Interface 폴더를 만들어 관리를 한 점 : 이것도 Api와 동일한 이유로 프로젝트 초기에는 Interface를 그 Interface를 사용하는 파일 안에서 관리를 하였는데 그렇게 되면 api 요청을 부를 때 export를 하여 사용할 경우에 어느 파일에 어떤 Interface가 있는지 확인하기가 쉽지 않게 되어 관리가 어렵다고 판단하여 Interface를 한 곳에서 관리를 하는 게 깔끔하다고 판단하여 Interface 폴더를 따로 생성하였다.

  • React 학습을 꾸준히 했던 것과 프로젝트 초기보다 React 실력을 많이 늘린 점

  • 원래는 전혀 git을 활용할 줄 몰랐는데 이번 프로젝트를 통해서 여러 git 명령어를 사용하고 많은 개념들을 익히게 된 점 : 기존에는 add, commit, push 정도만 사용할 수 있었는데 협업을 해야 한다는 강제적인 상황 덕분에 배워야겠다는 의지와 팀원들이 친절하게 알려주었기에 프로젝트를 진행하면서 fork를 이용하여 origin 레포지토리로 가져와서 branch를 생성하여 upstream 연결을 통한 pull request까지 익히게 되어 아직까지도 부족하지만 git에 대한 기본적인 사용법에 익히게 되어 많은 발전이 있었다.

프로젝트 진행 중 못한 점

  • 코드 컨벤션에서 정했던 변수명 처리에 대해서 잘 지키지 못한 점 : 처음에는 코드 컨벤션을 참고하여 코드를 작성했다가 시간이 지나서 일정상 바쁘게 되면서 코드 컨벤션을 잘 참고하지 않고 코드를 작성하다 보니 조금씩 코드 컨벤션을 지키지 않게 되었고 한번은 다른 팀원이 디버깅을 하면서 내가 작성한 파일명 때문에 곤란을 겪은 적이 발생하였고 그 일을 계기로 많은 반성을 하게 되었다.

  • slice에서 생성한 함수명과 api 파일에서 생성한 함수명을 깔끔하지 못하게 정했는 점 : 이것은 처음 코드 컨벤션을 정할 때 그때는 React에 대해 잘 모르는 것이 많았기에 이러한 부분에 대해서는 코드 컨벤션을 정하지 못하였고 그 결과 같이 프론트 엔드를 담당했던 팀원과 함수명 통일을 하지 못한 결과를 낳게 되었다.

  • 일정적으로 많이 밀린 탓에 원래 하고 싶었던 RTC 부분과 CI/CD를 하지 못했던 점

  • TypeScript를 많이 공부하지 못했고 그 결과 잘 활용하지 못했던 점

  • 일정적으로 많이 밀리게 되어서 테스트 및 성능 개선을 하지 못했던 점

  • 폴더 및 파일 구조가 깔끔하지 못했던 점

프로젝트 진행 중 배웠던 것

  • React 기본 개념 및 지식
  • Tailwind 사용법
  • git 및 JIRA 사용법
  • javaScript 문법 복습 및 실력 향상
  • css 기초 개념 복습 및 활용(flex, grid)
  • 백엔드에서 swagger를 잘 작성해 주면 프론트엔드 파트에서의 작업 효율이 올라간다는 것

프로젝트 진행 중 발생한 트러블 슈팅

  • Redux toolkit을 이용하여 여러 저장소로 분리한 상태에서 비동기적인 처리로 인하여 api 통신을 할 때 중요한 변수(토큰 등)들에 아무런 값이 담기지 않는 현상이 발생하여 에러가 많이 발생했었는데 이러한 원인으로 함수 내에 있는 로직들이 비동기적으로 작동을 하여 하나의 로직에서 값을 받아오기 이전에 다른 로직이 실행되었기 때문이라는 것을 알게 되었고 이를 해결하기 위하여 async await을 이용하여 함수 내의 로직을 동기적으로 처리를 했다.
const handleIsUpdateInfo = async (password: string) => {
    const passwordAuthToken = await passwordCheck(password);

    localStorage.setItem(
      "passwordAuthToken",
      passwordAuthToken.password_auth_token
    );

    if (passwordAuthToken) {
      setIsUpdateInfo(true);
    } else {
      console.log("에러가 발생했습니다.");
    }
  };
  • 로그인 기능을 구현하면서 처음에는 useState와 useSelector를 이용해 로그인 상태를 감지를 로직을 작성을 하였으나 로그인을 한 후 새로고침을 하게 되면 로그인 상태가 풀리게 되는 현상이 발생하게 되었고 이를 해결하기 위해 localStorage에 로그인 정보를 저장하도록 구현하였다.(후기를 작성하면서 react-persist를 이용하는 방법도 알게 되었다... 다음에는 이 방법을 사용해 보려고 한다.)

// 통합 로그인
export const loginSteach = createAsyncThunk<LoginReturnForm, LoginForm>(
  "login",
  async (loginFormData, thunkAPI) => {
    try {
      const formDataToSend: LoginForm = {
        username: loginFormData.username,
        password: loginFormData.password,
      };

      // 로그인 API 요청
      const response = await login(formDataToSend);

      // 로컬 스토리지에 정보 저장
      localStorage.setItem("auth", JSON.stringify(response));

      return response;
    } catch (error) {
      if (axios.isAxiosError(error) && error.response) {
        return thunkAPI.rejectWithValue(error.response.data);
      }
      return thunkAPI.rejectWithValue(error);
    }
  }
);
  • 검색 페이지와 navBar에서의 검색 기능을 구현을 할 때 처음에는 useState를 이용한 상태 관리를 통해서 검색 결과를 가져오려고 했으나 상태가 이전 상태로 검색이 되는 현상을 통해 상태 관리를 하기가 힘들었고 redux를 이용하여 slice를 이용해 검색 상태를 이용하려고 했으나 새로고침을 할 경우 이전에 검색했던 결과가 사라지는 현상이 발생하였고 또한 코드도 복잡하게 되어서 코드를 관리하기가 힘들겠다고 판단을 하여 다른 방법을 고민하던 와중에 팀원 중 한 명이 URL params를 이용하여 검색 기능을 구현해 보면 어떻겠냐는 조언을 듣고 params를 이용하여 검색 기능을 해결하였고 구현을 하니 우리가 흔히 많은 사이트에서 볼 수 있는 검색 URL을 볼 수 있었다.
검색 페이지

  // 검색 핸들러 함수
  const handleSearch = (e: React.FormEvent | null) => {
    if (e) {
      e.preventDefault();
    }

    const newSearchOption: SearchSendCurricula = {
      ...searchOption,
      currentPageNumber: 1,
    };

    setSearchOption(newSearchOption);
    const searchParams = new URLSearchParams();
    searchParams.set("search", searchOption.search);
    searchParams.set("curriculum_category", searchOption.curriculum_category);
    searchParams.set("order", searchOption.order);
    searchParams.set("only_available", searchOption.only_available.toString());
    searchParams.set("pageSize", searchOption.pageSize.toString());
    searchParams.set("currentPageNumber", "1");
    navigate(`/search?${searchParams.toString()}`);
  };

학생 navBar(선생님도 동일한 로직)


  // 검색 함수
  const handleSearchBar = (e: React.FormEvent) => {
    e.preventDefault();
    if (currentPath === "/search") {
      const searchParams = new URLSearchParams();
      searchParams.set("curriculum_category", curriculum_category || "");
      searchParams.set("search", inputSearch);
      searchParams.set("order", order || "LATEST");
      searchParams.set("only_available", only_available?.toString() || "false");
      searchParams.set("pageSize", searchData.pageSize.toString());
      searchParams.set(
        "currentPageNumber",
        searchData.currentPageNumber.toString()
      );
      setInputSearch("");
      // 검색어와 옵션들을 URL 파라미터로 포함시켜 이동
      navigate(`/search?${searchParams.toString()}`);
    } else {
      const searchParams = new URLSearchParams();
      searchParams.set("search", inputSearch);
      searchParams.set("order", searchData.order);
      searchParams.set("only_available", searchData.only_available.toString());
      searchParams.set("pageSize", searchData.pageSize.toString());
      searchParams.set(
        "currentPageNumber",
        searchData.currentPageNumber.toString()
      );
      setInputSearch("");
      // 검색어와 옵션들을 URL 파라미터로 포함시켜 이동
      navigate(`/search?${searchParams.toString()}`);
    }
  };

결론

😊 후기

이번 프로젝트는 지난 1학기 관통 프로젝트에 이어서 태어나서 두 번째로 하는 프로젝트였거니와 이번에는 6명이서 하는 프로젝트였기 때문에 나의 부족한 실력으로 인해 팀원들에게 피해를 주기 싫었기에 거의 매일 쉬지 않고 React 공부와 프로젝트를 하였으며 그 결과 아직 공부는 더 필요하지만 React에 대해 어느 정도 능숙하게 다룰 수 있게 되었다고 생각하여 조금이나마 수확을 얻었다고 생각한다.

하지만 그 와중에도 부족했던 것들이 있었는데 디자인적인 설계, 지저분한 코드, 코드 컨벤션을 잘 지키지 못했던 것, TypeScript를 제대로 공부하지 못했던 것, 테스트를 못 했던 것 등 부족한 부분이 많았었기 때문에 만족스럽지는 못했던 프로젝트 결과였다.

그래도 스스로 공부했던 React를 제외하고 배웠던 것들이 많았는데 1학기 때 배웠던 javaScript 문법을 복습하게 되었고 또한 더 많은 문법들을 알게 되었고 css도 flex와 grid에 대해서 배웠었지만 오랜만에 사용하다 보니 다시 개념 공부부터 시작을 하였고 프로젝트를 진행하면서 능숙하게 사용할 수 있게 되었다. 또한 프로젝트를 진행하면서 전공자 출신 싸피생 3명이 백엔드를 담당하였는데 그들이 프로젝트를 어떤 식으로 진행하였으며 비전공자인 나에게 어떻게 진행해야 하는지에 대한 것과 그 이외에도 git 사용법, swagger 사용법 등 잘 알려주었던 것들이 많아서 큰 도움이 되었다.

마무리로 부족한 실력으로 시작한 프로젝트였지만 답답해 하지 않으며 7주라는 시간 동안 팀적으로 불화가 없었고 잘 협력해준 우리 팀원들에게 정말로 고맙고 특히 프론트 엔드를 같이 담당했던 진송이 형에게 정말 감사의 말을 전하며 팀원 모두 싸피를 통해 좋은 곳에 취업을 하길 희망한다.


참고

깃허브 링크 : https://github.com/HG-KR98/steach-front

profile
Happiness is not a destination, it's a way of life.

2개의 댓글

comment-user-thumbnail
2024년 8월 26일

멋있어요

1개의 답글