PAWLAND 프로젝트는 반려동물 용품 오픈마켓, 즉 커머스 사이트로, 다양한 페이지와 복잡한 데이터 처리를 효율적으로 관리할 필요가 있었다. 특히 이탈률을 줄이기 위해 로딩 속도가 중요했고, 커뮤니티 활성화가 필수였기 때문에 잠재 고객을 유치하기 위한 SEO 또한 매우 중요했다. 또한, 커머스 사이트이기 때문에 이미지 최적화를 통해 사용자에게 원활한 쇼핑 경험을 제공해야 했다. 이러한 요구를 충족시키기 위해 Next.js를 채택하였다.
기능별로 코드를 분리하여 모듈화함으로써, 각 기능을 독립적으로 개발하고 유지보수 할 수 있도록, 그리고 코드의 구조가 명확해져서 어느 누가 봐도 코드를 쉽게 이해할 수 있도록 하는 것을 기대하며 FSD 폴더 구조를 도입했다. FSD 폴더 구조를 채택함으로 협업의 효율성과 코드의 재사용성을 가져가기 위하여 노력했다.
src
├── app
│ └─ detail
│ └─ notice
│ └─ profile
│ └── page.tsx
├── pages
│ └─ employer
│ └── api
│ └─ notice
│ └── api
├── widgets
│ └─ api
│ └── get-application.ts
│ └─ header
│ └─ footer
├── features
│ └─ accept-modal
│ └─ filter
│ └─ sort
├── entities
│ └─ post
│ └── hooks
│ └── utils
├── shared
│ └─ api
│ └─ hooks
│ └─ utils
│ └─ ui
│ └── table
│ └── button
└── └── input
FSD(Feature-Sliced Design) 폴더 구조의 높은 러닝 커브로 인해 오버 엔지니어링 문제가 발생했다. 단기간에 프로젝트를 마쳐야 하는 상황에서, 폴더 구조를 공부하며 적용하다 보니 시간이 더 오래 걸리고 개발에 집중하지 못하는 어려움이 있었다.
그러나 관심사 분리와 모듈화의 장점 덕분에 유지보수와 가독성이 향상되며, 큰 프로젝트에 도움이 될 것이라는 확신도 얻었다. 하지만 마감 기한이 있는 초보자에게는 부담이 될 수 있겠다고 생각했다. (나의 경우가 그러했다.) 그래서 프로젝트를 진행하면서는 우선적으로 개발에 집중하고, 발표가 끝난 후에 FSD에 대해 더 공부하며 적용시켰다.
nukeapp FSD 사용 예시
또한 폴더 구조의 정답이 정해져 있지 않아서 팀원 간의 코드 구조가 일관되지 않을 수 있다는 문제도 있었다. 우리는 큰 혼란을 겪기 전에 FSD 폴더 구조에 대한 명확한 가이드라인을 작성하고 팀원들이 이를 숙지하도록 했다.
또한 Header와 Footer를 어디에 둘지에 대해서 살짝 이슈가 있었다. 일반적으로는 Header와 Footer를 모두 Widgets에 위치시킨다. 하지만 PAWLAND 프로젝트의 Header는 여러 페이지에서 재사용되지만 복잡한 구조와 여러 기능을 포함하기 때문에 Widgets으로, Footer는 상대적으로 간단하고 고정된 정보와 링크를 제공하기 때문에 Shared에 위치시켰다. FSD 공식 문서에서 프로젝트 상황에 맞춰 폴더 구조를 유연하게 적용하라고 권장하고 있기 때문에, 팀원들끼리 조율을 통하여 유연하게 폴더 구조를 적용시킨 사례가 되었다.
참고하며 공부했던 FSD 관련 레퍼런스
https://github.com/moneyflow-dev/moneyflow
https://github.com/noveogroup-amorgunov/nukeapp
PAWLAND 프로젝트에서는 커뮤니티 글 작성과 상품 등록 시 제품 설명을 위한 텍스트 에디터로 React Quill을 선택했다.
NPM Trends에서 최근 1년 동안의 텍스트 에디터 다운로드 수 결과를 보면 주요 텍스트 에디터들 중 React Quill이 가장 높은 다운로드 수를 기록했다. 이는 많은 개발자들이 React Quill을 선호하고 사용하고 있다는 것을 의미한다.
@toast-ui/editor, @uiw/react-md-editor, react-draft-wysiwyg 등의 텍스트 에디터와의 비교에서 React Quill이 가장 높은 다운로드 수를 기록했다.
기능적인 부분에서도 차이가 있다. 비교한 다른 텍스트 에디터들은 대부분 마크다운을 지원하지만, 우리는 마크다운을 접해보지 못한 일반적인 고객층을 주요 타겟으로 하기 때문에 사용자 경험 측면에서 마크다운 기능이 필요하지 않았다. 또한, 복잡한 초기 설정도 문제였다.
이에 비해 React Quill은 설정이 비교적 간단하여 개발자의 입장에서도 이용하고 접근하기 쉬웠고, 코딩이나 마크다운 문법을 몰라도 누구나 쉽게 사용할 수 있어 비기술 사용자에게도 친화적이었다. 이러한 이유로 React Quill을 채택하게 되었다.
React Quill을 사용하면 이미지가 base64로 인코딩된다. base64 형식의 이미지는 파일 크기가 원본보다 증가하기 때문에 데이터베이스에 저장하기에 비효율적이고 성능이 저하될 수 있다. 그래서 이를 URL 형식으로 변환해 저장하는 과정이 필요하다.
PAWLAND는 서버 유지에 대한 지원이 한정되어 있는 부트캠프에서 진행한 프로젝트이다. 예산이 거의 없었기에 서버 비용을 최소화하고 보안을 유지하면서 효율적으로 파일을 업로드하는 방법이 필요했다. 그래서 많은 선택지 중 프리사인 URL을 선택하게 되었다.
AWS S3에서 제공하는 기능으로, 클라이언트가 AWS S3 버킷에 파일을 업로드하거나 다운로드할 수 있도록 임시로 유효한 URL을 생성하는 방법이다. 서버를 거치지 않기 때문에 서버 부하를 줄이고 보안을 강화하는 데 도움을 준다.
✅ 적용
백엔드에서 Presigned URL을 생성하는 엔드포인트를 만들었다. 클라이언트는 이 엔드포인트를 호출하여 S3에 업로드할 이미지를 위한 Presigned URL을 받는다. 그리고 클라이언트는 받은 Presigned URL을 사용하여 이미지를 S3에 직접 업로드한다. 그 후 백엔드에 저장된 이미지를 불러와 렌더링하는 로직을 구현한다.
✅ 구현 방식 고민
게시글 작성 도중 이미지를 삽입할 때마다 Presigned URL을 요청해 이미지를 S3에 업로드하는 방법과 최종 제출 시 모든 이미지를 한꺼번에 Presigned URL을 요청해 S3에 업로드하는 방법 중에 어떤 방법을 선택할지 고민했다.
✅ 첫 번째 방법
이미지를 삽입하는 즉시 업로드되어 최종 제출 시 병목 현상이 발생하지 않아 사용자 경험이 향상된다. 하지만 이미지를 삽입할 때마다 Presigned URL을 요청해야 하고, 작성 중 삭제한 이미지는 S3에서도 삭제해야 하는 단점이 있다.
✅ 두 번째 방법
최종 제출 시 모든 이미지를 한꺼번에 업로드하는 방식으로, 삭제된 이미지는 업로드할 필요가 없어 추가 요청이 줄어들어 간편하게 관리할 수 있다. 하지만 최종 제출 시 많은 이미지를 한꺼번에 업로드하면서 네트워크 병목이 발생할 수 있다.
✅ 최종 선택
인스타그램에서도 첫 번째 방법을 사용하여 태그를 작성 중일 때는 이미 사진이 업로드된다고 한다. 그리고 대부분의 경우 네트워크 요청 횟수나 병목 현상이 실제로 문제가 되지 않는다. (문제가 될 정도면 사용자가 엄청 많아야 한다.) 만약 문제가 된다면 그때는 서비스가 왕성하게 사용 중이라는 뜻이니 오히려 해피 코딩이 가능한 것이다. 그래서 유저 경험과 개발 경험 위주로 생각하기로 하여 첫 번째 방법을 선택했다.
📣 AWS S3 (Simple Storage Service)
AWS S3는 Amazon Web Services(AWS)에서 제공하는 객체 스토리지 서비스이다. S3는 데이터를 "버킷"이라고 불리는 컨테이너에 저장하며, 각 버킷 안에 파일(객체)을 저장할 수 있다. 주로 파일 백업, 데이터 아카이빙, 빅 데이터 분석, 웹사이트 호스팅, 애플리케이션 데이터 저장 등에 사용된다. 다양한 보안 기능을 제공하여 데이터의 안전한 저장을 지원하며, 사용한 만큼만 비용을 지불하는 종량제 요금 모델을 사용한다.
PAWLAND 프로젝트에서는 텍스트 에디터를 사용하여 HTML 코드를 한 줄로 전달했다. 이는 사진과 글의 순서가 중요하기 때문이며, 사용자가 작성한 그대로 데이터를 렌더링하기 위함이다.
이렇게 전달된 HTML 코드를 렌더링하기 위해 dangerouslySetInnerHTML
을 사용했다. 이 속성은 React에서 HTML을 직접 렌더링하는 방식으로, 사용자 입력을 HTML로 렌더링할 때는 XSS(교차 사이트 스크립팅) 공격에 취약할 수 있다.
❗️ XSS는 악성 사용자가 웹 페이지에 악성 스크립트를 삽입하여 다른 사용자의 데이터를 훔치거나 조작하는 공격 방식이다. 예를 들어, 댓글 입력란에 악성 코드를 삽입하여 사용자가 페이지를 열었을 때 쿠키 정보를 탈취하거나, 사용자 세션을 가로챌 수 있다.
dangerouslySetInnerHTML
참고 자료
dangerouslySetInnerHTML
을 사용할 때 발생할 수 있는 보안 문제를 해결하기 위해 Dompurify를 사용하여 HTML을 정화했다. Dompurify는 HTML 코드에서 특정 태그를 허용, 금지 등의 방식으로 보안을 강화할 수 있다. (예: p태그, img태그)
import DOMPurify from 'dompurify';
const example = (html) => {
return {
__html: DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'img'],
FORBID_TAGS: ['script', 'style'],
FORBID_ATTR: ['onclick', 'onload'],
}),
};
};
const MyComponent = ({ content }) => (
<div dangerouslySetInnerHTML={example(content)} />
);
export default MyComponent;
이상으로 PAWLAND 프로젝트를 진행하며 사용한 기술 스택과 개발 과정에 대한 포스팅을 마무리한다. 추가적으로 사용한 기술 스택도 있지만, 프로젝트 시간 관계상 다른 팀원이 세팅한 코드를 그대로 사용하여 깊게 공부하지 못했다. 앞으로 1-2주 동안 다른 팀원의 코드를 하나씩 깊이 분석하며, 다른 기술 스택에 대해 충분히 학습하고 이를 따로 벨로그에 정리할 예정이다. 이 다음은 PAWLAND 프로젝트 시리즈의 마지막 목차인 프로젝트 결과 및 회고를 다룰 것이다.