💡WEEK 4 (2022-1121 ~ 2022-1127)
퍼블리싱 단계를 마치고 본격적인 기능 개발에 돌입했던 4주차의 기록이다. 내가 맡은 부분은 게시글을 상세조회 했을 때 사용하는 답변 CRUD, 코멘트 CRUD, 좋아요, 북마크 기능 그리고 답변을 채택하거나 후원을 했을 때 상대방에게 메시지를 보내는 기능이었다.
프리 프로젝트때도 비슷한 페이지의 CRUD 코드를 작성해본 적이 있어서 빨리 끝낼 수 있다고 생각했는데, 이게 왠걸. 생각보다 간단하지 않았다. 기존 프로젝트에서는 CRA를 활용해 일반적인 리액트 그리고 자바스크립트로 프로젝트를 진행했는데, 이번 프로젝트에서는 next.js + Typescript 그리고 데이터 캐싱을 위해 SWR 이라는 라이브러리까지 추가로 도입했기 때문에, 사용 방법을 익히는데 시간이 정말 많이 소요되었다. 그러다보니 실질적으로 기능을 구현하는 시간이 부족했고 시간에 쫓기다보니 좀 더 진득하게, 꼼꼼히 보면서 코드를 쓰지 못한 것 같아 아쉬움이 많이 남는다.
또한 작업을 하다가 기획때 놓쳤던 다양한 상황에 대한 화면, 예를 들면 코멘트가 하나도 없을 때라던가, 답변이 하나도 달리지 않았을 때라던가, 로그인을 하지 않은 사용자의 접근시 알림을 주는 방법이라던가 이렇게 사소한 부분들이 하나씩 발견되면서 더 복잡하게 느꼈던 것 같다. 이번 주 멘토링을 하면서 멘토님께서 풀스택이 꼭 프론트 + 백엔드가 아니라 디자인 + 프론트 || 기획 + 디자인 + 프론트가 될 수도 있다는 말씀을 해주셨다. 내가 프론트엔드 개발자이더라도 협업할 수 있는 다양한 분야에 관심을 가지고 있다면 나중에 현업에서 소통하는데 큰 도움이 될 것 같다.
SSR 과 SSG
질문과 답변이라는 컨셉이 곧 우리 사이트의 컨텐츠이기 때문에, 이 사이트에서는 SEO 에 불리한 CSR 방식보다는 SSR 이 더 맞겠다는 판단이 들었다. 우리가 사용하는 next.js 가 SSR 을 편리하게 사용할 수 있도록 도와주는 getServerSideProps 같은 함수를 제공해주고 있기 때문에 SSR 구현이 그리 어렵지는 않았다.
한 가지 여기서 약간 멍청하게 헤맸던 것은, 처음에는 SSR 이 아니고 SSG 를 만들어야지! 라고 생각하고 접근했다는 점이다. SSG 란 Static Stie Generator 로 빌드 시점에 페이지를 만들어서, 사용자가 페이지를 요청하면 이미 만들어진 html 페이지를 보여주는 방식을 말한다. 즉 서버에 이미 만들어진 html 파일을 가지고 있는 것인데 우리 프로젝트의 경우에는 내가 데이터를 만들어서 넣어주는게 아니고 사용자가 입력/요청한 데이터를 바탕으로 페이지를 동적으로 생성해주는 것이기 때문에 SSG 는 맞지 않는 방식인 것 같다.
SSR + SWR (데이터 캐싱)
SSR 의 장점은 요청과 동시에 서버에서 페이지를 만들어서 보여주는 것이기 때문에, 데이터를 받아와서 브라우저에서 실행시켜서 렌더링하는 CSR 과 달리 별도의 로딩 상태가 필요없다는 점이다. 단점은 클라이언트 측에서 데이터 캐싱을 사용할 수 없다는 점이다. 게시글 상세 페이지에서 SSR 로 불러오는 질문 본문에 대해서 해당 데이터를 클라이언트 쪽에 props 로 전달해 SWRConfig fallback 설정으로 캐시 기본값으로 저장하는 로직을 작성했다.
export default function Page({ fallback }) {
// `SWRConfig` 경계 내부에 있는 SWR hooks는 해당 값들을 사용합니다.
return (
<SWRConfig value={{ fallback }}>
<Article />
</SWRConfig>
)
}
// fallback 은 이렇게 생겨야 한다.
fallback : {
"api/articles" : { props 로 내려오는 데이터 형식}
이렇게 활용하면, 게시글 리스트에서 이미 방문했던 게시글로 넘어가는 시점에 전환이 더 빠르고 부드럽게 느껴진다. 그런데 지금 이 회고를 작성하는 시점에 드는 생각이지만, 꼭 게시글 자체를 데이터 캐싱을 해야 할 필요가 있나? 라는 생각이 든다. 이미 한 번 본 페이지를 빠르게 로드하기 위함이지만 이 페이지 자체를 사용자가 왔다갔다 하면서 방문할 일이 있는지 의문이 든다.🥲
SWR mutation 활용
useSWR에는 data 뿐만 아니라, 해당 키(url)과 바인딩 된 mutation 함수도 같이 딸려온다. 이 함수를 사용하면 업데이트 요청을 보내면서 새로고침 되기 이전에, 캐시를 업데이트해서 사용자에게 먼저 보여줄 수 있다. 혹은 특정한 상황에서 특정한 데이터를 업데이트하라고 요청을 보내게 만들 수 있다.
// custom useSWR 로 data와 mutate 함수를 받아온다.
const { data: currAnswers, mutate } = useFetch(
`/api/articles/${articleId}/answers?page=1&size=5`,
);
// form 데이터가 유효하다면 요청
const onValid: SubmitHandler<FormValue> = async (data) => {
const response = await client.post(
`/api/articles/${articleId}/answers`,
data,
);
const newAnswers = response.data;
// 현재 데이터와, 응답으로 받아온 새로운 데이터를 합쳐서 캐시를 업데이트한다.
mutate({ currAnswers, ...newAnswers }, { revalidate: false });
};
아래와 같이 작성하게 되면, post 요청이 완료됨과 동시에 즉시 get요청을 보내 데이터를 업데이트한다.
// 데이터 요청 관련 함수
const postComment = async (data: FormValue) => {
await client.post(url, data);
mutate(url)
.then(() => {
setRenderingHeader((prev) => !prev);
})
.catch((err) => {
alert('코멘트 등록에 실패했습니다...!');
console.log(err);
});
};
한 화면에서 데이터를 업데이트하기 위해 다시 get 요청을 보내고 할 필요 없이 mutate 함수 안에 key(url) 를 넣어주면 해당 데이터를 자동으로 업데이트 해주어서 코드의 양이 많이 줄고, 또 get 요청 없이 즉시 화면에 먼저 반영할 수도 있어서 속도가 상당히 빠르게 느껴진다는 장점이 있다. 그리고 브라우저 탭을 클릭할 때마다 자동으로 데이터를 업데이트 해주기 때문에, 조금 더 실시간으로 업데이트 되는 것처럼 느껴진다. 이번 프로젝트에서 SWR의 mutation을 적극 활용했는데 무척 편리했던 것 같다.
next.js 는 SSR 기반이다. 그래서 SSR 은 로컬스토리지에 접근할 수 없다.
next.js 는 SSR 기반의 프레임워크이기 때문에, CSR이 실행되기 이전까지 브라우저의 로컬스토리지에 접근할 수 없다. 따라서 아래와 같이 접근해주어야한다.
let accessToken = '';
if (typeof window !== 'undefined') {
accessToken = localStorage.getItem('accessToken');
}
게시글 본문에 대한 데이터만 SSR 방식을 사용했기 때문에 큰 문제를 못느꼈다. 하지만 문제는... 우리 백엔드에서는 액세스 토큰의 유효성 여부를 액세스 토큰이 필요하지 않은 요청에서 확인하도록 되어있었는데, SSR 에서 보내는 요청에서는 로컬스토리지에 저장된 우리의 액세스 토큰에 접근할 수 없기 때문에 특정 데이터를 불러오면서 의도치 않은 값이 나오는 문제점이 있었다. 이를 액세스 토큰과 리프레시 토큰을 쿠키에 저장하여 해결할 수도 있었는데, 할 일이 너무 많은 상황에서 이 부분이 변경되는게 어렵다는 판단이 들어서 결국 CSR 방식으로 변경하게 되었다.
결국 이 부분까지 CSR 방식으로 변경하게 되면서 우리가 next.js로 부터 얻는 이점은 폴더 구조로 직관적인 라우팅 정도 라는 생각이 들었다. next.js 에서 제공하는 SSR 의 기능을 거의 활용하지 못하고 있었고 이 부분은 좀 아쉽다. 처음에는 그냥 아무 생각없이 로컬 스토리지에 저장하는게 편리하니깐 그렇게 하자! 고 의견을 냈는데, 이런 부분까지 조금 더 잘 알았으면 좋았을 것 같다.
useRef 와 scrollIntoView 를 활용해 특정 엘리먼트로 스크롤하기
리액트에서는 특정 DOM 을 가리키기 위해 ref 라는 속성을 사용하는데, 리액트의 useRef 로 만든 객체를 ref 속성에 할당해주고 이를 활용하면, 특정 DOM 으로 이동하는 스크롤 기능을 구현할 수 있다. 리액트에서는 컴포넌트에 변화가 있을 때 리렌더링이 발생하는데, useRef 를 사용하면 이 객체의 current 값이 변하더라도 리렌더링이 발생하지 않는다고 한다. (이 부분은 공부가 조금 더 필요할 것 같다.)
//등록해줄 ref 객체를 만들고 컴포넌트 ref 속성에 할당시켜준다.
const answerElement = useRef<null | HTMLDivElement>(null);
// 아까 업데이트했던 그 방금 등록한 답변의 id를 가져와주고
const newAnswerId = useRecoilValue(newAnswerIdAtom);
// useEffect 로 이녀석이 업데이트 될때마다 (=== 게시글이 새로 등록되면)
useEffect(() => {
// 현재 답변글 id 랑 아까 그녀석의 id가 같은지 확인하고 스크롤을 해준다.
if (newAnswerId === answer.answerId && answerElement.current)
answerElement.current.scrollIntoView({ behavior: 'smooth' });
}, [newAnswerId]);
리액트 퀼 + preSigned URL 이미지 업로드 후 프리뷰
지난 프리 프로젝트때는 위지윅 에디터로 toast UI 를 사용했었는데 (다른 팀원분이 짜주신 코드를 참고...^^) 이번에는 리액트 퀼을 도입하게 되었다. toast UI를 사용했을 때 다른 라이브러리들과 충돌이 발생하는지 계속 오류가 생겨서 결국 force 방식으로 install을 해줬는데, 리액트 퀼은 그런 문제 없이 안정적으로 사용할 수 있다는 다른 팀원분의 의견이 있어서 이것으로 결정하게 되었다.
preSignedURL 이란, 쉽게 말해 미리 공간을 찜꽁해놓고, 이리로 이미지를 업로드 하라고 S3에서 주는 url 이다. 서버쪽에서 S3 preSignedURL을 클라이언트에게 주면 클라이언트에서 이 url 로 PUT 요청을 보내 직접 S3 에 이미지를 업로드하는 방식이다. 그래서 에디터를 활용하면서 아래와 같은 방식으로 이미지를 업로드 할 수 있다.
1. 에디터에 이미지가 추가된 것을 감지한다.
2. 이미지가 추가되었을 때 즉시 서버에 preSignedURL을 요청한다.
3. URL을 받자마자 PUT 요청으로 이미지를 업로드 한다.
4. 이미지를 업로드 하면서 받은 S3 경로 (이미지 접근 경로) 를 가지고 에디터에 사진을 삽입한다.
5. 글 작성이 완료되면 글 (콘텐트)와 함께 해당 이미지를 업로드하면서 받은 fileId 를 함께 전송한다.
이 방식을 사용하면 이미지를 업로드 할 때 서버를 거치지 않고 클라이언트 - S3 이렇게 다이렉트로 연결할 수 있어서 서버의 부담이 적어진다고 한다. 게시글을 삭제할 때는 게시글과 함께 전송받았던 fileId 를 가지고 서버에서 삭제 처리를 한다.
위 로직 자체는 어려운게 아닌데, 문제는 에디터의 사용 방법에 대해서 잘 모르는 것이었다. 리액트 퀼은 모듈을 사용해 기능을 확장하는데, 처음에는 드래그-드랍 모듈을 적용하려고 시도했으나 과정이 너무 복잡해서 우선은 기본 이미지 업로드에 먼저 적용하기로 했다. 리액트 퀼에서도 에디터에 이미지가 왔다는 것을 감지하기 위해 커스텀 이미지 핸들러를 만들어 주고, ref.current 를 사용해 이벤트를 감지해주었다.
코드가 너무 긴 관계로 이 부분은 추후에 따로 글을 작성해야 할 것 같다.
📝아쉬운 점
사용하는 기술에 대한 공부
프로젝트를 진행하면서 빠르게 습득한 내용을 조금 더 들여다보고, 공부했었어야 하는데 그렇지 못했던 것 같다. 주말에라도 시간을 내어서 하는게 좋았을 것 같은데 개발이 지연되면서 거기에 맞추느라 급급해서 일을 쳐내기에 바빴던 것 같아 아쉽다. 또 SSR 에 대해서 제대로 모르면서 SSR을 도입하겠다고 깝친(?) 것 같기도 하고, 사전에 사용할 기술에 대해서 좀 더 면밀히 공부해보고 도입을 결정했으면 어땠을까 싶다. 역시 바쁘다는 핑계로 그러지 못한 것 같다.
팀 작업 내용 공유 및 일정 관리
오전 프론트 스크럼, 오후 전체 스크럼을 통해서 서로의 일정을 공유했지만 깃헙 이슈라던가 프로젝트로 조금 더 체계적으로 관리할 수 있을텐데 그러지 못했던게 좀 아쉽다. 지금은 규모가 작다보니까 서로 자주 소통하면서 해소할 수 있지만 관리 툴을 적극 활용해서 조금 더 조직적으로 할 수도 있을텐데 초반에 그러지 못했다보니 나중에 도입하는게 좀 힘들어졌다.
'프로젝트 > 모락모락' 카테고리의 다른 글
[리팩토링] 🔥모락모락 - 코멘트 컴포넌트 리팩토링 (0) | 2022.12.12 |
---|---|
[Project] 모락모락 프로젝트 5주차 회고😸 (0) | 2022.12.04 |
[Project] 모락모락 프로젝트 1,2,3 주차 회고 (중간 회고 ^^😸) (0) | 2022.11.20 |
[Project]스택오버플로우 클론코딩 3주차 회고 (0) | 2022.11.06 |
[Project]스택오버플로우 클론코딩 2주차 회고 (0) | 2022.10.30 |