[회고] 데브코스 팀 프로젝트 회고 #1 -Angola!

차차·2023년 10월 3일
4
post-thumbnail

🫥 우당탕탕 9월

9월 한 달 동안은 프로그래머스 데브코스에서 팀 프로젝트를 진행했다.

사실 요즘 약간의 후유증을 겪고 있는데 (감기/번아웃/날짜 개념 상실/붕 떠버린 기분) , 그 정도로 흠뻑 빠져들어서 한 달을 보냈다고 생각한다. 지금은 마치 바다에 잠겼다가 건짐 당한 기분이다. 🌊

하지만 바다에서 나만 건져지면 안되기 때문에 ... 구현 하면서 겪었던 상황들을 회상하며, 이번 프로젝트에서 얻어간 것들을 기록해보고자 한다. 상당히 긴 회고가 될 듯



🚩 프로젝트 시작

팀 프로젝트를 위해 기본적인 SNS 기능 (포스트, 팔로우, 댓글, 좋아요 등등) 이 있는 API 가 제공 되었고, 이 친구를 활용하여 기능을 구상하고 개발하는 형식이었다.

팀끼리 아이디어를 공유하고 선정하기 전, 내가 어떤 서비스를 만들고 싶은지 생각을 많이 했었다. 전공 수업 때 기획 단계를 진행하는 과제들이 많았기 때문에, 이러한 경험들로 부터 많은 도움을 받을 수 있었다.

나만의 기준을 몇가지 세운 바로는 다음과 같았다.

이름 바꿔치기 서비스 X

  • SNS 기반 서비스 이긴 하지만, 주제만 바꾸면 다른 서비스로 대체될 수 있는 프로젝트는 지양하고 싶었다.
  • 간단한 예를 들면, 오늘 메뉴 올리는 sns 일기 쓰는 sns
    이런 친구들은 주제는 다르지만 결국 똑같은 기능을 가지게 된다고 생각했다. 사용자가 어떤 행동을 하느냐에 따라서 서비스 정체성이 아예 바뀌어버릴 수 있다.

당장 사용할 수 있는 사용자가 많아야 하며, 꾸준히 피드백을 받을 수 있어야 함.

  • 한가지 테마로 너무 귀결되다 보면 당장 피드백을 받을 수 있는 사용자 층도 적어진다. 예시로 애니메이션 정보 공유 서비스! 이 친구의 사용자는 애니메이션을 많이 보는 사람들이며, 서비스가 활성화 되려면 해당 사용자 층에게 이 서비스가 도달될 때 까지 기다려야 한다.
  • 더 나아가서, 나와 우리 팀을 포함하여 데브코스 사람들이 사용자가 될 수 있어야 한다고 생각했다.

이러한 기준들로 나온 아이디어가 밸런스 게임 커뮤니티 이다.
팀원 들의 좋은 아이디어가 여러가지가 있었는데, 투표로 나의 아이디어가 채택되어 기쁨 반, 책임감 반을 지닌 채 본격적인 프로젝트를 시작했다 !



📒 API 활용하기

사실, 기본적인 필드만 있는 API 를 그대로 써서는 밸런스 포스트를 구현할 수 없다.
밸런스 게임에 참여하는 댓글 기능 역시, 사용자가 고른 항목에 대한 정보를 담고 있어야 한다.

밸런스 포스트를 위해서는, 한 줄 설명 + 항목1 + 항목2 총 3개의 필드가 필요하다.
댓글을 위해서는 선택한 항목 + 한 줄 의견 이 필요하다.

물론, JSON 을 파싱하고 문자열화 해서 데이터가 들어가고 나오는 것이기 때문에, 해당 필드가 있는 것 처럼 커스텀 할 수도 있었다. 하지만 문자열 검색 기능이 걸리기도 하고, 억지로 끼워 넣는 필드가 3개나 되기 때문에 복잡해 진다고 생각했다. 앙골라 프로젝트에게 맞는 다른 방법을 생각하고 싶었다!

split & join

이전의 개인 노션 프로젝트에서, 마치 이모지 필드가 있는 것 처럼 API 의 title 필드를 활용했던 기억이 어렴풋이 났다. 사용자는 데이터를 따로 작성하고, 클라이언트에서 합쳐서 보내고, 화면에 렌더링할 때는 다시 해체해서 보여주는 방법이다.

매우매우 특이한.. 키보드 입력으로는 불가능한.. 식별자 문자열을 사용하여, post 요청을 할 때는 join 을 거쳐서 보내고 get 요청을 한 후에 split 해서 받았다. 사용자 입장에서는 포스트 작성과 보기가 의도대로 작동하고, 개발자 입장에서는 함수 하나만 감싸서 데이터를 요청하고 받을 수 있도록 하였다.

const joinDataBySeparator = (...targets: string[]): string => {
	return targets
		.map((target) => target.replace(ANGOLA_SEPARATOR, ''))
		.join(ANGOLA_SEPARATOR);
};
const splitPostBySeparator = (target: string): PostData => {
	const [title, a, b] = target.split(ANGOLA_SEPARATOR);
	return { title, a, b };
};
const splitCommentBySeparator = (target: string): CommentData => {
	const [vote, comment] = target.split(ANGOLA_SEPARATOR);
	return comment ? { vote, comment } : { vote };
};

나중에 이런 방법을 또 쓰게 될까 해서.. 구현 했던 함수들 기록하기 🧐 (지금 보니 예외처리가 쫌 필요해 보인다)

이러한 과정들을 통해

한계라고 느껴지는 것들을 바로 수용하기 보다는, 머리 속에 있는 지식들을 활용해서 해결할 수 있는 방법에 대해 고민해야 한다는 점을 다시 깨달았다. 잠깐이라도 짱구를 더 굴려보자! 어려운 일이 아닐 수도 있다!



⚙️ Route 구성하기

데이터를 기반으로 하는 서비스이며, 브라우저의 새로고침+뒤로가기와 같은 히스토리 동작이 원활하게 작동하려면 데이터에 대한 정보, 현재 페이지에서 어떤 일을 하는 지는 주소창이 어느정도 알고 있어야 했다.

따라서 팀원들과 함께 상의하여 아래와 같이 route 를 구성했다.

react-router 라이브러리를 사용하긴 하지만, 각 페이지마다 알고 있어야 하는 parameter 와 query string 이 다르고 많다.

이러한 정보들을 각각의 페이지 안에서 올려다 보는 구조 보다는, route 꼭대기에 있는 Main 친구가 각 페이지들에게 prop 으로 뿌려주는 구조가 수정하기 더 편할 것이라 생각했다.

또한, 페이지 컴포넌트들은 어떤 것이 쿼리스트링이고 어떤 것이 파라미터인지 굳이 알 필요 없이, 그 안의 필요한 정보만 알면 되지 않나? 라는 생각이 들었다.

따라서 현재 페이지 주소로 부터 필요한 정보들을 가져오는 useCurrentPage 훅을 구현하여, Main 에서 하위 페이지 컴포넌트 들에게 주입시켜주는 방식을 구상해 보았다.

routes & redirects 배열

routes 는 라우트 페이지들의 정보를 담고 있으며, redirects 는 리다이렉트 처리해야 하는 주소들을 담고 있다.

아래와 같이 path, name, title, component, authRequired 프로퍼티를 적어주면 된다.

  • path : Route의 path에 해당
  • name : route 고유 식별자 역할
  • title : Header 에 보여야 하는 페이지 제목
  • component : Route의 element에 해당
  • authRequired : 로그인 필요한 지 정보
// routes 배열 요소
{
  path: '/',
  name: 'home',
  title: 'ANGOLA',
  component: HomePage,
  authRequired: false,
}

// redirects 배열 요소
{ from: '/user', to: '/' }

useCurrentPage Hook

Main 에게, 또는 필요한 다른 페이지들에게 현재 페이지의 라우트 주소 정보를 알려주기 위한 훅이다.

리액트 라우터의 useMatch(path) 훅을 사용하여, routes 배열 중 현재 페이지의 path 가 일치하는 요소가 있다면 해당 페이지 정보를 반환하도록 하였다.

예를 들어, 현재 페이지가 /search/post?sort=like 라면 아래와 같은 객체를 반환한다.

{
  name: string,               // routes 배열에 있는 정보
  title: string,              // routes 배열에 있는 정보
  params: { target: 'post' }, // 주소의 파라미터
  search: { sort: 'like' }    // 주소의 쿼리스트링
}

Main

Main 에서는 useCurrentPage 에서 받은 정보들로 Route 들을 구성한다.
각 페이지들은 parameter 와 query string 을 따로 받아올 필요 없이, prop 으로 받을 수 있다.

예를 들어 Search 페이지 컴포넌트의 경우, 무엇을 검색하는 지 + 정렬 기준이 무엇인 지에 대한 정보인 target 과 sort 를 직접 prop 으로 받는 것이다.

const Main = () => {
  const { title, name, params, search } = useCurrentPage();
  
	// ...

  return 
      // ...
        <Routes>

					// routes 배열을 순환하여 Route 생성

          {routes.map(({ path, name, component, authRequired }) => (
            <Route
              key={name}
              path={path}
              element={
                authRequired && !auth ? (
                  <Navigate to="/login" />
                ) : (
                  React.createElement(component, {
                    ...params,
                    ...search,
                  })
                )
              }></Route>
          ))}

					// redirects 배열을 순환하며 Route 생성

          {redirects.map(({ from, to }) => (
            <Route
              key={from + to}
              path={from}
              element={<Navigate to={to} />}
            />
          ))}

        </Routes>
			// ...
  );
};

결과

새로운 페이지를 작성해야 하거나 새로운 리다이렉트 처리를 해야할 때, Route 가 구현되어 있는 컴포넌트를 직접 건드려야 하는 일이 없어졌다.

변경 사항이 있을 경우, routes 디렉터리에 있는 routes & redirects 배열 데이터만 건드려주면 되기 때문이다.실제로 변경사항이 있었는데, 바로 로그인 리다이렉트 처리였다.

로그인을 안하고 직접 페이지에 접근했을 때 로그인 페이지로 이동시켜주어야 한다는 것을 다소 늦게 발견했다. 하지만 리다이렉트가 필요한 페이지도 있고, 필요하지 않은 페이지도 있었다.
따라서 route 배열 요소에 authRequired 를 추가하고, 이 프로퍼티를 활용해서 쉽게 해결할 수 있었다.

협업할 때에도, 팀원들도 매번 Main을 수정하지 않고 routes 배열에 데이터 등록만 함으로써 각자 구현한 페이지를 추가할 수 있었다. 뿌듯!



2편 바로가기

0개의 댓글