9월 한 달 동안은 프로그래머스 데브코스에서 팀 프로젝트를 진행했다.
사실 요즘 약간의 후유증을 겪고 있는데 (감기
/번아웃
/날짜 개념 상실
/붕 떠버린 기분
) , 그 정도로 흠뻑 빠져들어서 한 달을 보냈다고 생각한다. 지금은 마치 바다에 잠겼다가 건짐 당한 기분이다. 🌊
하지만 바다에서 나만 건져지면 안되기 때문에 ... 구현 하면서 겪었던 상황들을 회상하며, 이번 프로젝트에서 얻어간 것들을 기록해보고자 한다. 상당히 긴 회고가 될 듯
팀 프로젝트를 위해 기본적인 SNS 기능 (포스트, 팔로우, 댓글, 좋아요 등등) 이 있는 API 가 제공 되었고, 이 친구를 활용하여 기능을 구상하고 개발하는 형식이었다.
팀끼리 아이디어를 공유하고 선정하기 전, 내가 어떤 서비스를 만들고 싶은지 생각을 많이 했었다. 전공 수업 때 기획 단계를 진행하는 과제들이 많았기 때문에, 이러한 경험들로 부터 많은 도움을 받을 수 있었다.
나만의 기준을 몇가지 세운 바로는 다음과 같았다.
이름 바꿔치기 서비스 X
오늘 메뉴 올리는 sns
일기 쓰는 sns
당장 사용할 수 있는 사용자가 많아야 하며, 꾸준히 피드백을 받을 수 있어야 함.
애니메이션 정보 공유 서비스
! 이 친구의 사용자는 애니메이션을 많이 보는 사람들이며, 서비스가 활성화 되려면 해당 사용자 층에게 이 서비스가 도달될 때 까지 기다려야 한다.이러한 기준들로 나온 아이디어가 밸런스 게임 커뮤니티
이다.
팀원 들의 좋은 아이디어가 여러가지가 있었는데, 투표로 나의 아이디어가 채택되어 기쁨 반, 책임감 반을 지닌 채 본격적인 프로젝트를 시작했다 !
사실, 기본적인 필드만 있는 API 를 그대로 써서는 밸런스 포스트를 구현할 수 없다.
밸런스 게임에 참여하는 댓글 기능 역시, 사용자가 고른 항목에 대한 정보를 담고 있어야 한다.
밸런스 포스트를 위해서는, 한 줄 설명 + 항목1 + 항목2
총 3개의 필드가 필요하다.
댓글을 위해서는 선택한 항목 + 한 줄 의견
이 필요하다.
물론, JSON 을 파싱하고 문자열화 해서 데이터가 들어가고 나오는 것이기 때문에, 해당 필드가 있는 것 처럼 커스텀 할 수도 있었다. 하지만 문자열 검색 기능이 걸리기도 하고, 억지로 끼워 넣는 필드가 3개나 되기 때문에 복잡해 진다고 생각했다. 앙골라 프로젝트에게 맞는 다른 방법을 생각하고 싶었다!
이전의 개인 노션 프로젝트에서, 마치 이모지 필드가 있는 것 처럼 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 를 구성했다.
react-router 라이브러리를 사용하긴 하지만, 각 페이지마다 알고 있어야 하는 parameter 와 query string 이 다르고 많다.
이러한 정보들을 각각의 페이지 안에서 올려다 보는 구조 보다는, route 꼭대기에 있는 Main 친구가 각 페이지들에게 prop 으로 뿌려주는 구조가 수정하기 더 편할 것이라 생각했다.
또한, 페이지 컴포넌트들은 어떤 것이 쿼리스트링이고 어떤 것이 파라미터인지 굳이 알 필요 없이, 그 안의 필요한 정보만 알면 되지 않나? 라는 생각이 들었다.
따라서 현재 페이지 주소로 부터 필요한 정보들을 가져오는 useCurrentPage 훅을 구현하여, Main 에서 하위 페이지 컴포넌트 들에게 주입시켜주는 방식을 구상해 보았다.
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: '/' }
Main 에게, 또는 필요한 다른 페이지들에게 현재 페이지의 라우트 주소 정보를 알려주기 위한 훅이다.
리액트 라우터의 useMatch(path) 훅을 사용하여, routes 배열 중 현재 페이지의 path 가 일치하는 요소가 있다면 해당 페이지 정보를 반환하도록 하였다.
예를 들어, 현재 페이지가 /search/post?sort=like
라면 아래와 같은 객체를 반환한다.
{
name: string, // routes 배열에 있는 정보
title: string, // routes 배열에 있는 정보
params: { target: 'post' }, // 주소의 파라미터
search: { sort: 'like' } // 주소의 쿼리스트링
}
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 배열에 데이터 등록만 함으로써 각자 구현한 페이지를 추가할 수 있었다. 뿌듯!