연합프로젝트 회고
24년 1월 ~ 2월, CNU, Parrot, Release가 연합해서 진행한 프로젝트에 참여했다.
나는 프론트 부분으로 참여해 인공지능(CV) 2명 + 백엔드 2명 + 프론트엔드 나혼자 이렇게 5명이서 프로젝트를 진행했다.
사실상 처음해보는 협업이었기 때문에 어려운 점들도 많았지만, 그래도 배운게 많은 프로젝트였다.
기억은 사라지지만 기록은 영원하다. 나의 프로젝트의 노고가 사라지지 않길 바라며, 처음부터 훑어보며 기록을 남긴다.
주제 선정
프로젝트에는 디자이너, 기획자가 필요하다. 하지만 학교 개발동아리 특성 상 있을 수 없기 때문에, 개발자들끼리 선정해야 했다.
가장 먼저 한 것은 주제 선정이었다. 이제 곧 새학기가 시작되며 새내기들이 들어오고, 학교 사람들끼리 진행하는 프로젝트이기 때문에 학교 새내기와 관련된 프로젝트로 범위를 좁혔다.
뻔선 뻔후 시스템, 열품타, 강의계획 찾아보기, 학교 근처 맛집대결, 서강 테스트, 일기 분석, 블박, 공부 감시 등등 많은 의견이 나왔고 우리가 선정한 것은 밥 친구 찾기 플랫폼이였다.
우리가 착안했던? 참고한? 것은 환승연애, 썸원이였다. 말이 맞지않다고 생각할 수 있지만, 우리는 소개팅보다는 정말 '밥 친구' 찾기 플랫폼을 만들고 싶었기 때문에, 성별을 공개하지 않고, 전화번호는 입력받지 않고, 학교이에일 인증과 탈퇴불가능을 통해 재가입 불가, 신고기능 등을 통해 친구찾기에 집중할 수 있게 노력했다.
그리고 나서 간단하게 줄기를 잡고 프로젝트를 시작했다. 디자이너가 없기 때문에.. 디자인도 프론트엔드인 나의 몫이였다.
그래도 이미 한 번 피그마를 다뤄 본 경험이 있기 때문에, 빠르게 제작할 수 있었다.
차크라 UI 사용
우리가 생각했던 서비스가 생각보다 방대했다. 가이드페이지, 로그인 및 회원가입, 그리고 3개의 메인페이지를 만들어야 했기 때문에 생각보다 컴포넌트를 만드는 데 시간이 소요될 것 같았다.
그동안 내가 혼자 해왔던 프로젝트는 다 프론트 단에서 끝이났다. 무슨 소리냐 하면 깃에 commit해가며 만들기, 서버와 api 통신하기 등은 전혀 신경쓰지 않고 만들었다.
그리고 확장성 있는 컴포넌트 만들기는 이미 해본 경험이 있기 때문에 이번에는 내가 해보지 않았던 다른 것들에 집중하고 싶어서 컴포넌트는 이미 만들어져 있는 것을 사용하자! 결정했다. 그렇게 해서 찾은 차크라UI. 버튼, input, 스피너, 카드 등 대부분의 컴포넌트가 만들어져 있었기 때문에 보다 편하게 스켈레톤 페이지를 구현했다.
피그마에서도 차크라UI 와이어프레임이 있어서 도움이 되었다.
걸음마 떼기도 공부가 필요하다
매번 cra를 사용하면서 그 전에 어떤 일 들이 벌어지는 지 알 턱이 없었다. next를 사용한다 하더라도 초기세팅은 알 필요가 있었기 때문에, 리액트만을 사용해 프로젝트를 해결하기로 결정했다.
WebPack
웹팩은 번들러다. 파일이 커지면 빌드하는데 오랜 시간이 걸린다. 번들러는 파일들을 하나로 압축시켜주고, 이 과정을 번들링이라고 한다.
Babel은 사용하지 않았다. babel이 해결해주는 것은 크게 ES6->ES5 로 변경시켜주는 크로스브라우징, 그리고 구형 js에서 최신 js의 문법 및 함수를 사용할 수 있게 도와주는 폴리필이다. 하지만 대부분의 모던 브라우저는 ES6를 사용하므로, 큰 용량이 필요한 Babel을 사용하지 않았다.
요즘 많이 사용하는 vite를 사용하지 않았던 이유는 vite가 배포환경에서는 아직 불안정한 면이 있어서 rollup을 사용해 추가 설정이 필요하다고 해서 오류날 확률이 적은 webpack을 사용했다. 더군다나 나는 webpack이 느린걸 경험해보지 않았기 때문이다.
회고를 쓰는 이 시점에서는, webpack이 왜 느리다고 하는지 깨닫게 되었다. 혼자 한 프로젝트인데도 최종발표날 쯤이 되니 개발환경에서 빌드되는데 6초가 넘게 걸렸다. 협업이였다면 10초가 넘었을 거 같고, 그러면 개발하는데 상당한 딜레이가 걸릴 것 같다.
WebPack을 설명해줘
번들링 도구이다. entry를 통해 진입하고, 결과물을 output으로 출력한다. webpack은 js밖에 읽지 못한다. 따라서 loader로 ts, css를 읽을 수 있게 해줘야 한다. 마지막으로 plugin으로 결과물을 추가로 처리 해준다.
Vite는 뭐야?
esbuild를 사용해 사전 번들링이 가능하다. 추가적인 loader도 필요 없다. 하지만 배포과정에서 불완전하고, 트리쉐이킹, 코드스플리팅이 최적화되어있지 않다고 한다.
Formatter
eslint와 Prettier를 사용했다. 아직까지 나만의 컨벤션을 정한 것은 없다. 그냥 유동적으로 사용해도 문제 없을 거 같다.
rules: {
semi: ["error", "always"], // 세미콜론을 항상 사용하도록 설정
"comma-dangle": ["error", "always-multiline"], // 마지막 콤마를 항상 허용
},
extends: ["airbnb-base", "prettier"], //충돌방지
다만 rules,extends
정도는 기억하면 좋을 거 같다.
loader
file, ts loader를 사용했다. 위에서 말했듯 webpack은 js밖에 못 읽어서 이것들이 필요했다.
다만, 이거 하나로 file을 정확하게 읽을 수 있는 것은 아니다.
한참 헤매다가 깨달았는데, types.d.ts
파일에 jpg,png
등 이미지 파일을 정의해야 import 과정에서 any혹은 못 읽는 불상사가 생기지 않는다.
d.ts 파일
type을 일반적으로 정의하는 것과 동일하다. 하지만 ts를 컴파일하면 js가 되어 더미 파일이 하나 생기는 샘이고, import문을 추가로 사용해야 하기 때문에, d.ts 파일을 사용하는 것이 편한다.
svg
난 svg를 사용할 거니깐 @svgr/webpack
그리고 svg.d.ts
를 추가했다.
코드 구현하기
react-router-dom
SPA인 react에서 중첩라우팅이 가능하도록 돕는다. 리액트에 딸려 있는 것이라고 생각했는데 그게 아니고 추가적인 설치가 필요했다.
중첩라우팅이 안되면 뒤로가기, 새로고침은 사용할 수 없다. 무한 컴포넌트 갈아끼기 혹은 홈페이지 갈아타기 느낌.
layout
기본적으로 라우터들을 감싸는 레이아웃을 생성했다. 이게 없어도 가능하지만, 나는 모바일 환경을 중점적으로 만들꺼라, maxW
가 540px이고 나머지가 연한 회색인 레이아웃을 생성했다.
react-hook-form
회원가입, 로그인기능을 구현하기 위해 react-hook-form을 사용했다.
회원가입을 여러 화면에 걸쳐 하고 싶었다. react-hook-form은 자체 상태관리가 가능해서, 여러 컴포넌트를 구독시켜 쉽게 구현했다.
다른 것보다 이 라이브러리를 사용한 이유는, 지양되는 비제어 컴포넌트 방식이지만, form에서는 최적화 된 방법이라고 생각했고, onBlur, onChange등 추가적인 상태함수 관리가 필요 없었다. 또한 자체 함수인 watch, setError
등을 통해 값을 확인하고 에러를 설정할 수 있었다.
.gitignore
항상 cra를 사용해서 몰랐는데 .gitignore를 설정해주지 않아서 git에 node_modules, DS_Store, build 이렇게 필요없는 파일들이 올라가있었다.
root폴더에 .gitignore를 만들어 관리해주었다.
ticket 타입정의
가장 핵심인 열차 티켓의 interface를 정했다. 이 부분이 프로젝트에 있어서 가장 핵심적인 부분이고, 백엔드에서도 가장 중요시 되는 부분일 거 같아서 충분한 소통 후에 만들고 싶었는데, 결과적으로는 백에서 충분히 숙지하지 못한상태에서 제작한 거 같다. 객체 안에 객체배열 안에 객체배열이 있는 형식으로 제작을 했기 때문에 백에서 상당히 어려울 것 같았지만 일단 해보자고 해서 했다가, 결국 이 부분은 최종발표까지도 완성할 수 없었다.
아직도 프론트에서는 이게 최선이라고 생각한다. 다만 백엔드 입장에서 보면, 소켓을 만들던지, id로 관리 한다던지, 쌍으로 관리 한다던지 더 나은 방법이 있었을 것이다.
객체 안에 객체배열 안에 객체배열 형태로 만든 바람에 컴포넌트 내에서도 drilling이 일어났다. 뿐만아니라, 열차의 컴포넌트 구조를 생각하는데에도 시간을 투자해야 했다. 1일차인지 1일차 이후인지, 답변이 둘 다 이루어졌는지 혹은 한 명만인지 아니면 둘 다 이루어지지 않았는지, 렌더링 해야할 컴포넌트가 내꺼인지 상대꺼인지, 지금 보고있는 댓글이 나의 것인지 상대 것이지, 접근 할 수 있는 day인지 아닌지 등 고려해야할 게 많아서, 마인드맵 형식으로 경우의 수를 모두 그려 컴포넌트를 생성했다.
이거를 더 쉽게 해결 할 방법이 있는지는 잘 모르겠다.
publicPath 설정
서버:포트/
뒤에 슬래시가 하나 더 생기면 오류가 발생했다. 찾아보니 webpack 설정에서 publicPath를 설정하지 않아서 생긴 오류였다. 이게 없어서 번들링 output을 브라우저가 정확하게 찾지 못했다.
NavigationBar position
레이아웃을 해놓는 바람에, 네비게이션바의 position 값을 fixed로 설정하면 뷰화면 크기에 맞춰 커져버렸다. absolute를 취하면 스크롤을 하면 사라지고, sticky를 사용하면 다른컴포넌트가 위치를 읽었다. 그래서 어떡하지 하고 있었는데 fixed로 설정하고 maxW
를 설정하니 해결되었다.
기억나는 css 스킬
스크롤 바 지우기 스크롤 바가 있으면 UI가 깨지고 지져분해 보이는 것도 있고, 티켓카드를 좌우로 밀었는데 여기에 스크롤 바가 생기니 모양새가 빠졌다.
'::-webkit-scrollbar': {
display: 'none',
},진행 바 만들기 진행 정도를 알기 위해 ref를 사용했다. Wrapper에 ref를 찍어 사용했다.
const [progressingPerCent, setProgressingPerCent] = useState(0);
useEffect(() => {
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } =
progressRef.current || {};
console.log(scrollTop, scrollHeight, clientHeight);
setProgressingPerCent(
((scrollTop || 0) / ((scrollHeight || 1) - (clientHeight || 0))) * 100,
);
};
})- scrollTop: 현재 뷰에서 보이는 최상단의 높이. y축이 반대
- scrollHeight: ref 되어있는 태그의 높이
- clientHeight: 뷰의 height
착붙 되게하기 모바일화면에서 슬라이드가 넘어가는 느낌을 구현하고 싶었다. 또한 티켓카드를 옆으로 밀어 나가기, 신고하기 버튼을 보여주고 싶었다.
<div style="scroll-snap-type: x mandatory;">
<div style="scroll-snap-align: start;"></div>
</div>부모 div에게
width:100%
를 부여하고, 자식 div들에게flex-shrink:0
을 부여해 좌우로 슬라이드 가능하게 했다.다만 여기에 모바일처럼 몇 페이지에 있는지 확인할 수 있게 하고싶었고, 화살표를 통해 좌우로 움직일 수 있게 하고싶었다.
이것은 진행 바를 만들때 처럼
scrollLeft, clientWidth
를 통해 구현할 수 있었다.scrollLeft
를clientWidth
로 나눈 값을Math.round
를 통해 가장 가까운 정수로 반올림하여 현재 페이지가 어딘지 확인했고 버튼을 누르면 기존scrollLeft
값에clientWidth
를 더해주었다.- 단어 줄바꿈 될 때. 단어 단위로 변경되게 하기
wordBreak: 'keep-all',
공통 변수는 미리미리 준비
리액트는 선언형이기 때문에 분기처리가 좋지 않다는 것은 알고있다. 하지만 변수 하나만 다른 건 다른 컴포넌트를 만들기보다 분기처리를 하는게 편하겠다 싶어서, 데이카드 내부에서 나의 답변인지, 상대방의 답변인지 구분할때는 tab의 상태에 따라 렌더링 할 state를 바꾸어 주었다. 처음에는 return문 내에서 삼항연산자를 통해서 분기처리했는데 그러다보니 분기처리할 부분이 많아졌다. return문 내에서 미리 어느 값인지 확인하여 정해진 값으로만 렌더링하게 바꾸어주었다.
API 통신
Axios
서버와의 데이터 통신을 위해 내가 선택한 것은 axios. 기본적인 fetch를 사용할 수도 있다. 하지만 json형식을 되돌리는 과정도 필요했고 개발자입장에서 코드에 직관성도 있고, 몇번 다시 요청을 보낼 지, 파라미터를 넣을 지 말지 등이 이미 구현되어 있는 axios를 선택했다.
React Query
서버에서 가져오는 값을 관리하기 위해 리액트 쿼리를 사용했다. 리액트 쿼리는 데이터 캐시관리, 백그라운드 업데이트, 오래된 데이터 갱신등을 알아서 해주기 때문에 간편했다. 추가적으로 상태관리를 할 라이브라리를 사용하지 않고 리액트쿼리만으로 해결할 수 있었고, 요청의 상태 또한 알 수 알려줬다.
get요청은 useQuery, 나머지는 useMutation 함수를 사용한다.
useQuery는 바로 요청을 보낸다. 이때 고유한 key배열을 지정해야 한다. post, delete 요청 등에 의해 상태가 갱신되고 이후 key를 알려주면 데이터를 갱신시켜줄 수 있다.
useMutation은 mutate라는 또 다른 함수를 제공하고, 이 함수를 호출해 요청을 보낸다.
이 둘은 모두 사용자 경험 향상을 위한 여러 built-in 메서드들이 존재한다. 데이터 갱신, 데이터 상태확인 뿐만 아니라 window가 클릭되면 새로운 값으로 갱신, 성공 시 액션, error일 때 액션 등 대부분이 built-in 함수로 내장되어 있다.
api 통신 구현
처음으로 구현해보는 api 통신이였던 만큼 백지에 밑바탕 그림을 제대로 그려야지! 하고 열심히 찾아본 후 코딩을 시작했다. api 폴더를 따로 만들고, 요청을 보낼때 필요한 것은 payload, 응답받을때 필요한 것은 response로 정의하고 이 ts파일들을 types폴더에 만들었다.
axios도 바로 사용하지 않고 인스턴스를 만들어서 사용했다.
인스턴스를 만듦으로써 baseUrl을 지정할 수도 있고, 요청만료시간, 디폴트 헤더도 지정할 수 있었다.
인터셉터기능 역시 사용했다. 이는 아래의 토큰과 연관된다.
토큰
토큰을 사용하기로 했다. 배포에서 가장 중요한 것은 보안이라 생각했고 이번에는 배포할 생각이였기 때문에 토큰기능을 구현했다. 토큰을 위해 axios의 인터셉터를 사용했다.
인터셉터는 요청이 가기전에, 응답이 도착하기 전에 인터셉트해서 다른 행동을 취하겠다는 거였다. 두개의 인자로 각각 함수를 받는데, 성공했을 때와 실패했을 때의 행동을 정의할 수 있다.
인스턴스를 두 개로 분류하였다. 토큰이 필요한 요청, 필요하지 않은 요청. 로그인이전의 요청들은 토큰이 필요없고, 나머지는 필요했다.
토큰이 필요한 요청은 인터셉터가 필요했다. 요청을 보내면 인터셉트해와서 헤더에 토큰을 추가해 전송하였다. 쿠키에 넣어서 하는 방법이 훨씬 안정적이라고 하는데 나는 처음 구현해보는 거였기 때문에 이 방버을 택했다.
일반적으로 이렇게하면 끝이 나지만, 나는 리프레시 토큰까지 생각해서 인스턴스를 만들어 보았다. 일반적인 요청에서는 엑세스토큰을 보내는데, 만약 만료되었다면 이에관한 에러코드를 보낸다. 응답에도 인터셉트를 만들고, 만약 응답이 엑세스토큰 만료에 관한 응답이 왔다면, 잠시 이 응답을 보류한다. 그리고 다른 엔드포인트로 엑세스토큰이 만료되었다는 요청을 보내고, 이때에는 토큰에 리프레시 토큰을 담는다. 서버는 리프레시토큰을 확인하고 엑세스토큰을 반환, 필요하다면 리프레시토큰도 반환해준다. 엑세스토큰을 받으면 아까 보류했던 요청의 토큰을 갱신시켜주어 다시 요청을 보낸다.
컨벤션
아무래도 잘 모르는 상태에서 시작했던 프로젝트 였기 때문에, 컨벤션은 정하지 않고 진행했다. 그러다보니 데이터를 주고받을 때 오류가 발생했다. 열차의 영문이름, 카멜케이스, Id인지 ID인지등 조금씩의 차이로 통신오류가 떴다.
절대경로
절대경로를 처음부터 설정하고 싶었는데, 원하는 대로 되지않아 그냥 대충하자~ 하고 프로젝트를 시작했다. 그러다 에러가 뜨기도 하고, 자동완성이 된 것도 똑바로 인식이 안돼서 한번 제대로 찾아봐야겠다..! 하고 찾아봤다.
일반적으로 @
를 src/
로 설정하는 것 같아 나도 그렇게 해봤다.
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src/'),
},
},
계속 실패했던 이유는 tsconfig.json파일만 건드렸기 때문이였다. 내가 참고한 것들의 설정이 vite였는지, cra였는지 모르겠지만 거기서는 tsconfig.json만 고치면 해결이 되었던 것 같은데, 나는 내가 직접 만든 거였기 때문에 webpack의 설정도 건드려줘야 했다. resolve.alias를 설정해주는 제대로 인식할 수 있었다.
DS_Store 일괄제거
이미 확인했었지만 .gitignore를 설정하지 않아서 DS_Store가 올라가 있었고, 이건 폴더마다 있었기 때문에 하나하나 지우기엔 commit창이 더러워질 것 같았다. 찾아보니 다행히 코드 한줄로 일괄삭제 할 수 있었다.
find . -name .DS_Store -print0 | xargs -0 git rm -f --ignore-unmatch
파일명 정하기
변수명을 정하는 데에 신경을 쓰는 것처럼, 파일명을 정하는 것에도 신경써야겠다고 느꼇다. 프로젝트가 커지다보니 파일을 넘나들어야 했고, 단축키를 사용해 검색을 하며 옮겨다녔다. 그렇다 보니 대충 정한 파일들은 찾아가기가 힘들었고, FastTrain, FastTicket등 헷갈리는 파일명들이 검색을 힘들게 했다.
이미지 띄우기
이미지 띄우는 일은 정말 쉽지 않았다.. 개발환경에서는 잛 보이던 이미지가 이상하게 배포하고 나면 보이지 않는 현상이 있었다. 이를 해결하기 위해 /public/img
에 있던 파일을 /src/assets/img
로 옮기고, backgroundImage
css에서 절대경로로 호출해오던 것을 url함수 호출, import 함수 호출 형식으로 변경하고, 대소문자를 변경해보는 등의 작업을 해보았고, 결국은 import형식으로 구현했다. 그러다보니 추가적인 d.ts 타입정의가 필요했다.
FootNav바 나눠갖기
매칭페이지, 열차페이지, 마이페이지를 왔다갔다 할 수 있는 네비게이션 바를 만들었다. 근데 flex-basis
를 설정 안해주는 바람에 자체 크기가 달라 칸의 크기가 다르게 되었다. flex-basis를 100%로 설정해주어 모두 같은 크기를 갖게 만들어줬다.
이미지 전송하기
이미지도 일반적인 데이터를 전송하는 방식으로 전송하려고 했는데, 실제 전송되는 payload를 확인해보니 빈 객체가 전송되고 있었다. 알고보니, 이미지는 formData 형식으로 전송해야 했다.
사진을 전송하는 submitHandler에 const formData = new FormData();
를 정의하고, append
함수를 사용해 요청에 필요한 payload를 담았다. payload의 타입정의도 formData로 바꾸어주었다. 또한 FileList
형식을 보내는게 아니라 File
형식을 보내야 했다.
그럼에도 정상적인 전송이 이루어지지 않았는데, 이유는 axios 인스턴스에 디폴트로 지정한 header 때문이였다. Content-type: 'application/json'
형식으로 지정해버리는 바람에 정상적으로 formData를 받지 못했다. Content-type
은 내가 지정해주지 않아도 자동으로 지정되기 때문에 설정할 필요가 없었다. 이 부분을 제거하니, formData 형식을 전송할떄는 자동으로 Content-type
이 multipart/form-data
형식으로 바뀌었다.
발표
당일날까지도 시연영상을 준비했고, 결과는 1등을 했다. 학교 동아리다보니 전체적인 출발지점과 목표지점이 다를테고, 배포목적으로 열심히 프로젝트에 임한 우리팀이 1등할 수 있엇다. 나는 배민 3만원 경품당첨까지 되어 총 9만원을 탔다.
후기
나는 혼자하다보니, 내가 가는 길이 맞는지에 대한 의구심이 들었다. 하지만 이번 발표를 통해 그래도 내가 밟아가고 있는 길이 옳은 길이라는 것을 확신할 수 있었다.
하지만 자만하지 말아야된다고 생각한다. 1등을 한 이유는 내가 남들보다 빨리 시작해서이지, 잘 해서가 아니니깐.
프론트엔드는 레드오션이고, AI및 라이브러리의 발전도 빠르게 일어나고 있다. 이 세계에서 살아남기 위해선 나는 우두머리가 되어야 한다. 이미 앞서고 있는 그들을 따라잡기 위해선 나 역시 분발해야 한다.