🎮Web IDE 프로젝트: 알고리즘 파이터 회고

2024-06-28

알고리즘 파이터 프로젝트가 마무리되었다.
처음으로 협업을 통해 진행한 웹 프로젝트로, 많은 것을 배우고 느낄 수 있는 기회였다. 특히, 프론트엔드 부분에서 내가 맡았던 역할과 그 과정에서 알게 된 점들을 정리해보고자 한다. 프로젝트의 깃허브 링크는 여기에서 확인할 수 있다.

💡 배운 점들

1. TypeScript의 에러 처리 🛠️

이번 프로젝트에서 타입스크립트를 처음 사용하면서 맞닥뜨린 가장 큰 문제는 try-catch로 비동기 요청을 처리하는 과정에서 error가 unknown 타입이라는 에러 메시지가 출력되면서, catch 절 안에서 error.message와 같이 사용할 수 없었다.

타입과 에러에 대한 이해가 부족한 상태에서 프로젝트를 진행하며 겪었던 문제점인데, ChatGPT에서는 any 타입으로 처리하라고 하였다. 이렇게 하면 에러 메시지가 출력되지 않았지만, "이렇게 해도 되나?"라는 의구심에 여러 글들을 참고하고 멘토님의 도움을 받아 해결하였다. 가장 도움이 되었던 글은 다음 글이다.

자바스크립트에서는 모든 것이 throw 될 수 있다.

throw "하이";
throw 100;
throw null;
throw undefined;
throw { message: "바보 하이" };

위 예시처럼 다양한 형태의 에러가 발생할 수 있기에 실제 에러가 무엇인지 타입스크립트는 알 수 없다. 또한 비동기 요청의 경우 반환값이 undefined일 가능성도 있다. 이때 throw는 error의 타입을 좁히는 가장 좋은 방법이다.

//BAD
function createProject() {
  const user = getUser();
  if (!user) return;
  saveProject({ name: "", userId: user.id });
}
 
//GOOD
function createProject() {
  const uuser = getUser();
  if (!user) {
    throw new ReferenceError("User undefined");
  }
 
  saveProject({ name: "", userId: user.id });
}

이를 try/catch 문에서 사용할 때, 에러가 발생하면 try/catch 문에 걸릴 때까지 호출 스택을
버블링한다고 한다.

다음과 같이 try에서 발생한 에러가 catch에서 잡히게 된다.

try {
  throw new ReferenceError();
} catch (error) {
  console.error(error);
}

에러를 발견하게 되면, instanceof를 사용하여 타입을 상호작용이 가능한 특정 타입으로 좁힐 수 있다.

try {
  throw new ReferenceError();
} catch (error) {
  if (error instanceof ReferenceError) {
    console.error(error);
  }
}

위 내용을 활용해 커스텀 함수를 만들어 활용하시는 분들도 계셨다.

function getErrorMessage(error: unknown) {
  if (error instanceof Error) return error.message;
  return String(error);
}

반면, 다음 블로그와 같이 사용자 지정 에러클래스를 만들어 재사용 가능하게 끔 디자인 패턴을 사용하시는 분들도 있었다.

결국 이 프로젝트에서는 단순하게 instanceof를 활용하여 에러의 타입을 처리했지만, 타입스크립트에 대한 추가적인 공부 이 후 다시 해당 부분을 고민해봐야겠다.

2. 기술 스택을 선정하는 방법 🛠️

첫 프로젝트인 만큼 사용해보고 싶은 기술이 무척 많았다. 그중 대표적인 것이 전역 상태 관리 툴이었다. 이전에 Todo 앱으로 연습을 하면서 부모 컴포넌트에서 자식 컴포넌트로 Props를 전달하는 것이 많아지다 보니 코드가 매우 지저분해졌고, 이에 따라 전역적인 상태 관리의 필요성을 느끼게 되었다. 이번 프로젝트는 특히 일종의 게임을 만드는 만큼 Props의 사용 빈도가 늘 것으로 생각되어 기획 단계에서 전역 상태 관리를 반드시 사용하고자 다짐했었다.

이를 바탕으로 어떤 전역 상태 관리를 사용하는 게 좋을지 멘토님께 질문을 하였는데, 멘토님께서는 뜻밖의 미처 생각하지 못한 답변을 주셨다.

"전역상태관리에 잇어서 전역상태관리가 반드시 필수는 아니다"

멘토님께서는 Notion을 예로 들며 설명을 해주셨다. Notion은 상태 관리를 URL에서 진행하고, 전역 상태 관리를 안 한다고 한다. URL에서 쿼리, 파라미터 등을 이용할 수 있기 때문이다. 또, 모든 상태에 대해 전역으로 관리하게 되면 비즈니스 로직의 경우 코드의 가독성이 떨어지는 경우가 발생할 수 있다고 하였다.

따라서, 모든 것은 필수적인 게 아니라 상황에 맞게, 기술적인 근거를 들어 사용해야 한다고 말씀을 해주셨다.

이 말씀을 듣기 전까지는 코드의 복잡도나 중요도를 신경 쓰지 않고 단순하게 모든 코드들을 분리하려고 시도를 해왔었는데, 멘토님의 조언을 바탕으로 필요한 부분에서만 전역 상태 관리를 시도할 수 있게 되었다.

이 프로젝트에서는 다음과 같이 로그인 한 유저의 정보 관리, 라이트/다크 모드의 테마 관리에서만 전역 상태 관리를 사용했다.

export const useTheme = create(
  persist<ThemeType>(
    (set) => ({
      theme: "dark",
      changeTheme: () => {
        set((prevState) => {
          return {
            theme: prevState.theme === "light" ? "dark" : "light",
          };
        });
      },
    }),
    {
      name: "themeStorage",
    }
  )
);

3. 이미지의 업로드 방법 🌄

이번 프로젝트에서 이미지의 업로드가 사용되는 부분은 단 한 곳, 프로필 이미지 설정이었다. 게임이 주 콘텐츠인 우리 프로젝트의 특성상 게임에 참가한 유저들에게 상대방과 경쟁하고 있다는 경험을 제공해야 했고, 이에 따라 제시된 기획이 게임대기방과 게임 진행상태에서 상대방과 자신의 프로필을 확인할 수 있었야 한다는 점이었다.

일반적으로 이미지를 업로드 하기 위해서는 S3 버킷에 이미지를 올리고 이미지가 저장된 url을 데이터베이스에 저장하는 형태로 진행된다.

하지만, 약 2주라는 짧은 개발기간, 크램폴린 ide로 강제된 배포, '이미지 처리에 대한 배경지식 없음'이 어우러져 다음과 같은 형태로 이미지를 처리하도록 만들었다.

  1. 사용자가 Github issue 탭, 디스코드와 같이 마크다운을 입력할 수 있는 공간에 이미지 업로드 후 이미지url을 복사

  2. 프론트엔드에 복사한 url을 입력

  3. 저장 후 전송된 url을 string 형태로 백엔드에서 DB에 저장

전반적인 배경지식을 습득한 지금보면 터무니 없는 방법이지만 그 때 당시에는 다음과 같이 구현을 한 후 멘토님께 조언을 구했다.
멘토님께서는 다음과 같이 구현하면 안되는 이유 3가지를 말씀해주셨다.

  • 첫 번째 이유: 복사해서 저장한 URL이 항상 고정일 거라는 보장이 없다는 점이었다. Notion, Slack처럼 비공개 서버에서는 이미지에도 보안이 있는데, 이러한 경우 이미지를 다운로드할 때 일회성의 URL을 주는 경우도 있다. 이러한 경우 URL이 변경될 수 있다는 점이었다.

  • 두 번째 이유: CSP(Content Security Policy) 때문이라고 하셨다. 우리가 처음 시도한 방식처럼 외부의 URL을 입력받게 되면 사용자가 어느 URL을 입력하게 될지 알 수 없어 보안상으로 매우 취약해진다고 한다. 이를 위해 CSP를 설정하게 되어 외부 소스의 입력을 막아야 한다고 하셨다.

  • 세 번째 이유: UX상의 문제였다. 우리의 서비스에서 프로필 업로드 기능을 사용하기 위해 서비스 외부를 요구하는 강제성은 사용자의 경험에 매우 안 좋은 영향을 준다고 하셨다.

멘토님께서는 올바른 방법인 formData를 사용하라고 말씀해주셨으나, formData를 사용하게 될 경우 백엔드 api의 수정이 필요하였다. 이미 백엔드에서 api구현이 완료되어 수정이 불가하다는 답변을 받았기에 이런 식으로 구현을 할 수 밖에 없다면 프로필 업로드 기능을 제거하라고 말씀하셨다. 지금와서 복기해 보면 당시 유일한 해결책은 이미지의 S3 업로드를 프론트엔드 단에서 처리하여 api의 수정없이 해결 할 수 있었을 것 같다. 그러나 멘토링 직후 남은 시간은 약 4~5일 정도 남았을 시점에서 정신없이 개발을 하던 중에다가 관련 지식이 부족했던 지라 미처 방법을 생각하지 못했다.

따라서, 프론트엔드에서 고안한 방법은 이미지의 업로드가 아닌, 미리 지정된 캐릭터이미지를 프론트엔드에서 제공하고, 사용자는 캐릭터 중 한 개를 선택하게 함으로써 프로필을 설정하게 끔 구현하였다.

4.UI/UX 디자인의 중요성 🖌️

이 외에도 UI/UX 적으로 배운 점들이 많았다. 버튼의 위치, 사용자가 페이지를 읽어나가는 방향, 중요한 정보와 비중요한 정보의 배치에 따른 구분 등 미처 생각하지 못했던 점들에서 피드백을 받고 수정해 나갔다.

그동안 내가 사용하던 서비스(Velog, Naver 등)이 왜 이런 레이아웃과 배치, 기능들을 가지는지 의문점을 가지지 못했는데, 이 점을 알고 나니 기획자, 디자이너, 개발자들의 노고가 보이기 시작했다. 다만 이를 문서화하기에는 경험적으로 얻는 부분들이 많았기에 자세한 부분은 생략한다.


마치며 🚀

여러모로 많이 배울 수 있었던 첫 프로젝트였다. 프로젝트가 끝난 지 한 달 이상 지나 추가적으로 프로젝트를 보완하기 힘들고, 서버도 내려간 상태라 아쉬웠던 점들을 다른 방식으로 공부해 나가야겠다. 내용이 길어 이 글에서 다루기 힘든 내용**(Vite 마이그레이션, 이미지 업로드 최적화, CI/CD** 등등..)도 있는데 차차 정리해가며 포스팅해야겠다.