[프로젝트 회고] - 책 췤(chaeg check)

YunShin·2024년 5월 19일
2
post-thumbnail

2주 동안 '모비톤' 에 참가하여, 도서 조회&리뷰 서비스 '책 췤' 를 만들었습니다. 주어진 기간동안 나름 열심히 했지만 아직 개선해야할 부분이 많고, 크고 작은 에러가 계속 발견되고 있으니,, FE 개발 지망생으로서 조금 더 분발해야 할 것 같습니다..😅

하지만 프로젝트 완성도와는 별개로, 이전 경험에 근거하여 새로운 시도를 많이 했고, 좋은 깨달음을 얻게 되어 뿌듯함 또한 느끼고 있습니다. 어떤 생각으로 어떤 변화를 주었는지, 그 결과는 어떠했는지, "책 췍"의 첫번째 버전이 나온 현 시점에서 회고글를 남깁니다.

🔥 프로젝트 개요

🎸 서비스명 : 책 췍 (chaeg-check)
🗓️ 개발기간 : 2024.4.29 ~ 2024.5.12
👥 개발인원 : FE (3) / BE (1)
🔗 배포 주소 : https://chaegcheck.vercel.app/sign
🗂️ 프로젝트 저장소 : https://github.com/mobi-projects/mobi-3rd-1-typescript

'책 췍' 은 "책을 확인(check) 해라" 라는 의미를 가진 서비스 이름입니다. 😁
핵심 기능은 여러 도서 목록을 진열하고, 세부적인 도서 정보를 제공하는 것입니다. 물론 각 도서에 대한 댓글와 평점을 남길 수 있어요.!

aladin api 를 활용하여 도서 정보를 출력하고, 자체 서버에서 회원 정보와 책에 대한 리뷰 데이터를 관리하도록 설계했어어요.


🔎 제일 신경 쓴 부분

⛲️ 함수의 구현과 추상화

'책 췍' 프로젝트를 통해 얻은 가장 큰 소득은 함수를 어떻게 만들고 언제 사용하는지를 고민했던 시간이라고 생각해요. 이 프로젝트에서 저의 주된 임무는 axios, tanstack-query 의 메서드를 활용해서 데이터 처리 함수를 설계&구현하는 것이었습니다.

다행히도 프로젝트 시작 전에, "쏙쏙 들어오는 함수형 코딩" 이라는 책을 읽었던 덕분에, 굉장히 핵심적인 내용이었던 [함수의 분리와 추상화] 를 유감없이 실습할 수 있었습니다. 😅

아래서 자세히 얘기하겠지만, 팀 내에 디자이너가 부재한 상황에서 작업 속도를 늦추지 않기 위해 ui 코드보다 기능에 필요한 logic 을 우선하여 작업했습니다. 설령 함수의 동작이 너무 비효율적이라, 바로바로 바꾸고 싶은 욕구가 치밀어도 출력만 정상이라면 일단은 넘어갔습니다..

📕 첫 번째 Refactoring - SignIn

마침내 코드를 다시 손보는 시간이 찾아왔고, 저는 책의 내용대로 [함수의 분리와 추상화] 를 적용했습니다.

다음의 예는 "로그인" 기능 구현을 위해, 서버로 POST 요청을 보내는 코드입니다.

export const postUserSignIn = async ({
  password,
  userId,
}) => {
  try {
    // 👇 userId 와 pw 로 로그인 시도 (axios 메서드 호출)
    const response = await baseAxiosInstance.post(API_SIGN_IN, {
      password,
      userId,
    })
    // 👇 🔴 응답 결과 중, access-token 은 local stroage 에 저장 
    localStorage.setItem(AUTH_TOKEN, response.data.token)
    // 👇 🔵 응답에 실린 데이터 반환
    return response.data
    
  } catch (err) {
    throw console.log(err)
  }
}

참고로 응답이 정상이 경우, response 에는 사용자의 계정 정보가 포함되어 있었습니다. 🫠

👀 뭘 수정해야 하나?

일단 응답에서 access-token 을 local-stroage 에 이미 저장되었는데(🔴), 또 어떤 쓰임이 있다는 듯 외부로 반환(🔵)되고 있는게 눈에 띄였어요. 설계 상으로나, 직관적으로나 access-token 이 local-stroage 에 저장된 이상 그것이 함수 외부로 나가야 할 이유가 없었는데 말이죠.

token 뿐만 아니라, 반환에 대해서도 필요한 값만 추리는 함수가 추가된다면 가독성이 더 좋아질 것 같았아요. 처음엔 아래와 같이 수정하여, pull-request 를 열었습니다.

export const postUserSignIn: PostUserSignInFT = async ({
  password,
  userId,
}) => {
  try {
    // 👇 userId 와 pw 로 로그인 시도 (axios 메서드 호출)
    const response = await baseAxiosInstance.post(API_SIGN_IN, {
      password,
      userId,
    })
    // 👇 access-token 추출
    const accessToken = extractAccessToken({ response }) 
    // 👇 access-token 을 local stroage 에 저장
    saveToLocalStorage({ key: AUTH_TOKEN, value: accessToken }) 
    // 👇 응답결과를 User Type 으로 변환
    const user = convertSignInResToUser({ response }) 
    // 👇 User 객체 반환
    return user
  } catch (err) {
    throw new Error("로그인에 실패했습니다.")
  }
}

[실제 pr 보러가기 🛫]
(👇 이미지 클릭하면 해당 PR 로 연결됩니다.)

처음엔 이정도 수준으로도 꽤 만족하고 있었습니다.
책의 내용대로 추상화 과정을 거치니, 코드 줄 수가 늘어나긴 했습니다만, 코드의 논리적인 흐름(?) 이 보이기 시작거든요.
( 수정 전과 비교했을 때, access-token 의 추적이 쉽지 않나요?? 🥹 )

😓 왜 또 수정해야했나?

하지만 얼마 안가, 해당 함수 역시 잘못 구현했다고 생각했습니다.
왜냐하면 postUserSignIn() 의 구현이 함수 시그니처 와 일치하지 않기 때문입니다.

함수 시그니처 (Function Signature) 란 ??

  • 함수의 이름, 입력(파라미터) 정보, 반환 정보 등으로 해당 함수를 식별할 수 있는 요소입니다.
  • CS 관점에서 Compiler 나 Interpreter 가 호출된 함수를 올바르게 찾아가기 위해 확인합니다.
  • DX 관점에서 함수의 추상화 수준을 평가하는 요소로 사용합니다.
    • 📍 저는 이 포스터에서 DX 관점으로 글을 적습니다.

누군가 로그인 기능을 완성시키기 위해 제가 만든 함수를 사용한다면, 가장 먼저 보게 될 것은 함수의 이름일 것 입니다. 하지만 postUserSignIn() 란 이름을 보고 그 내부에 local-storage 를 건드리는 로직이 있을 거라고 누가 생각할 수 있을까요?

저 스스로 평가해봤을 때, 위의 이름을 가진 함수에게 기대할 수 있는 동작은 로그인 기능과 관련하여 POST 요청를 보내는 것 정도 입니다.
쉽게 말해서 "이름에 비해 기능이 과대하다" 는 느낌이었어요.

[실제 pr 보러가기 🛫]
(👇 이미지 클릭하면 해당 PR 로 이동합니다.)

📘 두번째 Refactoring - SignIn

로그인 로직을 다시 수정했습니다. postUserSignIn() 은 함수의 이름 그대로,POST 요청만 보내는 것으로 기능을 축소했어요.

export const postUserSignIn: PostUserSignInFT = async ({ reqBody }) => {
  try {
    const response = await baseAxiosInstance().post(API_SIGN_IN, reqBody)
    return response
  } catch {
    throw new Error("로그인에 실패했습니다.")
  }
}

👀 나머지 동작은 어떻게?

이제 반환된 결과를 이용해서 나머지 동작 (access-token 을 저정하는 등) 을 외부에서 진행해야합니다.

중요한 점은, 그러한 동작들은 요청이 성공했을 때 이뤄져야 한다는 것입니다. tanstack-query 의 메서드 중, useMutation() 은 data-fetching 이 성공 혹은 실패일 경우에 대한 동작을 각각 콜백함수로 전달할 수 있어요.

export const useMutationSignIn = () => {
  ....
  const { mutate: signIn, ...rest } = useMutation({
    ....
    mutationFn: ({ email, password }: SignFormType) => {
      ...
      // 👇 post 요청
      return postUserSignIn({ reqBody })
    },
    // 👇 요청 성공 시
    onSuccess: (response) => {
      // 1️⃣ 토큰 분리
      const accessToken = extractAccessToken({ response }) 
      // 2️⃣ 토큰 저장
      saveToLocalStorage({ key: AUTH_TOKEN, value: accessToken })
      // 3️⃣ 응답 결과를 원하는 형태 (User) 로 변환
      const user = convertSignInResToUser({ response }) 
      // 4️⃣ 성공 알림
      onAlert({ children: `${user.nickname} 님, 환영합니다.` })
    },

    // 👇 요청 실패시에는 실패 알림만..
    onError: (error) => onAlert({ children: error.message 
    ....
}

이정도면 꽤 괜찮지 않나요?? 🙂

📛 이름짓기의 중요성 - DX 개선

위에선 하나의 예시만 설명했지만, 많은 함수에 대해 그 이름과 동작이 적절한지를 생각하며 refactoring 했어요.

그러다보니, 함수명 혹은 변수명이 DX 관점에서 얼마나 중요했는지도 눈에 보였답니다.

다음 예시는 로그아웃 기능을 관리하기 위해 제가 만든 useSignOut() 이란 custom-hook 입니다.

🤔 수정 전, 로그아웃 hook 선언부

export const useSignOut = () => {
  ....
  // 👇 🔴 `useMutation()` 을 그대로 반환
  return useMutation({
    mutationKey: ...
    mutationFn: ...,
    onSettled: ...,
  })
}

return 문을 보면 useMutation() 을 그대로 반환합니다. [🔴]
사실 동작에는 아무 문제가 없어요. 하지만 이를 사용할 때, 약간의 불편함이 생깁니다..

사용하는 경우를 보면, 아래와 같이 mutate() 라는 이름의 메서드로 로그아웃이 실행됩니다.

🤔 수정 전, 로그아웃 hook 사용부

export const Header = () => {
  //...
  const { mutate } = useSignOut() // 👈 반환되는 속성 중,'mutate()'
  //...
  return(
    <div>
      {/*...*/}
      {/* 👇 버튼 누르면 로그아웃 */}      
     <IconTextButton callback={ mutate(...) } icon={Logout} text="LogOut" />
    </div>
  )
}

하지만 아까와 같이 이름에 주목해봅시다. mutate() 라는 메서드 이름을 보고 어떤 동작을 기대할 수 있을까요?

🤪 돌연변이가 발생되기를 기대한다면 사용해도 좋을 것 같아요. 👍

로그아웃이 이뤄질 거라고 기대하긴 힘든 이름입니다.
여기서, 저는 이름을 반드시 수정을 해야겠다고 느꼈어요.


🌀 수정 후, 로그아웃 hook 선언부

export const useSignOut = () => {
  ....
  // 👇 `mutate` 메서드 의 이름을 `logout` 으로 변경합니다.
  const { mutate: logout, ...rest } = useMutation({
    mutationKey: ...
    mutationFn: ...,
    onSettled: ...,
    },
  })

  return { logout, ...rest }
}

아주한 사소한 일 이지만, 이름을 바꿔서 반환해준다면 view 를 담당하는 팀원이 더 직관적으로 해당 기능을 사용할 수 있어요.

이걸 사용한다면?

🌀 수정 후, 로그아웃 hook 사용부

export const Header = () => {
  //...
  const { logout } = useSignOut() // 👈 'logout()' 반환
  //...
  return(
    <div>
      
      {/*...*/}
      {/* 👇 버튼 누르면 로그아웃 */}        
     <IconTextButton callback={ logout(...) } icon={Logout} text="LogOut" />
    </div>
  )
}

( "버튼을 누르면 mutate() 가 실행된다." 보다 "버튼을 누르면 logout() 이 실행된다" 가 훨씬 직관적이지 않나요?? 😅)


😌 So..

솔직히 그동안 "추상화" 라는 개념이 너무 막연했고, 함수의 호출 시점에 대해서 그다지 깊게 생각해보지 않았아요.
협업 경험이 많이 없어, 제가 작성한 함수는 다시 제가 사용할테니 함수 시그니처보다 함수 구현부의 맥락을 기억해내서 작업했기 때문입니다.
그런 면에서, 다른 개발자의 시선(?)으로 생각 본 위의 경험이 의미가 컸다고 생각합니다.!

약간 아쉬운 점이 있다면, 이번 프로젝트에서는 모든 함수를 동일한 기준으로 수정하지 못했어요.. 아직도 많은 함수가, 구현을 반영한 함수 시그니처를 갖지 못했습니다.!

수정하면서 제 고민이 계~속 달라져왔기 때문이기도 하고, 애초에 어떤 함수가 필요할지 설계 후에 시작하지 못한 탓도 있어요. 설계시간에 고민을 많이 할수록, 개발 중에는 고민을 덜하게 된다는 mobi 운영진들의 말씀을 이제야 실감합니다. 😁

앞으로도 저는 완벽한 함수를 한번에 적어내진 못할 것 같습니다. (그렇게 하려면 많은 시간과 고민이 필요할테니, 프로젝트를 속도가 더뎌지겠죠?)
하지만 어떤 logic 을 완성해야 할 때, 함수의 구현부보다는 함수 시그니처로 보고 그 방향이 적절한지 판단하는 습관을 들이려고 해요. 미약하게라도, dx 개선 효과가 있다는 것을 이번 프로젝트에서 확인했으니까요. 👍

✨ 새롭게 도전해봤습니다.


1. UI 디자인(💅🏼) < 기능구현(✨)


"디자이너가 없었기 때문에 프로젝트가 실패했다." 라고 생각하지는 않습니다. 하지만 팀원 중, 아무도 디자인 작업 경험이 없는 경우 대부분의 작업이 지연될 수 있다고는 생각해요. 초안이라도 참고할 페이지가 있고 없음에는 큰 차이가 있으니까요..

이번 모비톤을 시작하기 전, 그것과 관련한 고민이 아주 많았습니다. 전에 했던 프로젝트에서는 4주 의 프로젝트 기간동안 절반 정도를 Figma 에서 작업했었기 때문입니다. 심지어 이번엔 이전보다 더 적은 인원으로, 더 짧은 시간 안에 프로젝트를 완성해야 했어요. 긴박감(?)을 느낀 저는 팀원들에게 다음과 같은 시도를 해보자고 제안을 했습니다.

세부적인 디자인 작업은 뒤로 미루고, 로그인이나 데이터 패칭 로직을 먼저 구현해놓자고요.

프로젝트 1일차에 설계를 해보니, 모든 페이지마다 api 호출은 반드시 필요했습니다. axios 로 데이터 패칭 함수를 미리 정의둔다 한들, 그 코드는 디자인이 완성된다하더라도 수정될 일이 거의 없음을 예상했습니다.

로그인 기능 구현을 위해, 기본 input 태그 두 개만 놓고 일단 시작하자는 의견도 과감히 제시했었어요. view 를 제대로 꾸미지 못해도 기능이 완성되면 부족한대로 서비스를 할 수 있지만, 기능 구현 없이 서비스가 이뤄질 수 없기 때문입니다. 그만큼 저는 "프로젝트 완성" 에 목이 말랐습니다..

프로젝트 초반, PR 에 첨부된 이미지를 형편없는(?) ui 가 확인됩니다..

[실제 pr 보러가기 🛫]
(👇 이미지 클릭하면 해당 PR 로 이동합니다.)

실제로 이는 효과적인 방법이었다고 생각해요. 한 주동안 모든 페이지의 핵심 logic 을 다 만들었습니다. 남은 시간동안 인원을 나눠 view 를 꾸미는 일과, logic 을 refactoring 할 수 있었어요. 이는 병렬적으로 처리가 가능했기 때문에 PR 이 막힘없이 올라왔습니다. 🤩



2. Component 중심의 폴더구조

현재까지 8~9 개월 정도 react 로 작은 프로젝트들을 여럿 만들어왔습니다. 하지만 항상 함수 혹은 변수 등을 선언해야 할 때면, 어떤 파일에 선언해야 적절한지가 고민이었어요. 특정 컴포넌트에서 사용하는 custom-hook 혹은 일반 함수 등이, folder-tree 상 에서 멀리 떨어진 파일에 각각 선언된 경우가 특히 그러했습니다.

🙂 구체적인 예를 들어보겠습니다.

페이지네이션 컴포넌트의 로직을 담당하는 usePagination 이라는 hook 을 선언해야 하는 경우를 가정합니다.

"책 췤" 이전까지 저는 다음과 같이 선언했어요.

react-project
├─ ....
├─ src
│  ├─ App.tsx
│  ├─ ....
│  ├─ ....
│  │
│  ├─ components
│  │  ├─ pagination.tsx 👈 "페이지네이션-컴포넌트"
│  │  ├─ ....
│  │  └─ ....
│  ├─ constants
│  ├─ utils
│  │  ....
│  │  
│  ├─ hooks
│  │  ├─ use-pagination.ts  👈 "페이지네이션-컴포넌트 관련 hook"
│  │  ├─ ....
│     └─ ....
├─ ....
├─ ....

사실 이렇게 하는 것도 나쁘진 않습니다. components , hooks 폴더를 의미에 맞게 잘 쓰고 있는 거죠. 그래도 한편으론, 다음의 상황을 고려했던 것 같아요. 😅

 " usePagination 은 페이지네이션 컴포넌트에 완전히 종속된 hook 인데,
 두 파일이 가까이 붙어있다면 컴포넌트를 삭제하거나 수정할 때 편하지 않을까? "

마침 같이 했던 팀원분도 폴더 구조를 어떻게 구성하는 게 좋을지 고민하고 계시더라구요. 그래서 "chaeg-check" 에서는, 지금까지 서로 파일을 관리해왔던 습관(?) 들을 버리고, 컴포넌트가 중심이 되는 구조로 개편해보기로 했어요.

우리 팀이 정의한 "컴포넌트 중심 폴더 구조" 라는 것은 다음을 의미합니다.

컴포넌트 중심 폴더

react-project
├─ ....
├─ src
│  │
│  ├─ components
│  │  ├─ pagination
│  │  │  ├─ index.tsx              👈 "페이지네이션-컴포넌트(p-c) 정의"
│  │  │  ├─ pagination.type.ts     👈 "p-c 관련한 타입 관리 파일"
│  │  │  ├─ pagination.hook.ts     👈 "p-c 관련한 상태 변경 함수 관리 파일"
│  │  │  ├─ pagination.constant.ts 👈 "p-c 관련한 상수 관리 파일"
│  │  │  ├─ pagination.func.ts     👈 "p-c 관련한 일반함수 관리 파일"
│  │  │  ├─
│  │ ....
│  │  ....
│  │  
│  ├─ hooks
│  │  ├─ use-???.ts  👈 "특정 컴포넌트에 종속되지 않는 hook"
│  │  ├─ ....
│     └─ ....
├─ ....
├─ ....

위와 같이 컴포넌트의 이름으로 된 폴더 아래, {컴포넌트}.{관심사}.ts 의 규칙으로 파일을 두어, 그곳에 관심사에 대한 선언을 하도록 정했습니다.

처음 사용하는 구조인 걸 감안해서, 협업 간에 혼란이 최소화되도록 각 폴더의 barrel-file 에 가이드 주석을 남두었답니다..

[실제 pr 보러가기 🛫]
(👇 이미지 클릭하면 해당 PR 로 이동합니다.)

솔직히 말해서 굉장히 편리한 구조였습니다. 구체적으로 다음과 같은 장점을 느꼈어요.

  • 함수 혹은 변수를 선언할 위치에 대한 고민이 줄어듭니다. 👍
  • 관심사 분리가 편해져, 컴포넌트 선언부 시작줄부터 return 문까지의 거리가 짧아지는 효과가 있었습니다. 👍

단점도 적고 싶긴 하지만, 글쎄요.. 아직까지는 찾지 못했습니다.🙃
다른 프로젝트에서 타입파일 정도는 위와 같이 분리해보긴 했었지만, 이런 시도는 처음인데 생각보다 잘 정리되는 것 같아 재미도 있었어요. 다음 프로젝트에서도 팀원들에게 충분히 설득해서, 도입하고 싶다고 느꼈습니다.



3. 협업툴 & 문서화

"chaeg-check" 을 시작하기 전, 세웠던 계획 중 하나는 task 분배 및 일정관리, 코드 컨벤션과 같은 협업 문서까지 전~부 Github 에서 하는 것이었습니다.

이전 프로젝트의 경우, 문서관리는 Notion, 스프린트 관리는 Linear, 코드관리는 Github 에서 진행했어요. 각 서비스마다 유익함이 있고 실제 많은 프로젝트에서 여러 협업도구를 연동해서 사용하는 것으로 알고 있습니다.

하지만 저의 경우 협업 경험이 부족하기도 했고, Notion 과 Linear 사용하는 것에 많이 미숙했어요. 간단한 작업조차 여러 탭을 순방(?)한 뒤에 진행해야 하는 것에 피로감이 있었습니다. Github 와 연동하는 법에 대해서도 한참을 찾아다녔어요. 특히 Linear 는 한글 자료가 많이 없더라구요...(전 영어 잘 못합니다.!) 연동을 한다면 어디까지 자동화되고 편리해지는지를 몰라, 맨 처음 쓸 때는 확실히 사용 안하느니만 못했던 것 같아요..😢

시간적 여유도 없고, 코드 외적인 부분에서 오는 스트레스를 줄이기 위해선 익숙한 환경 위주로 작업하는 게 좋겠다고 판단했습니다. Github 는 모든 팀원이 매일 꼭 한번씩은 들리는 곳이기도 하고, 협업을 위해 제공하는 기능 역시 많이 있어요:) 저희 팀은 Github-Wiki 를 이용해서 코드컨벤션 등 팀 규칙이나 설계 문서를 공유했고, Github-Projects 를 통해 backlogging 과 sprint 관리를 진행했습니다.

기능이 단순하고, Github 와 관련해선 공유된 자료가 많아 러닝커브가 거의 없었습니다. issue 관리도 편리했어요. backlogging 했던 작업들이 바로바로 issue로 전환되는 게 보이니 좋았습니다 🙂

사실 각 작업의 일정을 제대로 설정하지 않는 등 똑바르게 사용했다고 말할 순 없어요.. 그래도 "작업" 단위 설정하고 구체적인 설명을 남기어 팀원들에게 분배하니, 처음으로 그럴싸한 협업을 진행했던 것 같습니다. 사실 다른 서비스도 기능은 거의 비슷하다고 하니까, 이번 경험을 바탕으로 다른 협업툴에도 조~금씩 도전해서 익숙해져보려 합니다.

profile
😎👍

0개의 댓글