Published on

지방적 사고 프로젝트 리팩터링 하기 With. 후기, 트러블 슈팅

여유가 생기면 꼭 해보고 싶었던 해커톤 프로젝트 지방적 사고를 근 며칠간 리팩터링 하며 고민하고, 배우고, 느꼈던 부분을 기록하고자 한다.



관련 글

👉 해커톤 후기


리팩터링 계기

1. 프런트엔드 개발자인 나, Deploy된 프로젝트가 하나도 없다?

이력서를 작성하고 채용공고를 살펴보다 보면, 특정 채용공고에서는 포트폴리오 혹은 배포된 URL을 요구하는 경우가 종종 있었다.

그동안 진행해 왔던 프로젝트는 아무래도 취준생 위주로 팀을 구성하기도 했고, 배포를 유지하며 발생하는 서버 비용은 결코 쉬쉬할 수 없기 때문에 현재 시점에 내가 자신 있게 내세울 수 있는 배포된 프로젝트는 단 하나도 없었다.

또한 과거 프로젝트가 배포된 상태에서는 가끔 힘들거나 지칠 때 그동안의 결과물을 보며 느끼는 성취감을 에너지로 삼기도 했는데, 프로젝트를 진행하며 결국 남는 게 없는 것 같은 아쉬움이 많이 들었고 늘 아쉬운 마음으로 남아있었다.


호랑이는 죽어서 가죽을 남기듯, 프런트엔드 개발자는 배포된 결과물을 남겨야 한다.


2. 썩히기에는 아쉬운 디자인 & 새로운 기술 스택 적용에 대한 욕심

해커톤 당시에도 느꼈지만, 기존에 내가 디자인하고 기획했던 형편없는 디자인에 반해 아기자기한 이미지, 아이콘과 페이지 UI가 정말 마음에 들었고 프로젝트 리팩터링을 결정하는데 작지 않은 부분을 차지했다고 생각한다.

앞서 언급한 이유가 결정에 영향을 미치긴 했지만, 가장 큰 부분을 차지했던 건 Next 프레임워크를 사용한 풀스택 개발에 대한 욕심이었다.

Next Full-stack 숙련도가 높지 않고, 레퍼런스가 부족하다는 부분이 마음에 걸렸지만 리팩터링에 앞서 문제가 되었던 부분을 생각하면 좋은 선택이었다고 생각한다.

리팩터링에 앞서 고민한 문제라면, 프런트보다는 백엔드 부분에 대한 고민이 컸는데, 초기에는 백엔드 기술 스택 중 기존에 다뤄 본 경험이 있던 Nest.js를 사용하는 것으로 자연스럽게 가닥을 잡게 되었다.

하지만 프로젝트 자체가 백엔드에서 처리할 일이 그렇게 많지 않았기 때문에 백엔드 프로젝트를 따로 운용하는데 아쉬움이 많이 남을 것 같았다.

그러던 중, 항상 말로만 들었던 Next는 백엔드의 역할을 대신 수행할 수 있다는 기억이 떠올랐고 규모가 작은 프로젝트인 만큼 현재 상황에 가장 적합하다는 생각이 들었다.

API 같은 경우는 Next 프로젝트 내에서 해결할 수 있지만, 데이터를 저장할 DB가 필요했기 때문에 Firebase, Supabase를 두고 고민하게 되었고 비용적인 측면과 웹에 더 최적화된 기능을 제공하는 Supabase를 선택하게 되었다.

사실 두 플랫폼에서 제공하는 기능은 거의 사용하지 않았기 때문에 제공하는 기능은 크게 와닿지는 않았고 둘 다 처음 사용해 보기 때문에 비용적인 측면이 선택에 가장 크게 작용했던 것 같다.



무엇을 했나?

1. SEO 관련 작업 진행

기존에는 SEO 작업이 중요하는 것은 알고 있었지만 HTTPS, 시맨틱 태그, 웹사이트 성능 개선 외에는 SEO 관련해서 큰 작업 경험이 없었던 것 같다.

내가 공부한 추가적인 SEO 방법으로는 robots.txt, sitemap 작성이 있었는데 이 두 파일은 검색 엔진이 웹사이트를 더 효율적으로 크롤링하고 인덱싱하는 데 중요한 역할을 수행한다. 파일만 추가하면 되는 정말 간단한 작업인데 그동안 어렵다는 선입견만 가지고 미뤄왔던 것 같다.


sitemap.ts

robots.txt

이외에도 직접적인 SEO 작업이라고 볼 수 없지만, 간접적으로 연관되어 있는 Open GraphTwitter Cards 소셜 미디어에 공유할 때 미리 보기 카드에 표시되는 내용 작업도 진행해 보았는데,

우리가 평소에 당연하다고 생각했던 링크 공유 시 노출되는 이 카드가 트래픽 증가를 유도해 SEO에 도움이 될 수 있기도 하고, 지방 친구의 고충을 링크로 공유해야 하는 서비스 특성상 매우 중요한 작업이라고 볼 수 있다.

Next에서는 내장된 Metadata를 사용해 간편하게 구현이 가능하다. 라우팅, 최적화를 포함해 Next라는 프레임워크에서 제공하는 유용한 기능이 많다는 것을 다시금 깨달을 수 있었고 큰 매력을 느낄 수 있었다.


2. 스토리북 UI 테스트 사용

스토리북 또한 언제나 적용해 보고 싶은 라이브러리 중 하나였는데, 매번 개발을 진행하다 보면 여러 상태를 가지는 UI가 발생하게 된다. 그때마다 어쩔 수 없이 더미 데이터를 입력해 UI 변경 상태를 확인할 수밖에 없었다.

매번 번거로운 과정이 발생할뿐더러, 협업 시 이 컴포넌트의 역할을 파악하기 위해 다른 팀원이 허비하는 시간이 발생하는 것은 불 보듯 뻔한 일이었다.

스토리북을 처음 사용해 보며 느낀 점이 있다면, 정말 많은 기능 ( Addon과 같은 )을 제공하고 있고 문서화, 자동 테스트 등을 통해 정말 편리하고 협업에 큰 도움을 줄 수 있다는 것이다.

직전 프로젝트에서 스토리북을 사용해 보지 못한 부분이 새삼 아쉽게 느껴진다. 이외에도 UI 접근성 테스트도 진행해 보고, 배포, CI/CD 구축도 경험해 볼 수 있었다.


3. Next Full-stack ( with. Supabase )

Firebase, Supabase 모두 사용해 본 경험이 없었기 때문에 러닝 커브 발생에 대한 우려가 많이 됐었다. 현 프로젝트 외에 다른 프로젝트를 병행해야 했기 때문에 많은 시간과 비중을 투자하기에는 부담스러웠기 때문이다.

하지만 생각보다 레퍼런스를 따라 진행하다 보니 설정 자체에 큰 어려움은 없었다. middleware.ts와 몇몇 설정을 추가해 연동하면, 간단하게 Supabase와 연동이 가능하다.

/** user 테이블 데이터 조회 */
export async function getUserData(userId: number): Promise<UserRow> {
  const supabase = await createServerSupabaseClient()

  const { data, error } = await supabase
    .from("user")
    .select("*")
    .eq('id', userId)
    .single()  // 단일 행 반환

  if (error) {
    handleError(error)
  }

  if (!data) {
    throw new Error("제공된 userId를 가진 사용자를 찾을 수 없습니다.")
  }

  return data
}

마치 ExpressNest 프레임워크를 사용하듯, JavaScript를 사용해 백엔드 부분 개발을 진행할 수 있는 부분이 매우 만족스러웠고 작은 프로젝트 규모에 맞는 가장 적합한 선택이었다는 생각이 든다.

데이터 페칭 시간이 긴 관계로 isPending, isLoading과 같은 옵션으로 제공하는 Loading Fallback을 활용해 어느정도 이 부분을 완화하고자 했다.


4. 코드 작성

이번 리팩터링은 그동안 사용해 보고 싶었던 기술 적용, 배포 외에도 그동안 습득한 내용을 적용해 보고자 하는 마음도 있었다.


코드를 작성하며 가장 염두에 두었던 부분은 크게 3가지다.

  1. 폴더 구조에 맞게 파일 분류하기 (재사용성 높이기)
  2. 시맨틱 태그 신경 쓰기
  3. 서버, 클라이언트 컴포넌트 - 최대한 서버 컴포넌트 활용하기 위해 노력하기

features 폴더에서 각 페이지(경로) 별 기능을 정리하고 shared 폴더에서 데이터 페칭, 공용 컴포넌트, 유틸 함수 등 공통적인 내용을 관리 및 재사용하기 위해 노력했다.

기존 프로젝트는 특별한 디자인 패턴과 같은 폴더 구조를 유지한 적이 특별히 없었는데, 이번 프로젝트에는 리액트 프로젝트를 기능별로 구조화하기 포스팅을 착안해 폴더 구조화를 진행했다.

이번 프로젝트는 재사용 가능한 페이지, 컴포넌트가 특히 많았기 때문에 재사용과 관련된 고민을 많이 해볼 수 있는 좋은 기회였다.



트러블 슈팅

1. isPending으로 인한 UI 버벅임

화면이 빠르게 전환되지만 자세하게 살펴보면, 출발, 목적지 입력 후 최적 경로 알아보기 버튼을 클릭했을 때 문제가 발생한다.


내가 예상했던 페이지 전환 흐름은,

  1. 출발 목적지 페이지에서 최적 경로 알아보기 버튼을 클릭
  2. 최적 경로 리스트가 있는 페이지로 이동이다.

현재 최적 경로 알아보기 버튼을 클릭할 경우 여러 API를 사용해야 하기 때문에 많은 시간이 발생하게 되는데, 이 과정에서 사용자들에게 Loading UI를 제공해 요청이 진행되고 있다는 상황을 알리고자 했다. 그런데 Loading UI가 렌더링 되고 페이지가 전환되는 과정에서 기존 UI가 잠시 노출되는 현상이 발생했다.

Loading UI ( 지하철, 시내버스, 시외버스 이미지가 랜덤으로 노출된다. )


React Query 라이브러리를 사용하고 있었기 때문에 Mutation에서 제공하는 isPending을 사용해 UI를 제공하고자 했다. 그러나 전혀 예상하지 못했던 UI 버벅임이 발생했고 곧바로 코드 분석에 들어갔다.

console.log를 활용해 원인을 분석해 본 결과 문제의 원인을 찾을 수 있었는데,

  1. saveRoute Mutation 비동기 함수가 진행되는 동안 <Loading/> UI가 fallback의 역할을 수행하게 된다.
  2. 비동기 요청 saveRoute이 종료되면 isPendingfalse로 바뀌며 기존 폼인 return 내부를 렌더링 하게 된다. 이 과정에서 아직 router.push가 실행돼 페이지가 전환되지 않았지만, 비동기 요청이 종료되었기 때문에 isPending 값이 false를 반환하게 되고 화면이 기존 폼으로 렌더링 된다.
  3. 따라서 버튼을 클릭했을 때 fallback UI -> 기존 화면 렌더링 -> 페이지 전환 단계가 발생하게 된 것이다.

isPending이 페이지 전환 전까지 Loading UI를 표시해 줄 것이라는 믿음에서 일어난 문제였다는 것을 알고 어찌 보면 당연하지만 너무 안일하게 생각한 자신을 반성할 수 있었다.

해결을 위해 비동기 요청이 끝나고 페이지 전환 전까지 기존 폼(UI) 노출을 제한하는 조건문을 추가하는 방법으로 가닥을 잡게 되었다.

isSaving 상태 변수를 추가해 비동기 함수 종료 -> 페이지 전환(router.push)이 이루어질 때까지 Loading UI를 렌더링하도록 코드를 수정해 주었다.

또한 기존 UI 같은 경우 중간에 렌더링 되지 않고 페이지가 전환되며, 에러(isError)가 발생했을 때는 기존 UI를 렌더링 해 주소를 다시 입력할 수 있도록 한다.

개선 후에는 중간에 기존 UI가 렌더링 되지 않고 교통 수단 저장동안의 Loading UI와 교통 수단 Query를 fetching 하는 동안의 Loading UI가 렌더링 되는 것을 확인할 수 있다.



2. build 도중에 발생한 알 수 없는 에러

  • HookWebpackError: Cannot read properties of undefined (reading 'length') at makeWebpackError

  • TypeError: Cannot read properties of undefined (reading 'length')


빌드 과정에서 정확한 원인을 추측하기 어려운 에러가 발생했다. 이 에러를 찾기 위해 생각보다 많은 시간을 소모하게 되었는데, 에러명이 명확하지 않았던 관계로 키워드를 찾는데 어려움을 느꼈고 내 문제 상황에 맞는 관련 레퍼런스도 없었다.

그렇게 레퍼런스를 찾던 중, 우연히 Stack Overflow에서 tailwind.config.js 파일에 커스텀 키워드 선언 관련해서 문제가 발생할 수 있다는 힌트를 얻을 수 있었다.

잘못된 코드

backgroundImage: {
  "custom-gradient":
    "linear-gradient(to #F8D169, #FC9A7B, #95ACFF, #77DBCE)",
},

리팩터링 전에 사용했지만, 현재는 사용하고 있지 않았던 위 코드 속 to 키워드가 문제가 되었는데, 올바른 코드는 to right or left 등 그라이데이션 적용 방향을 설정하지 않은 에러였다.

올바른 코드

backgroundImage: {
  "custom-gradient":
    "linear-gradient(to right, #F8D169, #FC9A7B, #95ACFF, #77DBCE)",
},

이 외에도 중첩된 ":(콜론)"이 문제를 야기할 수도 있다고 하니, 앞으로 빌드 과정에서 에러가 발생했을 문제 원인을 추측할 수 있는 가지수가 늘어난 작은 성과를 얻었다.




마치며

잠시 시간적인 여유가 생겨 그동안 버킷 리스트에 담아두었던 지방적 사고 리팩터링을 진행할 수 있었고, 그동안 관심 있었던 기술 스택이 운 좋게 요구 사항과 일치했기 때문에 적용할 수 있는 좋은 계기가 되었다.

새롭게 무언가 적용하고 공부하며 많은 의욕과 열정이 생겼고, 기존 코드 개선 및 새로운 기술을 습득하는 과정에서 나름 성장했다는 성취감도 느낄 수 있는 좋은 경험이 되었다.

하지만, 빠르게 변화하는 이 흐름에 적응하고 도전해야 한다는 것이 한편으로는 조금 걱정되기도 한다. 따라서 결국은 본질이 되는 기본을 확실하게 다지며 변화의 흐름에 맞춰갈 수 있도록 대비하기 위해 꾸준히 정진해야 한다는 것이다.

훗날 조금이라도 성장한 내가 이번에 작성한 코드를 보며 또다시 리팩터링을 계획할 수도 있을 것이다. 개발 -> 리팩터링의 순환은 아무래도 필연적인 것 같다. 짧은 기간 동안 리팩터링을 진행하며 즐거운 마음이 많이 들었다. 앞으로 법무 링크 프로젝트를 진행하며 틈틈이 리팩터링을 이어나갈 계획이다.



지방적 사고 Github

👉 지방적 사고