블로그

제이든(양준식)

인프런 비즈니스 신규 기능 업데이트

인프런 비즈니스 신규 기능 업데이트 소식을 전합니다. 지난해는 글로벌 서비스와 앱 서비스 출시를 중심으로 발전해온 한 해였습니다. 올해는 관리자분들의 운영 부담을 덜고, 수강생들의 학습 효과를 높이는 다양한 기능들을 선보일 예정입니다. 앞으로 더욱 자주 소식을 전해드릴 예정이니 많은 관심과 응원 부탁드립니다 :) INDEX01 - 인프런 비즈니스 승인 예약 기능 출시02 - 인프런 APP 출시03 - 인프런 글로벌 서비스 출시 01 — 인프런 비즈니스 승인 예약 기능 출시 ⏳'승인 예약' 기능이 새롭게 추가되었습니다! 이제 수강 신청 내역을 원하는 날짜와 시간에 맞춰 자동으로 승인할 수 있습니다. 기존에는 교육 시작일에 맞춰 직접 관리자 페이지에 접속해 수강 신청을 수동으로 승인해야 했지만, 이제는 미리 승인 일정을 설정하면 지정한 시간에 자동으로 승인 처리가 이루어집니다. 관리자의 운영 부담을 줄이고, 보다 효율적인 학습 환경을 만들 수 있도록 승인 예약 기능을 활용해보세요.  📌 CASE 01 - 지정된 수강 신청 기간이 있는 경우 (매월 N일 ~ N일)수강 신청 기간 : 학습자는 지정된 기간동안 자유롭게 수강 신청할 수 있습니다.관리자 승인 처리 : 수강 신청 기간 종료 후, 관리자가 신청 내역을 검토하여 승인 또는 반려 처리합니다.승인 예약 설정 : 관리자는 교육 시작일(OO월 OO일 OO시)에 맞추어 승인 예약을 설정합니다.자동 승인 처리 : 설정된 시간에 반려 건을 제외한 모든 수강 신청이 자동으로 일괄 승인됩니다. 📌 CASE 02 - 수강 신청 기간이 지정되어 있지 않은 경우 (매월 1일 ~ 말일)수강 신청 : 학습자들은 별도의 기한 없이 자유롭게 수강 신청할 수 있습니다.승인 예약 설정 : 관리자는 교육 시작일(OO월 OO일 OO시)에 맞춰 승인 예약을 설정합니다.자동 승인 처리 : 설정된 시간에 반려 건을 제외한 모든 수강 신청이 일괄 승인됩니다.📌 Tip - 매월 1일 교육이 시작되는 경우, 1일 자정(00:00)에 승인 예약 기능을 활용하시면 편리합니다. 02 — 인프런 APP 출시 📱인프런 모바일 앱, 사용해보셨나요? 이제는 노트북 없이도 출퇴근길, 카페, 이동 중 언제 어디서나 편리하게 강의를 들을 수 있습니다. 앞으로도 지속적인 기능 업데이트를 통해 더욱 완벽한 학습 도구로 발전해 나갈 예정이니, 많은 관심과 이용 부탁드립니다! 📌 POINT 01 - 인프런 모바일 앱 다운로드iOS와 Android 앱 모두 사용할 수 있어요.24년도 6월 이전에 다운로드 받았던 인프런 앱은 삭제하고, 신규 출시된 앱을 다운로드 받아주세요. 📌 POINT 02 - 오프라인 재생 기능 지원강의를 다운로드하여 오프라인에서도 시청할 수 있습니다.이동 중에도 데이터 걱정 없이 강의를 편리하게 학습하세요. 📌 POINT 02 - 강의실 제스처 지원IT 강의 특성상 코드 화면을 볼 일이 많죠? 많은 개발자분들이 요청하셨던 제스처 기능이 추가되었습니다. 롱프레스 → 2배속 재생더블탭 → 5초 건너뛰기제스처 → 전체 화면 전환줌 인/아웃 기능 → 코드 화면 확대 및 축소 가능 수강예정, 수강 중 강의와 완강한 강의를 구분해서 쉽게 확인할 수 있어요. 캡처를 이용해서 원하는 수업 구간을 사진 보관함에 저장할 수 있어요.개인 스크린샷, 영상녹화는 불가하니 이 점 유의해주세요. 영상뿐만 아니라 준비해 둔 수업자료도 놓치지 마세요.전체화면인 경우에는 플레이어 하단에 수업자료 버튼을 클릭해 보세요. 전체화면이 아닐 때 플레이어를 하단으로 쓸어보세요. 내 학습 리스트로 바로 갈 수 있어요. 플레이어 영역을 반복 터치해 보세요 *초 이동으로 원하는 시점에서 수강할 수 있어요. 03 — 인프런 글로벌 서비스 출시 🌏이제 인프런이 국경을 넘어 더 많은 학습자들과 함께합니다! 영어, 베트남어, 일본어 버전이 정식 출시되었으며, 모든 강의에서 다국어 자막과 더빙을 지원합니다. 원하는 언어로 강의를 학습할 수 있어, 더욱 자유롭고 편리한 학습 경험을 제공합니다. 인프런이 추구하는 "Learn, Share, Grow"의 가치가 어떻게 글로벌로 확장되는지, 함께 지켜봐 주세요! 📌 글로벌 학습 환경을 위한 핵심 기능다국어 자막 & 더빙 지원 – 한국어 강의도 영어, 일본어, 중국어 등 다양한 언어로 시청 가능글로벌 학습자 대상 콘텐츠 제공 – 해외 학습자도 국내 인기 강의를 모국어로 학습 가능📌 기업 맞춤형 글로벌 교육 지원해외 지사 직원 교육이나 국내 외국인 직원을 위한강의 콘텐츠가 필요한 기업은 편하게 문의 주세요.문의 | group@inflab.com📌 인프런 글로벌 서비스 둘러보기English - https://www.inflearn.com/enTiếng Việt - https://www.inflearn.com/vi日本語 - https://www.inflearn.com/ja 직원이 성장하고, 조직이 성공하는 인프런 비즈니스🔗 인프런 비즈니스 홈페이지🔗 인프런 비즈니스 콘텐츠  우리는 성장 기회의 평등을 추구합니다.©인프랩 | 경기도 성남시 분당구 판교로289번길 20, 3동 5F©InfLab. All rights reserved.

인프런비즈니스

제이든(양준식)

인프런 비즈니스 도입 검토 시 가장 자주 묻는 질문 Top 8

인프런 비즈니스의 특징은, 이미 많은 임직원이 인프런에서 학습한 경험을 바탕으로 기업 도입을 요청하는 경우가 많다는 점입니다. 하지만 정작 비즈니스 서비스는 직접 사용해본 경험이 없다 보니, 교육 담당자들의 문의가 이어집니다. 인프런 비즈니스 도입을 고민하는 기업 교육 담당자들이 가장 궁금해하는 질문 TOP 8, 지금 바로 확인해보세요! INDEX01. 비용은 어떻게 되나요?02. 결제는 어떻게 진행되나요?03. LMS 기능이 제공되나요?04. 원하는 강의만 지원할 수 있나요?05. 강의는 어떻게 구성되어 있나요?06. 수강 신청은 어떻게 이뤄지나요?07. 계정 관리는 어떻게 하나요?08. 수료증 발급은 가능한가요? 01. 비용은 어떻게 되나요?인프런 비즈니스는 매달 고정 비용이 청구되는 방식이 아니라, 수강 신청한 강의에 대해서만 비용이 발생하는 구조입니다. 학습자가 필요한 강의를 원하는 시점에 신청하고, 실제 수강한 강의에 대해서만 비용을 부담하기 때문에 불필요한 예산 지출을 최소화할 수 있습니다.비용은 월별로 정산되며, 해당 기간 동안 임직원들이 수강 신청한 강의 수에 따라 책정됩니다. 강의 금액 × 수강 신청 인원수 방식으로 계산되며, 기업은 실제 수강한 강의에 대해서만 결제하면 됩니다. 덕분에 예산을 효율적으로 운영하면서도 최적의 학습 경험을 제공할 수 있습니다.인프런의 평균 유료 강의 가격은 5만 4천 원이며, 전체 강의의 20%는 무료로 제공됩니다.일반적으로 유료 강의 비용이나 개수 한도를 설정하고, 무료 강의는 무제한으로 지원하는 방식으로 운영합니다. 02. 결제는 어떻게 진행되나요?인프런 비즈니스의 결제 방식은 교육 운영 방식에 따라 선불 충전과 후불 정산 중에서 적합한 결제 방식을 선택할 수 있습니다. ① 선불 충전(무통장 입금) 방식은 미리 예치금을 충전한 뒤, 수강 신청한 금액만큼 자동 차감되는 방식입니다. ② 후불 정산(무통장 입금 또는 카드 결제) 방식은 한 달 동안의 수강 신청 내역을 기준으로 익월 초에 결제가 진행됩니다.무통장 입금 시 세금계산서 발행이 가능합니다. 03. LMS 기능이 제공되나요?인프런 비즈니스는 기업 맞춤형 교육 운영을 지원하기 위해 관리자 페이지를 무상으로 제공합니다. 간단한 설정만으로 빠르게 교육 환경을 구축할 수 있어, 교육 담당자는 운영 부담을 줄이고, 임직원들은 자율적이고 체계적인 학습 환경에서 효과적으로 학습할 수 있습니다.관리자 페이지를 통해 강의 설정, 멤버 등록, 정산 관리 등을 간편하게 할 수 있으며, 예약 입과 및 학습 독려 메일 예약 발송 기능을 지원해 효율적인 교육 운영이 가능합니다. 또한, 실시간 학습 현황을 모니터링 할 수 있어 성과 관리가 용이하며, 임직원들의 학습 데이터를 기반으로 교육을 새롭게 기획할 수 있습니다.자체 LMS 시스템을 운영하는 기업의 경우, API 및 SSO 연동을 지원합니다. 04. 원하는 강의만 지원할 수 있나요?기업은 관리자 페이지에서 지원할 강의를 직접 설정할 수 있습니다. 특정 강의 카테고리를 지정하거나 개별 강의를 선택해 지원할 수도 있으며, 1인당 예산과 수강 신청 가능 개수를 설정해 체계적이면서도 자율적인 학습 환경을 조성할 수 있습니다.또한, 미지원 강의에 대한 설정도 가능합니다. 미지원 강의는 개인 결제로 진행하도록 설정할 수 있으며, 필요에 따라 홈페이지에서 미지원 강의를 아예 노출하지 않도록 제한할 수도 있어, 기업의 교육 운영 정책에 맞춰 유연하게 관리할 수 있습니다. 05. 강의는 어떻게 구성되어 있나요?인프런은 IT 직무에 특화된 교육 플랫폼으로, AI, 개발, 데이터 등 IT 핵심 직군을 위한 3,800개 이상의 강의를 제공합니다. 강의는 이론과 실습을 아우르는 체계적인 커리큘럼으로 구성되어 있어, 학습한 내용을 실제 업무에 적용할 수 있도록 돕습니다.또한, 인프런의 질의응답 시스템을 통해 학습자들은 강의 중 궁금한 내용을 질문할 수 있으며, 평균 48시간 내 답변을 받을 수 있습니다. 지식공유자와 AI 인턴이 학습자의 질문에 응답하며, 사후 관리까지 지원해 효과적인 학습이 이루어질 수 있도록 돕습니다.인프런 앱을 통해 출퇴근 시간에도 강의를 시청할 수 있습니다.인프런에서는 매월 평균 40~50개의 신규 강의가 개설되며, 비즈니스 멤버들에게 실시간으로 제공됩니다. 06. 수강 신청은 어떻게 이뤄지나요?수강 신청은 기업이 설정한 지원 범위(예산, 유료 강의 개수) 내에서 진행되며, 운영 방식에 따라 세 가지 방법을 선택할 수 있습니다.① 자율 수강 신청: 학습자가 직접 강의를 탐색하고 선택해 수강하는 방식으로, 개인의 관심사와 직무에 맞춘 학습 설계가 가능해 학습 효율과 만족도를 높일 수 있습니다.② 관리자 승인: 자율 수강 신청에 관리 단계를 추가한 방식으로, 관리자가 신청 내역을 승인하거나 반려할 수 있습니다.③ 관리자 수동 입과: 관리자가 특정 강의와 대상을 지정해 직접 등록하는 방식으로, 부서 별 역량 강화나 필수 교육 이수에 주로 활용됩니다.수강 신청 기간을 별도로 설정하여 운영할 수 있으며, 일반적으로 관리자 승인 단계를 두는 경우, 수강 신청 기간을 설정한 후 다음날 일괄적으로 입과하는 방식으로 운영됩니다. 07. 계정 관리는 어떻게 하나요?인프런은 1인 1계정 원칙으로 운영되며, 각 멤버는 본인 계정으로 필요한 강의를 신청하고 학습을 진행합니다.관리자는 회사 이메일을 이용해 멤버를 일괄 등록할 수 있습니다. 인프런 계정이 자동으로 생성되며, 이미 가입된 계정의 경우 비즈니스 서비스 학습 안내 메일이 발송됩니다. 아직 가입하지 않은 경우에는 인증 메일이 자동 전송되어 손쉽게 계정을 등록할 수 있습니다. 또한, 관리자 페이지에서 직접 멤버를 추가하거나 정보 변경이 가능하며, 이직 및 퇴사자의 멤버 삭제 처리도 손쉽게 진행할 수 있습니다.비즈니스 서비스 이용에 필요한 최소 인원 제한은 없습니다.인프런 비즈니스는 인원수에 따라 매달 고정 비용이 청구되는 방식이 아니므로, 등록된 멤버 중 실제로 교육에 참여한 인원의 수강 신청 내역에 대해서만 비용이 청구됩니다. 08. 수료증 발급은 가능한가요?인프런에서는 100% 완강한 경우에만 수료 처리되며, 학습자가 수료증을 직접 다운로드할 수 있도록 기능을 지원하고 있습니다. 학습자는 학습 목표 달성을 위해 수강 독려 시스템을 활용할 수 있으며, 스스로 목표를 설정하고 대시보드와 수강 독려 알림을 통해 학습을 지속할 수 있습니다. 또한, 관리자는 실시간 학습 현황을 확인하고, 자동 이메일 발송 기능을 활용해 주기적으로 학습을 독려하여 높은 참여율과 수료율을 달성할 수 있습니다.학습 현황은 관리자 페이지에서 엑셀로 다운로드할 수 있어 성과 관리에 효율적으로 활용할 수 있습니다.강의 완강 여부에 따라 수료는 시스템에서 자동 처리됩니다. 별도 수료 기준이 필요한 경우, 실시간 진도율 데이터를 활용해 수료 여부를 판별할 수 있습니다. 직원이 성장하고, 조직이 성공하는 인프런 비즈니스🔗 인프런 비즈니스 홈페이지🔗 인프런 비즈니스 콘텐츠  우리는 성장 기회의 평등을 추구합니다.©인프랩 | 경기도 성남시 분당구 판교로289번길 20, 3동 5F©InfLab. All rights reserved.

인프런비즈니스

수비드(강병수)

[인프런 비즈니스 PICK] 3월 추천 강의 안보면 후회합니다!

안녕하세요. 인프런 비즈니스팀입니다."업무 생산성? 커리어 업그레이드? 트렌드 따라잡기?"걱정 마세요! 인프런 비즈니스팀이 엄선한 강의를 소개해드리는 '인.비.픽!' 🎯업무 스킬 업그레이드는 물론, 트렌디한 최신 IT 강의까지 지금 바로 만나보세요! 🚀 📌 강의 큐레이션(1): 아바타 커뮤니티앱 만들기 (React Native) (클릭)#React Native #TypeScript #React-query #React #javaScript  ✅ 이 강의, 이런 분들께 추천해요!✔ React Native를 활용한 앱 개발에 관심 있는 개발자✔ JavaScript와 React 기초를 학습해 보신 분✔ 소셜 커뮤니티 앱 개발부터 배포까지 하나의 프로젝트를 완성해 보고 싶은 분🎯 이 강의에서 배우는 핵심 스킬🔹 React Native 기반 앱 개발iOS & Android 동시 개발컴포넌트 구조 및 UI 최적화🔹 소셜 커뮤니티 기능 개발프로필 생성, 피드 게시, 실시간 채팅 기능Firebase 기반 데이터베이스 활용🔹 풀스택 개발 경험클라이언트와 서버 연동API 설계 및 데이터 흐름 최적화🚀 이 강의를 들으면 이런 걸 할 수 있어요!✔ 백엔드와 연동하며 풀스택 개발 역량을 키울 수 있어요✔ React Native로 앱을 직접 개발하고 배포할 수 있어요✔ 단순 문법 설명이 아니라, 완성도 있는 앱을 직접 만들어볼 수 있어요🚀 React Native로 실전형 커뮤니티 앱을 만들어 보고 싶다면? 지금 바로 시작하세요! 💡🔥  📌 강의 큐레이션(2): [공식튜토리얼] Dialogue System for Unity (클릭)#Unity #Unity3d-ui  ✅ 이 강의, 이런 분들께 추천해요!✔ 게임 개발을 하면서 대화 시스템을 구현하고 싶은 개발자✔ Unity를 활용한 인터랙티브 스토리텔링에 관심 있는 분✔ NPC와 플레이어 간의 자연스러운 상호작용을 만들고 싶은 분✔ 게임 UI 및 UX 설계를 배우고 싶은 초·중급 개발자🎯 이 강의에서 배우는 핵심 스킬🔹 Unity 기반 대화 시스템 설계 및 구현게임 내 대화 흐름 관리 및 UI 디자인조건부 대화 시스템 및 플레이어 선택지 기능 구현🔹 NPC와 플레이어 상호작용 강화퀘스트 진행에 따른 대화 변화상태 기반 대화 시스템 구축🔹 Unity UI 시스템 활용대화창과 선택지 버튼 등 동적 UI 설계 및 구현텍스트 타이핑 효과와 같은 시각적 요소 추가🚀 이 강의를 들으면 이런 걸 할 수 있어요!✔ RPG, 어드벤처, 인터랙티브 스토리 게임에 활용할 수 있는 대화 시스템을 직접 제작✔ 게임 내 몰입감을 높이는 자연스러운 대화 연출 구현✔ 게임 개발자로서 Unity UI 및 데이터 관리 역량을 한 단계 업그레이드📢 더 몰입감 있는 게임을 만들고 싶다면? 지금 바로 시작해보세요! 🎮🔥  📌 스프링부트로 직접 만들면서 배우는 대규모 시스템 설계 - 게시판 (클릭)#SpringBoot #MySQL #Redis #Kafka #Java ✅ 이 강의, 이런 분들께 추천해요!✔ 스프링부트를 활용해 확장 가능한 대규모 시스템을 설계하고 싶은 개발자✔ Spring Boot, JPA, Redis, Kafka 등의 기술을 실무에서 활용하고 싶은 분✔ 백엔드 개발자로서 대용량 트래픽 처리 및 성능 최적화에 관심 있는 분🎯 이 강의에서 배우는 핵심 스킬🔹 Spring Boot 기반 대규모 시스템 설계모놀리식(Monolithic) vs 마이크로서비스 아키텍처(MSA) 이해클라우드 환경에서 확장 가능한 백엔드 설계🔹 성능 최적화 및 대용량 트래픽 대응Redis 캐싱 적용 및 DB 부하 감소 전략Kafka를 활용한 비동기 이벤트 처리🔹 실무에서 바로 사용할 수 있는 기술 스택 학습Spring Boot, JPA, MySQL, Redis, Kafka대규모 시스템을 위한 API 설계 & 성능 테스트🚀 이 강의를 들으면 이런 걸 할 수 있어요!✔ 대규모 트래픽을 감당할 수 있는 게시판 시스템을 직접 개발✔ Redis & Kafka를 활용한 성능 최적화 및 비동기 처리 적용✔ 기업 수준의 백엔드 설계를 이해하고 실무에서 활용할 수 있는 경험 확보📢 Spring Boot로 실무형 대규모 시스템을 구축하고 싶다면? 지금 바로 시작하세요! 💡🔥  직원이 성장하고, 조직이 성공하는 인프런 비즈니스🔗 인프런 비즈니스 홈페이지🔗 인프런 비즈니스 콘텐츠  우리는 성장 기회의 평등을 추구합니다.©인프랩 | 경기도 성남시 분당구 판교로289번길 20, 3동 5F©InfLab. All rights reserved.

인프런비즈니스

딩코딩코

이게 이해가 안돼? - #인프런 #지식공유경험나누기

"이게 이해가 안돼?"컴퓨터공학과 선배들이 저에게 자주 하던 말이었습니다.C++, 객체지향, 구조체 같은 낯선 단어들.바이트, 메모리, 운영체제처럼 머릿속에 쉽게 그려지지 않는 개념들.그 속에서 저는 길을 잃곤 했습니다.(실제 모교 강의실 사진입니다)모르는 내용을 이해하려고 질문을 해봐도 돌아오는 대답은 늘 같았습니다."전공책을 다시 읽어봐."적성에 맞지 않는다며 포기하고 싶었지만, 개발자의 꿈을 져버리고 싶지 않았습니다.몇 달 동안 여러 책을 찾아 읽으며 공부하니, 조금씩 개념이 잡히기 시작했습니다.그리고 다시 듣게 된 재수강 강의.그제야 이런 생각이 들었습니다."왜 이렇게 설명하지?" 그 강의를 이해하기 위해서는 사전에 배워야 하는 컴퓨터 관련 개념을 모두 알아야 했습니다.의존적인 설명 때문에 기본 개념조차 잡기 어려운 구조였습니다.이건 초심자를 위한 강의가 아니었습니다.처음부터 더 쉽게 배울 수도 있었을 텐데."이렇게 간단한 개념이었어? 그런데 왜 이렇게 어렵게 가르쳤을까?"라는 생각이 가득 차올랐습니다.그래서 저는 강의를 만들었습니다.가능한 한 개념을 독립적으로 설명하고 싶었습니다.일상에서 쉽게 연관 지을 수 있는 용어를 활용해 개념을 친숙하게 다가가도록 했습니다.그래야 누구나 편하게 코드를 작성할 수 있으니까요."이렇게 설명하면, 처음 보는 사람도 이해할 수 있을까?"그 고민이, 저의 가장 큰 숙제였습니다.이해가 잘 될지 궁금해서, 컴퓨터와 전혀 관련 없는 지인에게 설명해보기도 했습니다.(결과는… 실패였습니다. 😂) 그렇게 시행착오를 거쳐 만든 강의에서, 다양한 수강평을 받았습니다.누군가가 새로운 분야를 시작할 때 제가 자신감을 줬다는 것이 너무나 기뻤습니다.그 이후로 저는 다짐했습니다.사람들에게 지식을 최대한 쉽게 전달해야 겠다는 것을요. 내가 가진 지식을, 남들이 더 쉽게 이해할 수 있도록 돕는 것.이보다 더 기쁜 일이 있을까요? 초심자에게 친절한 강의를 만들어보고 싶다면,인프런에서 강의 만들기를 도전해보시길 추천드립니다!#지식공유경험나누기

커리어 · 자기계발 기타인프런지식공유경험나누기

leeebug

워밍업 클럽 스터디 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 <!-- JSX 정렬이 잘 안되서 렌더링 형태만 봐주세요! --> <div className="w-5/6 truncate"> <span className={`text-[0.7rem] font-semibold ${localUpdatedAt ? 'text-mint-800' : 'text-gray-500'}`} > {localUpdatedAt ? localUpdatedAt : localCreatedAt} </span> {localUpdatedAt && ( <span className="text-[0.7rem] font-semibold text-mint-800"> (수정)</span> )} </div>출력: 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기발자국회고과제미션

제이든(양준식)

인프런 비즈니스 시작하기 STEP 5

인프런 비즈니스 고객사는 기업 맞춤형 교육을 지원하기 위해 관리자 페이지를 무상으로 제공합니다. 매뉴얼 가이드를 따라 기업에 최적화된 교육 운영 환경을 쉽고 빠르게 구축해보세요. 교육 담당자는 운영 부담을 줄이고, 학습자는 체계적인 환경에서 자율적으로 학습할 수 있습니다. 인프런 비즈니스 시작하기 STEP 501. 인프런 비즈니스 서비스 가입하기02. 결제 수단 등록하기03. 지원 강의 설정하기04. 멤버 등록하기05. 기타 운영 설정 요청하기 01. 인프런 비즈니스 서비스 가입하기인프런 비즈니스 서비스는 별도 비용 없이 바로 가입할 수 있습니다. 인프런 계정만 있으면 가입과 동시에 새로운 그룹이 생성되고, 관리자 페이지가 제공됩니다.[1단계] 인프런 회원 가입(링크) 인프런 회원가입 : https://www.inflearn.com/user/signup비즈니스 서비스 가입을 위해 인프런 계정이 필요합니다.해당 계정은 기본 관리자로 등록됩니다. (추후 수정 및 추가 가능)[2단계] 인프런 비즈니스 서비스 가입(링크) 인프런 비즈니스 : https://www.inflearn.com/intro-group로그인 후, 인프런 비즈니스 홈에서 [시작하기] 버튼을 눌러 가입을 시작합니다.이용 약관에 동의하고, 회사 및 담당자 정보를 입력한 뒤 제출합니다.[3단계] 관리자 페이지 확인가입 완료 후, 인프런 홈페이지 우측 상단에 [비즈니스] 버튼이 나타납니다.정상적으로 관리자 페이지에 접근할 수 있는지 확인해주세요. 02. 결제 수단 등록하기결제 수단을 등록하면 관리자 페이지의 기능이 활성화됩니다. 인프런 비즈니스 서비스는 후불정산과 선불충전 중 선택할 수 있으며, 기업에 적합한 방식으로 등록해주세요.[1단계] 결제 수단 등록 안내 클릭관리자 페이지 상단에 "결제 수단을 등록해주세요"라는 노란색 배너 알림이 표시됩니다.우측의 [등록하기] 버튼을 클릭합니다.[2단계] 결제 방식 및 수단 선택후불정산 : 카드결제 또는 무통장입금(세금계산서 발행) 방식으로 결제할 수 있습니다. 선불충전 : 무통장입금 방식으로 예치금을 미리 충전한 후, 수강 금액에서 차감됩니다.[3단계] 카드정보 / 세금계산서 수신 이메일 입력카드 결제 시 : 카드 정보를 입력해주세요.무통장 입금 시 : 세금계산서 수신 이메일을 입력해주세요.(** 참고) 후불정산은 월 단위로 진행됩니다.수강 내역 발송 : 익월 2영업일 내후불 결제 진행 : 수강 내역 확인 후, 이상 없을 시 6영업일 이내 / 매월 10일 이전(** 참고) 선불충전 방식 이용 시 사전 문의가 필요합니다.결제 수단 등록 → 예치금 충전 문의 → 세금계산서 발행 및 입금확인 → 예치금 충전 완료 → 자동 정산문의: group@inflab.com 03. 지원 강의 설정하기교육에 필요한 강의를 설정하세요. 지원 강의 선택은 총 3단계로 진행되며, 임직원의 IT 역량 강화 목표에 맞춰 구성할 수 있습니다. 지원 범위 내에서 임직원들은 자유롭게 수강 신청하고 학습할 수 있습니다.[1단계] 가격 및 카테고리 설정강의 1개당 최대 가격을 지정할 수 있으며, 초과 시 미지원 강의로 자동 분류됩니다.카테고리별 지원 여부를 설정할 수 있으며, 하위 카테고리도 선택적으로 지정할 수 있습니다.[2단계] 필수/제외 강의 설정반드시 지원해야 하는 강의 또는 제외할 강의를 선택합니다.1단계 카테고리 설정과 별도로 지원/미지원 강의를 추가로 지정할 수 있습니다.[3단계] 최종 지원 강의 확인1단계와 2단계에서 설정한 내용을 반영한 최종 강의 목록을 확인합니다.[최종 선택 강의 목록 다운로드] 기능을 활용해 쉽게 확인할 수 있습니다.(** 참고) 미지원 강의 설정미지원 강의는 개인 결제로 수강할 수 있도록 설정할 수 있습니다.또는, 홈페이지에서 아예 노출되지 않도록 설정할 수도 있습니다.  04. 멤버 등록하기인프런 비즈니스 서비스에 구성원을 멤버로 등록해주세요. 기업 이메일을 활용하면 멤버 등록과 관리가 더욱 간편하며, 등록 즉시 자동 이메일 안내가 발송되어 학습을 빠르게 시작할 수 있습니다.1. 멤버 일괄 등록하기좌측 상단의 [리스트 다운] 버튼을 클릭하여 .csv 파일을 다운로드합니다.새롭게 등록할 멤버 정보를 파일에 입력합니다.원활한 관리를 위해 정확한 정보 입력을 권장드립니다. (** 필수 항목: 사번/학번, 이메일, 이름)[리스트 업로드] 버튼을 클릭하여 멤버 일괄 등록을 완료해주세요.(** 주의) 리스트 업로드 시 기존 명단이 교체됩니다. 기존 멤버에 새로운 멤버를 추가하려면, 기존 명단 아래 새로운 멤버 정보를 추가 입력한 후 업로드하세요.2. 멤버 개별 등록하기우측 상단의 [멤버 추가] 버튼을 클릭하여 개별 멤버를 등록할 수 있습니다.원활한 관리를 위해 정확한 정보 입력을 권장드립니다. (** 필수 항목: 사번/학번, 이메일, 이름) (** 참고) 멤버 등록 시 멤버 상태는 인프런 회원가입 여부에 따라 인증/미인증으로 구분됩니다.인증: 회원가입이 완료된 상태로 수강 신청 가능함. 멤버 등록 즉시 가입 완료 메일 발송되며, 학습 시작 안내 포함됨. 미인증: 회원가입이 완료되지 않은 상태로 비밀번호 설정 후 수강 신청 가능함. 등록 시 비밀번호 설정 안내 메일 발송되며, 설정 완료 시 상태가 '인증'으로 변경됨.  05. 기타 운영 설정 요청하기인프런 비즈니스 관리자에서 제공되는 기능 외에도, 인프런 어드민을 통해 기업 맞춤형 교육 환경을 구축할 수 있습니다. 필요한 설정이 있다면, 언제든 인프런 매니저에게 요청해주세요.1. 서비스 이용 기간수강 신청이 가능한 서비스 이용 기간을 설정할 수 있습니다.예를 들어, 연 단위로 교육을 운영하는 경우 1월 1일부터 12월 31일까지로 설정하고, 인당 수강 한도를 함께 지정하면 해당 기간 동안 정해진 범위 내에서 학습을 진행할 수 있습니다.2. 수강신청 기간수강 신청 기간을 매월 특정 기간(n일부터 n일까지)으로 설정할 수 있습니다.보통 수강 신청 기간을 운영할 경우, 관리자 승인 단계를 추가하고, 정해진 날짜에 교육을 시작합니다.(예시) 매월 1일 ~ 10일 수강 신청 > 11일 관리자 일괄 승인/반려 처리 > 15일 교육 시작3. 수강 승인 방식수강 승인 방식은 자동 승인과 수동 승인 중 선택할 수 있습니다.자동 승인: 학습자가 수강 신청하면 즉시 강의를 시청할 수 있습니다.수동 승인: 관리자가 수강 신청 내역을 검토한 후 승인 또는 반려할 수 있습니다.4. 1인당 수강 한도 설정 (금액, 개수)학습자가 신청할 수 있는 최대 금액과 유료 강의 개수를 설정할 수 있습니다.기업은 예산 범위 내에서 교육을 운영할 수 있으며, 학습자는 지원 한도 내에서 자유롭게 학습을 진행할 수 있습니다.일반적으로 서비스 이용 기간을 설정할 경우, 수강 한도도 함께 설정합니다5. 미지원 강의 구매 및 노출 여부기업이 지원하지 않는 강의에 대해 개인 구매 허용 여부 및 홈페이지 노출 여부를 설정할 수 있습니다.기본 설정에서는 미지원 강의도 홈페이지에 함께 노출되며, 학습자가 신청 시 개인 결제로 진행됩니다. 직원이 성장하고, 조직이 성공하는 인프런 비즈니스🔗 인프런 비즈니스 홈페이지🔗 인프런 비즈니스 콘텐츠  우리는 성장 기회의 평등을 추구합니다.©인프랩 | 경기도 성남시 분당구 판교로289번길 20, 3동 5F©InfLab. All rights reserved.

인프런비즈니스

lkwo

워밍업 클럼 3기 BE 클린코드 1주차 발자국

1주차를 마무리하며...업무와 같이 할 수 있겠지 싶었는데, 강의의 내용이 알차서 생각보다 힘들었습니다.코드를 따라치며 수강했지만... 사실 제대로 이해 못하고 따라만 친 부분도 상당히 있었는데,다시 내가 직접 리펙터링 해가며 따라가야지 다짐을 하며 1주차 발자국을 남깁니다.강의 정리클린코드란 무엇인가라는 질문으로부터 강의가 시작됩니다.좋은 코드란 무엇일까요?저는 강의의 내용을 듣고 이렇게 정리했습니다.좋은 글과 마찬가지로 클린코드는 잘 읽히는 코드한 문장 한 문장에 주제가 또렷해 전달하고자 하는 내용을 읽는 이에게 정확하게 전달하는 코드강의에서 중간중간에 선조와 후손이라는 용어를 사용하십니다. 내가 쓰는 코드가 꼭 내것만은 아니라는 점을 계속 상기시켜주시면서, 공용의 코드를 어떻게 다뤄야할지는 말씀해주시는게 많이 도움이 될 것 같습니다.추상화추상이란 '사물을 정확하게 이해하기 위해서 사물이 지니고 있는 여러 가지 측면 가운데서 특정한 측면만 가려내어 포착하는 것'을 말합니다.'비가온다' 라는 표현을 '대기내에 수증기가 응결하여 물방울로 변하고, 이 물방울이 중력에 의해서 지표면으로 떨어진다'라고 친구한테 말하면 갸우뚱할 것입니다.이처럼 추상은 중요한 정보만 남겨 전달함으로써 상대방의 이해를 돕는 기능을 하는걸 실생활에서도 볼 수 있습니다.코드에서의 추상화그러면 코드에서는 어떻게 중요한 내용만 가려내고, 구체적인 내용은 숨겨 읽는 이의 이해를 도울 수 있을까요?강의에서 나온 내용들 중 기억에 남는 방법들을 나열해봅니다.코드 블럭내에서 추상화 레벨을 통일하기객체에 적절한 역할과 책임을 부여하고 메시지를 통한 협력을 하는 구조로 만들기SOLID 원칙을 생각해가며 코드를 작성하기객체지향의 다양한 기법을 활용하기상속과 조합Value Object컬렉션으로 포장한 일급컬렉션 사용하기Enum다형성  그리고 읽는 사람을 배려한 다양한 코드 작성법을 나열해봅니다.변수, 메서드 추상화를 통해 이름 짓기매직 넘버, 매직 스트링 상수화하기공백 라인 활용해 블럭내 코드 분간해주기조건문에 부정어 연산자 `!`줄이기

백엔드클린코드

10

우빈님의 세심한 코드 리뷰 - 인프런 워밍업 클럽 3기 백엔드 코드 ✨

인프런 워밍업 클럽 3기 백엔드 코드 발자국 1주차 🙊 시작이 반이다... 벌써 중간점검 ?인프런 워밍업 클럽이 시작한지 2주가 지났다.. 앞으로 남은 발자국이 2개 뿐이다.. 👣이번주에는 중간점검으로 온라인 라이브가 진행 되었다. 온라인 라이브에 대한 내용은 다음과 같다.미션 Day 4 에 대한 공통 피드백Q&A에 대한 답변미션 Day 7 코드리뷰 진행각 세션은 놀라울 정도의 세심한 우빈님의 피드백 덕분에 많은 인사이트를 얻게 되어 좋은 시간이었다. 나도 첫번째로 코드리뷰를 신청하고 라이브 마지막에 코드리뷰를 받았는데 너무 좋은 경험이었다. 💪(커피를 좋아하시기로 유명한 우빈님께 Q&A 세션 도중 저가 커피 브랜드 중 어디 브랜드가 제일 맛있냐는 질문이 나왔는데.. 드셔 보신 적이 없다고 답변해 주신 부분이 인상적이었다.. ㅋㅋ 😁)✨ 우빈님의 세심한 코드 리뷰위에서 이야기했듯이 코드리뷰를 첫번째로 신청해서 우빈님께 온라인 라이브 시간에 코드리뷰를 받았다.해당 미션은 "스터디 카페" 프로그램을 리팩토링하는 미션이였는데.. 작년 4분기에 강의를 수강했을 당시에 3번이나 진행하였다.첫 번째 리팩토링, 강의를 듣기 전에 리팩토링두 번째 리팩토링, 강의를 수강하며 리팩토링세 번째 리팩토링, 강의를 수강 후 정리하며 다시 리팩토링이번이 네 번째 리팩토링이였는데 할 때마다 왜 새로운 것인지.. 🥲그래도 손이 기억이라도 한 듯 나름 순조롭게(?) 미션을 진행하게 되었고, 추가적으로 리팩토링을 진행하였고 해당 부분을 리뷰를 받고 싶어 신청하게 되었다.(우빈님께서 4번이라는 부분에 놀라셨는지(?).. 디스코드 스레드에 댓글을 남겨주셨다! 🤣)다시 돌아와서.. 코드리뷰 받은 내용은 아래와 같다.1⃣ 중요 도메인 StudyCafePassType의 구조화 🔗 Github PR 링크StudyCafePassType 구조화 리팩토링♻ 리팩토링 코드public enum StudyCafePassType implements PassTypeSelectable, PassTypeFormatter { // 📝 인터페이스 구조화 HOURLY("시간 단위 이용권") { // 📝 사용자 입력에 대한 구조화 @Override public boolean selected(String userInput) { return "1".equals(userInput); } // 📝 사용자 출력 포맷에 대한 구조화 @Override public String format(StudyCafePass pass) { return String.format("%s시간권 - %d원", pass.getDuration(), pass.getPrice()); } } }✏ 우빈님 리뷰Q. 클래스 내부에서 사용자 입력값 및 출력값에 사용하는 인터페이스를 구현함으로써 오버 엔지니어링이 된 것 같은 느낌이 드네요.. 🤦‍♂️ A. 저도 그렇게 생각해요.. ㅋㅋㅋㅋ 오버 엔지니어링이기보다 PassType은 중요한 도메인 모델인데, Input에서만 의미를 가지는 사용자 선택지가 침투하고 있다. 사용자 선택 방법이 "a", "b", "c"로 바뀐다면? 단순히 입력 방식을 바꿨을 뿐인데 무료 도메인 모델이 수정되어야 하는 엄청난 사태가 발생한다. 항상 구조화를 하는 것이 정답은 아니다. Output format도 마찬가지이다. 책임이 우선이다. 적절한 책임의 분배가 객체의 결합도를 낮추고 응집도를 높이는 것이다. 🤔 돌아보기단순히, OCP를 적용하기 위해 접근해서 리팩토링 했었는데..적절한 객체 책임 분리를 하지 못했으며, 중요한 도메인 모델을 수정하는 엄청 큰 사이드 이펙트가 일어날 수 있다는 점을 간과 했다는 것이다.다음부터는 구조화를 남발하지 않고 책임에 집중해서 리팩토링 해야겠다..2⃣ 이용권을 읽는 부분과 읽은 부분의 개념을 추출하여 객체 분리 🔗 Github PR 링크ReadLockerPasses 객체 분리♻ 리팩토링 코드public class ReadLockerPasses { // 📝 LockerPasses를 해석하는 객체 분리 private final List<StudyCafeLockerPass> passes; private ReadLockerPasses(List<StudyCafeLockerPass> passes) { this.passes = passes; } // 📝 lines을 해석하여 List<StudyCafeLockerPass> 객체를 만들어준다. public static ReadLockerPasses ofLines(List<String> lines) { List<StudyCafeLockerPass> passes = lines.stream() .map(ReadLockerPasses::ofLine) .toList(); return new ReadLockerPasses(passes); } private static StudyCafeLockerPass ofLine(String line) { String[] values = line.split(CSV_SPLITTER); // ⭐️ CSV라는 방식에 종속적 StudyCafePassType studyCafePassType = StudyCafePassType.valueOf(values[0]); int duration = Integer.parseInt(values[1]); int price = Integer.parseInt(values[2]); return StudyCafeLockerPass.of(studyCafePassType, duration, price); } // 📝 StudyCafeLockerPasses를 생성해준다. public StudyCafeLockerPasses toPasses() { return StudyCafeLockerPasses.of(passes); } }✏ 우빈님 리뷰Q. 일급 컬렉션을 적용하기 위해 toPasses() 메서드를 생성했는데 Read~라는 네이밍을 가진 클래스에 많은 책임이 부여된 것 같아 네이밍이 모호한 것 같습니다. 좋은 방법이 있을까요..? 🧐 A. 많은 책임이라고 생각하신 이유가 있을까요? "ReadLockerPasses는 어디선가 읽은 lines를 가지고 StudyCafeLockerPasses를 만들어준다"의 책임으로 보여서, 어색하지 않으며 테스트 코드 작성도 가능하다. 그와 별개로 CSV라는 방식에 종속되어있다. CSV형식이 다른 방식으로 바뀌었을 때 같이 바뀌어야 하는 부분이 CSV_SPLITTER 부분이다. 의도한 것 이라면 상관없다. 🤔 돌아보기Read라는 클래스명을 가지고 있어 StudyCafeLockerPasses를 생성해주는 메서드가 존재해 많은 책임이 있다고 생각했는데..우빈님 리뷰 이후에 다시 보니.. 그렇게 어색한가 싶기도 하다.. ㅎㅎ해당 클래스의 작성 당시 CSV 방식을 의존하려는 의도는 없었다. 단순히 읽은 부분의 개념을 추출한 것인데.. 위의 클래스는 CSV_SPLITTER상수가 사용되어 의도하지 않게 CSV라는 방식에 종속적이게 된 것이다.CSV라는 방식이 변경되면 객체 로직이 바뀌어야 한다.객체 구현 시, 종속성에 대해서 방어적으로 접근할 필요가 있어보인다.3⃣ ProvideException 커스텀 예외🔗 Github PR 링크ProvideException 커스텀 예외♻ 리팩토링 코드// 📝 이용권을 가져오는 과정에서 생긴 에러의 커스텀 예외 클래스 생성 public class ProvideException extends RuntimeException { public ProvideException(String message) { super(message); } } public class StudyCafePassMachine { public void run() { try { outputHandler.showPassOrderSummary(order); } catch (AppException e) { outputHandler.showSimpleMessage(e.getMessage()); } catch (ProvideException e) { // 📝 커스텀 예외 catch outputHandler.showSimpleMessage("이용권을 제공받을 수 없습니다."); } catch (Exception e) { outputHandler.showSimpleMessage("알 수 없는 오류가 발생했습니다."); } } }✏ 우빈님 리뷰Q. AppException 성격이랑 다른 것 같다고 생각되어 Provider 인터페이스에서 발생하는 예외 클래스 ProvideException를 별도로 생성하였습니다. A. 혹시 어떻게 다르다고 생각셨나요?? AppException의 의도는, 프로그램에서 발생할 수 있는 대부분의 애플리케이션 상황을 정의하는 최상위 예외 클래스이다. 만약 ProvideException을 별도로 표기하여 더 구체적인 상황을 나타내고 싶으면, AppException을 상속받아서 구성해야 한다. 그렇지 않으면 커스텀 예외 클래스가 늘어남에 따라 catch절도 같이 늘어날 것이다. 추가적으로, "이용권을 제공받을 수 없습니다."라는 메시지가 사용자 친화적이지 않다. 🤔 돌아보기리팩토링 당시, 초기 이용권을 가져와야만 프로그램이 실행된다는 관점에서 ProvideException의 커스텀 예외 클래스를 작성하였다.하지만 이용권을 가져오는 부분은 프로그램 내부에서 필요한 시점마다 호출하고 있어우빈님 리뷰대로 AppException 클래스를 상속받아서 작성하는 것이 더 나은 설계 같다.예외 메세지도 사용자 관점에서는 친화적이지 않은 것이 분명하다.내가 키오스크 시스템을 사용하다가 저런 메세지를 마주한다면... 화가 날 것 이다... 😡프로그램의 의도를 정확히 파악할 필요가 있어보인다. 또한 예외 메세지도 누가 보는지에 따라 고민해보는 습관을 길러야겠다.이렇게, 요청한 3개의 리뷰와 2개의 추가 리뷰를 받아 보았다..고작 3일 만에 7명이나 리뷰를 해주셨는데 세심하고 또 세심했다... 퀄리티가 상당했다.. ✨이번 온라인 라이브를 통해 우빈님에 대한 팬심과 존경심이 더욱 커졌다....! 📈리뷰해주신 내용으로 다시 리팩토링을 함으로써 한층 더 Readable Code에 대한 성장을 경험할 수 있었다. 🚀💡 자기만의 언어로 강의 키워드 정리하기 섹션 6. 코드 다듬기좋은 주석 - 주석의 양면성주석이 많다는 것 : 추상화가 덜 되고 가독성이 좋지 않은 코드 (코드 품질 저하 📉)주석이 필요한 경우 : 히스토리를 알 수 없을 경우, 주석으로 상세히 설명변수와 메서드 나열 순서변수 : 사용하는 순서대로 위치한다. (인지적 경제성 / 뇌 메모리 줄이기)객체의 공개/비공개 메서드 : 공개 메서드를 상단에 위치하고, 비공개 메서드 하단에 위치한다. 공개 메서드 중에서도 중요도의 순서에 따라 배치한다.공개 메서드 : 객체의 상태를 변경 하는 부분이 가장 상단에 위치하도록 - 상태 변경 >>> 판별 >= 조회비공개 메서드 : 출현한 순서대로패키지 나누기여러 파일들의 네임 스페이스를 관리하기 때문에 적당한 수준으로 잘 나누어야 한다.대규모 패키지 변경은 팀원과의 합의 필요 -> 추후 conflict가 생길 수 있다.기능 유지보수하기정렬 단축키, linting, style - sonarlint, editorconfig섹션 7. 리팩토링 연습메서드 추출로 추상화 레벨 맞추기Optionalreturn null / Optional 파라미터 사용은 안티패턴이다.객체에 메시지 보내기객체를 존중하고 메시지를 보내자.객체의 책임과 응집도⭐️ 추상화 관점의 차이 - FileHandler구현에 초점을 맞춘 추상화 VS 도메인 개념에 초점을 맞춘 추상화File을 read하는 부분의 로직들은 전부 FileHandler에 들어갈 것이다. 잘못된 객체 응집일 수도 있다..방법에 초점을 맞춘 설계 방식이 아닌 어떤 데이터를 가져오는 가에 대한 초점을 맞추는 것이 좋다.섹션 8. 기억하면 좋은 조언들능동적 읽기가지고 있는 리팩토링 기법들을 총동원해서 읽자. -> 리팩토링하면서 읽기눈으로만 보는 수동적 읽기는 권장하지 않는다.도메인 지식을 늘리기 위해서 능동적 읽기가 필요하다. (작성자의 의도 파악)오버 엔지니어링필요한 적정 수준보다 더 높은 수준의 엔지니어링예시 1. 구현체가 하나인 인터페이스구현체가 수정할 때마다 인터페이스도 수정해야 함코드 탐색의 어려움예시 2. 너무 이른 추상화정보가 숨겨지기 때문에 복잡도가 높아진다.후대 개발자들이 선대의 의도를 파악하기가 어렵다.은탄환은 없다클린 코드도 은탄환이 아니다.실무에서의 줄다리기지속 가능한 소프트웨어 품질 VS 기술 부채를 안고 가는 빠른 결과물 -> 클린 코드를 대비한 코드 센스가 필요하다.모든 기술과 방법론은 적정 기술의 범위 내에서 사용되어야 한다.항상 정답인 기술은 없다.한계까지 연습해보고, 적정 수준, 적정 시점을 깨닫는 것이 필요하다.섹션 3. 단위 테스트단위 테스트작은 코드(클래스 또는 메서드) 단위를 독립적으로 검증하는 테스트 -> 가장 기본이 되는 테스트검증 속도가 빠르고, 안정적수동 테스트, 자동화 테스트 -> 인지 필요사람이 검증하는 수동 테스트 -> sout으로 출력하고 눈으로 직접 확인기계가 검증하는 자동화 테스트Junit5, AssertJJunit5 : 단위 테스트를 위한 테스트 프레임워크AssertJ : 테스트 코드 작성을 원할하게 돕는 테스트 라이브러리 - 풍부한 API 메서드 체이닝 지원해피 케이스, 예외 케이스 -> 테스트 케이스 세분화예외 케이스 : 암묵적 혹은 드러나지 않은 요구사항에서 발견경계값 테스트범위, 구간, 날짜 경계값들로 테스트를 해야한다.테스트하기 쉬운/어려운 영역 (순수함수)테스트 하기 어려운 영역을 구분하고 분리하기외부로 분리할수록 테스트 가능한 코드는 많아진다.테스트하기 어려운 영역관측할 때마다 다른 값에 의존하는 코드 : 현재 날짜/시간, 랜덤 값, 전역 변수/함수, 사용자 입력외부 셰계에 영향을 주는 코드 : 표준 출력, 메시지 발송, 데이터베이스에 기록하기순수 함수 - 테스트하기 쉬운 영역같은 입력에는 항상 같은 결과외부 세상과 단절된 형태lombok@Data, @Setter, @AllArgsConstructor 지양양방향 연관관계 시 @ToString 순환 참조 문제섹션 4. TDD: Test Driven DevelopmentTDD 테스트 주도 개발 (Test Driven Development)로, 프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하도록 하는 방법론선 기능 구현, 테스트 작성의 문제점 (일반적인 개발) - 구현순서 : 기능 -> 테스트테스트 자체의 누락 가능성해피 케이스만 검증할 가능성잘못된 구현을 다소 늦게 발견할 가능성선 테스트 작성, 기능 구현 (TDD) - 구현순서 : 테스트 -> 기능복잡도(유연하며 유지보수가 쉬운)가 낮은 테스트 가능한 코드로 구현할 수 있게 한다.테스트가 힘든 코드를 위한 코드 작성이 가능예를 들면 LocalDateTime.now()의 경우 외부세계로 분리해서 테스트를 하기 편한 코드를 작성할 수 있다.프로덕션 코드를 작성한 후 테스트 코드를 작성하기 귀찮을수도..쉽게 발견하기 어려운 엣지 케이스를 놓치지 않게 해준다.구현에 대한 빠른 피드백을 받을 수 있다.과감한 리팩토링이 가능해진다.테스트 코드가 보장TDD의 관점이전의 관점 : 테스트는 구현부 검증을 위한 보조 수단변화된 관점 : 테스트와 상호 작용하며 발전하는 구현부레드 - 그린 - 리팩토링Red : 실패하는 테스트 작성Green : 테스트 통과 하는 최소한의 코딩Refactor : 구현 코드 개선 테스트 통과 유지애자일 방법론 vs 폭포수 방법론애자일 방법론 https://agilemanifesto.org/iso/ko/manifesto.html반복적 개발(Iterative Development): 짧은 개발 주기(스프린트)를 반복하며 지속적으로 개선.유연성: 요구사항 변경을 수용할 수 있도록 유동적으로 진행.고객 참여: 개발 과정에서 지속적인 피드백을 반영하여 사용자 중심 개발 가능.자율적인 팀 구성: 개발팀이 자체적으로 의사 결정을 하며, 빠르게 문제를 해결함.폭포수 방법론단계적 개발: 요구 분석 → 설계 → 구현 → 테스트 → 배포 → 유지보수 순서로 진행됨.문서 중심: 각 단계마다 문서화가 철저하게 이루어짐.선형 구조: 이전 단계가 완료되어야 다음 단계로 넘어갈 수 있음.변경이 어려움: 초기에 요구사항을 확정하면 이후 변경이 어렵고 비용이 많이 듦.익스트림 프로그래밍XP(Extreme Programming, 익스트림 프로그래밍)는 애자일 방법론 중 하나로, 빠른 개발 주기와 지속적인 피드백을 중심으로 하는 소프트웨어 개발 방법론이다. 고객의 요구사항 변화에 빠르게 대응할 수 있도록 짧은 개발 반복 주기(Iteration)와 강한 협업 문화를 강조한다.스크럼, 칸반스크럼애자일 프레임워크로, 일정한 스프린트 동안 작업을 계획하고 진행하는 반복적이고 점진적인 개발 방식이다. 짧은 개발 스프린트를 통해 빠르게 결과물을 만들고 지속적으로 개선하는 것이 핵심이다.1⃣ 백로그 작성 – 제품 백로그에 모든 요구사항을 정리2⃣ 스프린트 계획 – 스프린트 기간 동안 수행할 작업 선정3⃣ 스프린트 진행 – 개발 진행 및 매일 스탠드업 미팅4⃣ 스프린트 리뷰 – 개발 완료된 기능을 검토5⃣ 회고(Retrospective) – 개선점을 찾고 다음 스프린트에 반영칸반Workflow와 가시성을 중심으로 한 애자일 프레임워크로, 지속적인 개선과 작업량 관리를 중점적으로 다룬다. 작업을 시각적으로 표현하여 현재 진행 상황을 쉽게 파악할 수 있도록 합니다.1⃣ Backlog: 해야 할 작업을 모아둠2⃣ To Do: 현재 진행할 작업3⃣ In Progress: 진행 중인 작업4⃣ Review/Test: 리뷰나 테스트가 필요한 작업5⃣ Done: 완료된 작업섹션 5. 테스트는 []다.테스트 코드는 문서다.프로덕션 기능을 설명해주는 것이 테스트 코드 문서다.다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점을 보완할 수 있다.고민했던 내용(테스트 코드)을 팀 자산(소스 코드)으로 공유할 수 있다.@DisplayName - 도메인 정책, 용어를 사용한 명확한 문장메서드명만으로 어떤 것을 검증하고자 하는 의도 파악이 어려움Junit5에 추가한 어노테이션이다.문장 형태로 섬세하게 테스트 검증에 대한 내용을 어노테이션안에 작성한다.섬세한 DisplayName특정 시간 이전에 주문을 생성하면 실패한다. ❌영업 시작 시간 이전에는 주문을 생성할 수 없다. ✅도메인 용어를 사용하여 추상화된 내용을 담기 -> 메서드 자체의 관점 보다 도메인 정책 관점 (특정 시간 -> 영업 시작 시간 ✅)테스트의 현상을 중점으로 기술하지 말 것 (~실패한다 ❌)Given / When / Then - 주어진 환경, 행동, 상태 변화Given : 시나리오 진행에 필요한 모든 준비 과정When : 시나리오 행동 진행Then : 시나리오 진행에 대한 결과 명시, 검증TDD vs BDDBDDTDD에서 파생된 개발 방법시나리오 기반한 테스트 케이스 자체에 집중하여 테스트한다.Junit vs SpockSpock은 Groovy언어로 BDD 패턴을 적용해서 테스트 코드를 작성할 수 있다.언어가 사고를 제한한다.명확하지 못한 테스트 코드는 사고를 제한할 수 있다.문서로서의 테스트를 신경 쓸 필요가 있다. 🏃 돌아보며..미션과 발자국을 정신없이 진행하다 보니 벌써 남은 인프런 워밍업 클럽도 2주밖에 남지 않았다.처음에 OT 라이브 당시 러너가 120명 정도였는데, 이번 중간 점검 라이브 때는 60명 정도로 줄어들었다.강의 내용 자체는 어렵지 않지만.. 2개의 강의(14시간 + 12시간)를 한 달만에 들으면서 미션과 발자국을 진행하는 건 쉽지 않아 보인다..나는 미리 강의를 수강해서 다행이다라는 생각이 든다.. 😅하지만, 쉽지 않은 만큼 성실히 참여한다면 단기간 내 성장하는 데 큰 도움이 될 것이다.그리고 이번 주에 코드리뷰를 신청하기 잘했다는 생각이 들었다. 🍀위에서도 여러 번 언급했지만 우빈님의 세심한 리뷰 탓(?)에내가 미션을 수행하는 데 있어 우빈님보다 세심하게 집착 했었나..? 반성하게 된다... 😭마지막 주차 온라인 라이브에서도 테스트 코드에 대한 코드리뷰가 진행된다고 한다.기회가 된다면 또 한 번 코드리뷰를 받아 단골 손님이 되고 싶다. 😂2주를 걸쳐, 읽기 좋은 코드의 스터디 과정은 이번주로 막을 내렸다.다음 주차부터는 테스트 코드에 대해 본격적으로 스터디하는 과정이 진행된다.남은 2주도 화이팅하며 좋은 성장을 이루길 기대해 본다. 🔥끝으로, 3월 중순이 되니 이제 슬슬 봄 내음이 나는 것 같다.. 🌸얼어붙은 개발 시장에도 봄이 찾아왔으면 좋겠다.. 🧊발자국 2주차 끄읕 ![출처]인프런 워밍업 클럽 : https://www.inflearn.com/course/offline/warmup-club-3-be-code강의 : https://www.inflearn.com/course/readable-code-%EC%9D%BD%EA%B8%B0%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EC%82%AC%EA%B3%A0%EB%B2%95/dashboard

백엔드인프런워밍업클럽백엔드3기발자국박우빈클린코드읽기좋은코드

10 8일 전
당황한 수달

[인프런 워밍업 클럽 3기] PM/PO 1주 차 발자국

 스터디 완주를 위한 첫걸음: ‘발자국’을 남기며스터디 완주를 위해 학습 기록을 남기는 것도 중요하지만, 먼저 ‘발자국’이란 무엇이며, 왜 남기게 되었고, 어떤 이유로 이 강의를 선택했는지를 이야기해 보고자 합니다.  ‘발자국’이란 무엇인가?저는 시작하는 PM/PO들에게 알려주고 싶은, 프로덕트의 모든 것이라는 인강을 수강하며, 인프런 워밍업 스터디에 참여하고 있습니다.이 스터디에서는 완주를 위한 조건으로 ‘발자국’이 있는데, 이는 배운 내용을 바탕으로 작성하는 학습 일지이자 회고입니다.  왜 이 강의를 선택했는가?이전에 시작하는 PM/PO들을 위한 역할과 전문성이라는 인프런 밋업에 참여했고, 김민우 튜터님의 강연이 깊은 인상을 남겼습니다.마침 이 강의로 스터디가 개설되었고, 함께 학습하며 성장하기에 좋은 기회라고 생각되어 수강을 결정했습니다.  1주 차 동안 무엇을 배웠는가?가이드에서는 강의 내용을 요약하라고 했지만, 이미 많은 분들이 정리해 주셨을 것 같아😅저는 1주 차 동안 제 머릿속에 남은 것을 공유해 보려 합니다. 강의의 대상이 ‘시작하는 PM/PO’인 만큼, 이미 알고 있던 내용도 있었지만, 한편으로는 깊게 고민하지 못한 부분도 많았습니다.가장 인상적이었던 것은 “PM이란 무엇인가?” 라는 질문에 대한 튜터님의 정의였습니다. Product Manager란?valuable, usable, feasible, viable(VUFV)한 제품을 만들기 위해고객, 데이터, 인더스트리, 그리고 우리 사업에 대한 전문성을 팀에 기여하는 역할 저는 가끔 면접에서 이 질문을 받은 적이 있었는데, 인터넷에서는 명확한 정의를 찾기 어려워 정리하는 데 어려움을 겪고 직접 문의를 드리기도 했습니다.튜터님께서 이 질문에 대해 깔끔하게 정의해 주신 덕분에, 앞으로 면접에서 자신 있게 답변할 수 있을 것 같습니다. 또한, PM의 전문성에 대해서도 새로운 시각을 가지게 된 계기가 되었습니다.어떤 사람들은 PM을 ‘잡무 매니저’라고 하며 전문성이 없다고 평가하기도 합니다.하지만 잡무는 제품을 위해 필요하다면 해야 하는 부분이고,진정한 PM이라면 결국 VUFV한 제품을 만들기 위해 끊임없이 고민하고 발전하는 사람이라는 점을 되새길 수 있는 시간이었습니다. 앞으로도 VUFV한 제품을 만들 수 있는 PM이 되도록 계속해서 고민하고, 발전하는 사람이 되도록 노력해야겠습니다. 😊(솔직히 고백하자면, VUFV는 제가 만든 줄임말입니다🤣)  다음 주 학습 계획은?다음 주 강의 내용은 고객에 대한 전문성을 쌓는 강의들로 구성되어 있습니다.스케쥴에 맞춰 듣고, 현재 맡고 있는 제품에 대해 반영해 보는 기회로 삼아보고 싶습니다.  

기획 · PM· PO김민우튜터인프런워밍업클럽PMPO

lkwo

워밍업 클럽 3기 BE 클린코드 3주차 발자국

3주차를 마무리하며...테스트 코드에 대해서 공부하는 시간을 가졌습니다.실무에서도 red green refactoring을 습관화해서 사용하면 심리적으로 안정감 있는 직장 생활이 될 것 같다는 생각이 들었습니다.강의 내용정리테스트 코드테스트 코드(자동화 테스트)가 필요한 이유수동으로 테스트를 진행하면 시간이 비효율적으로 사용됩니다.인수인계가 어렵습니다.수동으로 테스트하기에는 휴먼오류에 대한 리스크가 존재합니다.테스트를 통해 얻고자 하는 것빠른 피드백과 자동화 그리고 안정감을 얻을 수 있습니다.단위 테스트작은 코드 단위를 독립적으로 검증하는 테스트를 단위테스트라고 합니다.외부에 의존하지 않고 검증을 하도록 설계하여 속도가 빠르고 안정적입니다. 단위 테스트 작성할 때 주의점암묵적이거 아직 드러나지 않은 요구사항이 있는가? 에 대한 의문을 가지며 테스트 케이스를 세분화 하여야 합니다.해피케이스, 예외케이스에 대해서 고루 케이스를 세분화하여야 합니다.테스트하기 어려운 값 (외부 값, 시간)에 대해서는 분리하여 테스트에 용이한 구조로 만드는 것이 좋습니다. Layered Architecture와 테스트Persistence Layer Test쿼리가 의도대로 잘 작성이 되었는지 확인합니다.쿼리를 구현하는 기술이 바뀌어도 기능의 동작을 보장하도록 테스트를 작성합니다.@DataJpaTest를 사용해 테스트를 진행하는 경우에는 Persistence Layer에 필요한 빈들만 주입받으며, 자동으로 테스트 후에 roll back을 해줍니다.Business Layer TestPersistence Layer와 상호작용을 통해 비즈니스 로직을 전개하며, 이를 테스트 합니다.요청값부터 만들어서 비즈니스 로직을 테스트 합니다.트랜잭션을 보장하는지 확인합니다.Presentation Layer Test주요 로직은 business+persistence의 트랜잭션 경계내에서 끝나므로, presentation 에서는 외부에서 들어오는 값 검증을 주로 수행합니다.의존 관계를 가짜객체를 사용해서 환경을 재현합니다.@WebMvcTest, @MockBean, MockMvc 객체 등을 사용합니다.미션 Day 11저는 스터디 카페를 프로그램에 테스트 코드를 추가 하였습니다. [깃 허브](https://github.com/lkwoung88/readable-code/commit/7be5050a1c76e78809d85258aadbddfc3a430f3a) 계산, 판정 로직에 대해서 테스튼는 큰 고민없이 작성하였지만, 파일에서 읽어오는 부분은 어떻게 테스트하면 좋을지 고민을 많이 하다가 결국에는 작성하지 못했습니다.

웹 개발테스트코드

10

살짝 서두른 Spring 기반 테스트 코드 - 인프런 워밍업 클럽 3기 백엔드 코드✨

인프런 워밍업 클럽 3기 백엔드 코드 발자국 1주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 2주차인프런 워밍업 클럽 3기 백엔드 코드 발자국 3주차🎼 알레그레토 : 조금 빠르게 3주 차부터는 미션과 발자국을 조금 서둘러 진행할 예정이다.앞서 언급했듯이, 이번 주부터 항해 플러스가 시작되어 최대한 진도를 빠르게 빼보려고 한다.현재 날짜(3월 17일) 기준으로 Day 16 미션과 3주 차 발자국을 작성 중이며,이번 주 안에 Day 18 미션과 4주 차 발자국도 작성을 완료하는 것이 목표이다.(가능할지는 모르겠지만 ㅎㅎ)조금 급하게 진행하는 감이 있어 개인적으로 많이 아쉽다.워밍업 클럽에만 온전히 집중할 수 있었다면 더 많은 성장을 할 수 있었을 텐데 말이다...하지만, 후회는 하지 않는다. 다음 기수의 존재는 우빈님만 아시겠지만, 다음에 또 없을 수도 있는 기회를 놓치고 싶지 않았기 때문이다. 👍나는 위의 이미지처럼 Trello를 활용해 인프런 워밍업 클럽에 참여하고 있다.미션 제출 날짜가 일정하지 않다 보니, 제출 하루 전에 노티를 받도록 설정해 두고 유용하게 사용 중이다. 🙃💡 자기만의 언어로 키워드 정리하기 섹션 6. Spring & JPA 기반 테스트Layered Architecture레이어드 아키텍처의 단점 : 기술에 대한 강결합이 심하다는 단점이 존재Hexagonal Architecture도메인 모델은 외부의 것들을 아예 모른다.도메인 모델 중심 (멀티 모듈 및 시스템이 커진다면..)단위테스트 vs. 통합테스트단위테스트 만으로는 커버하기 어려운 영역이 존재 (여러 모듈 및 여러 객체가 협력하기 때문에)통합테스트란?여러 모듈이 협력하는 기능을 통합적으로 검증하는 테스트단위 테스트만으로는 기능 전체의 신뢰성을 보장할 수 없다.IoC, DI, AOPORM, 패러다임의 불일치, HibernateSpring Data JPAQueryDSL@SpringBootTest vs @DataJpaTest@DataJpaTest는 @SpringBootTest보다 가볍다.@DataJpaTest보다는 @SpringBootTest를 더 선호@DataJpaTest는 @Transactional이 있어 롤백이 된다.@SpringBootTest는 클렌징을 해주어야 한다.@SpringBootTest vs @WebMvcTest@SpringBootTest는 E2E 테스트, 즉 통합테스트 시 사용하는 어노테이션이다.@WebMvcTest는 Presentation Layer에 대한 단독 테스트시 사용하는 어노테이션이다.다른 레이어들은 mocking을 통해 동작을 제어한다.@Transactional(readOnly = true)테스트에서 사용 시, 롤백 되는 것에 유의 해야 한다.트랜잭션 경계 설정을 해야한다.엔드포인트를 잘 설계해야 한다.Optimistic Lock, Pessimistic Lock낙관적 락 : 데이터 충돌이 자주 발생하지 않을 것이라 낙관적으로 가정하고, 트랜잭션을 진행하는 방식데이터를 읽을 때는 락을 걸지 않고, 데이터를 업데이트 시 버전 비교하여 충돌 여부 판단성능 저하를 최소화, 동시성을 높이는 데 유리비관적 락 : 데이터 충돌이 자주 발생할 것이라 비관적으로 가정하고, 트랜잭션이 데이터를 사용할 때 미리 잠금을 거는 방식데이터 일관성을 유지하는 데 초점트랜잭션이 완료될 때까지 다른 트랜잭션이 데이터를 수정할 수 없음데드락 발생 가능CQRS명령 조회 책임 분리 : Command Query Responsibility Segregation읽기(조회)와 쓰기(명령)의 책임을 분리하는 소프트웨어 아키텍처 패턴장점성능 최적화확장성 증가데이터 모델 최적화비지니스 로직의 명확한 분리단점복잡성 증가데이터 동기화 문제트랜잭션 관리 어려움@RestControllerAdvice, @ExceptionHandler@RestControllerAdvice : ControllerAdvice의 기능을 하는데 JSON으로 응답을 해주는 Advice커스텀 예외를 던지고 @RestControllerAdvice의 @ExceptionHandler에서 예외를 처리할 수 있다.Spring bean validation@NotNull, @NotEmpty, @NotBlank도메인 요구사항에서 나오는 validation과 책임 분리해야한다.Controller 단에서는 최소한의 validation을 통한 검증이 이루어져야 한다.@WebMvcTestObjectMapperJackson 라이브러리에서 제공하는 클래스로, Java 객체와 JSON 간의 변환을 담당하는 역할직렬화, 역직렬화를 수행Mock, Mockito, @MockBeanMock : 실제 객체 없이 동작을 모방하여 단위 테스트를 수행하는 가짜 객체Mockito : Java에서 Mock 객체를 쉽게 생성하고 관리할 수 있는 라이브러리@MockBean : Spring 컨텍스트에 Mock 객체를 등록하여 실제 빈을 대체@Mock : 순수한 자바에서 Spring 컨텍스트가 필요하지 않을 때 사용@MockBean : Spring 컨텍스트에서 특정 빈을 Mocking 하고 싶을 때 사용👨🏻‍💻 미션 회고[미션 Day 11][미션 PR]#6스터디 카페 이용권 선택 시스템 단위 테스트 작성미션 조건은 다음과 같다.✔각 프로젝트 모두 강의 중에 작성한 tobe 패키지 코드를 기준으로 함 (lesson 6-4 가 가장 마지막 버전)✔3개 이상의 서로 다른 클래스 & 총 7개 이상의 테스트 작성 ➡ 단, 같은 인터페이스를 구현하고 있는 구현체들은 1개 클래스로 간주한다.✔무엇을 테스트하고자 했는지를 잘 나타낸 @DisplayName 작성하기✔BDD(given/when/then) 스타일 따르기 (주석으로 표기)가장 작은 단위의 메서드 부터 단위 테스트를 작성하면서,테스트 커버리지를 높이기 위해, 모든 주요 로직에 대한 단위테스트를 작성하고자 했다.단위테스트를 작성하면서 강의에서도 강조한 내용 중에 하나인 @DisplayName을 잘 작성하기 위해 많이 고민했다.'권'이라는 Pass의 의미를 '패스'로 통일하여 일관성있게 작성하였다.읽는 사람으로 하여금 뇌 메모리를 적게 쓰게 하기 위해 @DisplayName도 최대한 추상화해서 작성하려고 노력했다.테스트의 현상을 중점적으로 작성하지 않으려고 하였다.하지만, 사용자 입력을 받는 클래스인 InputHandler의 단위테스트를 작성하는 과정에서 어려움이 있었다.InputHandler 내부에서 static으로 선언되어 있어 mocking도 하기 어려웠다.public class InputHandler { private static final Scanner SCANNER = new Scanner(System.in); }프로덕션 코드를 수정하지 않는다는 요구사항을 지키면서는 테스트가 어려웠다. 만약 프로덕션 코드를 수정한다면 테스트가 가능해질 것이다.👆 Scanner를 외부세계로 분리하면 테스트가 가능할 것 같다.✌ InputHandler 상위의 인터페이스를 생성 후, 테스트 전용 support 성격의 구현체를 만들어서 테스트가 가능할 것 같다. 미션 제출 후, 차후에 프로덕션 코드를 수정해서 테스트를 작성 해볼 예정이다.🏃 돌아보며..Day 11 미션은 제출이 끝이 아니라, 중간점검 라이브에 이어 마지막 라이브에서 코드 리뷰 단계가 남아있다.중간 점검 때 세심한 코드 리뷰를 받고 수정하면서 많은 성장을 했기에 이번에도 신청하지 않을 이유가 없었다.다른 러너 분들도 꼭 받아봤으면 한다. 😊아직까지 혼자 신청해서 뻘줌해서가 아니라... 진짜 좋은 기회이자 경험이다... 🤣코드 리뷰 대상자로 뽑히길 기대하며.. 마지막 4주차도 화이팅 💪[출처]인프런 워밍업 클럽 : https://www.inflearn.com/course/offline/warmup-club-3-be-code강의 : https://www.inflearn.com/course/readable-code-%EC%9D%BD%EA%B8%B0%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EC%82%AC%EA%B3%A0%EB%B2%95/dashboard

백엔드인프런워밍업클럽백엔드3기발자국박우빈테스트코드실용적인테스트가이드

10 1일 전
kailis

발자국 2주차: 읽기 좋은 코드 == 쓰기 좋은 코드

 읽기 좋은 코드, 쓰기 좋은 코드  이번 주의 발자국 회고. Readable 코드에 대한 이야기가 마무리되는 주간이다. 이번에는 강의 듣기와 함께 미션을 토대로, 내가 실제로 코드를 구현해 보는 시간을 가져갈 수 있었는데, 중간 점검 타임에서 과제에 대해서 조금 더 짚어주시면서 내 코드를 돌이켜보는 기회를 얻을 수 있었다. 사실 Spring과 JPA를 사용하는 평소 개발 방식은 많은 부분 "이미 모듈화된" 것들을 사용하는 경우가 많다. 특히 어노테이션을 쓴다든지 하는 케이스의 런타임 객체 관리 위임과 같은 상황에서 우리는 책임의 분리를 덜 고려하고도 편안한 개발을 할 수 있다. 단적인 예만 해도 Dispatcher Servelt은 우리의 xml 등록을 대리해 주고, 스프링은 Bean을 대신 관리해서 DL을 말려준다. (ㅋㅋ) 이런 것들이 감춰져 있기 때문에 우리는 서비스 로직을 조금 더 경량화할 수 있다. 그런데 기껏 스프링이 열심히 도와준 걸 망치면 안 되지 않을까? 필드를 얼마나 넣을 것인지, 이 객체의 책임 = 변경 가능 요소는 몇 개인지와 같은 것들을 잘 고려해 보면서 리팩토링 과제를 진행해 보았다. 그리고 이러한 미션을 점검하는 중간 회고. 중간 점검 회고 우선 중간 점검에서 했던 질문 시간이 꽤 재미있었던 기억이 난다. 멘토님의 커피 사랑 ㅋㅋㅋ 을 알 수 있는 좋은 기회이기도 했고... 나중에 게이샤 커피 꼭 먹어 보겠습니다. 공통 리뷰와 함께 개별 신청자에 대한 리뷰를 진행하는 2시간 가까이의 시간이었다. 목이 아프셨다고 했는데, 다음날 출근해서 말 한마디 못하시는 건 아니었는지 염려가 된다. 주요 내용은 장표를 공유해 주셨는데, 내가 이 내용 외에도 다른 분들 코드 피드백을 들으면서 인상깊었던 부분들을 정리해 보았다. 사물함 사용 가능 여부 등의 ENUM 처리: 추천컬렉션을 가공하는 로직이 생기면 일급 컬렉션을 고민해 볼 것Get / Set이 아닌 연상되는 단어 선택한 점이 좋음if-else보다 if-early return을 추천Mutable 컬렉션보다는 한번에 Immutable 컬렉션을 만들 것많은 클래스에서 사용한다 == 하나의 객체에 책임이 과도하게 몰려 있는 것은 아닌가? : 객체 분리의 신호탄IO 로직이 변경되어도 우리의 도메인 로직은 순수하게 보존되어야 함 이런 이야기가 있으면 나는 이중에서 나에게 도움이 가장 많이 된 것들을 꼽고는 하는데, 이번 글에서는 꼽을 수가 없다. 정말 모든 관점이 큰 도움이 되었던 것 같다.  강의 회고 하고 싶은 것 현재 진행 중인 실무 프로젝트에 배운 내용을 점진적으로 적용해보기"능동적 읽기" 방식으로 오픈소스 코드를 분석하며 좋은 패턴 학습하기 (TOBE - 진짜?) 이번 주간으로 <Readable Code: 읽기 좋은 코드를 작성하는 사고법>에 대한 강의가 끝이 났다. 사실 강의는... 진짜 솔직하게 말하면 아는 내용이 많다고 생각하면서 봤었는데. 실제로 코드를 리팩토링 하는 과정에서 그 생각이 제법 오만이라는 생각을 계속해서 하게 되었다. 역시 이론과 실제는 다르고, 이상과 활용은 천지차이다. 이번 강의와 미션을 통해 클린코드와 객체 지향 설계의 중요성을 실제로 체감할 수 있었다. 특히 회사에서 가장 의식적으로, 많이 노력하려고 했던 것. "코드는 작성하는 시간보다 읽는 시간이 훨씬 많다"는 강의 내용을 들었던 것을 염두에 두고 코드를 작성하고자 노력을 많이 했다. 사실 가장 어려운 부분은 적절한 추상화 레벨을 결정하는 것인 것 같다. 어떻게 인터페이스를 나누고 책임을 나눠야 하는지... 너무 세부적으로 메서드를 분리하면 오히려 코드 흐름이 파악하기 어려워질 것이고, 너무 크게 묶으면 단일 책임 원칙을 위반하게 될 것이다. 이러한 관점에서 하나 인상깊었던 것이 떠올랐는데, 멘토님이 예로 들어 주셨던 조건 분기문에 대한 코드 분리가 그것이다. 예를 들면, if(type.equals("blahblah")) 일 때 if(isEditable()) 으로 코드를 바꾸고, isEditable에 대한 함수를 하나 더 빼는 형식이다.  이 조건일 때 이런 행위를 한다는 분기를 하나의 함수로 표현하는 것.  사실 객체의 상태를 객체 안에서만 넣는 바람에 밖에서 나눠 볼 생각을 못했던 것 같은데.... 결국 코드는 1줄이나 2줄인 게 중요한 게 아니라, 들이는 공수를 대비해서, 로직을 망가뜨리지 않는 범위에서의 효율을 추구하는 일이라는 것. 이 균형을 맞추는 것이 클린코드의 핵심이라는 것을 조금 더 깨달았다.  읽기 좋은 코드 == (나중에) 쓰기 좋은 코드 따라서 이번에 정립하게 된 것. 읽기 좋은 코드는 동시에 쓰기 좋은 코드이다. 사실 읽기 좋은 코드를 짜다 보면 옆에서 "왜 굳이?" 라는 표현을 들을 수 있다는 생각이 든다. 그런데 나는 읽기 좋은 코드가 오히려 쓰기 좋은 코드라고 생각한다. 보다 정확하게 말하면, "내가 나중에 쓰기 좋은 코드"라고 생각한다. 단적인 예로 일급 컬렉션. 일급 컬렉션은 개발자의 책임을 줄여 로직이 망가질 확률을 최소화하는 일이다. 만약 특정 변수를 주입해서 판단하는 로직이 있다면, 그 로직은 변수에 종속적이며, 추후 개발자의 판단에 종속된다. 우리는 판단을 최소화함으로써 행동을 제약하고, 제약한 행동은 나중에 내가 쓰기 좋은 코드를 낳는다. 행복한 선순환이다. 하지만.  "오만"을 참는 법  처음부터 완벽한 코드는 오만이다 이번 중간점검 때 멘토님이 하신 말씀이 있다. 우리는 개발자고, 회사에 고용되어 일하는 것은 프로라는 뜻이다. 그리고 프로가 가장 중요하게 여겨야 할 것은 시간 관리다. 즉, 코드 퀄리티를 우선하다 주객이 전도되는 상황을 일으키는 것은 옳지 않다는 이야기다. 이러한 코드에 대해서는 추후 반영 시 가능하다면 리팩토링을 하는 식으로 점진적 개선을 할 수 있어야 한다. 이상적인 클린코드를 추구하다가 실용성을 간과한 경우를 자주 접하게 된다. 가령 진짜 if문 하나로 해결되는 문제를 객체로 분리했을 때 생기는 문제라든지.... 최신 트렌드의 개발에서 개념을 이해하는 것이 아니라 해당 개념을 적용하고만 싶어서 처리하는 케이스가 그런 상황을 발생시키는 것이 아닐까. 아주 자주 나오는 격언. "은탄환은 없다." 멘토님을 통해서 이번 기회에 또 한 번 상기할 수 있었다. 완벽한 동그라미의 바퀴를 만들고자 하지 말고, 굴러가게 만드는 것이 1번이다. 그 다음 세공하면 된다. 상황에 맞는 적절한 수준의 클린코드 적용을, 리팩토링을 위한 리팩토링을 계속해서 경계해야 한다고 또 한 번 다짐.  

팥우유

[인프런 워밍업 클럽 Full Stack 3기] 2주차

1. 학습 내용1.1. Supabase Storage파일과 이미지를 저장하고 관리하기 위한 서비스 (아마존 S3와 유사)1.1.1. 주요 개념버킷(Buckets): 파일을 논리적으로 구분하는 컨테이너객체(Objects): 저장된 개별 파일정책(Policies): 파일에 대한 접근 권한 규칙 2. 미션2.1. 미션 내용파일 목록에서 각 파일의 "마지막 수정 시간"을 표시2.2. 미션 진행- 리액트 쿼리로 요청, 응답 받은 이미지 데이터 객체 내 update_at 키-값을 이미지 컴포넌트에 표시함으로써 작업을 진행했습니다.3. 추가 학습 및 적용 기술3.1. pnpm 도입과 경험npm의 문제점(패키지 중복 설치, 디스크 낭비)에 불편함을 느껴 이번 기회에 pnpm을 도입했습니다. 설치 속도가 확연히 빨라진 것을 체감할 수 있었습니다.3.1.1. npm과 pnpm의 주요 차이점저장 구조와 디스크 사용량npm: 프로젝트마다 의존성 중복 저장으로 디스크 낭비pnpm: 전역 저장소에 패키지를 한 번만 저장하고 심링크로 연결하여 공간 절약성능npm: 중복 다운로드로 인한 네트워크 부하 및 시간 소요pnpm: 공유 저장소와 링크 기반 구조로 설치 속도 향상, 병렬 처리 최적화3.2. material-tailwind 경고 제거material-tailwind 컴포넌트 사용 시 발생하는 경고 메시지를 d.ts 파일을 통해 제거했습니다. 이 방법이 안전한지 고민했지만, 큰 문제가 없다고 판단하여 적용했습니다. 인프런 커뮤니티의 질문과 답변이 해결에 도움이 되었습니다.3.3. supabase storage type 적용db type과 달리 storage type은 supabase cli를 통해 자동 생성할 수 없다는 점을 알게 되었습니다. 대신 `@supabase/storage-js` 플러그인을 통해 필요한 type을 활용할 수 있었습니다.// UploadedImage Component import { FileObject } from '@supabase/storage-js'; // ...existing code... export default function UploadedImage({ file: { name, updated_at }, }: { file: FileObject; }) { return (// component) }3.4. prettier 설정GitHub Copilot의 도움을 받아 Next.js 프로젝트에 적합한 옵션으로 설정했으며, prettier-plugin-tailwindcss를 통해 Tailwind 클래스 자동 정렬 기능을 추가했습니다.// .prettierrc { "singleQuote": true, "semi": true, "useTabs": false, "tabWidth": 2, "trailingComma": "es5", "printWidth": 80, "arrowParens": "avoid", "jsxSingleQuote": false, "bracketSpacing": true, "bracketSameLine": false, "htmlWhitespaceSensitivity": "css", "requirePragma": false, "insertPragma": false, "proseWrap": "preserve", "endOfLine": "auto", "plugins": [ "prettier-plugin-tailwindcss" ] }3.5. eslint 설정@tanstack/eslint-plugin-query를 도입하여 React Query 사용 시 모범 사례를 따르도록 설정했습니다. 이를 통해 쿼리 키 검증, 의존성 확인 등의 이점을 얻을 수 있었습니다.// .eslint.json { "plugins": ["@tanstack/query"], "extends": ["next/core-web-vitals", "plugin:@tanstack/query/recommended"] } 4. 아쉽게 적용하지 못한 기술아래 3 가지 항목들은 모두 조사, 기능 개발 계획, 프로젝트에 일부 적용까지 하기도 했으나 시간이 부족해 결국 완성되지 못한 기능들입니다.4.1. 한글 파일명 업로드 문제 해결Supabase Storage에서 한글 이름의 파일을 업로드할 수 없는 문제에 직면했습니다. 원인은 파일명 인코딩 과정이 없어서 발생한 문제였습니다. 두 가지 해결책을 구상했지만 시간 부족으로 완성하지 못했습니다.파일정보 DB 테이블 접근법: 파일명과 UUID를 매핑하여 DB에 저장하고, 실제 스토리지엔 UUID로 업로드하는 방식customMetadata 활용: Supabase Storage의 메타데이터 기능을 활용하는 방식4.2. 직접 Tailwindcss 컴포넌트 구현 시도material-tailwind 대신 Tailwindcss만으로 모든 컴포넌트를 스타일링해보고 싶었으나, 시간 부족으로 실현하지 못했습니다.4.3. 컴포넌트 구조 설계의 고민개발자 관점에서 명확하고 구분하기 쉬운 파일 구조를 만들기 위해 다양한 React 컴포넌트 구조 패턴을 조사했습니다.각 패턴의 장단점을 분석하며 프로젝트에 가장 적합한 구조를 고민했지만, 시간 관계상 실제 적용은 제한적이었습니다.제가 찾아본 React 주요 패턴들은 다음과 같습니다. 이해를 돕기 위해 각 패턴마다 특징 및 예시 코드까지 준비하였으나 글이 너무 길어져 읽는데 어려움이 있을 것 같아 최종적으로 간단하게 한 줄로 요약했습니다.4.3.1. Presentational and Container Pattern로직과 UI를 분리하는 패턴으로, 재사용성과 테스트 용이성이 향상되지만 props drilling 문제가 발생할 수 있습니다.4.3.2. Compound Component Pattern복합적인 UI를 구성하는 관련 컴포넌트들을 그룹화하고 내부적으로 상태를 공유하는 패턴입니다. API 사용 경험은 향상되지만 TypeScript 타입 정의가 복잡해질 수 있습니다.4.3.3. Render Props Pattern컴포넌트의 렌더링 로직을 prop 함수로 전달하는 방식으로, 로직 재사용은 용이하지만 콜백 중첩으로 인한 디버깅 어려움이 있을 수 있습니다.4.3.4. Custom Hook Pattern로직을 훅으로 추출하여 여러 컴포넌트에서 재사용하는 패턴입니다. React의 핵심 패턴 중 하나로, UI와 로직의 분리가 명확합니다.4.3.5. Context API Pattern여러 컴포넌트에서 데이터를 공유하기 위한 패턴으로, props drilling을 방지할 수 있지만 불필요한 리렌더링이 발생할 수 있습니다.4.3.6. Atomic Design PatternUI 컴포넌트를 원자(Atoms), 분자(Molecules), 유기체(Organisms), 템플릿(Templates), 페이지(Pages)로 나누는 구조로, 체계적인 UI 구성이 가능하지만 초기 설계 시간이 많이 소요됩니다.4.3.7. Client/Server Component PatternNext.js 14 App Router의 핵심 패턴으로, 서버에서 데이터를 페칭하고 클라이언트에서 인터랙션을 처리하여 번들 크기를 최적화합니다.4.3.8. Server Components and Suspense Pattern데이터 로딩 상태를 선언적으로 처리하는 패턴으로, 점진적 UI 로딩을 지원하고 사용자 경험을 향상시킬 수 있습니다. 5.마무리이번 스터디를 통해 많은 것을 배우고 적용해보는 즐거움을 느꼈습니다. 계획했던 것보다는 적게 구현했지만, 새로운 기술들을 탐색하고 실험해본 경험은 매우 가치 있었습니다. 더 많은 공부 시간을 확보하니 다양한 시도를 해볼 수 있었지만, 동시에 욕심이 커져 모든 계획을 실현하지는 못했습니다. 3주 차에는 개인 약속으로 인해 학습 시간이 줄어들 것 같지만, 지금까지의 경험을 바탕으로 더 효율적으로 학습하고 구현해보겠습니다.무엇보다, 호기심을 가지고 새로운 기술을 탐색하고 적용해보는 과정 자체가 개발자로서 성장하는 중요한 발판이 된다는 것을 다시 한번 느낄 수 있었습니다. 

당황한 수달

[인프런 워밍업 클럽 3기] PM/PO 2주 차 발자국

PM/PO로 성장하기 위해, 고객과 데이터에 대한 전문성을 쌓는 것이 얼마나 중요한지 배운 한 주였습니다. 이번 주 강의(시작하는 PM/PO들에게 알려주고 싶은, 프로덕트의 모든 것)는 크게 두 가지 섹션(고객/데이터에 대한 전문성)으로 구성되었습니다.  2주 차 동안 무엇을 배웠는가?1. 고객 전문가가 되기 위한 접근법PM은 단순한 기획자가 아니라 고객의 문제를 해결하는 사람입니다.이를 위해 고객을 깊이 이해해야 하며, 그 방법 중 하나가 심층 인터뷰와 사용성 인터뷰입니다.강의에서는 단순히 "고객을 만나면 좋다"가 아니라,리서치를 왜 하는지어떤 목적을 가지고 진행해야 하는지각 인터뷰 방법을 어떻게 실행하는지그리고 구체적인 사례를 바탕으로 다뤄졌습니다.특히 튜터님이 직접 진행하셨던 실제 리서치 사례(모집 방법, 사전 질문을 통한 필터링 기법 등)를 공유해 주셔서, 단순한 이론이 아니라 실무에서 어떻게 활용할 수 있을지 감을 잡을 수 있던 소중한 시간이었습니다. 2. 목적이 있어야 의미가 있는 데이터PM이 데이터를 활용할 때도 "어떤 데이터를 모을 것인가"가 아니라 "이 데이터를 통해 무엇을 검증할 것인가"가 중요하다는 것을 다시한번 느꼈습니다. 단순히 데이터를 축적하는 것이 아니라, 이를 통해 의사결정을 내릴 수 있어야 한다는 점을 강조하셨습니다.2주 차 회고이번 주 강의를 통해 가장 크게 배운 것은 "무엇을 하든 목적을 명확히 해야 한다"는 점입니다.특히 저는 과거에 사용성 인터뷰와 심층 인터뷰를 진행한 경험이 있지만, 지금 돌아보면 아쉬운 점이 많았습니다. 당시에는 일단 하면 답이 나올 것이라는 막연한 기대만 있었고, 명확한 검증 목표 없이 진행했기 때문입니다.이번 강의를 통해 내가 했던 실수를 인지하고, 어떻게 개선할 수 있을지 고민할 기회가 되었습니다. 또한, 실시간 온라인 라이브에서 튜터님께 직접 질문하며 부족했던 부분을 보완할 수 있었던 것이 특히 좋았습니다. 다음 주 학습 계획은?다음 주는 지표를 깊이 탐구하는 강의로 구성되어 있습니다. 일정이 다소 빠듯하긴 하지만, 강의 목차를 보니 익숙한 개념들이 포함되어 있어 복습하는 마인드로 접근하려 합니다. 다만, 단순한 복습이 아니라 내가 미처 인지하지 못했던 핵심 포인트를 짚어내는 것에 집중하려고 합니다. 기존에 알고 있던 개념이라도 더 깊이 이해하고 실무에서 활용할 수 있도록, 중요한 부분을 꼼꼼히 파악하며 학습할 계획입니다.

기획 · PM· PO김민우튜터인프런워밍업클럽스터디PMPO

강동훈

[인프런 워밍업 클럽 3기 - CS] - 2주차 미션 (자료구조와 알고리즘)

재귀함수에서 기저조건을 만들지 않거나 잘못 설정했을 때 어떤 문제가 발생할 수 있나요?재귀함수는 함수 내부에서 자기 자신을 다시 호출하여 작업을 수행하는 함수를 의미한다. 즉, 자기 자신을 무한대로 호출하여 작업하기 때문에 함수 종료 조건인 기저조건을 설정하지 않는다면, 해당 함수가 실행됨에 따라 무한대로 콜스택에 메모리가 얹히게 되고 스택 오버플로우가 발생하여 프로그램이 강제 종료된다.// 기저 조건 없는 경우 function factorial(n){ return n * factorial(n - 1) } // RangeError : Maxmum call stack size exceeded // 기저 조건 설정 function factorial(n) { if (n == 0) return 1; return n * factorial(n - 1); }0부터 입력 n까지 홀수의 합을 더하는 재귀 함수를 만들어보세요.하위조건 : n - 1이 홀수인지 확인하고 홀수일 경우 n을 더하고 짝수일 경우 0을 더함기저조건: n이 0 이하일 경우 0을 반환하고 함수 종료function sumOdd(n){ // 재귀 로직 if (n <= 0) return 0; let oddNum = n % 2 === 0 ? 0 : n; return oddNum + sumOdd(n - 1); } console.log(sumOdd(10)) // 25다음 코드는 매개변수로 주어진 파일 경로(.는 현재 디렉토리)에 있는 하위 모든 파일과 디렉토리를 출력하는 코드입니다. 다음 코드를 재귀 함수를 이용하는 코드로 변경해보세요.const fs = require('fs'); // 파일을 이용하는 모듈 const path = require('path'); // 폴더와 파일의 경로를 지정해주는 모듈 function traverseDirectoryRecursive(directory) { const files = fs.readdirSync(directory); // 1. 인자로 받은 폴더 내부 파일들 추출 for (const file of files) { const filePath = path.join(directory, file); // 2. 파일 경로 합치기 const fileStatus = fs.statSync(filePath); // 2. 파일 정보 얻기 if (fileStatus.isDirectory()) { // 3-1. 폴더일 경우 재귀 console.log('디렉토리:', filePath); traverseDirectoryRecursive(filePath); } else { // 3-2. 파일일 경우 출력 console.log('파일:', filePath); } } } traverseDirectoryRecursive('.'); // 현재 경로의 모든 하위 경로의 파일, 디렉토리 출력하위 조건:인자로 받은 Directory의 파일과 폴더를 읽어온다파일 경로를 합치고 파일 정보를 얻어온다폴더일 경우, 재귀함수를 통해 내부 폴더의 파일과 폴더를 읽는다파일일 경우, 파일을 출력한다.기저조건:현재 폴더 내부 모든 파일 수만큼 반복📔 회고알고리즘 문제가 아닌 실전에서 사용할 수 있는 재귀 함수로 응용을 해보니 생각보다 하위조건을 파악하고 기저조건을 설정하는 것이 쉽지 않다는 것을 깨달았다. 처음에는 계속해서 코드를 읽어보면서 익숙하지 않은 fs모듈에 대해서 먼저 파악해보고, 제공되는 메서드들을 익혀보았다. 그렇게 코드의 흐름을 익혀가면서 반복되는 부분을 구분하였고, 재귀적으로 해결할 수 있는 부분은 while 문이라는 것을 파악했다. 기존에 스택을 통해서 파일들을 가져오고 데이터를 쌓아오면서 while 문을 통해 스택에 있는 데이터를 다시 출력하는 코드였다는 것을 파악하였고, 이를 재귀적으로 변경하기 위해서는 스택 자료구조를 사용하지 않고 하나의 함수에 하나의 폴더를 읽어오고 재귀적으로 함수를 다시 호출하면서 폴더 내부의 파일을 찾아가는 형식으로 수정할 수 있다는 것을 파악했다. 그렇게 하위조건을 설정하였고 기저조건을 만들어서 성공적으로 재귀함수로 코드를 수정할 수 있었다.이렇게 알고리즘을 응용하여 실전에서 사용할 수 있다는 것을 크게 깨달았고, 앞으로 알고리즘을 배울 때도 실전에서도 사용될 수 있는 다양한 사례를 함께 찾아보면서 공부하면 더 알고리즘 개념을 탄탄히 가져갈 수 있을 것 같다.

알고리즘 · 자료구조자료구조인프런워밍업

lkwo

워밍업 클럽 3기 BE 클린코드 2주차 발자국

2주차를 마무리하며...2주 차에는 예제 코드의 리팩토링을 직접 해보는 과제와 중간 점검이 있었습니다.강의를 보며 따라 할 때는 괜찮았지만, 막상 코드에서 리팩토링을 하려니 무엇부터 시작해야 할지 막막하더군요.리팩토링은 강의만 본다고 해결되는 영역이 아니라는 것을 느꼈고, 체화되기까지 시간이 필요하겠다고 생각한 한 주였습니다.강의내용 정리좋은 주석이란?주석이 많다는 것은 코드에 적절한 추상화를 적용하지 못해, 주석으로 코드를 설명하려는 것은 아닌지 의심해봐야 합니다.좋은 주석이란 코드로 전달할 수 없는 정보를 담는 것입니다.예를 들어, 의사 결정의 히스토리 같은 내용을 기록하는 것이 좋은 주석입니다.또한, 주석도 코드와 마찬가지로 버전 관리가 필요합니다. 관련된 의사 결정이 변경되었다면, 주석도 잊지 말고 함께 수정해야 합니다. 변수와 메서드 나열변수는 사용하는 위치와 최대한 가까이 두어 뇌 메모리 부담을 줄이는 것이 좋습니다.메서드와 변수의 접근자 종류에 따라 위치를 정리할 수 있습니다.메서드는 접근자 종류뿐만 아니라, 중요도와 로직의 종류를 기준으로 배치하면 더 정리된 코드를 만들 수 있습니다. 패키지 나누기패키지는 단순한 디렉터리가 아니라, 문맥적인 정보를 제공하는 역할을 합니다.적절하게 패키지를 나누면 코드의 가독성과 유지보수성이 향상됩니다.  IDE 활용하기정렬 단축키를 활용하여 코드 스타일을 통일합니다.Linting & 스타일 도구를 사용하여 코드 품질을 높일 수 있습니다.SonarLintEditorConfig리팩토링 직접 해보기리팩토링리팩토링에는 합리적인 이유가 필요하다리팩토링을 할 때는 단순히 코드 줄 수를 줄이는 것이 목표가 아니라, 객체의 역할과 책임을 올바르게 분배하고, 적절한 추상화를 적용하는지가 중요합니다. 이를 위해 리팩토링 과정에서 내가 합리적으로 수정하고 있는지 계속 의심하며 진행했습니다. (제가 정말 합리적으로 했는지는 모르겠습니다만...) 리팩토링 순서 정하기처음 코드를 봤을 때, 어디서부터 손을 대야 할지 막막함을 느꼈습니다. 그래서 나름의 리팩토링 순서를 정하고 진행해보았습니다.내가 읽기 좋은 코드로 변경하기공백 추가하기 (가독성 개선)함수 분리하여 추상화하기테스트하기 쉬운 구조로 변경하기역할과 책임을 고려하여 인터페이스와 객체로 분리하기 가독성 개선하기패키지 분리하기   리팩토링에는 정답이 없다리팩토링은 정해진 답이 없다는 점이 가장 어렵게 느껴지는 부분인 것 같습니다.  중간점검Day 4 미션 피드백아래는 제가 미션으로 제출한 코드입니다.이번 점검에 피드백으로 얻은 새로운 인사이트를 나열해봅니다.반환 타입이 boolean인 경우, 예외를 발생시키기 전에 해당 메서드의 사용 현황을 먼저 파악한 후, 상황에 맞게 리팩토링해야 한다.예외를 던지는 것은 비용이 많이 들 수 있으므로, 필요할 때만 신중하게 사용해야 한다.  private static boolean VALID = true; public boolean validateOrder(Order order) { if (order.isItemEmpty()) { throw new AppException("주문 항목이 없습니다."); } if (order.isTotalPriceLessThenZero()){ throw new AppException("올바르지 않은 총 가격입니다."); } if (order.hasNotCustomerInfo()) { throw new AppException("사용자 정보가 없습니다."); } return VALID; }Day 7 미션 피드백물론 리뷰를 할 때, 말을 조심스럽게 해야할 테지만, 코드 리뷰의 목적은 다 같이 좋은 코드를 만들어 보자는 좋은 의미입니다.코드 리뷰를 할 때는 인격적인 모독을 하지 않는다.코드 리뷰를 받을 때는 코드와 나를 동일시 하지 않는다.배운 점 정리 정적 메서드 팩토리를 만들 때는 생성자를 private으로 감춘다.null 대신 Empty 객체를 만들어서 처리한다.단수/복수를 신경 써서 변수명과 메서드명을 짓는다.리팩토링에는 정답이 없다. 하지만, 효율적인 구조와 다양한 의견이 있다. 

웹 개발클린코드

Yang HyeonBin

[인프런 워밍업 클럽 3기] 풀스택 과정 2주차 발자국 👣

2주차에 배운 내용을 정리해본다.깃허브 링크 1. 배운 내용1. Next.js에서 메타데이터 정의하기<meta> 태그를 이용해 사이트 정보를 정의하려면,서버 컴포넌트 파일에서Metadata를 정의해줘야 함// page.tsx import { Metadata } from "next"; import Ui from "./Ui"; // 페이지의 메타데이터를 정의 // use client에서는 사용 불가 - 클라이언트 코드는 Ui.tsx에서 정의하는 이유 export const metadata: Metadata = { title: "Dropbox Clone", description: "A minimalist Dropbox Clone", }; export default function Home() { return <Ui />; } 2. 파일 드랍 존 만들기<input /> 태그를 이용, type="file"3. supabase1. storage bucket 만들기업로드 가능한 파일 종류 설정 가능만들 때 Allowed MIME types 옵션에서 image/* 등의 조건을 추가하면 됨2. policy 생성사이드바 policy 메뉴에서 생성 가능이름, 가능한 액션 종류 선택, 누구에게 가능하게 할지 선택 가능4. 파일 드래그앤드롭 - react-dropzone 라이브러리 사용사용법은 npm 공식 문서의 코드 조각을 확인https://www.npmjs.com/package/react-dropzone  2. 이슈 사항1. storage의 get url 형태 변경: getImageUrl 함수 커스텀 어려움이미지의 만료일을 지정할 수 있게 변경됨그러면서 token이라는 서치 파라미터가 필수값으로 추가된 듯토큰을 누락한 형태로 확인 시 에러가 발생하며 이미지 로드 실패{"statusCode":"400","error":"Error","message":"querystring must have required property 'token'"} bucket을 public으로 전환하고, supabase에서 제공하는 getPublicUrl 메서드를 사용storage에서 bucket 이름 옆 드롭다운 메뉴 → edit bucket → public으로 설정 getImageUrl 함수 내부를 아래와 같이 수정const { data } = supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET!) .getPublicUrl(path); /** * A simple convenience function to get the URL for an asset in a public bucket. If you do not want to use this function, you can construct the public URL by concatenating the bucket URL with the path to the asset. * This function does not verify if the bucket is public. If a public URL is created for a bucket which is not public, you will not be able to download the asset. * * @param path The path and name of the file to generate the public URL for. For example `folder/image.png`. * @param options.download Triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. * @param options.transform Transform the asset before serving it to the client. */ getPublicUrl( path: string, options?: { download?: string | boolean; transform?: TransformOptions } ): { data: { publicUrl: string } } { const _path = this._getFinalPath(path) const _queryString = [] const downloadQueryParam = options?.download ? `download=${options.download === true ? '' : options.download}` : '' if (downloadQueryParam !== '') { _queryString.push(downloadQueryParam) } const wantsTransformation = typeof options?.transform !== 'undefined' const renderPath = wantsTransformation ? 'render/image' : 'object' const transformationQuery = this.transformOptsToQueryString(options?.transform || {}) if (transformationQuery !== '') { _queryString.push(transformationQuery) } let queryString = _queryString.join('&') if (queryString !== '') { queryString = `?${queryString}` } return { data: { publicUrl: encodeURI(`${this.url}/${renderPath}/public/${_path}${queryString}`) }, } } StorageFileApi.ts를 참고하면 다양한 메서드가 있음 - createSingedUrl를 이용하면 expiresIn을 직접 지정 가능. 이걸 이용하면 bucket이 public이 아니어도 가능할듯 /** * Creates a signed URL. Use a signed URL to share a file for a fixed amount of time. * * @param path The file path, including the current file name. For example `folder/image.png`. * @param expiresIn The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute. * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. * @param options.transform Transform the asset before serving it to the client. */ async createSignedUrl( path: string, expiresIn: number, options?: { download?: string | boolean; transform?: TransformOptions } ): Promise< | { data: { signedUrl: string } error: null } | { data: null error: StorageError } > { try { let _path = this._getFinalPath(path) let data = await post( this.fetch, `${this.url}/object/sign/${_path}`, { expiresIn, ...(options?.transform ? { transform: options.transform } : {}) }, { headers: this.headers } ) const downloadQueryParam = options?.download ? `&download=${options.download === true ? '' : options.download}` : '' const signedUrl = encodeURI(`${this.url}${data.signedURL}${downloadQueryParam}`) data = { signedUrl } return { data, error: null } } catch (error) { if (isStorageError(error)) { return { data: null, error } } throw error } } /** * Creates multiple signed URLs. Use a signed URL to share a file for a fixed amount of time. * * @param paths The file paths to be downloaded, including the current file names. For example `['folder/image.png', 'folder2/image2.png']`. * @param expiresIn The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute. * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. */ async createSignedUrls( paths: string[], expiresIn: number, options?: { download: string | boolean } ): Promise< | { data: { error: string | null; path: string | null; signedUrl: string }[] error: null } | { data: null error: StorageError } > { try { const data = await post( this.fetch, `${this.url}/object/sign/${this.bucketId}`, { expiresIn, paths }, { headers: this.headers } ) const downloadQueryParam = options?.download ? `&download=${options.download === true ? '' : options.download}` : '' return { data: data.map((datum: { signedURL: string }) => ({ ...datum, signedUrl: datum.signedURL ? encodeURI(`${this.url}${datum.signedURL}${downloadQueryParam}`) : null, })), error: null, } } catch (error) { if (isStorageError(error)) { return { data: null, error } } throw error } } 2. server action 파일에서 console.log는 개발자 도구가 아닌 터미널에 찍힌다는 사실..storageActions.ts에 정의한 uploadFile 함수를 수정하고 제대로 데이터가 들어가는지 확인하려고 console.log를 사용아무리 해도 useQuery까지는 잘 로그가 찍히는데, uploadFile에 작성한 로그가 브라우저 개발자도구에 전혀 찍히지 않아 뭐가 문제인지 한참 헤맴코드 에디터 터미널에 찍히고 있었음.. ㅠㅠ3. 파일명에 한글이 포함될 경우 업로드 안되는 오류디스코드에 다른 러너분들이 공유해준 정보에 따르면 supabase storage는 AWS의 S3 스토리지와 호횐되어, 파일명도 AWS S3의 Safe charaters - 영문, 숫자, 일부 특수기호 만 허용한다고 함어떻게 저장할까?base64 인코딩을 통해 S3-safe한 이름으로 변경해 저장하면 업로드 가능⇒ 저장 및 다운로드, 이름 표시하는 코드에서 인코딩/디코딩 함수를 사용하게 변경 완료어떻게 검색할까?인코딩/디코딩된 값으로 검색 호환이 안됨⇒ db에 저장해야 함db에 저장하도록 변경했는데, 한글 초성만 검색됨..todo-list 검색 때와의 차이가 뭐길래 안되는거지? 3. 미션 - 파일의 마지막 수정(업로드) 시간 표시하기storage를 확인하면, Added on, Last modified 정보가 있음file이 어떤 형태인지 로그 찍어 확인file.updated_at 키에 저장된 값임을 확인, 이 값을 사용추가로 file.metadata에 파일 타입과 사이즈 등의 정보도 있어 그 정보들도 적절히 표시하도록 함 (ui는 supabase storage를 참고함)

웹 개발Next.jsSupabaseReact.jsFile

LC-02s

[인프런 워밍업 클럽 3기] 풀스택 스터디 2주차 미션 회고 발자국

학습 내용 요약 인프런 워밍업 클럽 3기 풀스택 스터디 2주차에는 Supabase Storage를 활용하는 방법을 다루었습니다. 인프런에서 발자국을 작성할 때 강의를 보지 않고도 강의 내용을 파악할 수 있을 만큼 자세한 내용을 작성하는 건 지양해 달라고 가이드한 만큼 강의에 대한 필기는 최소화 하고 회고 위주로 적어보겠습니다. Supabase Strorage 기능 간단 정리Supabase의 Storage 기능은 파일 저장을 위한 서비스로, AWS S3와 같은 오브젝트 스토리지 기능을 제공함. Next.js, React, Flutter, Node.js 등 다양한 환경에서 사용할 수 있으며, PostgreSQL 기반의 권한 관리(RLS)를 지원하는 것이 특징임. 1. 파일 저장 및 관리이미지, 동영상, 문서 등 다양한 파일 형식을 저장 가능파일 업로드, 다운로드, 삭제, 이동 등의 기능 제공버킷(Bucket) 단위로 파일을 관리2. 권한 및 보안 (RLS)Row Level Security (RLS): PostgreSQL과 동일한 방식으로 접근 권한을 설정 가능공개(Public) 및 비공개(Private) 버킷 지원JWT 기반 인증을 사용하여 사용자별 접근 제한 가능3. 파일 접근 방식퍼블릭 파일: 누구나 URL을 통해 접근 가능프라이빗 파일: 인증된 사용자만 접근 가능 (Signed URL 활용)서명된 URL (Signed URLs): 일정 시간 동안만 유효한 URL 생성 가능4. 폴더 및 파일 구조디렉토리(폴더) 개념 지원폴더 내에서 파일 정리 및 관리 가능5. API 및 SDK 지원Supabase Client SDK를 통해 간편한 파일 업로드 및 관리 가능RESTful API 제공 (HTTP Client를 사용하여 직접 호출 가능)6. 이미지 변환 및 최적화Supabase Storage Image Transformation 지원 (이미지 크기 조절, 포맷 변경 등)CDN을 통해 빠른 이미지 제공 가능  Dropbox 클론 미션 회고 풀스택 스터디 2주차 미션은 강의에서 진행하는 Next.js와 Supabase Storage를 활용한 Dropbox 클론 앱에 사진 별 마지막 수정 시간을 표시하는 것이었지만, 저는 기타 편의기능을 더 추가해 보았습니다. 진행했던 미션은 해당 링크에서 보실 수 있습니다.  미션 수행 내용아래는 제가 수행한 미션에 대한 내용을 정리해보았습니다. 기능 명세이미지 파일 업로드 기능드래그 앤 드롭 기능다중 업로드 기능업로드한 이미지 파일 조회 기능키워드 검색 기능이미지 파일 다운로드 기능업로드한 이미지 파일 수정 기능이미지 파일명 변경 기능업로드한 이미지 파일 삭제 기능 강의에서는 기본적으로 파일 입출력에 관련된 기능만 다루었지만, 저는 실제 사용성을 고려하여 파일 업로드 전 미리보기 기능, 이미지 다운로드 기능, 파일 이름 변경 기능 등을 추가해 보았습니다. 또한 강의에서는 react-dropzone 라이브러리를 사용해서 드래그 앤 드롭 기능을 구현하였지만, 저는 프로젝트의 기본적인 스타일 프레임워크로 만타인을 사용하고 있었기 때문에, 만타인에서 따로 지원하는 @mantine/dropzone 라이브러리를 사용하여 구현하였습니다. 미션에 사용한 기술들은 아래에 따로 정리해 두었습니다. 사용할 때마다 느끼는 거지만 만타인은 정말 편한 것 같아요. 사용 기술프레임워크: Next.js v15, React v19데이터베이스: Supabase서버 상태관리: Tanstack Query v5클라이언트 상태관리: Zustand v5스타일 프레임워크: TailWindCSS v3, Mantine v7, Tabler Icons모노레포: Turbo Repo패키지 매니저: pnpm   트러블 슈팅아래는 미션을 진행하면서 만났던 문제들을 해결하는 과정에 대한 내용입니다. 파일명에 한글이 포함될 경우 Supabase Storage에 업로드하지 못하는 문제이미지 파일 이름에 한글이 포함될 경우 업로드가 되지 않는 문제가 있었습니다. 이슈를 찾아보니 Supabase Storage의 정책적인 문제였고, AWS의 S3 서비스도 동일한 문제를 가지고 있었기에 아래 조치들을 취하였습니다. 조치 1.처음 취했던 조치는 아래와 같이 nanoid 라이브러리를 활용하여 중복되지 않는 이름을 생성 후 기존의 파일 이름을 대체하는 방식을 사용했었습니다. 하지만 해당 방식을 사용하면 기존의 파일 이름이 사용자가 식별하지 못하는 값으로 대체되는 문제와, 중복되는 파일을 확인할 수 있는 방법이 없어지는 문제가 있어 최종적으로는 사용하지 않았습니다.'use server' import { nanoId } from 'nanoid' export const uploadImages = async ({ files, }: UploadImagesParams): Promise<{ data: { id: string; path: string } | null }[]> => { const client = await createServerSupabaseClient() return await Promise.all( files.map((file) => { const extension = extractExtension(file.name) const path = `/${nanoId() + '.' + extension}` return client.storage .from(process.env.SUPABASE_BUCKET_NAME!) .upload(path, file, { upsert: true }) }), ) } 조치 2.두 번째로 취한 조치는 조금 번거롭긴 하지만 파일과 1 대 1 로 대응되는 데이터베이스 테이블을 만들어서 관리하는 방식을 사용하였습니다. Supabase에서 지원하는 uuid를 활용하여 테이블의 Primary Key를 설정해 주었고, 이미지 업로드 시 먼저 테이블에 기존 파일 이름을 기반한 데이터 insert 후 생성된 uuid를 사용하여 파일명을 재설정하는 방식으로 우회하였습니다. Supabase에서 지원하는 uuid를 사용했기에 nanoid 같은 별도의 식별자 생성 라이브러리를 관리하지 않을 수 있었습니다.export const uploadImages = async ({ files, }: UploadImagesParams): Promise<{ data: { id: string; path: string } | null }[]> => { const client = await createServerSupabaseClient() const databaseQueries = files.flatMap((file) => { return client .from('minibox') .upsert({ name: file.name }) .select() .then((result) => result.data?.[0] ?? null) }) const targetFiles = await Promise.all(databaseQueries) const storageQueries = targetFiles.map((data) => { if (!data) { return { data: null } } const extension = extractExtension(data.name) const path = `/${data.id + '.' + extension}` const file = files.find((file) => file.name === data.name)! return client.storage .from(process.env.SUPABASE_BUCKET_NAME!) .upload(path, file, { upsert: false }) }) return await Promise.all(storageQueries) }업로드한 파일명이 한글일 경우 올바르게 검색 되지 않는 문제 (feat. MacOS)MacOS 환경에서 업로드한 파일을 별도의 후처리 없이 그대로 데이터베이스에 업로드 했더니 한글이 포함된 파일명에 대해서 아래와 같이 문자열 포함 여부를 판단하는 ilike 쿼리가 제대로 동작하지 않는 문제가 있었습니다.export const getImages = async ({ query = '' }: GetImagesParams): Promise<DroppedImageFile[]> => { const client = await createServerSupabaseClient() const imagesDataAll = await client.from('minibox').select('*').ilike('name', `%${query}}%`) } 원인을 분석해보니 아래와 같이 파일 이름에 한글이 포함되어 있을 시 자음과 모음이 모두 분리된 상태로 저장되어 있어 특정 키워드 포함 여부를 올바르게 판단하지 못해 발생한 문제였습니다.// input 'temp-훈이머리귤.jpeg'.split('')조치기존에 사용하던 ilike 쿼리를 제거하고, 자바스크립트에서 지원하는 String.prototype.normalize 메서드를 사용하여 기존 데이터에 대한 정규형 정준 결합(Normalization Form Canonical Composition) 절차를 거친 후 필터링을 거치는 방법으로 해결하였습니다.export const getImages = async ({ query = '' }: GetImagesParams): Promise<DroppedImageFile[]> => { const client = await createServerSupabaseClient() const imagesDataAll = await client.from('minibox').select('*') const targetData = imagesDataAll.data .filter(({ name }) => name.normalize('NFC').includes(query)) .map(({ id, name }) => `${id + '.' + extractExtension(name)}`) }  후기저는 이제껏 프론트엔드 개발을 접해보면서 한 번도 이미지 파일에 관련된 기능을 작업해보지 않았었습니다. 물론 서버 액션을 사용해서 조금 더 간략한 인터페이스를 사용했기에 실제 FormData 인터페이스를 사용하여 통신 로직을 작성하는 경험은 해보지 못했다는 한계가 있지만, 이번 미션을 통해 자바스크립트로 이미지 파일을 핸들링하는 방법과, Storage 서비스를 연동하여 저장까지 모두 접해볼 수 있어서 개인적으로는 만족스러웠던 한 주였던 것 같습니다. 긴글 읽어주셔서 감사합니다. ☺  

프론트엔드워밍업클럽3기풀스택Next.jsSupabase

정예은

[워밍업클럽3기] 백엔드 코드 - 박우빈 발자국 2주차

강의 수강 노션 링크https://www.notion.so/DAY06-1b2010f075ca80d09e08d4dd35376dd5?pvs=4https://www.notion.so/DAY07-1b3010f075ca80b4945cd230929481ff?pvs=4https://www.notion.so/DAY09-1b4010f075ca805f8e1ec01a7c00b16c?pvs=4https://www.notion.so/DAY10-1b4010f075ca80a7a731da0aa16b8e43?pvs=4출처[워밍업클럽 리더블코드 ][워밍업클럽 테스트코드] 👣발자국2주차👣🏫배운 내용 🏫주석의 양면성자주 변하는 정보는 최대한 주석 사용 지양하기우리가 가진 모든 표현방법을 총 동원해서 → 코드에 녹여 → 주석 사용 지지뢰찾기 리팩토링게임의 상태를 주석으로 설정하는 대신 → ENUM으로 관리그래서 외부에서 호출해서 사용하기무한루프 반복 구조는 위험특정 상황에서만 반복문 돌도록 바꿔줘야한다.지뢰찾기 같은 케이스에서는 게임이 “진행중” 일때만 반복문 돌 수 있도록 처리하기변수와 메서드의 나열 순서💡상태변경 메서드 >> 판별 메서드 >> 조회 메서드 순으로 나열 하자이때, 메서드 우선순위는 공개 메서드에서 private메서드 순으로 내려와야한다.  자동테스트?그동안 내가 학원에서 배워온 건, 수동테스트 였나보다스프링부트에서 애너테이션을 활용하여 수동사냥만 해왔던 것..인가?그리고 단위로 /unit으로 쪼개서 (메소드,클래스별로) 테스트를 진행 ⇒ 단위테스트그러다보니 검증속도도 빠르고 안정적임  JUnit이란?단위 테스트를 위한 프레임워크 → 퀜트백 프레임워크풍부한API제공해주는 프레임워크로 테스트 코드 작성해보자   JUnit vs assertJ 두개의 차이점AssertJ 의 장점자연어 가까워 가독성이 좋다체이닝 방식이 가능함JUni5의 단점assertEquals는 단순히 "Expected: A, Actual: B" 결과물만 추출함기능이 단순하고 제한적  assertJ 다양한 메서드기본적인 검증 isEqualTo() : 두 값이 같은지 비교 isNull() : 값이 null인지 비교 isTrue() : 값이 true인지 비교컬렉션 검증 hasSize() : 컬렉션의 크기 비교 —> 리스트의 사이즈 찾기 contains() : 컬렉션에 특정 요소가 포함되어 있는지 확인 isEmpty() : 컬렉션이 비어 있는지 확인 🎶경계값 조건 * 정수가 3이상일때, A라는 조건 만족해야함.해피케이스경계값 활용하기 !즉, 3에대한 테스트를 짜보자.5에 대해는 만족하지만, 3이 만족이 안될 수 있잖아 !예외 케이스2로 조건값보다 더 아래쪽 범위로 테스트 하기❗인사이트❗칭찬깃 사용법에 대해 좀더 연구하고, 프로젝트에 적용하며 강의를 따라가려고 노력하였다.깃에 대해 전혀 몰랐던 사람으로,,, 개발 공부하기 위해선 깃 활용이 무척이나 중요하다는 걸 깨달았다.코드리팩토링시 강의를 보며 , 강사님이 로직 처리를 하는 한단계 한단계씩 끊어서 정리하였다.테스트코드 진행시, 테스트하기 어려운 부분을(요구사항에 맞게 테스트 로직을 짰는데, 그 요구사항이 개발하는 시점의 요구사항이랑 충돌이 될때 )잘 이해하고 숙지하며 이 로직에 대해선 따로 분리하여 테스트 코드 관리하는 시야가 필요하다. 아쉬움그러나, 동영상 일시정지를 하고 노션에 정리한다고 한들, 온전히 내것이 되는가? 아쉬움이 남아있다.이상태로 다시 한번 해보세요~ 주어지면 , 아무것도 못한다.내 스스로 코드를 짜보는 학습이 필요할 것 같다.이번 미션11 코드 제출도 어디서 어떻게 시작 해야 할지 막막하다 앞으로 어떻게?지금도 지뢰찾기 코드 마스터 하지도 않고, 미션 제출도 선생님 코드 따라치기만 했었다. 지뢰찾기 로직을 파악하기엔 내 머리가 아직 준비가 안되었고, 내 마음의 여유가 준비되지 않은 상태였다. 현재 국비학원 졸업작품으로 팀프로젝트를 지난주에 시작 하다보니, 우선순위는 팀포폴이다. 그래서 시간을 내어 지뢰찾기 코드를 마스터 하기에는 조금은 어려울 듯 하여 ,,,, 팀포폴이 어느정도 마무리가 되어가면 그때 지뢰찾기 자바 코드 눈에 익히고 리팩토링 수업을 다시 들으며 공부를 해야 할 것 같다는 생각이 든다.   🧑🏻‍💻두번째 중간점검 나의 코드를 다른분 코드와 비교해보자 !미션4 미션 공통 피드백static정적 메소드는 빼자 ( 인텔리제이 단축키 사용한 사람 적.발) 풀스택 취업 준비백엔드의 매력은 ?눈에 예쁘게 보이는거 좋아하는데 → 프론트 개발자도 고민 → 프론트 앤드를 어느정도 잘 할줄 아는 백앤드 개발자가 되기로 함성향상 잘 맞을 것 같았다. 복잡한 방식을 여러 방법으로 접근 할 수 있는게 성향이 잘 맞았다. 따라치기만 하는 지금 상황어려움 보다는 익숙함의 문제이다 . 어려움건 10%일뿐 .익숙하지 않아서 거부감이 드는 것 일뿐,진짜 어려운건 아님 . 석박사 해야지 알수있는 정도는 아님메타인지 및 의도적으로 수련하는 것이 가장 빠르고 명확하다 💡될때까지 반복해라.💡계층구조 패키지 나누기 기준이란?도메인중심 ( 유저, 히스토리, 오더 )도메인별로 관심사가 명확해짐프로젝트가 커서, 도메인별로 떨어져야한다 → 아주 좋아유저가 회원이라는 도메인이 정말 중요해서 떼어야함 → 아주 유리 하다컨트롤러,서비스 계층들이 각각 저 도메인별로 나누어져 있다보니, 패턴이 달라질 수 있음공통기능이 멀리 떨어져 있으니, 공통기능이 중복으로 생성 될 우려가 있음 레이어 중심 ( 컨트롤러,서비스,모델)한눈에 레이어러 보기 좋아.도메인간 결합도가 증가해서 MSA전환이 불리하다 💡작은 프로젝트이면 레이어중심이 좋다💡큰 프로젝트는 도메인 중심으로 잡자. 개발 면접a 먼저 개발 지식 질문 CS기초, 스프링등b 이력서 기반 질문인성 질문 (개발에 대한 태도 )질문의 빈도는 A>B>C그러나 C가 별로이면 무조건 탈락취업준비에 대해회사가 원하는 기술들 JD가 무엇인지 공통적으로 찾고 있는 기술스택이 무엇인지 찾아보기그리고, 그 회사만이 찾고있는 기술,팀 도메인이 무엇인지 챙겨보기예상질문리스트 검색해서 → Interview Question Driven 취준 하기

백엔드박우빈워밍업클럽클린코드백엔드발자국

이재준

[한국산업기술협회 세미나 및 박람회] 스마트공장 SMT/PCB 불량유형별 분석대책 실무

SMT/PCB 품질 확보, 중요한 과제!!!불량 유형별 개선사례를 우리 기업에 적용 가능할까?다른 기업은 품질관리를 어떻게 하고 있을까? 최근 SMT 및 PCB 제조 공정에서다양한 불랑 사례가 지속적으로 보고 되고 있습니다!!!(솔더 브릿지, 부품미삽, 비아 홀 불량, 레이어 간 단락 등) 제조 공정서 발생하는 불량 줄이고, 신뢰성 높이는 것이기업 경쟁력의 핵심 요소!!! 이에 한국산업기술협회는SMT 및 PCB 최신 불량 유형 및 원인을 분석하고,효과적인 해결방안을 논의하며,생산성과 품질 향상 및 원가절감을 위한실질적인 인사이트를 제공하기 위해 세미나를 기획했습니다. 그리고 세미나에 참가하면"2025 스마트 SMT&PCB 어셈블리 박람회" 무료 참관 가능하다는 사실!!! 빠른 대안을 기획하고 운영하는 자만이미래를 선도할 수 있습니다. 많은 관심과 신청 부탁드립니다. 일시 및 장소 : 2025. 4. 3.(목), 10:00~17:00 / 수원컨벤션센터 106호사전등록 : 2025. 4. 2.(수) 까지참가비 : 200,000원 (사전등록) / 250,000원 (당일 현장 접수)참가비 할인 : 180,000원 (1업체 3인이상 접수시)신청 문의 : 02-6959-5562 / puma1708@kitanet.or.kr사전등록신청 : (구글폼) https://forms.gle/GmNgrLNAJa3sjDEYA세미나 등록 혜택 : "2025 스마트 SMT&PCB 어셈블리 박람회 무료 참관"기타 안내 : 교재, 강연파일, 다과, 중식 제공

반도체SMTPCB스마트SMTPCB어셈블리박람회세미나한국산업기술협회SMTPCB불량유형별분석대책실무품질확보품질관리원가절감

thagyun

[인프런 워밍업 클럽 3기 풀스택] 2주차 발자국

[풀스택 완성] Supabase로 웹사이트 3개 클론하기 (Next.js 14) - 로펀 2주차는 Supabase Storage를 어떻게 사용하는지 학습했다. Supabase Storage 설정Supabase > DashBoard > Project > Storage버킷 생성하기Name of Bucket: 버킷 이름Public bucket: trueAdditional configuration>Allowed MIME types: image/* (이미지만 허용)Policies>For Full customizationPolicy name: 정책 이름Allowed operation: 누구나 CRUD를 할 수 있기 때문에 모두 허용Target roles: anon으로 누구나 허용Review 클릭>Save policy 클릭CRUD 각각에 대한 policies 생성server action에서 storage 접근하기export async function uploadFile(formData: FormData) { const supabase = await createServerSupabaseClient(); const files = Array.from(formData.entries()).map( ([name, file]) => file as File ); const results = await Promise.all( files.map((file) => supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .upload(file.name, file, { upsert: true }) ) ); return results; }Server Action을 사용하여 Supabase Storage에 파일을 업로드하는 함수이다. FormData를 받아 파일을 읽고, Supabase Storage에 업로드하는 방식으로 동작한다. const supabase = await createServerSupabaseClient();서버 측에서 Supabase 클라이언트를 생성하여 Storage API를 사용할 수 있도록 한다. const files = Array.from(formData.entries()).map( ([name, file]) => file as File );FormData에서 모든 항목을 배열로 추출하여 File 객체로 변환하여 파일을 추출한다.const results = await Promise.all( files.map((file) => supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .upload(file.name, file, { upsert: true }) ) ); return results;Promise.all()을 사용하여 여러 개의 파일을 동시에 업로드한다.supabase.storage.from(bucket).upload(filename, file, options)을 통해 지정한 버킷에 파일을 업로드한다.{ upsert: true }: 동일한 이름의 파일이 있을 경우 덮어쓴다. (insert + update) 업로드 된 결과를 반환한다. 2주차 미션github: https://github.com/thayoon/nextjs-supabase-dropbox-cloneDropbox Clone 프로젝트에 파일의 마지막 수정(업로드) 시간을 표시하는 기능을 추가하세요.파일 목록에서 각 파일의 “마지막 수정 시간”을 표시📌 참고 문서: Supabase Storage - 파일 목록 가져오기미션 해결 방법:list() 응답값 확인Supabase의 list() 함수를 사용하면 파일 정보를 가져올 수 있다.참고 문서에서 확인한 응답값은 다음과 같다:{ "data": [ { "name": "avatar1.png", "id": "e668cf7f-821b-4a2f-9dce-7dfa5dd1cfd2", "updated_at": "2024-05-22T23:06:05.580Z", "created_at": "2024-05-22T23:04:34.443Z", "last_accessed_at": "2024-05-22T23:04:34.443Z", "metadata": { "eTag": "\"c5e8c553235d9af30ef4f6e280790b92\"", "size": 32175, "mimetype": "image/png", "cacheControl": "max-age=3600", "lastModified": "2024-05-22T23:06:05.574Z", "contentLength": 32175, "httpStatusCode": 200 } } ], "error": null }이 중에서 updated_at이 파일의 마지막 수정 시간을 나타낸다. Server Action에서 list() 호출 및 데이터 반환actions/storageActions.tsexport async function searchFiles(search: string = "") { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .list(null, { sortBy: { column: "updated_at", order: "desc" }, search, }); handleError(error); return data; }파일 목록을 가져오는 searchFiles() 함수를 구현한다.참고 문서를 통해 sortBy 옵션을 적용하여 updated_at 을 기준으로 내림차순 정렬하여 최신 파일이 먼저 오도록 설정한다. 클라이언트 컴포넌트에서 데이터 가져오기components/dropbox-image-list.tsx "use client"; import { useQuery, useMutation } from "@tanstack/react-query"; export default function DropboxImageList({ searchInput }) { const searchImageQuery = useQuery({ queryKey: ["images", searchInput], queryFn: () => searchFiles(searchInput), }); return ( <section className="grid lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-2"> {searchImageQuery.isLoading && <Spinner />} {searchImageQuery.data && searchImageQuery.data.map((image) => ( <DropboxImage key={image.id} image={image} /> )} </section> ); } useQuery를 사용해 서버에서 데이터를 가져온다.가져온 데이터를 DropboxImage 컴포넌트로 전달한다. 마지막 수정 시간 표시components/dropbox-images.tsx"use client"; import { IconButton, Spinner, Checkbox } from "@material-tailwind/react"; import { getImageUrl } from "utils/supabase/storage"; export default function DropboxImage({ image }) { // 마지막 수정 시간 한국 시간 변환 const updated = new Date(image.updated_at) .toLocaleString("en-CA", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Asia/Seoul", }) .replace(",", ""); return ( <div className="relative w-full flex flex-col gap-2 p-4 border border-gray-100 rounded-2xl shadow-md"> {/* Image */} {/* fileName */} {/* update time */} <p className="flex justify-end text-xs text-gray-500"> 마지막 수정: {updated} </p> {/* trash Button */} </div> ); } updated_at 값을 toLocaleString()을 사용해 한국 시간으로 변환하고 화면에 표시한다.추가 구현 사항긴 파일명 생략 표시<div className="truncate">{image.name}</div>className에 truncate를 적용하여 긴 파일명을 한 줄로 표시하고 넘칠 경우 "..."으로 생략한다. 사진 업로드 오름차순/내림차순 정렬처음에는 사용자의 정렬 방식 선택에 따라 서버에서 데이터를 다시 호출하도록 구현했지만, 비효율적이라고 판단하여 클라이언트에서 정렬을 처리하는 방식으로 변경했다.1차 시도 - 서버에서 정렬된 데이터 요청actions/storageAction.tsexport async function searchFiles(search: string = "", isLatest) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .list(null, { sortBy: { column: "updated_at", order: isLatest ? "desc" : "asc" }, search, }); handleError(error); return data; } isLatest 값에 따라 정렬 순서를 desc(최신순) 또는 asc(오래된순)으로 설정사용자가 정렬 방식을 변경할 때마다 서버 요청이 발생하여 비효율적이다.2차 시도 - 클라이언트에서 정렬 처리components/dropbox-image-list.tsx"use client"; import { Spinner, Menu, MenuHandler, MenuList, MenuItem, Button, Typography, } from "@material-tailwind/react"; import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; const sortMenu = [ { title: "최신순", isLatest: true }, { title: "오래된순", isLatest: false }, ]; export default function DropboxImageList({ searchInput }) { const [openMenu, setOpenMenu] = useState(false); const [isLatest, setIsLatest] = useState(true); const searchImageQuery = useQuery({ queryKey: ["images", searchInput], queryFn: () => searchFiles(searchInput), }); return ( <div> {/* 정렬 버튼 */} <div className="flex flex-row-reverse"> <Menu open={openMenu} handler={setOpenMenu} allowHover> <MenuHandler> <Button variant="text" className="flex items-center gap-1 align-middle text-base font-normal capitalize tracking-normal" > {isLatest ? "최신순" : "오래된순"} <i className={`fas fa-angle-down transition-transform ${ openMenu ? "rotate-180" : "" }`} /> </Button> </MenuHandler> <MenuList className="hidden gap-3 overflow-visible lg:grid"> <ul className="flex w-full flex-col gap-1"> {sortMenu.map(({ title, isLatest }) => ( <MenuItem key={title} onClick={() => setIsLatest(isLatest)}> <Typography variant="h6" color="blue-gray" className="mb-1"> {title} </Typography> </MenuItem> ))} </ul> </MenuList> </Menu> </div> {/* 이미지 리스트 */} <section className="grid lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-2"> {searchImageQuery.isLoading && <Spinner />} {searchImageQuery.data && (isLatest ? searchImageQuery.data.map((image) => ( <DropboxImage key={image.id} image={image} /> )) : searchImageQuery.data .slice() .reverse() .map((image) => <DropboxImage key={image.id} image={image} />))} </section> </div> ); }서버 요청은 기본적으로 최신순으로 설정하고, 클라이언트에서 데이터를 reverse()하여 정렬을 변경하는 방식으로 개선했다. 사진 다중 삭제사진을 다중 선택하여 삭제하는 기능은 체크박스를 활용해 구현했다.사용자는 "전체 선택" 및 "선택 삭제" 기능을 통해 한 번에 여러 사진을 삭제할 수 있다.components/dropbox-image-list.tsx "use client"; export default function DropboxImageList({ searchInput }) { // ... const [allSelected, setAllSelected] = useState(false); const [isSelected, setIsSelected] = useState([]); const searchImageQuery = useQuery({ queryKey: ["images", searchInput], queryFn: () => searchFiles(searchInput), }); const deleteFileMutation = useMutation({ mutationFn: deleteFile, onSuccess: () => { searchImageQuery.refetch(); }, }); function handleChecked(isChecked) { setAllSelected(isChecked); if (isChecked && searchImageQuery.data) { setIsSelected(searchImageQuery.data.map((image) => image.name)); } else { setIsSelected([]); } } return ( <div role="section"> <div className="flex justify-between"> <div className="flex justify-center items-center gap-3"> <Checkbox color="blue" label={ <Typography> 전체 선택 ({isSelected.length}/ {searchImageQuery.data && searchImageQuery.data.length} {!searchImageQuery.data && 0}) </Typography> } checked={allSelected} onChange={(e) => handleChecked(e.target.checked)} /> <Button className="rounded-full" size="sm" variant="outlined" color={isSelected.length > 0 ? "blue" : "gray"} disabled={isSelected.length > 0 ? false : true} onClick={() => { setIsSelected([]); setAllSelected(false); deleteFileMutation.mutate(isSelected); }} > {deleteFileMutation.isPending ? <Spinner /> : "선택 삭제"} </Button> </div> // ... </div> <section className="grid lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-2"> {searchImageQuery.isLoading && <Spinner />} {searchImageQuery.data && searchImageQuery.data.map((image) => ( <DropboxImage key={image.id} image={image} isSelected={isSelected} setIsSelected={setIsSelected} setAllSelected={setAllSelected} totalLength={searchImageQuery.data.length} /> ))} </section> </div> ); } 전체 선택: 사용자가 "전체 선택" 체크박스를 클릭하면, 모든 이미지가 선택된다.선택된 이미지 수와 총 이미지 수가 표시된다.선택 삭제: 사용자가 선택한 이미지들을 삭제할 수 있는 "선택 삭제" 버튼을 제공한다. 이미지가 선택되었을 때만 활성화된다.상태 관리: isSelected 배열에 선택된 이미지의 이름을 저장한다. allSelected 상태로 전체 선택 여부를 관리한다.선택 삭제 버튼을 클릭하면 deleteFileMutation을 호출하여 isSelected을 전달하여 삭제를 처리한다.삭제 작업이 완료되면 searchImageQuery.refetch()를 호출하여 이미지 리스트를 최신 상태로 갱신한다.components/dropbox-images.tsx"use client"; export default function DropboxImage({ image, isSelected, setIsSelected, setAllSelected, totalLength, }) { const isChecked = isSelected.includes(image.name); const handleChecked = (checked) => { setIsSelected((prev) => { if (checked) { const newSelected = [...prev, image.name]; if (newSelected.length === totalLength) setAllSelected(true); return newSelected; } else { setAllSelected(false); return prev.filter((item) => item !== image.name); } }); }; // ... return ( <div className="relative w-full flex flex-col gap-2 p-4 border border-gray-100 rounded-2xl shadow-md"> {/* Image */} {/* fileName */} {/* update time */} {/* multiple checkBox */} <div className="absolute top-4 left-4"> <Checkbox color="blue" className="border-2 border-white bg-white/30 checked:border-white checked:bg-blue-500" checked={isChecked} onChange={(e) => handleChecked(e.target.checked)} /> </div> {/* trash Button */} </div> ); } 개별 체크박스: 각 이미지에 대해 체크박스를 제공하고 사용자가 선택한 이미지를 isSelected 배열에 추가하거나 제거한다.상태 변화: 체크박스를 클릭하면 해당 이미지가 선택되거나 선택이 해제되고 선택된 모든 이미지가 삭제될 때 "전체 선택" 체크박스도 자동으로 갱신된다. 상태 연동: isChecked 개별 이미지에 체크박스의 체크 여부를 결정한다.isSelected 배열에 현재 이미지의 name 값이 포함되어 있는지 true, false로 설정한다.개별 이미지의 체크 여부가 isSelected 상태와 동기화된다.actions/storageActions.tsexport async function deleteFile(fileName: string[]) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .remove(fileName); handleError(error); return data; } supabase.storage.from(bucket).remove(['filename'])을 통해 지정한 버킷에서 배열에 포함된 모든 파일을 삭제 요청한다. 2주차 회고☀이번주는 강의를 들으며 여러 추가 기능이 생각나 실습하는 과정이 더욱 즐거웠다.다른 러너분이 한글 파일명 업로드 오류를 찾아내고 해결하는 모습을 보고 대단하다고 느꼈다.실습에서 영어 파일명으로만 업로드 했기 때문에 이런 오류가 발생하는지 몰랐다.상황을 공유해주신 러너분 덕분에 새로운 사실을 알게 됐다. (감사합니다!)다음에 시간이 난다면 강사님께서 말씀하신 해결 방법을 적용해 보고 싶다.목요일에 중간 점검 시간에 QnA 시간을 가졌는데, 정말 도움이 많이 됐다.특히, 포트폴리오 작성 요령과 개발자로서 필요한 역량을 채우는 방법을 핵심적으로 짚어주셔서 큰 도움이 되었다.나만의 특색을 찾고 포트폴리오에 잘 정리해 봐야겠다 느꼈다.벌써 2주차가 끝났는데, 배포까지 빠르게 진행해 보고 싶다. 다음주도 화이팅! 

치현

[인프런 워밍업 스터디 클럽 3기 풀스택] 2주차 발자국

학습 내용인프런 워밍업 클럽 스터디 2주차로, 이번 주는 드롭 박스 프로젝트와 함께 Supabase의 Storage를 다뤄볼 수 있는 시간이었다. Supbase Storage1. 기본 구성 요소Files: 모든 종류의 미디어 파일 저장 가능 (이미지, GIF, 비디오 등)Folders: 파일을 체계적으로 구성하기 위한 디렉토리 구조Buckets: 파일과 폴더를 담는 최상위 컨테이너 (접근 규칙별로 구분)2. 접근 제어 모델Private Buckets (기본값) RLS(Row Level Security) 정책을 통한 접근 제어JWT 인증 필요Signed URL을 통한 임시 접근 가능Public Buckets파일 조회 시 접근 제어 없음URL만 있으면 누구나 접근 가능업로드/삭제 등 다른 작업은 여전히 접근 제어 적용3. 보안 기능RLS 정책 설정 가능SELECT (다운로드)INSERT (업로드)UPDATE (수정)DELETE (삭제)소유권 관리owner_id 필드로 리소스 소유자 추적JWT의 sub claim 기반 소유권 할당4. 이미지 변환 기능 (Pro Plan 이상)실시간 이미지 최적화크기 조정품질 조정 (20-100)WebP 자동 최적화변환 옵션resize 모드: cover, contain, fillwidth/height 지정 (1-2500px)최대 파일 크기: 25MB최대 해상도: 50MP5. 인증 방식S3 액세스 키서버 사이드 전용모든 버킷에 대한 완전한 접근 권한세션 토큰클라이언트 사이드 사용 가능RLS 정책 기반 제한된 접근6. 통합 기능Next.js 이미지 로더 지원AWS S3 호환성PostgreSQL DB와 연동7. 제한사항파일명은 AWS S3 명명 규칙 준수 필요HTML 파일은 보안상 plain text로 반환이미지 변환 기능은 Pro Plan 이상에서만 사용 가능미션 2 구현 내용과제 구현 저장소Dropbox 중파일의 마지막 수정(업로드) 시간을 표시하는 기능 추가 하기 export interface FileObject { name: string bucket_id: string owner: string id: string updated_at: string created_at: string last_accessed_at: string metadata: Record<string, any> buckets: Bucket }=> DropboxImage 컴포넌트가 prop으로 받는 image의 타입은 FileObject로 그 중 업로드시간은 created_at을 의미하기에 이를 이미지에 추가하였다.(사진 참고) 포인트 1: 한글 파일명 es-hangul 사용// 안전한 파일명 생성을 위한 유틸리티 export class FileNameConverter { // 안전한 문자 패턴 정의 private static readonly SAFE_CHARACTERS = /^[a-zA-Z0-9!\-_.*'()]+$/; private static generateRandomString(length: number = 8): string { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; return Array.from({ length }, () => chars.charAt(Math.floor(Math.random() * chars.length)) ).join(""); } // 파일명이 안전한 문자들로만 구성되었는지 확인 private static isSafeFileName(name: string): boolean { return this.SAFE_CHARACTERS.test(name); } // 안전하지 않은 문자를 포함한 파일명을 안전한 형식으로 변환 private static convertToSafeFileName(name: string): string { try { // 파일명 정규화 const normalized = name.trim().normalize(); // 한글이나 특수문자가 있는지 확인 const hasKorean = /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(normalized); const hasSpecialChars = /[^A-Za-z0-9]/.test(normalized); if (!hasKorean && !hasSpecialChars) { return normalized; } // 한글이 있는 경우 로마자로 변환 시도 if (hasKorean) { const romanized = romanize(normalized); if (romanized && romanized !== normalized) { // 로마자 변환 결과에서 안전하지 않은 문자 제거 return romanized.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase(); } } // 변환 실패 시 랜덤 문자열 생성 return this.generateRandomString(); } catch (error) { console.error("Conversion error:", error); return this.generateRandomString(); } } // 원본 파일명을 안전한 형식으로 변환 static encode(fileName: string): string { console.log("Original filename:", fileName); const extension = fileName.split(".").pop() || ""; const nameWithoutExt = fileName.slice(0, fileName.lastIndexOf(".")); const safeName = this.isSafeFileName(nameWithoutExt) ? nameWithoutExt : this.convertToSafeFileName(nameWithoutExt); console.log("Safe filename:", safeName); return `${safeName}_${Date.now()}.${extension}`; } // 파일명에서 타임스탬프 제거하여 원본 이름 추출 static decode(fileName: string): string { const [name] = fileName.split("_"); return name || fileName; } }포인트 2 : 업로드 날짜 표시export function formatDate(timestamp: string): string { const date = new Date(timestamp); const now = new Date(); const diff = now.getTime() - date.getTime(); // 1일 이내 if (diff < 24 * 60 * 60 * 1000) { const hours = Math.floor(diff / (60 * 60 * 1000)); if (hours < 1) { const minutes = Math.floor(diff / (60 * 1000)); return `${minutes}분 전`; } return `${hours}시간 전`; } // 30일 이내 if (diff < 30 * 24 * 60 * 60 * 1000) { const days = Math.floor(diff / (24 * 60 * 60 * 1000)); return `${days}일 전`; } // 그 외 return date.toLocaleDateString("ko-KR", { year: "numeric", month: "long", day: "numeric", }); } 회고파일명 변환하는데 생각보다 시간이 많이 소요됐다.여찌저찌 구현은 헀지만, 이미지가 어떻게 encoding되고 decoding되는지 일련의 과정에 대한 공부가 필요함을 느끼는 이번주 였다.  

풀스택풀스택인프런워밍업스터디클럽Next3기SupabaseReact프론트엔드2주차발자국

Masocampus

[GEN AI 인사이트] AI도 실수한다? 그래서 등장한 CriticGPT!

AI는 완벽할까요? 사실 그렇지 않아요! AI도 실수를 하고, 오류를 범할 수 있죠. 🤖그래서 나온 것이 CriticGPT입니다! AI가 생성한 코드 속 오류를 찾아내는 AI, CriticGPT를 함께 알아볼까요?CriticGPT는 GPT-4 기반 AI로, ChatGPT가 생성한 코드에서 오류를 찾아내는 역할을 해요.AI가 스스로의 실수를 바로잡는 시대, 신기하지 않나요? 😃AI의 답변이 점점 더 정교해질수록, 그 안에 숨은 오류를 찾는 일이 더 어려워지고 있어요.✅ ChatGPT의 답변이 점점 더 정확해지면서 오류 검출이 복잡해짐✅ 강화 학습 과정에서 AI 트레이너의 부담 증가💡 그래서 등장한 CriticGPT! AI의 답변을 분석하고 오류를 지적하도록 학습되었어요.기존에는 사람이 직접 AI의 오류를 찾아야 했어요. 하지만 실수 가능성이 있었죠.❌ 일반 검토 방식 → 사람이 직접 오류를 찾음 → 실수 가능성 증가⭕ CriticGPT 활용 → 보다 철저한 검토 가능 → 불필요한 오류 지적 감소👉 AI 트레이너가 CriticGPT를 활용하면, 보다 정확하고 신뢰도 높은 피드백을 작성할 수 있어요!CriticGPT는 단순한 AI가 아니에요! 꾸준한 학습을 통해 점점 더 정교한 오류 탐지 능력을 갖추고 있어요. AI 트레이너가 일부러 코드에 버그 삽입 CriticGPT가 오류를 찾아내고 비평 작성 여러 비평을 비교하며 정확성 높은 피드백을 학습 이를 반복하며 더 정밀한 오류 탐지 능력을 갖춤이렇게 꾸준한 학습을 통해 AI의 신뢰도를 높이는 역할을 하고 있어요! 😊🔹 Step 1: ChatGPT가 생성한 코드 샘플 선택🔹 Step 2: 코드에 버그 삽입 → CriticGPT가 오류를 찾아내도록 학습🔹 Step 3: 여러 비평을 생성 후 분석 → 오류 검출률, 정확성, 신뢰도를 평가👉 이 과정을 반복하면서 CriticGPT는 점점 더 똑똑해져요! 🚀CriticGPT도 완벽한 것은 아니에요. 몇 가지 한계가 존재하죠.⚠ 짧은 코드 위주로 학습됨 → 긴 코드 분석 능력 부족⚠ 환각 현상(hallucination) 발생 → 존재하지 않는 오류를 생성할 가능성⚠ 복잡한 문제 평가 어려움 → AI가 정확하게 평가하지 못할 수도 있음하지만 이러한 한계를 극복하기 위한 추가 연구가 진행 중이에요! 앞으로 더 발전할 CriticGPT, 기대되지 않나요? 😃💡 CriticGPT 덕분에 AI의 신뢰도가 더 높아지고 있어요!✅ AI 검토 AI의 등장 → CriticGPT가 AI 오류를 잡아냄✅ 정확한 피드백 제공 → AI 트레이너의 부담 완화✅ 더 발전하는 AI → CriticGPT 덕분에 더욱 신뢰할 수 있는 AI 시대AI의 가능성을 깨우는 마소캠퍼스와 함께, 더 스마트한 AI 활용법을 배워보세요! 😊마소캠퍼스와 함께 AI를 활용해 업무 혁신을 이뤄보세요! 효율적이고 스마트한 일의 방식을 통해 성장할 수 있도록 도와드릴게요. 📌 관련 강의 <ChatGPT 최신 모델 프롬프트 엔지니어링 바이블>실전형 프롬프트 엔지니어링을 익히고, AI 챗봇(ChatGPT)을 활용해 성과 극대화!

AI 업무 활용aiai도구criticgptai오류인공지능ai검토ai학습마소캠퍼스ai활용gpt

채널톡 아이콘