회고
2주 동안 정신없이 개발하고, 버그 잡고, 리팩토링하며 어느새 블로그 쓸 틈조차 없이 시간이 흘렀다. 정글에서 초췌한 몰골로 다니지 않겠다고 다짐했지만... 나만무 기간만큼은 그 다짐을 지키기 어려웠다.
그래도 팀원들과의 회식만큼은 빠짐없이 챙겼다. 용인 중앙시장의 ‘평원집 족발’, 바로 앞 ‘신통 치킨’, 편맥까지! 개발이 아무리 바빠도 회식은 또 빠질 수 없지!
나의 생활 패턴은 밤낮이 완전히 바뀌어버렸다. 밤새 코드 짜다가 아침 해를 보고서야 잠을 잤다. 날씨 탓도 있었다! 이전에는 선선한 날씨에 잠깐 산책하거나 운동도 했지만, 요즘 같은 찜통더위에는 밖에 나갈 엄두조차 내기 어려웠다.
이렇게 몰아치듯 달린 나만무 기간. 그 안에서 어떤 성장과 시행착오가 있었는지 하나씩 정리해보려 한다.
1️⃣ 이미지 접근 방식 개선: public-read 정적 URL → Presigned GET 방식으로 보안 강화
🧩 문제
기존에는 이미지 업로드는 presigned URL을 사용했지만, 불러올 때는 public-read를 허용한 정적 URL을 클라이언트에 내려줬다.
<img src="https://bucket.s3.ap-northeast-2.amazonaws.com/used-products/01a6fd80-977c-4c8c-8693-efed024003c2" />
이 방식은 간편했지만…
- S3 버킷 전체에 대해 public-read 권한을 열어둬야 해서 보안상 매우 취약
- 이미지 접근을 누구나 할 수 있어 권한 제어가 불가능
- 향후 유저별 접근 제한이 필요한 경우 확장 불가능한 구조
💡 해결 방식
S3 조회도 presigned GET URL로 전환
서버에서 key
값을 기반으로 presigned GET URL을 생성해주고, 클라이언트는 해당 URL로 제한된 시간 내에만 이미지 접근 가능
<img src="https://s3.ap-northeast-2.amazonaws.com/recho-img/used-products/22669e4c-69e8-4b13-a347-c3de271457e8?...&X-Amz-Signature=..." />
모든 조회는 getDownloadUrl(key)
메서드를 통해 presigned URL을 발급받도록 구조 정비
🤔 고민했던 점
- static URL에서 presigned URL로 바꾸면 브라우저 캐싱이 어려워지고, 만료 시간 관리가 필요
- 조회할 때마다 서버를 거쳐야 하므로 최적화 포인트를 어디에 둘지 고민됨
⚙️ 구현 방식
🔸 백엔드 (NestJS)
async getDownloadUrl(key: string): Promise<string> {
const command = new GetObjectCommand({ Bucket, Key: key });
return await getSignedUrl(this.s3, command, { expiresIn: 3600 });
}
- GET presigned URL 발급 (1시간 유효)
- 클라이언트는 이제 S3 정적 URL을 알 필요 없이 서버에서 받은 signed URL로만 접근
🔸 프론트엔드
- 게시글 상세, 마이페이지 등 이미지가 필요한 곳에서
key
만 전달 → 서버에서 signed URL 받아<img>
에 설정 - 예전처럼 URL만 렌더링하면 되던 구조에서, 비동기 요청 후 렌더링하는 흐름으로 코드도 일부 변경
✅ 그 결과
- S3 버킷을 완전히 private 상태로 전환하면서도 이미지 접근 가능
- presigned URL의 유효 시간 내에서만 접근 가능하므로 보안 강화
- 향후 유저 인증 기반의 다운로드 제한 등도 유연하게 적용 가능
- 서버와 클라이언트 모두에서 key 기반으로만 관리하게 되어 정책 일관성 유지
2️⃣ 게시판 필터링 기능 구현 및 타입 연동
🧩 기능 정의
- 악기 거래 게시판에서는 악기 분류별로 필터링 기능 제공
- 세션 모집 게시판에서는 날짜, 지역, 실력,
💡 해결 방식
- 백엔드의 ENUM 타입을 프론트에서도 타입으로 직접 사용
- 게시판 필터 (거래 타입, 상태, 카테고리, 지역, 실력 등) 정교화
⚙️ 구현 방식
- URLSearchParams 기반 요청
- QueryBuilder 조건 필터링 처리
- 프론트에서 ENUM 매핑 및 타입 연동
🤔 추가 고민해볼 점
- 백엔드에서 필터 항목을
ENUM
으로 정의함으로써 타입 안전성과 프론트 연동 측면에선 편리했지만, 항목 추가·변경 시 직접 코드를 수정하고 배포해야 하는 구조는 유지보수 측면에서 불편함이 존재함 - 추후에는 관리자 패널 등을 통해 필터 항목을 동적으로 관리할 수 있도록 ENUM → DB 기반 코드 테이블 전환을 고려해볼 필요가 있음
- 이번에는 빠른 개발과 타입 연동의 이점을 고려해 ENUM을 선택했으나, 확장 가능성과 운영 편의성을 위해 장기적으로는 유연한 구조 전환이 필요
3️⃣ 영상 프리뷰 연동: 바이닐과 게시판 연결
🧩 문제
바이닐에 올린 영상 콘텐츠를 게시글과 연동할 수 없었고, 시각적으로 보여줄 방법도 없었음
💡 해결 방식
- 게시글 상세에 바이닐 영상 프리뷰 노출
- 마이페이지 등에서도 최신순 프리뷰 정렬 적용
⚙️ 구현 방식
/search-video/preview
API 호출로 프리뷰 조회- 게시글 내에서 프리뷰 썸네일 및 연결된 영상 보기 지원
4️⃣ 이미지 리사이징 개선: 품질 저하 방지 및 퍼포먼스 최적화
🧩 문제
기존에는 무조건 0.2배로 압축해 화질 저하가 있었고, 모든 이미지가 로딩되어 성능 저하 발생
💡 해결 방식
- 조건부 리사이징 도입 (용량/너비 기준 초과 시만 리사이징)
- 원본 + 썸네일을 동시에 저장하고, 목록에서는 썸네일만 사용
⚙️ 구현 방식
- canvas 기반 리사이징
- 썸네일 key 따로 분리해 저장 (
thumbnail
suffix)
🤔 추가 고민해볼 점
- 현재 클라이언트에서 canvas API를 이용해 이미지 리사이징을 수행하고 있다. 일반적인 이미지(5MB 이하, 3000px 이하)에서는 큰 문제가 없지만, 용량이 큰 이미지나 고해상도 원본의 경우 브라우저 메모리를 많이 사용할 수 있다.
- 특히 모바일 디바이스나 성능이 낮은 환경에서는 리사이징 시 지연이 발생하거나, 브라우저가 멈춘 것처럼 보일 수도 있다.
- canvas API는 기본적으로 싱글 스레드로 동작하기 때문에, UI를 블로킹할 가능성도 존재한다. 따라서 향후에는:
- Web Worker로 분리해 리사이징 비동기 처리
- FFmpeg.wasm 또는 Pica 같은 성능 최적화된 라이브러리 활용
- 서버 측 리사이징 기능 제공 (ex: Lambda, CloudFront Function 등)
등의 방식으로 클라이언트 부하를 줄이는 리팩토링 여지도 고려해볼 수 있다.
5️⃣ 세션 모집 로직 리팩토링
🧩 문제
- 세션별 정원과 지원자 수가 불일치
- 전체 모집 인원만 증가 → 세션 단위 인원 초과 가능
- 모집 인원 변경 시 기존 지원자를 고려하지 않음
💡 해결 방식
- 세션 단위 nowRecruitCount 기준으로 먼저 증가
- totalRecruitCount = 세션 합산값으로 동기화
- 모집 인원 < 기존 지원자 수일 경우 에러 처리
⚙️ 구현 방식
- POST 지원 → 이후 GET 요청으로 반영된 상태 즉시 갱신
- 에러 메시지 분기 (400, 403 등)
- 세션별 인원 대신 총 지원자 / 총 정원 UX로 간략화
6️⃣ 마이페이지 개선 및 통합 피드
💡 작업 내용
- DB 구조에서
user
직접 참조로 수정 - 마이페이지에 중고거래, 커뮤니티, 바이닐 게시글을 썸네일 피드로 통합 표시
- 썸네일 정렬 기준 → 최신순으로 변경
7️⃣ 프론트 UI 컴포넌트화 및 toast 개선
🧩 문제
- input, textarea 등 다양한 UI 요소가 페이지마다 중복 구현되어 있었고
- react-hot-toast 사용 시 duration 3000으로 설정해도 route 변경 시 toast가 사라지지 않는 문제 발생
💡 해결 방식
- 공통적으로 사용되는 UI 요소는 모두 컴포넌트화
useEffect
를 통해 페이지 진입 시toast.dismiss()
호출하여 자동 제거
useEffect(() => {
toast.dismiss();
}, []);
🤔 고민했던 점
App.tsx
에서useLocation()
을 사용해 라우트 변경 시toast.dismiss()
를 호출하는 방식도 고려했으나, 라우팅 전역 처리 방식은 예기치 않은 부작용이나 유지보수 이슈가 있을 수 있어, 해당 페이지에서 명시적으로 toast를 제거하는 쪽으로 방향을 잡음
🧺 그 외 리팩토링 및 마이너 수정
- 모바일 input 태그 줌인 방지 (index.css, index.html)
- TextArea 줄바꿈 저장 css 수정
- Presigned URL 방식으로 모든 이미지 접근 구조 변경
- 프론트 링크 오류 해결
- 바이닐 프리뷰 BE 리팩토링
🎨 개발 외 최종 발표 준비!
마지막 주차는 코드 작업은 마지막 폴리싱 작업만 진행하고 최종 발표를 위한 준비가 메인 과제였다. 그 중 나는 최종 발표용 포스터 디자인을 맡아서 진행했다. 기존 기수들의 포스터도 참고했지만, 그저 그대로 따라 하기보다는 조금 더 예쁘고, 랜딩페이지 느낌이 나는 디자인으로 만들어보고 싶었다. 솔직히 말하면, 발표가 끝난 뒤에 포트폴리오에도 넣을 수 있을 정도로 보여줄 수 있는 퀄리티로 만들고 싶었다. 그냥 딱 봤을 때, "이거 잘 만들었다" 싶은 느낌? 그래서 색감이나 여백 같은 것도 꽤 고민하면서 디자인했다.
🧠 기술적 챌린지 정리하면서 느낀 점
우리 팀은 기능을 나누어서 개발을 했기 때문에 내가 개발하지 않은 기능에 대해서 이해하는 것이 먼저였다. 그렇기 위해서 코드를 다시 뜯어봐야 했다. PR 로그를 다시 읽고, 구조를 파악하고, 왜 이렇게 구현했는지를 정리하는 데 생각보다 시간이 꽤 걸렸다. 특히 어려웠던 건, 내가 이해하는 것도 중요하지만 “이걸 다른 사람한테 어떻게 쉽게 설명하지?” 이걸 고민하는 게 더 어렵더라. 그래도 이 과정을 통해 내가 만든 기능 하나하나를 다시 정리할 수 있었고, 마치 코드 리뷰를 역으로 내가 내 코드에 해보는 느낌이 들어서 꽤 유익했다.
최종 발표가 끝난 이후에는 포스터와 함께, 이때 정리한 기술적 챌린지 회고를 따로 블로그 글로 올릴 예정이다! 지금보다 조금 더 다듬어서, 진짜 프로젝트를 마무리하면서 했던 고민들과 해결 과정들을 잘 담아보려고 한다.
'Today I Learned' 카테고리의 다른 글
안녕, 나의 여름이여 (5) | 2025.08.06 |
---|---|
[TIL] 나만무 6/30 ~ 7/6 (4) | 2025.07.07 |
[TIL] 나만무 6/23 ~ 6/29 (2) | 2025.06.30 |
[TIL] 나만무 6/18 ~ 6/22 (1) | 2025.06.23 |
[크래프톤정글] Week0 : 입소 3일차 TIL (3/12) (0) | 2025.03.13 |