깃허브
프로젝트를 기획하게 된 계기는 정말 단순했다.
사람은 누구나 귀여움에 약해요 !
지금 생각해봐도 행복한 웃음이 나온다.
실전 프로젝트를 시작하기 전에, 그러니까 자기객관화가 조금 부족했던 그 때.
나는 욕심이 정말 많았다.
6주라는 시간동안 '나' 라는 개발자의 성장을 보여줄 수 있는 방법에는 어떤 것이 있을까 ?
타입스크립트나 리코일 도입, web RTC, 소켓을 이용한 실시간 통신,
인터렉티브 모션을 적용한 멋진 뷰 등등 정말 여러가지 프로젝트를 생각했던 것 같다.
이러한 고민은 오히려 스트레스로 다가왔다.
조금 더 잘해야 돼. 더 보여줘야 돼. 하는 생각때문에.
고민과 스트레스는 길지 않았는데,
결국 해답은 우리가 즐거운 프로젝트를 만들자! 였다.
물론 새로운 배움과 기술을 적용해 멋진 프로젝트를 만드는 것도 중요하다.
하지만 이번 프로젝트가 우리의 졸업은 아니었으니까.
제대로 된 기본기도 없이 이것저것 배워서 부랴부랴 개발하는 것 보다
우리가 지금까지 배운 기술들을 잘 갈무리해서 즐거운 프로젝트를 만드는 시간이라고 생각했다.
단지 포트폴리오를 위한 개발을 하는건 적어도 지금 당장은 하고싶지 않았다.
우리 조 모두는 귀여움에 약했다.
귀여움을 어떻게 하면 우리의 프로젝트에 녹여낼 수 있을까 고민하다가.
일기장에 관한 아이디어가 떠올랐다.
개인적인 일상을 기록할 수 있는 일기장에서 한 발자국 더 나아가,
귀여운 테마의 그림일기를 만들어보자.
거기서 한 발자국 더 나아가서 친구나 연인끼리 특별한 순간과 행복한 시간을 공유하면서
언제 어디서든 추억을 꺼내볼 수 있는 서비스를 만들어보자.
기획이 확정되었을 때 분위기가 생생하게 기억나는데,
모두들 행복한 웃음을 지으면서 신나서 이런게 좋지 않을까요, 저런게 좋지 않을까요 하며
아이디어를 내곤 했었다.
사실 프로젝트 자체보다 조원 분들이 조금 더 귀여웠던 것 같다.
캔버스에 관한 고민을 프로젝트 초기에 가장 많이했던 것 같다.
정말로 빈말이 아니고 일주일동안 관련 레퍼런스만 하루에 열 시간 이상 확인하면서
직접 만들어보고 비교해보면서 어떻게 구현하는 것이 가장 우리의 프로젝트에 맞을지 고민했다.
처음에는 자바스크립트 canvas API로 구현했었다.
간단한 도형을 그리거나 선 굵기, 색상변경 등은 어렵지 않았지만
도형을 선택하거나, 이동시키거나, 회전시키는 등의 로직은
개발적인 이해 뿐 아니라 어려운 수학적인 로직이 많이 들어가는 작업이었다.
또한, 캔버스의 어려운 로직들이 브라우저의 추가적인 연산을 필요로 하는 작업이 많았다.
예를들어, 그림을 이동시키는 로직의 경우
'전에 그렸던 그림을 지우고 새로운 장소에 렌더링한다'
같은 방식으로 로직을 구성한다.
따라서 성능 면에서도 좋지 못했다.
토론 끝에 이러한 기능들을 자체적으로 제공하면서
성능의 최적화도 라이브러리 자체에서 지원해주는 Fabric js 를 이용해 캔버스를 구현했다.
해당 라이브러리가 리액트에서 갖는 조금 큰 단점이 있는데,
React Strict Mode 에서 그림 객체 선택기능을 오류로 간주하여
작동하지 않는 문제가 있다. stackoverflow 참고
Redux toolkit 을 항해기간 내내 사용해 가장 익숙하고 다루기 쉬운 상태관리 도구이지만,
서버사이드의 상태관리를 하기위해 thunk를 사용할 때, 에러처리나 로딩 뷰 등을 위해
불가피하게 반복되는 코드가 많았고 React query를 도입해보자는 의견이 나왔다.
기술 자체의 러닝커브가 낮은 편이고 데이터의 캐싱을 통해
서버의 부담도 줄여줄 수 있다고 판단했기 때문에 적극적으로 나서서 도입을 종용했다.
공식문서가 친절하게 잘 나와있는 것도 기술도입에 중요하게 작용했다.
정말 놀랄만큼 보일러 플레이트가 획기적으로 줄어들었고
개발자도구도 직관적으로 되어있어 개발을 진행할 때 상당히 편했다.
어느정도로 편했느냐고 하면 모든 상태관리를 리액트 쿼리로 할 수 있지 않을까 하는 생각까지 하게 됐다.
프로젝트 발표 이후에 궁금해서 리액트 쿼리로 클라이언트 상태관리를 하는 방법을 다시 찾아봤다.
여러가지 실험을 통해 카운터 어플리케이션을 만들어봤다.
참고한 링크
import { useQuery, useQueryClient } from "@tanstack/react-query";
export const useSetValue = (key) => {
const queryClient = useQueryClient();
return (state) => queryClient.setQueryData(key, state);
};
export const useGetValue = (key, initialData) => {
return useQuery(key, {
initialData,
staleTime: Infinity,
}).data;
};
다른 전역 상태관리 라이브러리 처럼 사용할 수 있도록 custom hook을 만들었다
staleTime을 infinity로 설정한건 클라이언트의 데이터는 재설정할 때 까지 stale 되지 않기 때문이다.
import { useState } from "react";
import { useGetValue, useSetValue } from "../hooks/queryHooks";
const Counter = () => {
const [initValue, setInitValue] = useState(0);
// initValue 를 onChange로 받아올 state를 설정
const setValue = useSetValue(["num"]);
// Value를 Set 할 함수 설정
const num = parseInt(useGetValue(["num"], ""), 10);
// num key의 value를 가져오는 함수, react query가 데이터를 string 으로
// 변환해 저장하기 때문에 parseInt 하는 과정이 필요했다.
// 이 부분은 확실히 불편한점
const setNumHandler = () => {
if (isNaN(initValue)) return;
setValue(initValue);
};
// 입력한 initValue가 숫자가 아니면 함수를 종료.
const plusNumHandler = () => {
setValue(num + 1);
};
const minusNumhandler = () => {
setValue(num - 1);
};
return (
<>
<button onClick={plusNumHandler}>+1</button>
<button onClick={minusNumhandler}>-1</button>
<input
value={initValue}
onChange={({ target: { value } }) => setInitValue(value)}
// 구조 분해 할당으로 target value를 직접 set 함수에 전달
></input>
<button onClick={setNumHandler}>init</button>
</>
);
};
export default Counter;
완벽하진 않지만, 충분히 클라이언트 사이드 상태관리도 가능했다.
그러나 사실 이에 대해 여러가지 레퍼런스를 찾아보면
React Query는 데이터 관리에 중점을 두기 때문에
UI 상태 또는 양식 상태와 같은 애플리케이션의 다른 유형의 상태를 관리하는 기능을
제공하지 않는다고 한다.
이전의 실험에서는 이러한 이유 때문에 관련 로직을 구현하는 리소스가 커서
가벼운 클라이언트 전역 상태관리 라이브러리를 사용하는 것이 바람직하다는 결과를 도출했었다.
툴킷 이외에도 여러가지 상태관리 라이브러리를 사용해보면 조금 더 명확하게
취사선택 할 수 있을 것 같다.
우리의 서비스가 완성되고 가장 먼저 시도해봤던건 lightHouse 성능 측정 도구였다.
불러올 그림일기 데이터가 많아지거나 캔버스에 이미지 데이터를 가득 담으면
딜레이가 일어나는 현상이 있었기 때문이다.
서비스의 모토가 소중한 시간을 공유하고 나눌 수 있는 서비스였는데,
앱을 사용하는 사용자의 경험이 나쁘면 정말 큰일이다.
1차 시도
처음으로 시도했던 방법은 메모이제이션 작업이다.
React.memo로 상위 컴포넌트를 래핑하면 상태가 변경됐을 때만
재렌더링을 하기 때문에 불필요한 재렌더링을 방지할 수 있다.
수시로 업데이트 되지 않는 복잡한 컴포넌트를 최적화 할 수 있는 기능이다.
useCallback, useMemo 도 비슷한 역할을 하여 성능에 도움을 줄 수 있을거라고 기대했다.
추가적으로는 컴포넌트를 간단하게 분리했다.
자주 쓰이는 모달 컴포넌트, 버튼, 인풋 컴포넌트를 분리하고
함수들을 훅으로 정리하는 리펙토링 작업을 간단하게 진행했다.
메모이제이션 작업 이후
최적화 작업을 진행했음에도 눈에 띄는 성능 최적화를 이루지 못했는데,
팀 내부적으로 bundle.js의 크기가 비정상적으로 크다는 점에 주목했다.
관련 레퍼런스를 검색하다 보니 코드 스플리팅이라는 키워드를 얻을 수 있었다.
코드 스플리팅은 전체 애플리케이션의 모든 코드를 한번에 로드하는 대신,
사용자가 반드시 필요한 코드만 일단 로드하고 나중에 필요한 코드들을 로드하는
기술이다. 로드 시간이 단축되고 성능 향상을 얻을 수 있다고 한다.
여러가지 코드 스플리팅 방법이 있지만 우리 팀은 Suspense, React.lazy 를 사용했다.
리액트 라이브러리 자체적으로 지원하며 추가적인 라이브러리 설치가 필요 없으며
간단한 키워드로 진행할 수 있기 때문이었다.
단, 애플리케이션 전체에서 사용하는 컴포넌트는 React.lazy를 사용하지 않는 것이
바람직 하다. 캐싱 데이터의 비교 작업을 진행하기 때문이라고 한다.
그 비교작업은 당연하지만 리소스가 필요하다.
이번 프로젝트에서는 실험적으로 전체적으로 적용해보았다.
코드 스플리팅 이후
결과적으로는 눈에 띄는 성능 향상을 얻을 수 있었다.
버벅이는 현상도 확연하게 줄어든 것을 경험할 수 있었다.
우리 조에서 내가 제일 귀여움에 약하다.
개발을 진행하면서 무럭무럭 완성되는 프로젝트를 보며
너무 행복했고 여러가지 기술적인 시도와 많은 시행착오를 겪을 수 있어서 즐거웠다.
또한 마음이 맞는 팀원들과 6주간 즐겁게 개발할 수 있었다.
개발을 모두 끝내고 유저테스트를 진행할 때, 내심 조금 걱정했다.
그런데 많은 분들이 너무 귀엽다고, 재밌는 프로젝트라고 칭찬해주셨고,
자식을 낳아본 적은 없지만 내 자식이 어디서 칭찬을 듣고 온 것 처럼 뿌듯했다.
포트폴리오를 잘 만들어야 된다는 목적의식 때문에,
처음에 생각했던 것 처럼 그냥 이것저것 기술을 막 우겨넣은 프로젝트를 진행했더라면
물론 그것도 그것 나름대로 재밌었겠지만,
가장 만들고 싶었던 귀여운 프로젝트를 멋지게 만들었고
아주 대단하진 않지만 사용자들에게 소소하게 행복을 줄 수 있는
그런 애플리케이션을 만들었다는 건,
아마도 지금까지 살면서 했던 경험중에 가장 행복한 경험이 아니었을까 생각한다.
비록 취직을 하게되고 개발을 업으로 하게 되면 내가 하고싶은 개발만 할 수 있는건 아닐것이다.
다만, 이 소중하고 행복했던 경험을 잊지 않고
그저 일 이라서가 아니라 정말 행복해서 개발을 하고싶다.
감정적인 부분을 제외한다면 아쉬웠던 점도 굉장히 많았다.
우선, 자바스크립트의 기본기와 신문법에 대한 이해가 아직 부족하다는 점.
필터 기능을 구현하면서 어떻게 하면 로직을 최소화 하면서
부드럽게 동작하는 함수를 만들 수 있을까 굉장히 많이 고민했다.
필터기능을 구현할 때, 해당 기능을 백엔드 API로 구현하는 방법도 고려되었으나,
내가 쓴 일기장을 필터링 하는 기능인데 필터를 바꿀 때 마다 API 요청을 하는 방식이
부자연스럽다고 생각했고, 필터를 바꿀 때 마다 로딩화면을 띄우는 것 보다
리액트 쿼리의 캐싱 기능을 고려해 캐싱된 데이터를 화면만 바꾸어 보여주는 것이
조금 더 사용자 입장에서 자연스러울거라고 판단했다.
따라서, 시간복잡도를 최소화 하면서 많은 일기들을 부드럽게 바꾸어 보여줄 수 있는
로직이 필요했다. 이 기능에서 가장 어려웠던 부분은
같은 날 작성한 일기는 같은 날짜로 묶어주는 부분이었다.
완성한 로직은 다음과 같다.
const orderPostsByDate = (data) => {
// data를 date를 기준으로 정렬한다.
const orderedPosts = {};
// 빈 객체를 정의
if (!isLoading) {
// useQuery로 불러온 일기의 로딩이 끝나면
data.forEach((item) => {
// data에 forEach 로직을 수행.
const temp = item.createdAt.slice(0, 10);
// data의 createdAt을 묶어주기 위해 slice로 잘라 정의.
// 형식은 YYYY-MM-DD
if (orderedPosts[temp]) {
// 정의한 객체에 key 값을 기준으로 일치하는 원소가 있다면,
orderedPosts[temp].push(item);
// item을 해당 key 값의 원소로 push
} else {
orderedPosts[temp] = [item];
// 그렇지 않다면 해당 key 값을 가지는 원소를 새로 생성
}
});
}
return orderedPosts;
// 정렬된 객체를 반환
};
단순히 날짜를 기준으로 정렬하는 로직을 짜는데 꼬박 반나절이 걸렸다.
알고리즘 문제를 꾸준히 연습했다면 오래 걸리지 않았을 것 같다는 생각을 그 당시에도,
지금도 하고 있다.
프론트엔드 개발자도 자료구조나 알고리즘에 대한 기본적인 이해는 있어야 한다는 이야기를
들어본 적 있다. 실천은 못했지만.
이제는 좀 실천을 할 때가 됐다.
두번째로, 클린코드에 대한 고민이다.
코드를 짜는 방법은 정말 여러가지가 있으며 정답은 개발자 스스로 찾아야 된다는 말에 동의한다.
그러나, 통용적인 정답은 있음에 틀림없다.
정말 훌륭한 코드들을 보고있으면 마치 귀여운 고양이나 강아지를 본 것 처럼
기분이 좋다. 내 코드는 조금 덜 그렇던데.
이번 프로젝트에서 그런 고민을 굉장히 많이 했음에도 불구하고,
코드가 너무 길어지다 보니까 무뎌지게 되었다.
그런 부분은 가장 아쉬웠던 점이라고 할 수 있겠다.
마지막으로는 역시 리액트 숙련도에 관한 부분이다.
코드 스플리팅이나 메모이제이션 같은 개념을 알고는 있었지만
실제로 어느 부분에, 어느 순간에 적용해야 하는지 알지 못했다.
또한 어떤 코드가 유지보수가 가능하고 재사용이 가능한 코드인지,
이 부분에서 렌더링이 왜 일어나는지,
렌더링이 자주 일어난다고 해서 항상 좋지 않은 코드인지,
프로젝트가 끝나고 나서도 많이 고민하는 문제다.
그밖에도, 숙련도가 부족해서 온전하게 사용하지 못한 리액트 쿼리 라던가,
리덕스로 어떤 부분을 전역 상태 관리 해야할 지,
어떤식으로 컴포넌트를 분리하는게 효과적일지 정답을 찾지 못했다.
그러나, 방향성은 알 수 있었다.
꾸준히 고민할 문제겠지만 여전히 즐거울 것 같다.
가장 오랜기간 개발했고
처음으로 유저들에게 테스트와 피드백을 받아봤고
그만큼 행복했던 프로젝트여서 회고도 조금 길어지게 됐다.
그래서 부랴부랴 끝내는 걸지도 모르겠다.
끝으로는, 함께 개발을 진행하면서 행복을 공유했던 모든 분들께 정말 감사드리고
다들 행복하세욥 🥰