블로그
전체 52025. 04. 07.
1
워밍업 클럽 스터디 3기 FS - 스터디 후기
워밍업 클럽을 처음 알게 된 건 사실 이번 3기가 아니라 지난 2기 때였다.당시에도 이 스터디에 흥미가 있었지만 개인적인 사유로 아쉽게도 참여하지 못했는데, 이번 3기에 Next.js 과정이 포함되었다는 소식을 듣고 주저 없이 참여 신청했다.결과적으로는 아주 만족스러운 선택이었다. SLL 회고Startany를 배제하고, 모든 컴포넌트에 명시적 타입 선언을 적용해보았다.단일 책임 원칙을 기반으로, UI와 비즈니스 로직을 분리하는 컴포넌트 구조화를 시도해보았다.클라이언트와 서버 상태를 별도로 관리하기 위해서 React Query를 도입해보았다.Zod + React Hook Form 조합을 처음 적용해보면서 스키마 기반 유효성 검증의 흐름을 경험해보았다.LearnUI와 비즈니스 로직을 분리하는 패턴은 가독성 향상에는 도움이 되었지만, 반대로 전체적인 복잡도가 증가할 수 있다는 점도 느꼈다.Next.js 환경에서는 SWR보다 React Query가 더 세밀한 제어와 상태 관리에 적합하다는 것을 체감했다.기능을 직접 구현하는것도 충분히 의미가 있지만, 이미 검증된 라이브러리를 적절히 도입하는 것이 생산성 측면이나 유지보수 측면에서 더 큰 도움이 될 수 있다는 점을 배웠다.Love타입 설계와 컴포넌트 구조를 직접 개선해본 경험 자체만으로 코드에 대한 자신감과 설계 감각이 향상되었음을 체감했다.주차별 다른 주제의 과제를 만들어가는 과정을 통해서 작은 성공의 중요성을 다시 한 번 느낄 수 있었다. 마무리짧다면 짧은 4주의 기간동안 매주 스스로 도전하고 개선해가는 과정을 통해 한 걸음 더 성장할 수 있었다고 느꼈다.혹시나 아직도 워밍업 클럽 참여를 망설이고 있다면 꼭 한 번 경험해보길 추천하며 스터디 회고를 마친다.
풀스택
・
인프런
・
인프런워밍업클럽
・
스터디3기
2025. 03. 30.
0
워밍업 클럽 스터디 3기 FS - 4주차 발자국
3주차 과제를 일찍 마무리하고 4주차는 조금 일찍 학습을 시작했기 때문인지 유난히도 길었던 마지막 주차도 끝나간다.이번주는 특히나 굉장히 많은 이슈를 경험했다. 결론부터 말하자면 채팅방 생각보다 구현하기 쉽지 않았다. 간단하게 하나만 언급하자면 URL로 장난질 치는 것에 대한 방어로직 구현이 특히 어려웠다. 사실 예전에 firebase 기반으로 미니 SNS를 구현했던 경험이 있는데 이번에 채팅을 구현했으니 이 프로젝트를 그대로 고도화해서 SNS를 완성해볼 계획이다. (실제로 완성할 수 있을지는...)📝 4주차 학습Supabase Auth이메일/비밀번호, OAuth, Magic Link, SMS 인증 등 다양한 인증 방식을 지원하는 인증 서비스supabase.auth.signUp, signInWithOAuth, getUser() 등으로 유저 관리와 세션 제어가 가능JWT 기반으로 RLS와 연동되며, 로그인 상태 자동 유지 및 세션 갱신 기능도 제공Supabase RealtimePostgreSQL의 Listen, Notify 기능을 기반으로 실시간 데이터 동기화 제공테이블 변경(Insert, Update, Delete)을 클라이언트에서 실시간으로 브로드캐스트supabase.chaeenel()로 원하는 이벤트를 구독Supabase RLS(Row Level Security)데이터베이스의 행(Row) 단위로 접근 제어를 설정하는 보안 기능Create Policy를 적용하여 유저별로 조회/ 수정 권한을 세밀하게 조정활성화 시 명시적인 권한 정책 필수아래는 개인적으로 나머지 공부로 학습하고 적용해본 라이브러리입니다.ZodTS 환경에서 런타임 스키마 검증과 타입 추론을 제공하는 유효성 라이브러리z.object() 등 메서드로 구조화된 데이터의 유효성 검사 수행 및 타입 자동 생성서버 및 클라이언트 모두에서 안전한 폼 및 api 검증에 활용React Hook Form과 함께 사용하기 유용한 라이브러리React Tostify토스트 메시지를 손쉽게 띄울 수 있는 라이브러리간단한 API로 다양한 유형의 토스트 알림 제공강력한 커스터마이징 제공Kyfetch API 기반의 모던한 HTTP 클라이언트간결한 문법 제공자동 재시도, 에러 핸들링, 인터셉터 등 확장성과 다수의 편의 기능 포함📋 4주차 미션💬GitHub 저장소👉체험하러 가기 미션 해결 과정 요약이번주 미션의 필수 구현 과제는 Supabas Auth를 사용한 회원가입, 로그인 기능 구현 및 Supabase Realtime을 활용한 1:1 채팅 기능 구현하기였다. 추가 구현 과제는 메시지 삭제, 메시지 알림, 메시지 읽음 여부 표시, 채팅 신고, 유저 차단 기능 등 자유롭게 구현하기였는데 시간 관계 상 전부 구현하긴 어려워서 비교적 쉬운 메시지 삭제와 메시지 알림을 제한적으로 구현했다. 원래는 DB 스키마를 꼼꼼하게 고민하고 시작했어야하는데 급하게 하다보니 처음 계획했던 내용과 많이 달라졌다.myon_users.id -> auth.users.idSupabase Auth로 회원가입된 유저만 등록 가능myon_rooms.userA_id -> myon.users.idmyon_rooms.userB_id -> myon.users.id회원가입된 유저만 채팅에 참여 가능myon_messages.sender_id -> myon_users.id회원가입된 유저만 채팅 전송 가능myon_rooms.last_message_id -> myon_messages.id가장 최근 메시지 미리보기 시 테이블 join에 활용myon_users.username회원가입 시 입력한 닉네임을 기반으로 한글과 특수 문자 등을 제거한 후 중복 발생 시 유틸함수를 통해서 suffix를 불여서 고유한 username을 자동 생성(회원가입 시 입력 폼의 간소화를 위한 선택)✅ 이메일 로그인, 회원가입GET app/auth/signup/callbackPOST api/user/email/register✅ OAuth 로그인, 회원가입GET app/auth/oauth/kakao/callbackPOST api/user/oauth/register우선 회원 기능부터 만들기 시작했는데 강의 패턴을 참고하여 자동 생성되는 auth.users 테이블만 사용하여 회원 로직을 만들었는데 메타 데이터의 형태가 provider 별로 일정하지 않고.. 무엇보다도 auth.users 테이블은 커스텀이 제한적이기 때문에 public.users 테이블을 별도로 관리하였다. 카카오 계정 로그인의 경우 user_metadata를 커스텀 인터페이스로 관리하여 타입 오류를 방지하였다.또한 auth.users 테이블만 단독 사용시의 문제는 회원가입 단계에서 사전에 이메일 중복 검증이 어렵다는 점도 단점이었다.찾아보니 보안상의 이유로 Supabase 내부적으로 auth.users 테이블을 직접 조회하는 기능은 별도로 제공하지 않아서 가입 요청 후에 에러를 캐치할 수 있는 구조이기 때문에 이부분도 public.users를 조회하여 이메일 중복 검증을 통과한 경우에만 회원가입 요청을 할 수 있도록 처리했다.회원가입이나 로그인 인풋 유효성 검증은 Zod + react-hook-form 라이브러리도 대체했다.이메일 회원가입이메일 로그인✅1:1 채팅POST, GET api/rooms/[roomId]POST api/messagesGET api/messages/[roomId]문제는 채팅 기능 구현이었는데 채팅 기능 자체는 Realtime 구독으로 어렵지않게 완성했으나 문제는 방어로직 구현이었다. 몇 가지 예시를 들자면 /direct-message 로 접근 시에 해당 페이지에서 나 자신을 제외한 모든 유저 리스트를 불러온 뒤, /direct-message/:roomId 로 동적 라우트를 구현할 때, 처음에는 고유성을 보장하기 위해서userA_username-userB_username-suffix 형태로 roomId를 생성하는 유틸 함수를 사용했는데 렌더링 시 마다 suffix가 변동되기 때문에 서버단에서 해당 URL이 유효한 URL인지 검증하기가 쉽지 않아서 userA_username과 userB_username을 정렬하여 항상 동일한 roomId를 생성하는 순수 유틸 함수로 변경하여 해당 문제를 해결하였다.export function generateRoomId({ usernameA, usernameB }: { usernameA: string; usernameB: string }) { const sortedUsernames = [usernameA, usernameB].sort() return `${sortedUsernames[0]}-${sortedUsernames[1]}` }과제 추가 구현 기능✅ 메시지 삭제(Soft Delete)PATCH api/message/[messageId]메시지 삭제는 두가지 방식이 있는데 DELETE 메서드를 사용하여 DB Row에서 아예 삭제하는 하드 삭제와 실제 DB Row에서 삭제하지 않지만 is_delete 같은 플래그를 true 하여 클라이언트단에서 감추는 방식인 소프트 삭제 방식이 있다. 하드 삭제의 경우 DB 공간 절약이 필요하거나 탈퇴 회원 정보 등 영구 삭제가 필요한 경우에 적합하고 소프트 삭제의 경우는 복구가 필요하거나 삭제 이력을 추적해야하는 경우에 적합한데 채팅은 로그를 남기는게 중요해서 개인적으로는 소프트 삭제로 구현했다.메시지 호버 시 삭제 아이콘 표시삭제된 메시지✅ 메시지 알림(토스트 메시지 활용)별도 API route 없이 구독으로 구현그냥 마무리하기 아쉬워서 추추가 기능으로 구현했다. 예전부터 토스트 메시지에 관심이 많았는데 직접 구현해보니 생각보다 비효율적이라서 react-tostify 라는 라이브러리를 적용했다.토스트 메시지는 스크롤이 최하단이 아닌 지난 메시지를 읽고 있을때만 우측 상단에 스택 형태로 알림을 보내도록 구현했다. 👀 4주차 회고이번 주는 지난 스터디 기간 동안 진행했던 프로젝트를 배포하는 과정이 포함되어 있었기 때문에, 추가적인 기능보다는 안정적인 배포에 중점을 둘 계획이었으나.. 다행히도 지난주에 미리 매를 맞아두었기 때문에 이번 주 과제 배포는 크게 문제 없이 마무리할 수 있었다.다만 실제 배포 경험이 많지 않다 보니 환경 변수 관련 이슈를 자주 겪게 되었고, OAuth Redirect URL 설정 누락, 빌드 시 타입 오류 등의 경험으로 배포 시 고려해야 할 요소들을 더 잘 이해하게 되었다. 앞으로는 다른 프로젝트 배포 시 참고할 수 있도록 트러블슈팅 내역을 꼼꼼하게 정리하는 습관을 들일 계획이다.이번주에 과제를 진행하면서 딱 한가지 아쉬웠던 점이라면 2주차부터 꾸준히 적용해오던 Container-Presentational Component 패턴을 이번에는 적용시키지 못했다는 점이다. 이번주 과제가 전반적으로 복잡도가 높다보니 관심사 분리를 코드에 녹여내지 못했으나 점진적으로 리팩토링을 통해서 개선해나가기 위해서 백로그에 기록해두었다. 스터디 이후..이번에 구현한 채팅 기능은 실제 배포를 해보니 전송 시 약간의 딜레이가 발생하는 것을 발견했다. 지금 타이밍에서 메시지 전송에 대한 낙관적 업데이트를 적용해서 최적화하는것이 가장 시급한 과제라고 생각한다.끝으로 이번 프로젝트는 이후에 포트폴리오로 활용할 수 있도록 고도화 작업을 이어갈 생각이며, 동시에 타입스크립트에 대한 이해를 더 심화시키고, 쉽진 않겠지만 최근 관심이 생긴 테스트 코드 작성 관련 학습도 병행해나가 보려 한다.정말 마지막으로.. 풀스택 과정을 포함한 모든 3기 스터디 러너분들, 멘토님들과 서포터분들, 워밍업 클럽 관계자 여러분들 모두 고생하셨습니다👏 여러분들 덕분에 좋은 인사이트 얻어갑니다 :)
풀스택
・
워밍업클럽
・
3기
・
풀스택
・
Next.js
・
4주차
・
회고
・
미션
2025. 03. 20.
0
워밍업 클럽 스터디 3기 FS - 3주차 발자국
워밍업 클럽도 벌써 3주차!처음 스터디를 시작할 때와 비교하면 가장 큰 소득은 React Query에 익숙해졌다는 것 그리고 무언가에 몰입하면서 성취감을 느꼈다는 것이다.단순히 기능을 구현하는 걸 넘어서 최적화나 에러 핸들링까지 고민하는 과정이 꽤 재밌었다.이번주에는 인피니트 쿼리와 추가 기능으로 좋아요 기능을 구현해야해서 지난주와 마찬가지로 조금 일찍 학습을 시작했다.깃 레포는 역시 첫번째 과제에 사용했던 템플릿을 거의 수정없이 그대로 사용해서 역시나 개발환경 구축은 무리없이 진행했다.다만, 한 가지 아쉬운 점이라면 터보레포 같은 모노레포 도구를 도입했어야 하지 않았나 하는 생각이 들었다는 점이다.현재 방식은 각 주차별 과제를 독립적인 레포로 관리하고 있는데, 공통 유틸이나 자주 사용하는 설정 파일을 계속 복붙하는 과정에서 의외로 피로감을 느꼈다.우선 마지막 과제까진 지금의 방식을 유지하고 스터디 마무리 이후에 터보 레포에 대해서는 개별적으로 학습을 해볼 예정이다.📝 3주차 학습useInfiniteQuery무한 스크롤 및 페이지네이션을 위한 React Query 훅fetchNextPage를 사용해 추가 데이터 요청getNextPageParams로 다음 페이지 여부 관리 useInViewreact-intersection-observer 라이브러리의 훅특정 요소가 화면에 보이는지 감지(뷰포트 진입 여부로 확인)무한 스크롤 구현 시 useInfiniteQuery와 함께 사용threshold, rootMargin으로 감지 범위 조절 가능📋 3주차 미션💬 GitHub 저장소🚀 데모 영상 보러가기미션 해결 과정 요약이번주 미션의 필수 구현 과제는 무한 스크롤과 SEO 추가, 영화 검색 기능 구현하기였다. 추가 기능으로 영화 좋아요 기능을 구현했는데, 예전에 SNS를 만들때 경험해봤던 기능이라 쉽게 구현할 수 있을 것이라 기대했다. 하지만 예상과는 달리, 유저 식별 기능이 없다는 점이 문제였다. SNS 좋아요 기능 구현 당시에는 사용자 ID 기반으로 좋아요를 관리했지만 이번 프로젝트는 익명 유저 환경이라 데이터를 어떻게 저장하고 관리할지 고민이 필요했다.처음에는 movies, users, liked_movies 3개의 테이블을 생성하여 user_id, movies_id를 복합키로 설정해 브라우저별 익명 유저를 관리하는 방식을 시도했으나 구현 복잡도가 너무 높아지는 문제로 단순화하는 방식으로 변경했다.movies 단일 테이블에 like_count 필드를 추가하고 브라우저별로 좋아요 상태를 관리하는 방식으로 해당 기능을 구현했다. 이 방식의 단점은 브라우저 변경 시 개인별 좋아요 리스트를 추적할 수 없다는 점이지만 애초에 유저 식별 기능을 배제한 상황에서 선택할 수 있는 최적의 방식이라고 판단하여 적용했다.그리고 강의에서는 movies.id를 auto increment id로 구현했지만 더 나은 확장성을 위해서 uuid를 고려했다. 다만 uuid는 URL에서 사용하기 불편하여 가독성이 좋은 slug 칼럼을 별도로 추가하였다. API 요청 파라미터를 id에서 slug로 대체하면서 가독성과 SEO 최적화까지 함께 챙겨갈 수 있었다.slug Column 추가ALTER TABLE myreel_movies ADD COLUMN slug TEXT UNIQUE;중복되는 Row 제거 (제공되는 DB에 중복되는 데이터가 9건 발견되었다.)DELETE FROM myreel_movies WHERE id NOT IN ( SELECT id FROM ( SELECT id, title, order_index, ROW_NUMBER() OVER (PARTITION BY title ORDER BY order_index ASC) AS row_num FROM myreel_movies ) ranked WHERE row_num = 1 );영화 title 기준으로 slug 생성예시 - 'Dune: Part Two' -> 'dune-part-two'UPDATE movies SET slug = LOWER(REGEXP_REPLACE(title, '[^a-zA-Z0-9]+', '-', 'g')) WHERE slug IS NULL;과제 추가 구현 기능✅ 영화 좋아요 추가api/movies/:slug/likeconst likeMovie = async () => { try { const res = await fetch(`${baseUrl}${API_ENDPOINTS.LIKE(slug)}`, { method: 'POST', }) if (!res.ok) { throw new Error(CLIENT_ERROR.MOVIE_LIKE_FAILED.message) } const data: LikeMovieResponseDTO = await res.json() setLikeCount(data.like_count) // 서버에서 받아온 새로운 좋아요 수로 업데이트 setIsLiked(true) // 로컬 스토리지에 영화 추가 또는 업데이트 const likedMovies: LikedMovie[] = JSON.parse(localStorage.getItem('likedMovies') || '[]') // 이미 좋아요를 누른 영화가 있다면, likeCount를 업데이트 const existingMovieIndex = likedMovies.findIndex((movie) => movie.slug === slug) if (existingMovieIndex >= 0) { likedMovies[existingMovieIndex].likeCount = data.like_count // 좋아요 수 업데이트 } else { // 좋아요를 누른 적이 없다면 새로 추가 const newLikedMovie = { slug, likeCount: data.like_count } likedMovies.push(newLikedMovie) } localStorage.setItem('likedMovies', JSON.stringify(likedMovies)) } catch (error) { console.error(error) } }✅ 영화 좋아요 삭제api/movies/:slug/unlikeconst unlikeMovie = async () => { try { const res = await fetch(`${baseUrl}${API_ENDPOINTS.UNLIKE(slug)}`, { method: 'POST', }) if (!res.ok) { throw new Error(CLIENT_ERROR.MOVIE_UNLIKE_FAILED.message) } const data: LikeMovieResponseDTO = await res.json() setLikeCount(data.like_count) setIsLiked(false) // 로컬 스토리지에서 해당 영화 정보 삭제 const likedMovies: LikedMovie[] = JSON.parse(localStorage.getItem('likedMovies') || '[]') const updatedLikedMovies = likedMovies.filter((movie) => movie.slug !== slug) // 로컬 스토리지 갱신 localStorage.setItem('likedMovies', JSON.stringify(updatedLikedMovies)) } catch (error) { console.error(error) } }개인 챌린지 기능✅ 메인 페이지 최상단으로 가는 버튼 추가메인 페이지에서 500px 이상 스크롤 내릴 경우 최상단으로 이동하는 버튼 생성behavior: 'smooth' 로 부드럽게 이동 ✅ 검색 결과 없을 경우, 좋아요 많은 순 추천 영화 6개 노출되는 기능 구현좋아요가 많은 영화 외에도 최근 개봉한 영화 같은 다양한 리스트 제공 예정api/movies/most-liked👀 3주차 회고지난주에 적용했던 매니져 컴포넌트 / UI 컴포넌트로 분리하는 방식이 Container-Presentational Component 패턴 과 유사한 방식이라는 것을 다른 러너분의 발자국을 통해 알게되었다. 궁금해서 조금 더 찾아보니, 이 패턴은 과거 클래스형 컴포넌트 시절에는 HOC(High Order Component)와 함께 많이 사용되었지만, 함수형 컴포넌트에서도 여전히 유효한 방식이라는 것을 알게되었다. 이번주에는 기존 패턴을 유지하면서도, 비즈니스 로직을 최대한 커스텀 훅으로 분리하는 연습을 진행했다. 이를 통해 컴포넌트의 역할을 더욱 명확하게 나누고, 재사용성과 유지보수성을 높이는 방향으로 조금씩 개선되고 있다는 것을 체감했다.👻 배포 관련 이슈 (3월 22일 추가)4주차에 스터디 기간 개발한 4개의 프로젝트를 모두 배포하는것이 기존 스터디 일정이지만.. 시간적 여유가 생겨서 1~3주차 프로젝트를 미리 배포해봤다. vercel은 기존에 사용하던 툴이었는데 한번에 3개의 프로젝트를 배포하려고 시도하는 과정에서 수 많은 에러를 경험했다. 4주차 프로젝트 배포시, 추후 다른 프로젝트 배포시에 참고할 수 있도록 간단하게 정리해본다. ✅ @/components/... 앨리어스 관련 캐싱 이슈문제 개발 환경에서는 정상 작동하던 import가 Vercel 배포 시에만 Module not found 에러 발생원인Vercel의 캐싱 문제 또는 파일명 인식 관련 문제(대소문자, 내부 경로 변경 후 캐시 꼬임)해결@ 앨리어스 문제를 의심하여 상대 경로로 변경 후 재배포 시도 -> 해결 안됨컴포넌트 경로의 대소문자 확인후 재배포 시도 -> 해결 안됨 Title.tsx 파일명을 AppTitle.tsx로 변경하여 강제로 캐시 무력화 후 재배포 시도 -> 해결 ✅ params 비동기 처리 관련 타입 에러 (Next.js 15)문제page.tsx에서 params를 비동기적으로 처리하려 하자, params 타입이 Promise로 인식되어 타입 오류 발생원인 (깃헙 이슈 참고)Next.js 15 내부적으로 PageProps가 비동기적 처리를 기대하거나 타입 추론이 변경됨params 타입이 Promise로 추론되어 관련 에러 발생PageParams 제네릭 타입 해석 충돌next dev에서는 정상 작동하지만 next build 시 오류 발생해결params의 인터페이스를 명시적 타이핑 -> 해결 안됨params의 타입 any로 명시하고 타입 단언으로 처리 -> 해결 안됨배포 시 안정성 확보를 위해서 Next.js 14 + React 18 버전으로 롤백 -> 해결✅ Tailwind CSS 적용 안됨문제배포된 페이지에서 Tailwindcss 클래스가 적용되지 않음원인Next.js 15 -> Next.js 14, React 19 -> React 18로 롤백하는 과정에서 관련된 의존성 충돌이 일어난것으로 예상됨해결Tailwindcss, postcss, autoprefixer 의존성 삭제 후 캐시 초기화 후 재설치 -> 해결 ✅ 환경 변수(NEXT_PUBLIC_BASE_URL) 미설정으로 fetch 실패문제빌드 시 fetch 요청이 localhost:3000으로 날아가면서 ECONNREFUSED 에러 발생원인Vercel 환경 변수 설정 시 NEXT_PUBLIC_BASE_URL 값을 localhost:3000으로 설정하여 에러 발생해결해당 환경 변수를 실제 배포 URL로 변경 후 재배포 시도 -> 해결
풀스택
・
워밍업클럽
・
3기
・
회고
・
발자국
・
3주차
2025. 03. 13.
2
워밍업 클럽 스터디 3기 FS - 2주차 발자국
인프런 워밍업 클럽 스터디에 참여하고 벌써 2주 차도 마무리에 접어들고 있다. 4주간의 스터디이기 때문에 생각보다 일정이 타이트하여 시간관리가 무엇보다도 중요한 시기라고 생각한다.이번 주에는 파일 업로드 기능을 구현해야하기에 1주 차 과제를 급하게 마무리하고 지난주 토요일부터 서둘어서 미리 학습을 시작했다.깃 레포지토리의 경우에는 지난주에 사용했던 템플릿을 거의 수정없이 그대로 사용해서 개발환경 구축은 크게 어렵지 않았다.이번 주 강의에서는 Supabase Storage를 사용했는데 API가 잘 준비되어있어서 사용 방법을 익히는데도 크게 어렵지는 않았다. 다만 아래에서도 언급하겠지만 Supabase Storage는 AWS S3 기반으로 구현되어 강력한 네이밍 규칙이 적용되어 개인적으로는 Supabase Storage와 Supabase DB를 함께 사용했다. 📝 2주차 학습Supabase Storage클라우드 기반 객체 저장소로, AWS S3와 유사한 방식으로 파일을 저장하고 관리하는 서비스파일 및 이미지 업로드 및 관리 기능 제공PostgreSQL과 연동 가능권한 관리(RLS) 및 퍼블릭/프라이빗 파일 설정 가능Supabase SDK 또는 Restful API로 사용 가능 ✔ 파일명 규칙 (Supabase & AWS 공통) ASCII 문자, 숫자, 일부 특수 문자 허용 (- _ . /) 파일명을 /로 구분하여 폴더처럼 사용 가능 (folder/image.png) 공백 포함 가능하지만, URL Encoding이 필요할 수 있음 파일명에 한글, 이모지, 특수문자가 포함될 경우 정상적으로 업로드되지 않을 가능성이 있음 → URL-safe 변환 권장 React DropzoneReact에서 간편하게 파일 Drag & Drop 기능을 구현할 수 있는 라이브러리HTML5 File API를 활용하여 파일 업로드를 쉽게 구현할 수 있는 기능을 제공파일 타입, 크기, 개수 등 다양한 제약 조건 설정 가능비동기로 파일을 처리할 수 있는 onDrop 이벤트 제공 📋 2주차 미션💬 GitHub 저장소🚀 데모 영상 보러가기미션 해결 과정 요약2주 차 미션은 Next.js, React Query, TailwindCSS를 사용하여 이미지 업로드 앱을 구현하기였다. 필수 구현 기능으로는 이미지 업로드 기능(클릭 업로드 방식과 Drag & Drop 방식, 다중 업로드)과 이미지 삭제와 이미지 검색 기능 구현하기였다. 추가 기능은 파일의 마지막 수정 시간을 화면에 출력하는 UI 구현하기였다. 여기에 과제의 완성도를 높이기 위해서 개인적인 챌린지로 파일명에 한글 또는 특수문자 포함된 파일 업로드 기능, 1MB 미만으로 이미지를 압축하는 기능, 다운로드 기능을 추가로 구현하였다. 과제 추가 구현 기능✅ 마지막 수정 시간 표시const { error: insertError } = await supabase.from(DB_TABLE_NAME).insert({ name: file.name, originalName: originalFileName, imageId: uploadedFile.id, imageUrl: publicUrl, createdAt: new Date(file.lastModified).toISOString(), })생성: DB에 파일 데이터 업로드 시 createdAt에 file.lastModified를 ISOString 형식으로 저장 if (dbData) { const { error: updateError } = await supabase .from(DB_TABLE_NAME) .update({ name: file.name, originalName: originalFileName, imageUrl: publicUrl, updatedAt: new Date().toISOString(), }) .eq('imageId', uploadedFile.id)수정: DB에 해당 ID가 존재할 경우 updatedAt(string | null)에 현재 시간을 ISOString 형식으로 저장// DropImageManager 컴포넌트에서 생성 시간, 수정 시간을 포멧팅하여 DropImage 컴포넌트에 프롭스로 전달 const localCreatedAt = getLocalTime(image.createdAt) const localUpdatedAt = image.updatedAt ? getLocalTime(image.updatedAt) : null {localUpdatedAt ? localUpdatedAt : localCreatedAt} {localUpdatedAt && ( (수정) )} 출력: updatedAt이 존재할 경우 updatedAt과 (수정) 을 함께 출력, updatedAt: null이라면 createdAt를 출력 개인 챌린지 기능✅ 파일명 자동 변환 후 이미지 업로드하는 기능을 구현 (UX 개선)파일명 검증: 정규식을 활용하여 한글 및 특수 문자 포함 여부를 확인자동 변환: 검증 후 8자리 랜덤 문자열로 안전한 파일명 생성업로드 처리: 변환된 파일명으로 File 객체 생성 후 formData.append로 원본 파일명 함께 전송서버 액션: Supabase Storage에 저장 후, 완료 시 DB에 메타데이터 저장하여 연동결론: 파일명 변환을 자동화하여 업로드 오류를 방지하고, 원본 파일명도 유지하여 검색 및 관리 UX 개선✅ 파일 용량이 1MB 초과 시 자동 압축 후 업로드하는 기능을 구현 (UX 개선)browser-image-compression 라이브러리를 사용하여 파일의 용량 검증 후 1MB 초과 시 이미지 압축 후 업로드결론: 이미지 최적화로 업로드 속도 향상, 스토리지 비용 절감 효과✅ Blob URL을 활용한 다운로드 기능 추가 (UX 개선)Blob URL 생성: 업로드된 이미지를 fetch()로 가져와 Blob으로 변환다운로드 기능 구현: window.URL.createObjectURL(blob)으로 브라우저에서 직접 다운로드 가능하도록 처리결론: Blob URL 다운로드 방식을 적용하여 최적화된 이미지를 빠르게 다운로드 받을 수 있도록 개선🚧 기능 구현 시 어려웠던 부분Supabase Storage에 전달하는 File 객체 커스텀 불가원본 파일명을 추가하려 했으나, File 객체 자체를 수정하는 것이 제한적이다.파일 객체를 복사하여 원본 파일명을 추가하는 방법 시도전개 연산자를 사용하여 객체 복사 후 원본 파일명을 추가하려 시도하였으나 file 객체는 일반적인 방법으로는 복사할 수 없는 특별한 객체이다.ExtendedFile 확장 클래스로 인스턴스를 생성했으나 서버에 전달되지 않는 문제 발생확장된 ExtendedFile 객체를 formData에 담아 서버로 전송했지만, 서버에 정상적으로 전달되지 않았다.최종 해결 방법formData.append("file", file) formData.append("originalFileName, file.name)file 객체와 원본 파일명을 함께 서버로 전송 후 가공하여 Supabase Storage의 파일명에는 안전한 파일명만 저장하고 DB에 스토리지Id, 원본 파일명, 안전한 파일명, 이미지URL 등 정보를 저장했다. 🧾 ERD 다이어그램👀 2주차 회고아직 갈 길이 멀지만, 리팩토링을 통해 Next.js의 장점을 살릴 수 있는 구조로 점점 개선되어가는 과정을 경험하면서 이번 주 역시 알차게 보냈다고 생각한다.이번 주는 특히 MVP 패턴과 비슷한 형태로 컴포넌트 구조를 잡는 것에 익숙해지는 것을 개인적인 목표로 삼았다. 처음부터 MVP 패턴을 염두해 두고 설계한 것은 아니었지만, 진행하다보니 자연스럽게 MVP와 유사한 패턴으로 정리되어 가는 것을 느꼈다.화면 렌더링 시 상호작용이 필요하지 않은 정적인 요소들까지 클라이언트 컴포넌트로 관리하면 불필요한 하이드레이션 부담이 증가할 수 있다는 점을 다시 한번 체감했다.클라이언트 컴포넌트 내에서도 역할을 나눠 서비스 레이어나 상태 관리만 담당하는 매니져 컴포넌트와 프롭스로 상태를 전달받아 단순히 화면을 렌더링을 담당하는 UI 컴포넌트로 분리하는 연습을 진행했다.이러한 구조로 개선하면서 클라이언트 컴포넌트의 부담을 줄이고, 유지보수성을 높이는 방향으로 점차 최적화되고 있다는 점이 느껴졌다. 아직 개선해야 할 부분이 많지만 점진적으로 개선하여 더 나은 아키텍쳐를 만들어가는 과정이 의미 있었다고 생각한다.
풀스택
・
워밍업클럽
・
3기
・
발자국
・
회고
・
과제
・
미션
2025. 03. 07.
0
워밍업 클럽 스터디 3기 FS - 1주차 발자국
인프런 워밍업 클럽 스터디 3기 풀스택 과정을 시작하고 정신차려보니 벌써 금요일이 되었다.1주 차는 본격적으로 Next.js를 다루기 위한 워밍업 단계라고 생각한다. 이번주는 개발 환경을 어떻게 구축하는지와 Next.js의 주요 기능 등을 빠르게 학습했다. 또한, React Query, Recoil 등의 상태 관리 라이브러리의 기본적인 사용법도 강의를 들으면서 복기하였다.과제의 경우, 먼저 빠르게 해결할 수 있는 부분을 최대한 완성하고, 추가 기능을 구현하는 데 집중했다. 동시에 UI와 UX를 개선하는 것을 이번 주 목표로 삼아 보다 완성도 높은 결과물을 만들고자 했다. 📝 1주 차 학습SupabaseNoSQL 기반의 Firebase의 대체제로, PostgreSQL 기반의 BaaS를 제공하는 플랫폼서버리스 환경에서 빠르게 백엔드를 구축할 수 있도록 지원데이터베이스, 인증, 스토리지, 실시간 기능, 서버리스 함수, 관리 대시보드 지원Next.jsReact 기반의 풀스택 개발을 하기에 최적화 된 웹 프레임워크SSR, SSG를 지원API 라우트 기능을 제공하여 별도의 백엔드 서버 없이도 간단한 API 기능 수행이미지 최적화, 자동 코드 분할을 지원하여 개발 생산성을 높임서버 액션을 지원하여 API 라우트 없이도 클라이언트에서 서버 코드 호출 가능레이아웃 또는 컴포넌트별로 메타 데이터 설정 가능TailwindCSS유틸리티 퍼스트 CSS 프레임워크미리 정의된 클래스를 조합하여 스타일을 적용하는 방식 (tailwind.config.ts에서 커스텀 클래스 정의 가능)JIT 컴파일을 지원하여 실제로 사용된 클래스만 포함하여 CSS 파일 크기 최적화 적용반응형 디자인 지원(sm:, md:, lg: 등)다크 모드 및 테마 설정 지원(dark: )공식 및 커스텀 플러그인을 활용하여 다양한 추가 기능 제공React Query(Tanstack Query)데이터 패칭, 캐싱, 동기화 등을 쉽게 관리할 수 있게 도와주는 React 라이브러리로 주로 서버 상태 관리 용도로 사용자동 데이터 패칭, 자동 캐싱, 자동 리패칭을 제공하여 API 서버 호출을 최소화로 관리Mutation을 통한 데이터 수정쿼리 무효화를 통해 데이터를 최신 상태로 유지RecoilReact 상태 관리 라이브러리로 Context API보다 유연하고 확장성 있는 방법을 제공전역 상태 관리, 컴포넌트 간 상태 공유, 파생 상태 계산, 비동기 작업 처리 기능 제공2025.3.7 기준 약 2년간 업데이트가 이루어지지 않고 있기 때문에, Zustand, Jotai, RTK 등의 대체 라이브러리 추천 📋 1주 차 미션💬 깃헙 저장소🚀 과제 시연 영상 보러가기미션 해결 과정 요약1주 차 미션은 Next.js와 React Query 그리고 TailwindCSS를 사용하여 Todo List 앱 만들기였다. 기본적인 CRUD 기능을 구현하는 과제로, 목표 추가, 목표 수정, 완료 표시, 삭제 등의 작업을 통해 상태 관리와 비동기 데이터 처리를 익히는데 워밍업 미션으로 아주 적합하다고 생각한다. 강의에서 구현한 Todo List를 기반으로 앱을 구현하면서 총 세 가지에 초점을 맞춰서 진행하였다.CSS Transition을 사용하여 UX 개선CSS Transition을 활용하여 인터페이스의 상호작용을 부드럽고 직관적으로 처리스켈레톤 UI를 적용하여 UI와 UX 개선초기 렌더링 시 목표 리스트가 출력되는 부분에 스켈레톤 UI를 적용하여, 어느 부분에 컨텐츠가 표시될지에 대한 명확한 시각적 피드백 제공React Query 캐싱과 디바운스를 활용하여 불필요한 API 호출 감소초기 렌더링 시 데이터를 패칭하는 부분과 키워드 검색 시 데이터 패칭하는 부분이 동일한 액션을 공유하여 키워드 입력 시마다 불필요한 데이터 패칭이 지속적으로 이루어지는 문제를 인지하였음.export async function getTodos(): Promise { const supabase = await createServerSupabaseClient() const { data, error } = await supabase .from('todos') .select('*') .order('created_at', { ascending: true }) if (error) { handleError(error) } return data ?? [] }해당 로직의 문제점은 키워드 입력시마다 즉시 API 요청이 발생하여 검색 결과가 매번 새로고침되는 증상을 발견하였고 키워드 검색 시 디바운스를 적용하여 의도적으로 API 요청을 지연시켜서 약간의 API 요청 감소 효과가 발생하였음 const [searchInput, setSearchInput] = useState('') const [debouncedSearchInput, setDebouncedSearchInput] = useState('') // 검색어 입력 디바운스 useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchInput(searchInput) }, 600) return () => clearTimeout(timer) }, [searchInput])의도적으로 600ms의 딜레이를 발생시켰음에도 여전히 검색 시 마다 API 요청이 발생하여, 키워드 입력 시마다 스켈레톤 UI가 표시되어 개선 방법을 고민하던 중에 키워드 검색 기능과 데이터 패칭 액션 기능을 분리시키기로 결정하였음 const filteredTodos = todosQuery.data?.filter((todo) => todo.title.toLowerCase().includes(debouncedSearchInput.toLowerCase()) )결론적으로 초기 렌더링 시 Todo List 데이터 패칭 액션으로 호출하는 방식으로 유지하고, 키워드 검색 기능은 캐싱된 데이터를 별도로 필터링하는 로직으로 분리하여 불필요한 API 요청이 감소하는 효과를 가져옴 👀 1주 차 회고첫 주부터 다사다난한 일주일을 보냈다. 그동안 사용했던 Next.js가 사실 React에 더 가까운 구조였음을 깨닫게 된 한 주였다. 다시 시작하는 마음으로, 시간 날 때마다 틈틈이 복습하고 복기하는 과정을 반복해야겠다는 생각이 든다.이번 워밍업 클럽의 목표는 Next.js의 심화 기능을 익히는 것과 그동안 신경 쓰지 않았던 UI와 UX 개선에 힘쓰는 것이다.스켈레톤 UI, CSS Transition, 캐싱을 활용한 API 호출을 최소화하는 방법 등을 활용하는 방법과 더불어서 React Suspense, Optimistic UI 등의 기능을 이번 워밍업 클럽 과제와 접목하여 실제 프로젝트에 적용해 볼 예정이다.
풀스택
・
워밍업클럽
・
3기
・
발자국
・
회고
・
과제
・
미션