import-H
는 프론트 4명, 백엔드 2명으로 팀을 만들어서 개발중인 프로젝트이다.
notion
: https://jamong1.notion.site/Import-H-d4f69f3c20ce4a22a5b0f6ac952da2d2
지난 1년간 약 30명이 활동하는 프로그래밍 스터디를 운영하며, 관리자 역할을 혼자 하다보니 신경써야 하는 부분도 많았고, 스터디 팀원 관리도 미흡한 점들이 많았다. 이를 해결하기 위해 스터디 사이트를 만들게 되었다.
로그인과 회원가입은 각 input에 들어오는 값을 검사해서, 형식이 맞지 않으면 제출이 되지 않도록 만들었다.
배너 부분은 관리자가 직접 추가,삭제할 수 있게 만들었으며, 주로 소개하고 싶은 정보를 공유할 때 사용된다.
좋아요, 최신 순으로 글이 불러와진다.
게시판은 총 3개로 나뉜다.
게시글에는 좋아요 기능과 댓글 기능을 넣었다.
게시글은 toast-ui editor를 사용해 글을 쓸 수 있게 만들어줬다. 추가적으로 이미지 업로드 기능도 사용자에 더 친화적이도록 velog의 이미지 업로드 형식(서버에 base64로 이뤄진 이미지를 보내 저장된 서버 주소를 가져오는 방식)을 참고해 만들었다.
이외에도 개발중인 페이지들은 프로필 조회,수정 페이지, 스터디 페이지 등이 있다.
redux는 새로고침하면 store에 있던 정보가 날아간다. 따라서 지속적으로 저장되어야 하는 경우, localStorage 혹은 sessionStorage같은 로컬 저장소에서 정보를 저장해둔 뒤, 불러와 사용해야 했다.
useEffect로 매번 isAuth를 체크해줘야 하는 번거로움 => redux-persist를 도입하게 된 계기이다.
기존에는 redux-toolkit의 slice 파일에서 비동기 함수들을 만들어 이를 처리했었다. 하지만 직관적이지 않았고, 비동기 처리에 따른 action 분리가 필요했다.
따라서 createAsyncThunk
를 사용해 비동기 처리 부분을 변화시켰다.
하지만 여기서 문제는 전역 상태를 관리하는 redux가 오히려 전역 상태보다 비동기를 더 많이 관리하게 됐다는 점이다.
비동기로 인해 과도하게 길어지는 slice, 이러한 고민은 나만 가지고 있지 않았다.
=> https://techblog.woowahan.com/6339/ 배민 기술 블로그에서는 나와 같은 고민을 하다 react-query를 사용해 비동기 처리 방식을 바꾸어 store를 가볍게 만들었다고 한다.
지금은 비동기 처리를 모두 createAsyncThunk
를 사용해 하고있지만, 추후 프로젝트가 유지, 보수 단계에 들어가면 점진적으로 RTK-query
를 사용해 비동기 처리를 담당할 예정이다.
사실 프로젝트를 진행하면서 테스트 코드를 이전에 짜보지 않았기 때문에 약간의 두려움을 느꼈었다.
어떤 것들을 테스트하는지 알아보다 우리의 프로젝트를 봤는데, 너무 많은 것들을 테스트해야했기 때문이다. 하지만 이참에 경험을 늘리기 위해 테스트를 도입했고, 현재는 부분적으로 테스팅을 했다. (navbar, comment)
useSelector, useDispatch를 테스트하는 방법을 찾는데 되게 오래걸렸다,,
테스트는 문서에서 얘기했던 효과보다 장점이 더 잘 체감되었다.
테스트를 짜면서 코드를 한 번 더 생각하게 되고, 이렇게 코드를 생각하다보면, 내가 놓쳤던 경우들과 잘못된 코드들을 상당히 많이 발견할 수 있었다.
또한, 어떻게 해야 테스트를 효율적이게 짤 수 있을지 고민하다 컴포넌트에 담긴 양이 너무 비대하다는 것을 알고난후, 이를 줄이는 작업을 진행하였다.
그 결과, 이전보다 하나의 컴포넌트에 담당하는 역할이 줄어들게 되었다.
프로젝트를 얘기하면서 프로젝트 디렉토리 구성을 빼먹을 수 없다. 프로젝트 디렉토리는 크게 Pages와 Component로 분리했는데, 아직 컴포넌트를 나누는게 미숙해서 그런지 썩 맘에들지 않는다.(뭔가 지저분해보임)
이는 리액트 설계 원칙인 관심사에 따라서 코드를 분리하고 단일 책임을 가지는 컴포넌트를 만들어야 한다
를 정확하게 지키지 못했기에 발생한 문제로, 현재 컴포넌트 분리 작업을 진행하고 있다.
이번 프로젝트 전까지 api를 직접적으로 연동해본 적은 한 번 밖에 없어 걱정을 많이 했었다. 하지만, 이전 프로젝트에서 겪었던 api로 인한 소통 문제를 알고있었기 때문에, 이번에는 api 문서를 확실하게 만들어 프로젝트를 진행하였다.
api 문서를 백엔드 팀장과 같이 만들면서 정말 크게 성장할 수 있었는데, 우선 프론트에서의 동작 흐름 뿐만이 아니라, 백엔드의 동작 흐름에 대해서도 고민할 수 있는 계기가 되었다.
또한 새로 추가해야 하는 api, 혹은 api안에 담긴 요청들을 수정할 때, 백엔드나 프론트엔드 하나에서만 해당 정보가 공유되지 않고, 문서를 통해 동일하게 변경 사항이 공유된다는 점이 가장 매력적으로 다가왔다.
이로 인해 api를 잘못 작성해서 프로그램이 동작하지 않는 실수가 대폭 줄어들었다.
notion에서 지금까지 진행했던 회의와 api 문서, 에러 해결 방법, 프로젝트를 진행하면서 도움됐던 링크들을 모두 문서화시켜서 기록했다. 처음에는 되게 귀찮았지만, 하면 할수록 프로젝트가 점점 발전하고, 이전에 겪었던 문제들을 쉽게 해결하고, 쉽게 공유할 수 있다는 점이 강점으로 나타났기에 앞으로 다른 프로젝트를 진행해도, 이런 문서화 작업은 꼭 진행할 예정이다.
https://jamong1.notion.site/Import-H-d4f69f3c20ce4a22a5b0f6ac952da2d2
사실 백엔드에 대한 경험이 별로 없어, 이번 프로젝트를 진행하며 백엔드 회의에 많이 참가하여 백엔드의 프로세스를 어느정도 경험하였다. 그 중 가장 인상적이였던 것은 db 설계 시간이였다. 직전 학기에 db를 배웠기때문에, 금방 db를 설계할 수 있을 줄 알았지만, 생각보다 고려해야 할 점이 많았다. 대표적인 예로는, 게시판과 게시글 db를 따로 나누는 이유에 대해 고민했을 때이다.
기존에 우리가 설계한 db는 게시판 테이블을 분리하지 않고 설계했는데, 규모가 더 큰 사이트들의 db 설계를 보니 게시판 테이블과 게시글 테이블을 분리해서 사용하고 있었다. 이러한 원인은 검색 속도의 향상과 정규화라 결론짓고 우리의 프로젝트에서도 해당 내용대로 db를 설계하였다.
프론트 팀원이 4명이나 되고, 각각의 실력이 모두 달랐기 때문에, 팀원을 어떻게 분배해야 하는지 많은 고민을 했다. 그 결과 각 팀원의 역할을
공식 문서는 대부분 영어로 되어있어 기존에는 블로그 글을 많이 봤었다.
하지만 프로젝트를 진행하며 난관에 부딛혔고, 이를 해결하기 위해 공식 문서를 꼼꼼히 읽어봤다.
놀랍게도 많은 답이 공식 문서에 담겨있었다.
기존에는 커밋 메세지 규칙을 정하지 않았지만, 커밋이 쌓이다보니 addPost 개발
이라는 메세지를 봤을 때, 새로운 기능을 추가한건지, 성능을 개선한건지 알기 불편했다. 따라서 커밋 메세지 규칙을 추가하게 되었다.
프로젝트에서 로그인은 JWT를 사용했다.
JWT는 인증이 필요한 요청이 있을 때, accessToken을 헤더에 전달해서 보내줘야 한다.
또한, 만약 accessToken이 만료되면 refreshToken으로 새로 발급받아야 하는데, 이러한 과정을 매 비동기처리마다 일일히 작성하기에는 너무 번거로웠다.
따라서 이런 공통된 사전 기능을 instance를 통해 구현하였다.
여러 참고 자료들을 모아 구현한 axios Instace.
인증이 필요한 API 요청을 보낼 때, axios가 아닌, axiosInstance를 사용하면 해당 기능들이 자동으로 실행되도록 만들었다.
import axios from "axios";
import jwt_decode from "jwt-decode";
import dayjs from "dayjs";
const baseURL = "http://localhost:8090";
let authTokens = localStorage.getItem("authTokens")
? JSON.parse(localStorage.getItem("authTokens"))
: null;
const axiosInstance = axios.create({
baseURL,
});
axiosInstance.defaults.headers.common[
"Authorization"
] = `${authTokens?.accessToken}`;
axiosInstance.interceptors.request.use(async req => {
console.log("interceptor is working");
req.headers.Authorization = `${authTokens?.accessToken}`;
if (!authTokens) {
authTokens = localStorage.getItem("authTokens")
? JSON.parse(localStorage.getItem("authTokens"))
: null;
req.headers.Authorization = `${authTokens?.accessToken}`;
}
const user = jwt_decode(authTokens.accessToken);
let isExpired = dayjs.unix(user.exp).diff(dayjs()) < 1;
console.log("isExpired", isExpired, dayjs.unix(user.exp).diff(dayjs()));
if (!isExpired) {
return req;
} else {
const response = await axios.post(`${baseURL}/v1/reissue`, {
accessToken: authTokens.accessToken,
refreshToken: authTokens.refreshToken,
});
localStorage.setItem("authTokens", JSON.stringify(response.data.data));
authTokens = response.data.data;
req.headers.Authorization = `${authTokens?.accessToken}`;
return req;
}
});
export default axiosInstance;
// https://github.com/divanov11/refresh-token-axios-interceptors/blob/master/frontend/src/utils/axiosInstance.js 참고함