📝 도입 배경
- 본 프로젝트 (삐삐:Best Interior)는 인테리어 정보 교환을 위한 커뮤니티로, 고화질의 사진 리소스를 사용하는 작업이 많았고, 서버에서 고용량의 사진 데이터를 받아오는 과정에서 발생 할 수 있는 사용자 불편함을 고려해야 하는 상황이었습니다.
- 또한, 고화질의 사진 리소스들은 불러오는데 많은 네트워크 비용이 들고, 장애 발생 가능성 또한 고려해야만 했습니다.
- 이러한 상황을 고려하여, 2가치 최적화 방안을 도입하였습니다.
🔨 최적화 방안 - (1). Pagenation 구현
- 고용량의 사진 데이터를 받아오면서 발생할 긴 로딩을 방지하기 위해, Pagenation을 구현하였음
- 각 페이지(쇼룸, 팁)에 12개의 개시글 단위로 Pagenation을 구현하였으며, 이를 통해 리소스가 큰 요청들을 분할하여 네트워크 트래픽을 최적화 하였음
- 즉, 한번에 모든 데이터를 불러오는게 아니라 12개단위로 끊어서 GET요청을 보냈음
- 이와 더불어 무한스크롤 기능을 구현하여 동적으로 추가 컨텐츠들을 로드함으로써 서버 부하와 초기 로딩시간을 줄였으며, 페이지 이탈율을 줄여 사용자 경험을 향상시키고자 하였음
// 새로운 페이지 데이터를 불러오는 함수
const loadMoreData = async (url) => {
try {
toast.loading("로딩중..."); // 데이터 로딩 중 토스트 메시지 표시
const res = await api({ ...configParams, url });
if (res.data.isLast === false) {
setShowroomData((prevData) => [...prevData, ...res.data.data]);
} else {
setShowroomData((prevData) => [...prevData, ...res.data.data]);
// 마지막 페이지 설정
setIsLastPage(res.data.isLast);
}
toast.dismiss(); // 로딩 메시지 닫기
} catch (error) {
console.error("Error loading more data:", error);
toast.error("데이터를 불러오는 중에 오류가 발생했습니다."); // 에러 시 토스트 메시지 표시
toast.dismiss(); // 에러 메시지 닫기
}
};
// IntersectionObserver를 사용하여 스크롤 감지
useEffect(() => {
// IntersectionObserver를 생성하고 등록
if (!isLastPage && !loading && isFirstPageRendered.current == true) {
// isLast가 false일 때만 IntersectionObserver 등록
const newObserver = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !loading) {
page.current += 1; // 페이지 번호 증가
const updatedUrl = `/feed/${feedCode}/${inputValue}${filterCode}?page=${page.current}`;
loadMoreData(updatedUrl); // 새로운 페이지 데이터를 불러오는 함수 호출
}
},
{
threshold: 0.1, // 스크롤이 target에 도달하면 새로운 데이터를 요청하는 함수 실행
}
);
// 현재 컴포넌트의 target 요소를 설정
if (target.current) {
newObserver.observe(target.current);
}
// 컴포넌트가 언마운트될 때 Observer를 해제
return () => {
if (target.current) {
newObserver.unobserve(target.current);
}
};
}
}, [filterCode, loading, searchKeyworld, isLastPage]);
- 또한, 응답 데이터로 다음페이지 존재여부라는 isLast 필드값을 받아서, isLast가 true일때는 더이상 추가 api 요청을 안하도록 최적화 하였음.
📸 (1) 구현 결과
🔨 최적화 방안 - (2). 정적리소스 서버 구현
- 유저들의 프로필 사진이나, 작성한 게시글의 cover 사진 및 본문의 사진과 같은 정적리소스들의 네트워크 장애 발생 가능성을 고려하여 API 서버와 따로 분리, 정적리소스를 위한 서버(EC2)를 별도로 구축하였음.
- 예를들어, 유저가 글작성 과정에서 커버이미지 사진을 업로드시, 커버이미지를 별도의 정적리소스 서버로 업로드하기 위한 요청을 보내고, 해당 요청이 정상적으로 수행되면,
- API서버는 정적리소스 서버에 업로드한 커버이미지 파일의 URL을 반환하고,
- 글작성을 완료하면 커버이미지 값으로 정적리소스 서버의 이미지 URL값을 저장하는 방법으로 구현하였음.
{
"data": [
{
"feedId": 12,
"createdDateTime": "2023-09-14T03:37:34.692896",
"modifiedDateTime": "2023-09-14T03:37:34.692896",
"title": "title1",
"content": "content1",
"views": 0,
// 정적리소스 서버의 이미지파일 url을 DB에 저장
"coverPhoto": "https://bbibbiimage.s3.ap-northeast-2.amazonaws.com/archive/4a898f29-f9e5-4146-b7da-2e34b63b5889.png",
"roomType": "TYPE02",
"roomTypeName": "아파트",
"roomSize": "SIZE03",
"roomSizeName": "20평",
"roomCount": "COUNT05",
"roomCountName": "5개 이상",
"roomInfo": "INFO01",
"roomInfoName": "원룸",
"location": "LOCATION03",
"locationName": "충청",
"memberId": 1,
"nickname": "nickname1",
"memberImage": "image2",
"myIntro": "intro1",
"likeCount": 0,
"likeYn": null,
"repliesCount": 0,
"bookMarkCount": 0,
"bookMarkYn": null,
"followYn":true,
"replies": null
}
]
}
이미지파일 업로드 로직 일부 ( 커버사진 )
const imageUpload = (e) => {
const file = e.target.files[0];
const reader = new FileReader();
const formData = new FormData();
formData.append("coverPhotoImage", file);
reader.onloadend = () => {
setCoverImage(reader.result);
};
if (file) {
reader.readAsDataURL(file);
}
axios
.post(`${process.env.REACT_APP_API_URL}/imageUpload/coverImage`, formData)
.then((response) => {
setCoverImage(response.data);
})
.catch((error) => {
window.alert("이미지 업로드에 실패하였습니다.");
setCoverImage(null); //coverimage값 초기화
});
};
- API서버에서 정적리소스 서버로 해당 이미지를 업로드하는 endpoint로 요청을 보내고, API 서버에서 정적리소스 서버로 이미지를 업로드 한 요청이 성공할 경우, 해당 이미지의 URL값을 받아서 상태로 저장하는 로직
🔨 최적화 방안 - (3). 임시저장 기능 구현
- 모든 이미지들은 정적리소스 서버에 저장되기 때문에, 중복 요청을 보내게 될경우 불필요한 네트워크 비용이 발생한 가능성이 있음
- 예를들어, 한 유저가 게시물을 작성하는 과정에서 정상적으로 각종 이미지를 업로드 완료하였으나, 실수로 페이지를 닫게 되어 다시 똑같은 글을 작성하는 경우를 가정할 경우
- 해당 이미지는 이미 정적리소스 서버에 저장되어 있으나, 똑같은 글을 다시 작성하게 되면서 동일한 이미지를 다시 정적리소스 서버에 저장하게 됨으로써 불필요한 네트워크 비용이 발생하게 되는 상황을 고려해야 함
- 이를 위해, 글 작성 과정에서 임시저장 기능을 추가하여 중복 요청 가능성을 줄이고자 하였음
임시저장 기능 구현 코드 일부( 로컬스토리지에 저장하는 방식 )
// 임시저장 클릭했을때 실행되는 핸들러함수 = > 로컬스토리지에 저장
const saveToLocalStorage = () => {
try {
const tempSaveData = {
coverImage, // 정적리소스 서버의 URL
title,
editorContent, // 본문 컨텐츠 내용 + 정적리소스 서버의 URL
selectedValues,
createdAt: new Date(), // 현재시간까지 저장
};
localStorage.setItem(
"tempSaveShowroomData",
JSON.stringify(tempSaveData)
);
// 성공메세지
toast.success("임시저장이 완료되었습니다!");
} catch (error) {
//실패메세지
// Display an error toast notification if something went wrong
toast.error("임시저장에 실패하였습니다.");
}
};
// 로컬스토리지에 임시저장값이 있으면 해당값 불러오기 위한 useEffect
useEffect(() => {
const savedData = localStorage.getItem("tempSaveShowroomData");
setTimeout(() => {
if (savedData) {
const tempSaveData = JSON.parse(savedData);
const createdAtString = tempSaveData.createdAt;
const createdAtDate = new Date(createdAtString);
const formattedDate = createdAtDate.toLocaleString();
const userConfirmed = window.confirm(
`(${formattedDate})
작성중인 글을 불러오시겠습니까?
취소를 누를 경우 작성중인 글은 삭제됩니다.`
);
if (userConfirmed) {
const parsedData = JSON.parse(savedData);
setCoverImage(parsedData.coverImage);
setTitle(parsedData.title);
setEditorContent(parsedData.editorContent);
setSelectedValues(parsedData.selectedValues);
toast.success("작성중인 글을 불러왔습니다.");
} else {
// 취소시 삭제
localStorage.removeItem("tempSaveShowroomData");
toast.error("작성중인 글을 삭제하였습니다.");
}
}
}, 300);
}, []);
📸 (2) ~ (3) 구현 결과
- 정적리소스 서버 이용 Part : 커버사진, 본문 내의 이미지
- 임시저장의 경우, 이전에 임시저장된 내용이 있는 경우 confirm창을 띄우고, 확인을 누를 경우 임시저장된 데이터를 불러오도록 설정
🌱 느낀점
- 기존의 진행했던 토이프로젝트의 경우 기능 구현에만 초점을 맞추어 작업하다 보니, 최적화 부분을 신경쓰지 못했는데 이번 기회에 성능 개선과 최적화 작업을 진행하며 사용자 경험을 개선할 수 있어서 한단계 더 성장한것 같았다!
- 정적리소스 같은 서버 인프라 관련 내용은 BE나 devOps에서 하는거라고 생각 할 수 있으나, 시장이 필요로하는 개발자로 성장하기 위해서는 해당 지식을 알고 있는 것이 경쟁력이 있을거라고 생각한다.
- 또한, 서비스에서 발생할 수 있는 여러 장애상황(ex: 네트워크 장애 등)에 대한 대비책을 마련해본 경험은 실무에서도 충분히 활용할 수 있지 않을까 생각한다.
'Project > 삐삐(BIBI: Best Interior)' 카테고리의 다른 글
댓글, 답글 기능 구현 중 발생한 에러 (0) | 2023.09.19 |
---|---|
[JWT] 토큰 인가 및 토큰 재발급 관련 트러블슈팅 (0) | 2023.09.18 |
[AWS S3] 클라이언트 배포 시 404 에러 (0) | 2023.09.17 |