블로그

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

강의별 회고제품 발견(Product Discovery), 시장에서 성공하는 제품을 만들기 위해핵심 포인트:“Product Discovery”란, 고객이 겪는 문제를 정확히 정의하고, 이를 해결하는 과정에서 가치를 창출할 방안을 탐색·검증하는 단계.“고객 문제 이해 → 아이디어 및 가설 설정 → 검증(실험)”을 반복하며, 유의미한 피드백 루프를 만드는 것이 중요.이 과정을 간과하면, 사용자에게 진정 필요한 제품이 아닌, 내부 가정만으로 만든 ‘실패할 가능성 높은’ 제품이 나오기 쉬움.인사이트:프로젝트를 시작할 때, 빠르게 코드를 짜기보다, “누구를 위해 어떤 문제를 해결해야 하는지”부터 깊이 파고들어야 함을 재확인.Discovery 단계에서 수많은 가설이 제기될 수 있는데, 이를 정교하게 정리·우선순위화해야 리소스를 효율적으로 투입할 수 있음. Product Discovery – 실험, 가설, 가정, 베팅핵심 포인트:Discovery에는 크고 작은 실험(Experiment)이 필수적이며, 가설(Hypothesis)과 가정(Assumption)을 명확히 구분할 필요가 있음.가설은 “~할 것이다”라고 예상되는, 검증이 필요한 주장.가정은 “이미 사실로 전제”하고 있는 부분(필수 조건). 하지만 추후 틀렸음이 드러나면 전체 전략이 흔들릴 수 있어, 역시 재검토해야 함.“베팅(Betting)” 개념: 한정된 리소스 속에서 가장 효과적이라 생각되는 실험에 투자(베팅)하고, 결과를 통해 다음 단계로 확장 혹은 중단을 결정.인사이트:“단순히 기능을 릴리즈해 보고 반응을 지켜보는 것”이 아니라, 명확한 가설과 메트릭을 설정하고, 실험 디자인을 꼼꼼히 해야 한다는 점이 중요.베팅은 결국 우선순위 결정의 문제. 팀 내에서 “무엇에, 왜 리소스를 투자하는지”를 공유하면 합의된 실험 문화를 형성할 수 있을 듯. Product Discovery – 기본 전제조건은 전략핵심 포인트:Discovery 과정에서 가장 중요하지만 흔히 놓치는 부분이 “제품 전략”임.제품 전략(비전, 목표, 방향성)이 선행적으로 설정되지 않은 채로, 다양한 실험 아이디어가 난무하면 우선순위 혼란이 발생하고, 결과적으로 일관된 제품 경험을 만들기 어려움.팀 전체가 공유하는 전략을 기반으로, Discovery의 각 단계를 수행해야 함.인사이트:나는 Discovery를 할 때 사용자의 니즈만 강조했었는데, 회사나 팀의 전략과 맞닿아야 실제로 실행력이 생긴다는 점을 다시금 느꼈음.Discovery 자체가 의미 있으려면, “우리가 궁극적으로 달성하려는 비전”을 분명히 하고, 해당 비전에 부합하는 가설·실험을 설계해야 한다. 체계적인 제품 발견을 위한 개념적 지도, 기회 솔루션 트리(Opportunity-Solution Tree)핵심 포인트:“Opportunity-Solution Tree”는 사용자가 겪는 문제(=기회)를 구조적으로 나누고, 각 문제를 해결하기 위한 솔루션(아이디어)들을 트리 형태로 시각화하는 방식.트리를 통해 여러 기회를 놓치지 않고 파악하면서도, 어떤 솔루션이 어느 기회에 기여하는지를 한눈에 파악 가능.Discovery의 복잡성을 줄이고, 팀원 간 커뮤니케이션을 명확히 하는 데 유용.인사이트:문제·기회를 단순히 “리스트업”하는 데서 끝내지 않고, 우선순위를 매기거나, 솔루션과 기회의 연결 관계를 트리로 표현하면, 의사결정이 명확해짐.지금 진행 중인 프로젝트에도 이 트리를 적용해, ‘가장 중요한 문제’부터 해결하는 과정을 체계적으로 해볼 수 있겠다. 체계적인 제품 발견을 위한 방법론, North Star Framework핵심 포인트:North Star Metric(NSM)과 그를 뒷받침하는 하위 지표(Leading indicators)를 설정해, 궁극적 제품 가치가 무엇인지, 그리고 그 가치로 가는 핵심 경로를 추적하는 방법론.Discovery 단계에서도 North Star Framework를 적용해, 프로덕트 성공의 궁극적 지표를 정의하고, 그 지표를 개선하기 위한 여러 실험과 기능을 설계·검증할 수 있음.인사이트:기존에 North Star Metric은 “유지율”이나 “DAU” 같은 단순 사용자 수치쯤으로만 생각했는데, 제품의 본질적 가치와 연결된 지표를 설정해야 한다는 점을 배움.Discovery 과정 내내, “우리가 설정한 NSM에 어떻게 기여하는가?”를 고민하면, 실험을 평가하는 기준이 훨씬 뚜렷해진다. 프로덕트 그로스(Product Growth) 입문하기주요 내용:프로덕트 그로스의 개념과 배경‘그로스’는 단순 마케팅이 아니라, 제품 전체 수명 주기에서 “획득 → 활성화 → 잔존 → 확산 → 수익화”를 개선하는 일련의 활동을 의미Growth 팀(또는 Growth 담당)이 있다면, 제품 관점의 실험과 데이터 분석을 통해 지표를 지속적으로 개선해 나감느낀 점:과거에는 마케팅 채널에 돈을 많이 쓰는 것만이 ‘그로스’라고 생각했는데, 프로덕트가 스스로 성장을 견인할 수 있는 구조(바이럴, 리텐션, 업셀링 등)가 중요함을 배움“획득-활성-유지” 사이사이에 데이터를 기반으로 한 실험이 필수적이라는 점이 인상적 첫 번째 그로스 레버, 제품으로 고객 획득(Acquisition)하기주요 내용:Acquisition 단계는 어떻게 제품을 통해 신규 사용자를 효율적으로 유입시킬 수 있는가에 초점자사 사이트나 앱 안에 Referral(추천), 바이럴 루프, 사용자간 초대 기능 등을 설계해, “제품의 내재적 기능”만으로도 획득 채널을 만들 수 있음“온보딩(첫 사용)”을 매끄럽게 만들어 초기 이탈을 줄이는 기법도 강조느낀 점:우리가 보통 생각하는 광고, SEO, 콘텐츠 마케팅 외에도, 제품 자체 기능으로 신규 사용자를 끌어들이는 설계가 중요“가입 이후 바로 이탈”을 방지하려면, 온보딩 플로우가 쉽고 명확해야 한다는 점을 다시 깨달음  두 번째 그로스 레버, 리텐션(Retention): Activation & Engagement(1) Activation활성화(Activation): 신규 사용자가 “가치”를 충분히 느끼도록 핵심 기능을 빠르게 경험하게 하는 것Activation Point(핵심 액션)를 찾고, 사용자가 이 액션에 도달하도록 UX 설계·알림·가이드를 제공(2) Engagement연속적인 활용(Engagement): 활성화 이후에도 사용자가 계속해서 앱/서비스를 사용하도록 만든 구조DAU, 세션 길이, 재방문율 등 구체적인 지표로 모니터링사용자의 피드백 루프, 보상(게임화 요소), 커뮤니티 기능 등이 대표적인 Engagement 전략느낀 점:Retention이야말로 제품의 장기적 성장을 결정짓는 핵심 요소. DAU나 WAU가 없다면, 신규 유입을 아무리 해도 빠져나가버리기 때문Activation과 Engagement는 사실상 연결되어 있으며, “처음 경험의 만족도 + 지속적으로 다시 오게 만드는 가치” 이 두 축이 매우 중요 세 번째 그로스 레버, Monetization주요 내용:제품에서 가치를 느낀 사용자가 실제로 유료 결제나 업셀링을 통해 매출을 발생시키는 구조과금 모델(구독형, 일회성 결제, 광고, 프리미엄 등)을 정할 때, 사용자가 언제·어떻게 결제하게 될지를 제품 흐름과 자연스럽게 연결해야 함Price Testing, 구독 vs. 일회성 결제 실험 등 데이터를 통한 검증 과정이 필수느낀 점:Monetization은 “돈을 받을 것인가?”가 아니라, “사용자가 기꺼이 지불할 가치를 제공하는가?”로 접근해야 한다는 점그로스 관점에서, Monetization도 실험과 지표 추적으로 개선할 수 있다는 사실이 흥미로움(예: 결제 전환율, 체험판 전환율 등) 그로스 모델(Growth Model): 우리 제품의 성장 메커니즘 도식화하기주요 내용:전체 그로스 과정을 시각적 모델로 표현해, 어디서 사용자가 유입되고, 어떤 요인으로 유지·이탈·전환이 일어나는지 한눈에 보이도록 만듦예) AARRR(아아르) 프레임워크(유입(Acquisition) – 활성(Activation) – 잔존(Retention) – 매출(Revenue) – 추천(Referral))를 기반으로, 우리 제품 특유의 루프나 경로를 그려서 목표 지표와 인과관계를 정리느낀 점:실제 팀에서 Growth Model을 그려보면, “어떤 부분이 병목(bottleneck)인지”, “가장 먼저 개선해야 할 구간이 어디인지”가 명확해짐제품팀·마케팅팀 등이 모두 하나의 그로스 모델을 공유하면, 같은 언어로 협업이 가능해질 듯

PM/PO발자국4주차

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

학습 내용<제품 발견(Product Discovery)>-무엇을 만들지 결정하는 과정-고객의 문제와 니즈에 집중, 고객에 대한 심층적인 이해를 바탕으로 제품 조직이 솔루션을 만들어야 함.-이터레이션을 통해 아이디어 테스트 필요-valuable, usable, feasible, viable한 제품을 발견해야 함. Product Discovery Techniques제품 조직의 일: Product Discovery& Product Delivery Problem/Opportunity Discovery제품 조직이 어떤 문제/기회에 집중할지 찾는 과정문제/기회 발견을 위해 제품 조직이 하는 일: 고객 리서치, 데이터 분석, CS 수집, 시장 동향 파악 등 Solution Discovery문제를 어떻게 해결할지, 또는 기회를 어떻게 잘 활용할지 찾는 과정이터레이션을 반복하며 좋은 솔루션을 찾아 나가야 함.(아이디어->검증->반응 확인->개선 반복)제품이나 기능을 기획함.유저 인터페이스, 인터랙션을 디자인함.프로토타입을 만듦.코드로 제품을 구현함.고객에게 프로토타입이나 제품을 내놓고 피드백을 확인함. 문제 정의는 분석의 영역, 문제 해결은 창조의 영역 Product Delivery안정적, 확장 가능성, 성능, 유지보수-> 퀄리티 있는 제품을 만들어서 시장에 내놓는 것 <Product Discovery -실험, 가설, 가정, 베팅>Product Discovery는 가설을 수립하고 검증하는 일련의 실험 과정 Assumptions: 우리가 고객이나 제품에 대해 갖고 있는 믿음이나 추측Value Assumptions: 고객이 이 제품, 기능, 솔루션을 원할 것. 여기에서 가치를 느낄 것. 가치를 얻기 위해 필요한 행동을 할 의지가 있을 것. Usability Assumptions: 유저가 우리 제품 사용/이해/기능 발견 가능한가?Feasibility Assumptions: 기술적 구현 가능성 Viability Assumptions: 사업적 타당성->비용과 수익, 법과 규제, 운영 등 Assumptions이 있을 때 선택: 실행하기 vs 테스트하기->Risk, Evidence 기준으로 판단검증방법: A/B 테스트, 인터뷰, 설문조사, 시제품 데이터 분석, 사용성 테스트 등낮은 비용으로 빠르게 검증할 방법 찾기 <Product Discovery -기본 전제조건은 전략>Guiding Policy에 맞는 아이디어들을 집중적으로 싫행하게 됨.전략에서 중요한 것은 뭘 하지 않을 것인가를 정하는 것우리가 집중하는 영역에서 성과를 내려면 뭘 하면 좋을지 고민하면 됨. <Opportunity Solution Tree vs North Star Framework>Opportunity Solution Tree: 고객에게 가치를 제공함으로써 사업 성과를 내자, 꾸준한 고객 리서치 강조North Star Framework: 지표를 성장시켜서 성과를 내자, 지표 설정과 분석 강조 <Opportunity Solution Tree>-목표 성과 정의-기회: 니즈, 페인포인트, 욕구-제품의 역할: 고객에게 가치 제공+사업적 성과 창출 기회 어떻게 찾을까?정성적 리서치, 고객 직접 만나기심층 인터뷰, 사용성 테스트기회 매핑: 사용자 여정, 사용자 과업, 생성형 AI 도움 기회와 솔루션 다양하게 탐색하기-리스크가 크고 근거가 낮은 Assumption -> 테스트-리스크가 낮고 확신이 높은 Assumption -> 실행 <North Star Framework>좋은 North Star Metric: 제품 조직이 영향을 끼칠 수 있다고 믿어지는 레벨의 지표, 그 지표가 개선되었을 때 중장기적 사업 성과에 큰 도움이 될 것이라고 믿어지는 지표,'고객들이 우리 제품에서 얼마만큼 가치를 얻어가고 있는가?'를 보여주는 지표 꾸준히 업데이트하기-북극성 지표: 6개월~1년-인풋 지표-기회, 솔루션 <Opportunity Solution Tree and North Star Framework>'성과 내기'라는 ill-structured problem에 구조 부여Opportunity space 와 Solution space 충분히 탐색의사결정에 도움을 주는 개념적인 지도 <Product Growth>Growth Levers: 어느 손잡이를 당기면 성장할 수 있을까? 고객 여정에 변화를 줌으로써 우리 사업의 성장에 영향을 끼치는 방법들3가지 Growth Levers: Acquistion, Retention, Monetization Acquistion사용자들이 다른 사용자들을 초대함제품이 광고판이 됨UGC가 광고판이 됨UGC가 검색엔진에 노출됨다른 제품과의 연동 Activation인센티브를 걸지 말고 제품의 핵심 가치를 빠르게 경험할 수 있게 만들어주자.습관을 형성하는 Hooked Model:Trigger, Action, Reward, Investment 인게이지먼트Depth, Frequency, EfficiencyBehavior=Motivation*Ability*Prompt인게이지먼트를 높이려면 우리 제품의 핵심적인 인게이지먼트 시스템 개선 필요 Monetization-누구에게서 돈을 받을 것인가?사용자/광고주/제3의 다른 조직사용자 중 어느 그룹-무엇에 대한 대가로 돈을 받을 것인가?Value Unit 설정하기, Usage-Based Pricing-얼마를 받을 것인가?Cost, Competition, ValuePerceived Value-어떻게 수익을 극대화할 수 있을까?그물 넓히기, 돈 내고 싶게 만들기, Prompt 주기인지편향 <Growth Model>그로스 모델: 우리 제품의 성장 메커니즘을 도식화한 것-> 만드는 이유? 우리 제품이 성장하는 메커니즘을 이해하고, 각 요소들이 어떤 역할을 하는지 이해하면, 그 요소들에 영향을 끼쳐서 성장을 만들 방법도 알 수 있으니까그로스 모델링: 그로스 모델을 만드는 작업'우리 제품은 이런 식으로 성장해'라는 그림 그리는 작업정성적 모델링부터 시작그로스 레버 고려하여 하기 회고제품 조직이 해야하는 많은 일들을 알게 된 한 주였다. 문제/기회 발견과 솔루션 발견의 중요성을 배웠다. 또한 제품 조직이 의사 결정을 내릴 때 사용하는 다양한 방법들에 대해 알게 되었는데, 어떠한 경우에도 100% 확신은 없으므로 결단력도 PM에게 중요한 요소라는 생각이 들었다. 이를 위해서는 다양한 제품을 써보고, 공급자의 입장과 소비자의 입장을 면밀히 들여다보고, 성장에 도움을 주는 것이 무엇일까에 대한 고민을 많이 해보아야 할 것 같다.  

기획 · PM· POPMPO프로덕트매니저김민우

[워밍업 클럽 3기 BE code] 4주차 발자국

 강의 수강 Mock Dummy: 아무 것도 하지 않는 깡통 객체Fake: 단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체(ex. FakeRepository)Stub: 테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체. 그 외에는 응답하지 않는다.Spy: Stub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체. 일부는 실제 객체처럼 동작시키고 일부만 Stubbing할 수 있다.Mock: 행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체Stub vs. MockStub: 상태 검증(State Verification)Mock: 행위 검증(Behavior Verification)  더 나은 테스트를 작성하기 위한 구체적인 조언  1. 한 문단에 한 주제!반복문, 조건문 지양Parameterized Test 활용2. 완벽하게 제어하기현재시간, 랜덤값 등은 분리해서 상위 레벨로 올리기3. 테스트 환경의 독립성을 보장하자.테스트가 실패하더라도 그 부분은 when, then 절이어야 함given 절에서 테스트가 깨지면 왜 실패했는지 유추하기 힘들어짐테스트에서는 팩토리 메서드도 지양, 생성자나 빌더로 생성하기4. 테스트 간 독립성을 보장하자.두 가지 이상의 테스트가 하나의 자원을 공유하면 안됨(static 변수 x)테스트는 순서와 무관해야하고 각각 독립적으로 작동해야 함하나의 인스턴스가 차례대로 변화하는 과정을 테스트하고 싶다면 DynamicTest 사용5. 한 눈에 들어오는 Test Fixture 구성하기Fixture: 고정물, 고정되어 있는 물체테스트를 위해 원하는 상태로 고정시킨 일련의 객체 (주로 given절 구성할 때)setUp에서 공통된 Fixture들을 구성하고 테스트할 수 있지만, 공유 변수는 테스트간 결합도가 생기게 만들기 때문에 지양하는 것이 좋음setUp에 given절이 위치해 있으면 문서로서의 기능도 사라짐setUp을 구성할 때는 "각 테스트 입장에서 봤을 때, 아예 몰라도 테스트 내용을 이해하는 데에 문제가 없는가?", "수정해도 모든 테스트에 영향을 주지 않는가?" 질문을 던져볼 것data.sql을 활용해서 테스트 데이터를 넣어놓을 수 있지만, 이는 데이터가 파편화되어서 무얼 테스트하는지 파악하기 어렵게 함프로젝트가 커질수록 data.sql 관리가 어려워짐프로젝트에서 필요한 빌더메서드를 만들어서 사용할 때는, 필요한 파라미터만 사용할 것빌더를 클래스마다 만들려면 힘드니까 모아서 사용하면 안될까? - 추천하지 않음실무에서 사용하는 객체는 필드가 수십개가 되는 경우도 있음그럼 한 클래스에 각자 사용하는 빌더를 계속 만들기 시작하다보면 관리가 어려워짐코틀린을 사용하면 어느정도 해소가 됨6. Test Fixture 클렌징deleteAll()은 건별로 지우는 다수 쿼리때문에 속도 저하deleteAllInBatch()는 관계만 잘 생각하면 더 좋은 방법@Transactional 롤백을 주로 사용하긴 함Spring Batch같은 걸 사용한 Batch 통합테스트를 쓰면 여러 트랜잭션이 참여를 하기 때문에 트랜잭션 롤백이 사용하기 어려워 질 수 있음 그럴때는 deleteAllInBatch() 사용7. @ParameterizedTest하나의 테스트케이스인데 값을 여러 개로 바꾸어가며 테스트해보고 싶을 때 사용8. @DynamicTest하나의 환경을 설정해놓고 시나리오를 테스트하고 싶을 때9. 테스트 수행도 비용이다. 환경 통합하기서버가 뜨는 횟수가 많아지면 시간적 비용이 큼상위 추상클래스를 만들어서 각 테스트가 상속받게 함Mocking을 하면 새로운 환경이기 때문에 새로 서버가 띄워짐 Mocking 처리한 것들을 가장 위로 올리거나, 테스트환경을 분리해야함Q. private 메서드의 테스트는 어떻게 하나요?할 필요가 없고, 하려고 해서도 안된다.클라이언트 입장에서는 공개 API만 알면 됨 private 메서드를 테스트하고 싶은 시점에 해야 할 고민 - "객체를 분리할 시점인가?"팩토리 클래스의 public 메서드로 변경해서 테스트Q. 테스트에서만 필요한 메서드가 생겼는데 프로덕션 코드에서는 필요 없다면?만들어도 된다. 하지만 보수적으로 접근하기! 회고  테스트코드 강의를 마무리하며 워밍업스터디 3기가 끝이 났다.강의를 수강하면서 어렵기도 했지만 유익한 팁들을 많이 얻었던 것 같다.하지만 진도를 따라가기가 조금은 벅차서 아직 내 것으로 만들지는 못했다.이제 내 프로젝트에 배운 부분들을 하나씩 적용해보면서 학습을 진행할 생각이다. 

인프런 워밍업 스터디 클럽 3기 백엔드-code 4주 차 마지막 발자국

이 글은 박우빈님의 Practical-Testing 강의 를 참조하여 작성한 글 입니다.또 다시 일주일이 지나 어느새 마지막 발자국만을 남겨두었다.반신반의하며 시작했지만, 한 달 동안 무려 2개의 강의를 완강하고 미션까지 모두 수행한 내 자신이 참 대견하다! 히히 😊벌써 1분기가 끝난게 믿기지 않지만, 그래도 이것저것 공부하며 알차게 살았더니 이번 1분기는 아쉽지 않게 보내줄 수 있을 것 같다. 특히 테스트 코드와 관련해 많은 지식을 얻을 수 있었다.강의 내용이 알찼던 것은 물론이고, 미션을 수행하며 쌓은 지식을 정리하고 다른 사람의 코드를 읽는 과정에서도 많은 배움이 있었다.무엇보다 우빈님께 직접 코드 리뷰를 받으면서 내 코드의 개선점을 확인하고, 작성 과정에서 궁금했던 부분을 직접 물어볼 수 있었던 경험이 정말 값졌다.테스트 코드를 작성할 때마다 내가 올바르게 작성하고 있는 지에 대한 의심이 항상 존재하였는데, 코드 리뷰를 통해 테스트 코드 작성에 대한 자신감이 한층 더 생긴 것 같다!ㅋㅋㅋ또한 리뷰를 통해 많은 고민 및 궁금증을 해결할 수 있었고, 가독성 및 유지보수 하기 좋은 테스트 코드 작성법 및 테스트 작성의 중요성을 한층 더 깨닫게 된 것 같다!다음에 스터디 클럽이 또 열린다면,,무조건 참여하길 바란다👍🏻학습 내용 요약 Mock을 마주하는 자세@Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이위 링크에 미션 수행하면서 학습한 내용을 정리해 두었으니 참고 바란다! 더나은 테스트를 작성하기 위한 구체적 조언완벽하게 제어하기테스트 코드를 작성할 때 모든 조건들은 완벽히 제어가 가능해야 한다.LocalDateTime.now()와 같이 제어할 수 없는 값은 최대한 지양하자!  테스트 간 독립성을 보장하자공유 변수 사용x 한 눈에 들어오는 Test Fixture 구성하기Test Fixturegiven절에서 생성했던 모든 객체들을 의미테스트를 위해 원하는 상태로 고정시킨 일련의 객체BeforeEach, BeforeAll, AfterEach, AfterAll셋업에 유치한 이런 공통의 픽스처들은 테스트와 결합도 생기게 만듦픽스처들을 수정하거나 하는 경우에 모든 테스트에 공통으로 영향을 주기 때문에 지양하는 것이 좋음사용하는 기준:각 테스트 입장에서 봤을 때 아예 몰라도 테스트 내용을 이해하는 데 문제가 없을 때만 사용하기수정해도 모든 테스트에 영향을 주지 않는 경우에만 사용하기테스트 시 sql로 given 객체 생성하지 말자!given절이 파편화되어 뭘 테스트 해야하는지 파악하기 어려워짐프로젝트가 커질수록 데이터 구조, 필드 등 변경이 발생하면 sql문 관리가 어려워짐 Test Fixture 클렌징deleteAllInBatch테이블 전체를 bulk성으로 날릴 수 있는 좋은 메서드순서를 잘 고려를 해야함(중간테이블 먼저 삭제해주어야 함)deleteAllselect후 delete하기 때문에 테이블에 있는 데이터 수 만큼 쿼리가 실행됨순서 고려x 테스트 수행도 비용이다. 환경 통합하기service와 repository부분을 하나의 추상클래스(e,g,.IntegrationTestSupport)를 상속받게 함으로 서버 실행 횟수를 줄이기! Q. 테스트에서만 필요한 메서드가 생겼는데 프로덕션 코드에서는 필요 없다면? 무엇을 테스트하고 있는지를 명확히 인지하기현재 동작하고 있는 프로덕션 코드를 테스트 한다면 테스트에서는 필요한데 프로젝트에서는 필요 없는 그런 메소드들이 나올 수가 있다.이런 경우 만들어도 된다. 하지만 최대한 지양 하자. getter, 기본 생성자, 생성자 빌더, 사이즈 이런 것들 어떤 객체가 마땅히 가져도 될 마땅히 가져도 되는 행위라고 생각이 되면서 미래에도 충분히 사용될 수 있는 성격의 메소드일 것 같다. Spring REST Docs테스트 코드를 통한 API 문서 자동화 도구 API 명세를 문서로 만들고 외부에 제공함으로써 협업을 원할하게 함 기본적으로 AsciiDoc을 사용하여 문서 작성  장점테스트를 통과해야 문서가 만들어짐(신뢰도 높음)프로덕션 코드에 비침투적단점코드 양이 많다설정이 어렵다 미션Day 18 - @BeforeEach, given절, when절에 항목 배치나의 코드: https://inf.run/fK9MY 🧐 고려한 부분남아있는 데이터가 테스트에 영향을 끼치지 않도록 각 테스트 수행 전 @BeforeEach를 통해 데이터를 초기화 시켜주었다.모든 댓글 테스트에서 필요한 사용자와 게시물은 테스트의 기본 전제 조건이므로, 사용자 생성 및 게시물 생성 메서드를 별도로 분리하여 중복을 제거하였다. 우빈님의 코드 리뷰 (Feat. Day 11 - 스터디 카페 이용권 선택 시스템 테스트 코드 작성하기 )나의 코드: https://inf.run/xPthb Q. 나의 질문: @CsvSource내 enum(passType) 문자열 직접 작성으로 인한 유지보수 비용 증가 문제A. 우빈님 답변: passType이 변경되면, 이를 테스트하고 있는 부분이 같이 영향을 받게 되고변경한 사람은 테스트 수행 시점에 테스트가 깨진 것을 보고 영향 범위를 인지할 수 있다.따라서 테스트 코드가 없어서 영향도를 인지하지 못하는 것보다 나은 것이라 생각한다!물론 테스트 코드를 수정해야 하는 비용은 들겠지만, 테스트 코드는 원래 프로덕션 코드와 같이 상생하는 코드이므로,프로덕션 코드를 팔로업하면서 같이 변경되는 것이 더 자연스럽다!@ParameterizedTest @CsvSource({ "HOURLY, HOURLY, true", "HOURLY, WEEKLY, false" }) @DisplayName("이용권 내 이용권 타입이 비교하는 타입과 같은지 비교할 수 있다.") void isSamePassType(StudyCafePassType passType, StudyCafePassType expectedPassType, boolean expectedResult) { // given StudyCafeSeatPass pass = StudyCafeSeatPass.of(passType, 2, 4000, 0.0); // when boolean result = pass.isSamePassType(expectedPassType); // then assertThat(result).isEqualTo(expectedResult); } Q. 나의 질문: 일급컬렉션 내 테스트만을 위한 메서드 추가 없이 테스트 한 방법A. 우빈님 답변: 기본적으로 프로덕션에 없는 코드를 테스트 코드만을 위해 추가하는 것은 '지양'하는 것이 맞다.하지만 해당 기능이 단순하고, 미리에도 충분히 활용될 수 있다면 아주 보수적으로 추가해도 좋다고 생각한다.e,g,. 단순히 일급컬렉션 내부의 원소 개수를 반환하는 size(), 내부 항복이 존재하는지 확인할 수 있는 isEmpty() 등 해당 테스트에서 검증하고 싶은 것은 전체 개수가 아닌 타입별 개수이므로, 타입별 개수를 반환하는 메서드를 추가하기에는 애매해 보인다.따라서 어쩔 수 없이 아래 코드 처럼 작성하는 것이 맞으나, findPassBy()와 결합도가 생기는 것을 감안하면 될 것 같다고 하셨다.하지만 findPassBy() 단위 테스트는 필수!!@DisplayName("파일을 읽어 이용권 목록을 가져올 수 있다.") @Test void getSeatPasses() { // given SeatPassFileReader seatPassFileReader = new SeatPassFileReader(); // when StudyCafeSeatPasses seatPasses = seatPassFileReader.getSeatPasses(); // then assertAll( () -> assertThat(seatPasses.findPassBy(StudyCafePassType.HOURLY)).hasSize(6), () -> assertThat(seatPasses.findPassBy(StudyCafePassType.WEEKLY)).hasSize(5), () -> assertThat(seatPasses.findPassBy(StudyCafePassType.FIXED)).hasSize(2)); } 아래 테스트는 변수명 잘 지었다고 받은 칭찬 리뷰이다😆 마무리한 달동안 정말 많은 것을 배웠다,,,희희단순히 일방적으로 인풋만 넣는게 아니라,매일 강사님 그리고 스터디원분들과 소통하면서 함께 학습하니 더욱 단기간에 성장할 수 있었던 것 같다ㅎㅎㅎ미션도 너무너무 재밌었고, 그에 대한 우빈님의 피드백까지 받으니 정말이지 알차고 소중한 경험이었다.다음에 워밍업 스터디 클럽이 또 하게 된다면, 주변에 적극적으로 홍보해야겠다!!스터디를 기획 및 운영하신 모든분들, 강사님, 그리고 스터디에 참여한 스터디 분들 모두 고생 많으셨습니다☺ 

백엔드클린코드테스트코드발자국워밍업스터디클럽

szun

워밍업 클럽 스터디 3기 (PM) 4주차 발자국

강의명: 시작하는 PM/PO들에게 알려주고 싶은, 프로덕트의 모든 것코치: 김민우링크: https://inf.run/JMgwt4주차 강의 포인트 제품 발견 (Product Discovery)어떤 제품을 만들지 결정하는 방법구시대적인 제품 개발 프로세스를 비판하기 위해 탄생 제품이 실패하는 이유제품을 잘 모르는 사람들이 아이디어를 냄제품 조직에 권한과 자율성이 없음제품 로드맵 자체의 문제검증을 마지막 단계로 넘겨서 큰 기회 비용이 유발 제품 개발에 관한 잘못된 전제들소프트웨어 제품을 만드는 일은 간단하고, Non - Technologist들이 주도할 수 있다는 전제소프트웨어 개발이 예측 가능한 프로세스라는 전제로드맵에 있는 아이템들이 사전에 예측한 만큼 임팩트와 성과를 낼 것이라는 전제 Problem / Opportunity Discovery제품 조직이 어떤 문제(기회)에 집중할 지 찾는 과정 Solution Discovery문제를 어떻게 해결할지, 또는 기회를 어떻게 잘 활용할지 찾는 과정 Product Discovery는 전체적으로 '가설(Assumption)'을 수립하고 검증하는 일련의 과정AssumptionPM에게 Assumption이란?우리가 고객이나 제품에 대해 갖고 있는 믿음이나 추측 Assumption의 유형Value(Desirability) AssumptionsUsability AssumptionsFeasibility AssumptionsViability Assumptions Assumption이 있을 때 두 가지 모드실행하기Assumption이 현실에 부합할 것이라고 믿고 실행하는 것. 기회비용의 지출을 감수하는 것.테스트하기기회비용 리스크를 줄이기 위해 현실과 부합하는지 검증 실행 vs. 테스트 판단 기준Risk : 틀리는 경우 우리는 얼마나 큰 타격을 입는가?Assumption의 위험도를 따지고 검증할지 여부를 판단 Evidence : 얼마나 탄탄한 근거를 가지고 있는가?Assumption 검증하기Assumption 검증에서의 함정'검증 = 실험 = A/B 테스트'라고 생각하는 것Assumption에 맞는 검증 방식을 사용하면 됨전형적인 방법론을 무비판적으로 사용하는 것중요한 것은 Key Assumption을 정의하고 검증하는 것 Value Assumptions 검증문제 Assumption 검증 방법심층 인터뷰간단한 설문조사시제품 반응유저 행동 데이터 분석Usability Assumptions 검증사용성 테스트 초점 있는 Product Discovery를 하려면 꼭 필요한 것지양해야 할 접근법 : 초점 없는 아이디어 난사, 즉 제대로 된 전략의 부재전략의 역할제대로 된 전략이 있으면 선택과 집중을 할 수 있음Guiding Policy(전략적 방향성)에 맞는 아이디어들은 집중적으로 실행하게 됨전략에서 중요한 것은 '뭘 하지 않을 것인가'를 정하는 것 Opportunity Solution Tree & North Star Framework우리 조직이 집중할 목표 성과 하나를 정의하고목표 달성을 위해 활용할 수 있는 다양한 기회 영역을 파악하고 정의하는 방법Opportunity Solution Tree목표 성과 정의하기기회 맵핑(Opportunity Mapping)여러 기회를 하나의 상위 기회 및에 그룹핑(Grouping) 하는 것집중할 영역 정하기기회들을 비교하여 하나의 집중할 영역을 결정솔루션 탐색기회와 솔루션 다양하게 탐색. 이때 중요한 것은,- Opportunity space를 충분히 탐색- 다양한 아이디어들의 Assumption을 점검하고, 가장 유력한 솔루션을 검증하고 실행하기기회란?고객이 느끼는 불편함, 해결이 필요한 상황 + 고객이 충족시키고 싶어하는 욕구즉, 니즈, 페인 포인트, 욕구기회는 어떻게 찾아낼 수 있을까?고객에 대한 깊은 이해가 필요어떤 기회를 선택할지 판단하기 어려운 이유서로 다른 식으로 표현됐지만, 사실 같은 기회들완전히 분리되어 있지 않고 서로 연결되어 있는(하지만 완전히 똑같지는 않은) 기회들어떤 기회는 굉장히 크고 추상적North Star Framework기회 솔루션 트리보다 조금 더 '지표'에 초점을 맞춘 방법좋은 North Star Metric제품 조직이 영향을 끼칠 수 있다고 믿어지는 레벨의 지표그 지표가 개선됐을 때 중장기적 사업 성과에 큰 도움이 될 것이라고 믿어지는 지표'고객들이 우리 제품에서 얼마만큼 가치를 얻어가고 있는가?'를 보여주는 지표North Star Framework 설계하기Output 지표(NSM) 설정Input 지표 설정Input 지표를 높이기 위한 Opportunity 정의Intervention : 기회 정의 후 솔루션 아이디어 구상Opportunity Solution Tree와 North Star Framework의 공통점'성과 내기'라는 ill-structured problem에 구조를 부여하는 행위Opportunity Space와 Solution Space를 충분히 탐색하는 것이 중요의사결정에 도움을 주는 개념적인 지도 Product GrowthGrowth Work란?기존 시장에 더 많은 유저/고객들이 우리 제품을 도입하도록 하고, 더 활발히 사용하게 만듦으로써 가치를 창출하는 일제품이 크게, 빠르게 성장하려면?많은 사람들이 원하는 것을 만들기 = PMF그들에게 도달하고, 그들이 제품을 이용하도록 만들기 = Growth그로스 레버 (Growth Lever)고객 여정에 변화를 줌으로써 우리 사업의 성장에 영향을 끼치는 방법들 첫 번째 그로스 레버, AcquisitionAcquisition에서 제품의 역할사용자들이 다른 사용자를 초대하게 만들기 (Referral)레퍼럴이 잘 작동하려면고객이 제품에 만족해야 함고객이 제품의 가치를 금방 느낄 수 있어야 함 (Quick time-to-value)많은 사람에게 어필할 수 있는 제품이어야 함 (Broad value proposition)제품이 광고판 역할을 하게 하기UGC(User-Generated Content)로 사용자 유입시키기유저 획득 2가지 방법사용자들의 UGC 공유검색 엔진에 UGC의 노출다른 제품과의 연동을 통한 획득 두 번째 그로스 레버, Retention리텐션은 그로스에서 가장 중요한 지표 리텐션의 인풋 - 아웃풋 관계다음 요소들이 리텐션의 증가로 이어짐고객이 제품을 이용해서 문제를 해결하는 것그로 인해 가치를 느끼는 것계속해서 문제를 해결하기 위해 이용하고자 하는 것리텐션의 주요 레버 두 가지 Activation, EngagementActivationActivation의 3단계Setup - Aha - Habit유저 온보딩신규 유저가 제품에 안착하기까지, 즉 Habit Moment에 도달하기까지 일련의 과정Setup Moment의 증가를 위한 행동사용자 데이터 수집연결 관계 만들기유저 맥락에 맞는 설명Aha Moment의 증가를 위한 행동제품의 가치를 빠르게 경험할 수 있도록 온보딩을 구성Habit Moment의 증가를 위한 행동Hook ModelTrigger : 사용자에게 자극이 주어지는 것External Trigger : 사용자 행동을 촉발하기 위한 외부 자극Internal Trigger : 사용자의 마음 속에서 저절로 일어나는 트리거Action : 사용자가 행동을 하는 것구체적인 행동을 유도 해야 함.Reward : 행동에 대한 보상이 주어지는 것물질적인 보상보다는 심리적인 보상이 습관을 형성하는데에 더 강력한 역할Investment : 시간과 노력을 투자하는 것제품에 투자할수록 사용자가 인지하는 제품의 가치가 높아짐 EngagementEngagement 레벨을 높이는 세 가지 요소DepthFrequencyEfficiencyBJ Fogg의 Behavior ModelBehavior = Motivation x Ability x PromptMotivation : 동기가 충분히 클 때Ability : 능력을 충분히 가지고 있을 때, 즉 행동을 하기가 얼마나 쉬운가/어려운가Prompt : 트리거가 적당히 주어질 때 세 번째 그로스 레버, MonetizationMonetization에 관한 기본 질문누구에게 돈을 받을 것인가?우리 제품이 얼마나 많은 사용자를 모을 수 있는가사용자를 많이 모으는 것이 우리 제품에 얼마나 중요한가위의 기준을 통해 "누가 돈을 낼 의지와 능력이 있는지" 판별무엇을 대가로 돈을 받을 것인가?Value Unit : 판매가자 구매자에게 제품을 제공할 때, 구매자에게 제공되는 가치의 단위. 이는 곧 과금 단위가 됨.예시) Usage-Based Pricing = 사용량에 비례하여 과금Incentive Alignment : 고객의 이익과 우리의 이익이 최대한 합치되어야 한다.얼마를 받을 것인가?Cost : 제품을 고객에게 제공하기 위해 비용이 얼마나 드는가?가격은 비용을 커버할 수 있어야 함.소프트웨어 제품은 CAC(고객 획득 비용)를 고려해야 함.Competition : 경쟁사는 얼마를 받는가?Value : 고객은 우리 제품이 얼만큼 가치가 있다고 생각하는가?우리 제품의 가치를 고객에게 어떻게 인지 시킬 것인지 생각해야 함.Value PropositionPositioning'우리 제품에 대한 고객의 Perceived Value에 따라 우리가 받을 수 있는 가격이 달라질 수 있다. 고객의 인식에 어떻게 영향을 끼칠 수 있을까?' 라는 식으로 접근어떻게 수익을 극대화 할 수 있을까?가격 높이기판매 수량 늘리기 Growth Model, 우리의 제품 성장 메커니즘 도식화 하기Growth Model우리 제품의 성장에 영향을 끼치는 중요한 요소들을 도식화 한 것.우리 프로덕트가 성장하는 메커니즘을 이해할 수 있음.각 요소들이 어떤 역할을 하는지 이해하면, 그 요소들에 영향을 끼쳐서 성장을 만들 방법을 알 수 있음.정성적 그로스 모델Acquisition, Retention, Monetization, 이렇게 세 가지 그로스 레버를 고려하여 모델링 하기.4주차 회고이번 주 강의는 PM으로써 제품을 찾는 단계부터 그 제품을 성장시키는 단계까지에 대한 내용을 알 수 있는 강의였다.처음 강의를 듣게 된 계기는 PM이 어떤 일을 해야 하는지, 어떤 역량이 필요한지에 대한 보다 객관적인 정보를 얻고 싶은 마음이었다. 4주차까지 강의를 듣고 난 후, 나의 의문이 다소 우문이었다는 생각을 하게 되었다. 어쩌면 PM이라는 직무는 객관적일 수 없는 직무인데, 그 곳에서 객관성을 찾고 있었던 나를 알 수 있었다. 다만 PM이 어떤 태도를 가지고 과업에 임해야 하는지, 어떤 지식들이 PM에게 도움이 될 수 있는지에 대해 잘 알 수 있는 강의였던 것 같다. PM이라는 직무는 지식을 쌓는 것보다도 경험이 가장 중요한 직무인 것 같다. 앞으로 더욱 다양한 경험을 통해 나의 실력을 향상시키고자 한다.

기획 · PM· PO

김예원

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

3주 차 학습 내용섹션 5. 데이터 전문성: 프로덕트 지표 프레임워크지표란 무엇인가, Proxy 지표지표를 토대로 의사결정하고 실행할 수 있게됨상시 모니터링 지표와 그때그때 확인하는 지표Acquisition우리는 충분히 많은 신규 유저/고객을 획득하고 있는가?신규 유저/고객을 비용 효율적으로 획득하고 있는가? Activation신규 획득한 유저들이 프로덕트의 핵심 가치를 경험하는 습관을 형성하는 것 Engagement사용자들이 제품에 관심을 갖고, 이용하고, 관계 맺는 것 Retention고객이 이 제품을 계속해서 이용하는 것 Monetization, Metric Hierarchy매출 관련 지표와 지표의 위계 구조섹션 6. 데이터 전문성: Event-Based Product AnalyticsEvent-Based Analytics, 데이터 축적을 위한 기본 개념 이해하기이벤트를 기반으로 하는 데이터 분석Property는 우리가 일일이 정의하고 기록해야 함 필수 작업, Event Taxonomy 설계하기Top-Down 접근Bottom-UP 접근꼭 필요한 이벤트부터 트래킹Event Taxonomy 문서 만들기 3주 차 학습 회고스스로 칭찬하고 싶은 점열심히 필기하며 미션 기간에 맞게 인강을 수강한 점아쉬웠던 점 / 보완하고 싶은 점 / 다음 주에는 어떻게 학습할지아쉬웠던 점과 보완하고 싶은 점은 없고, 다음 주에도 열심히 💪🏻💪🏻💪🏻3주 차 미션 해결 과정어떤 관점에서 접근했는지 / 문제를 해결하는 과정은 무엇이었는지 /왜 그런 식으로 해결했는지 / 미션 해결에 대한 회고3주 차 미션은 “프로덕트 지표 설정하기”였습니다. B2B 디지털 금 거래 서비스를 B2C로 확장하는 초기 단계라는 점을 고려해, 서비스의 특성과 사용자 행동 흐름에 맞는 지표를 선정하고 계산식과 후속조치를 정의했습니다. 금 거래라는 자산 중심 서비스 특성상 총 거래액, 거래 건수, 거래 성사율, 플랫폼 내 금 보유량을 핵심 지표로 설정했고, 각 지표의 변동에 따라 어떤 의사결정을 내려야 할지 구체적으로 시뮬레이션했습니다. 특히 매수·매도·송금·주얼리 구매 등 실제 거래 여정을 바탕으로 사용자의 중도 이탈 가능 지점을 파악하고, 거래 완주를 높이기 위한 UX·정책 개선 관점에서 접근했습니다. 단순한 숫자 측정이 아니라 지표를 통해 문제를 정의하고 개선 방향을 도출하는 것이 중요하다는 걸 배웠습니다.

기획 · PM· PO김민우인프런워밍업클럽시작하는PM/POPMPO

LC-02s

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

학습 내용 요약인프런 워밍업 클럽 3기 풀스택 스터디 4주차에는 Supabase의 Real Time Database와 Authentication, RLS(Row Level Security) 기능을 활용하여 실시간 채팅 기능을 지원하는 인스타그램 클론 프로젝트를 진행해 보았습니다. 인프런에서 발자국을 작성할 때 강의를 보지 않고도 강의 내용을 파악할 수 있을 만큼 자세한 내용을 작성하는 건 지양해 달라고 가이드한 만큼 강의에 대한 필기는 최소화 하고 회고 위주로 적어보겠습니다. Supabase Realtime Database 주요 기능데이터 실시간 업데이트데이터베이스의 INSERT, UPDATE, DELETE 이벤트를 감지특정 테이블, 컬럼 또는 조건에 따라 변경 사항을 필터링 가능PostgreSQL 기반 트리거 (Trigger) 활용기존 Postgres 데이터베이스와 완벽하게 통합트리거(Trigger)를 사용해 복잡한 로직을 추가 가능채널 기반 구독 (Channels & Broadcast)특정 테이블 또는 쿼리에 대해 채널을 구독(Subscribe) 하여 변경 사항 수신클라이언트 간 브로드캐스트(Broadcast) 메시지 전송 가능Row Level Security (RLS) 지원실시간 데이터도 Postgres RLS 규칙을 준수특정 사용자만 특정 데이터에 대한 실시간 업데이트를 수신 가능저지연(Low Latency) 데이터 업데이트PostgreSQL의 LISTEN/NOTIFY를 사용하여 빠른 데이터 전송웹소켓(WebSocket) 기반으로 구현되어 빠른 응답 가능 Supabase Authentication 주요 기능다양한 로그인 방식 지원이메일/비밀번호 로그인 (기본 제공)소셜 로그인 (OAuth) → Google, GitHub, Apple, Discord 등Magic Link (비밀번호 없이 이메일 링크 클릭으로 로그인)OTP (SMS 기반 인증)SAML, OpenID Connect 지원 (엔터프라이즈 인증)Row Level Security (RLS) 통합Postgres의 Row Level Security (RLS) 를 활용하여 사용자별 데이터 접근 제한 가능auth.uid() 함수를 사용하여 현재 로그인한 사용자 필터링 가능JWT 기반 인증 및 커스텀 클레임로그인 시 JWT(JSON Web Token) 가 발급됨필요에 따라 사용자 정의 클레임(Custom Claims) 추가 가능비밀번호 재설정 및 사용자 관리이메일을 통한 비밀번호 재설정 기능 제공사용자 프로필 정보 (auth.users 테이블) 관리 가능기기 기반 인증 및 다중 세션 관리기기별 세션을 유지하고 관리할 수 있음다중 기기 로그인 지원 Supabase RLS 주요 기능사용자별 데이터 접근 제어로그인한 사용자만 자신의 데이터에 접근하도록 설정 가능auth.uid()를 사용해 현재 로그인한 사용자의 UID를 자동 감지PostgreSQL 정책(Policy) 기반 권한 관리CREATE POLICY 를 사용하여 테이블 단위의 접근 정책 설정 가능특정 역할(Role) 또는 조건을 만족하는 사용자만 데이터 접근 허용JWT 기반 인증과 연동Supabase의 Authentication(JWT) 와 함께 사용 가능JWT의 사용자 ID(UID), 이메일, 역할(Role) 정보를 활용하여 정책 적용유연한 권한 설정읽기(Read) 전용 정책 설정 가능소유자(Owner) 기반 정책을 설정하여 자신의 데이터만 수정 가능특정 사용자 그룹(Role)에게만 접근 권한 부여 가능  인스타그램 클론 미션 회고풀스택 스터디 4주차 미션은 강의에서 진행하는 Next.js와 Supabase Realtime Database, Authentication, RLS를 활용한 인스타그램 클론 앱에 여러 편의 기능을 추가하는 것과, 지금까지 진행했던 모든 프로젝트들을 배포까지 해보는 것이었습니다. 이번에도 역시 강의와는 다르게 실제 인스타그램의 MVP를 구현하는 것을 목적으로 했으며, 4주간 학습했던 내용들을 최대한 담아보았습니다. 진행했던 미션은 해당 링크에서 보실 수 있습니다. 미션 수행 내용아래는 제가 수행한 미션에 대한 내용을 정리해보았습니다. 로그인 기능 이메일 기반 로그인Confirmation URL 방식프로필 설정아이디 설정 기능이름 설정 기능프로필 이미지 설정 기능유저 탐색 기능 유저 프로필 조회 기능팔로잉 수 조회 기능팔로워 수 조회 기능팔로잉 기능 팔로잉 목록 조회 기능팔로워 목록 조회 기능실시간 채팅 기능 메시지 삭제 기능메시지 읽음 상태 확인 기능채팅방 접속 상태 확인 기능  사용 기술프레임워크: Next.js v15, React v19데이터베이스: Supabase서버 상태관리: Tanstack Query v5클라이언트 상태관리: Zustand v5스타일 프레임워크: TailWindCSS v3, Mantine v7, Tabler Icons모노레포: Turbo Repo패키지 매니저: pnpm  트러블 슈팅 Next.js useSearchParams 훅 빌드 환경 에러개발환경에서는 다른 문제가 없었는데, 빌드 시점에 Next.js 에서 제공하는 useSearchParams 훅을 사용한 로직이 아래의 에러를 반환하는 문제가 있었습니다. usesearchparams() should be wrapped in a suspense boundary at page "/404". 관련 이슈를 찾아보니 Next.js 14 버전 부터 useSearchParams 훅이 기본적으로 suspense boundary 에서 동작하도록 수정되어서 발생하는 이슈였습니다. 저의 경우에는 탐색 페이지의 검색어, 팔로잉 페이지의 탭메뉴, 리다이렉트 시 토스트 메시지 노출 로직에 쿼리 파라미터를 사용했기에 useSearchParams 훅을 사용한 부분을 모두 아래와 같이 Suspense 로 감싸주어 해결하였습니다. const FollowingPage: React.FC = () => ( <WithAuth> <Suspense> <FollowingTabs /> </Suspense> </WithAuth> )const SearchPage: React.FC = () => ( <Suspense> <div className="p-4 pb-7 pt-5"> <SearchBar /> </div> <SearchProfileResultList /> </Suspense> )export const Provider: React.FC<React.PropsWithChildren> = ({ children }) => ( <QueryProvider> <MantineProvider theme={defaultThemeSchema} defaultColorScheme="auto"> <ColorSchemeScript defaultColorScheme="auto" /> <ModalsProvider labels={{ confirm: '확인', cancel: '취소' }}> <Suspense> <SearchParamsMessengerProvider> {children} <Toaster position="bottom-right" /> </SearchParamsMessengerProvider> </Suspense> </ModalsProvider> </MantineProvider> </QueryProvider> ) Module Federation (Micro FrontEnd)모노레포로 모든 프로젝트를 구성했기에 마지막 배포 미션에서 4개의 프로젝트를 모두 배포하는 대신 1개의 프로젝트만 관리할 목적으로 마이크로 프론트엔드 기술을 적용해 보고 싶었습니다. 빌드 시점에 4개의 프로젝트를 하나의 앱으로 통합하여 하나의 도메인을 공유하는 상태로 엔드포인트만 바뀌는 방식으로 배포해보고 싶었는데, 마이크로 프론트엔드를 구성하는 핵심 기술인 Module Federation의 Next.js 플러그인이 App Router는 지원하지 않더라구요. 저는 이미 App Router로 모든 로직을 작성한데다 지금 시점에서 모두 Pages Router로 포팅하기에는 시간이 부족하다 판단하여 해당 방식은 포기하였습니다. 그래서 대체재로 Wrapper 형식의 앱을 추가로 생성한 후 Next.js의 Multi Zones 기능과 Middleware, Rewrites 기능을 조합하여 4개의 프로젝트를 묶는 방식을 고려해봤었는데, 해당 방식은 결국 4개의 프로젝트를 개별로 배포해야하는 문제가 여전히 존재하여 결국 포기하게 되었습니다. 아무리 찾아봐도 마땅한 대안이 없더라구요. 해당 내용은 직접적인 트러블 슈팅은 아니지만 아쉬워서 남겨보았습니다.  후기처음 OT 때도 코치님이 말씀해주셨지만, 이번 주차에서는 미션의 난의도가 확 올라갔던 것 같습니다. 저는 특히 테이블 설계가 어려웠던 것 같아요. 기본적인 테이블들을 정규화를 거쳐 분리해놓으니 각 기능 별로 JOIN 로직을 거치는 단계가 복잡해져서 API 로직을 작성하면서도 이게 맞나 라는 생각이 들었던 적이 많았던 것 같습니다. 백엔드와 작업할 때 상황별로 필요한 인터페이스를 하나의 API에 모아 달라고 요청하는 경우가 많았었는데, 그게 백엔드 입장에서는 생각보다 쉽지 않은 일일 수 있었겠구나 라는 생각도 하게 되었습니다. 이번 4주차 미션을 끝으로 워밍업 클럽을 완주하게 되었는데, 개인적으로 풀스택을 지향하기에 앞으로 백엔드를 더 공부해 볼 계획이라 이번 경험이 앞으로의 학습에 많은 도움이 될 것 같아요. 이번 스터디의 핵심인 Supabase는 한달 동안 사용해보니 커스터마이징 하고 싶은 부분이 몇가지 있어 Supabase 하나로 기존 백엔드의 역할을 모두 대체하는 것은 한계가 있다고 느껴졌지만, 백엔드를 모르는 프론트엔드 개발자도 손쉽게 하나의 서비스를 온전히 구현할 수 있을 정도로, 쉽고 강력하다는 장점이 크기에 앞으로도 간단한 MVP를 구현해볼 일이 있다면 종종 사용해 볼 것 같아요. 풀스택 3기 러너분들 모두 수고 많으셨습니다!  긴글 읽어주셔서 감사합니다. ☺ 

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

jomootgrn88

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

강의수강supabase Auth다양한 인증방식을 지원하는 인증 시스템이메일 인증, 소셜 로그인 등 다양한 방법을 지원confirmation URL 방식supabase가 사용자의 이메일로 확인 링크를 전송사용자가 해당 링크를 클릭하면, supabase에서 해당 이메일을 확인하고 인증 완료6 - Digit OTP 방식이메일이나 SMS를 통해 6자리 숫자를 전송사용자는 이 코드를 입력하여 본인 확인supabase Realtime데이터베이스의 변경 사항을 실시간으로 클라이언트에 전달하는 기능을 제공broadcast모든 사용자에게 동일한 데이터를 전송하는 방식presence현재 연결된 사용자를 실시간으로 추적하는 방식postgres changes데이터베이스에서 발생하는 변경 사항을 실시간으로 추적하는 방식supabase RLSRow Level Security데이터베이스 테이블에 대해 구체적으로 접근 권한을 설정할 수 있게 해줌 미션채팅 메시지 삭제사용자가 특정 채팅 메시지를 삭제다른 사용자가 작성한 메시지는 삭제 불가능메시지 클릭 시 모달 화면을 띄어 삭제 여부 결정삭제된 메시지 대신 "이 메시지는 삭제되었습니다" 같은 알림 표시  메시지 삭제 기능은 message 테이블의 is_deleted 컬럼 값을 이용하기로 했다. is_deleted 가 true 이면 삭제된 메시지고, false면 삭제되지 않은 메시지다.  구현 전 생각했던 방식은 is_deleted 값을 변경하는 action 함수를 만들고, 특정 메시지를 클릭 시 모달 창을 띄어 해당 메시지를의 삭제 여부를 묻는다. 삭제한다고 하면 action 함수를 호출해 is_deleted 값을 변경한다. 또한 상대방 채팅에서도 실시간으로 변경하기 위해 channel에서 update event도 구독한다./actions/chat-actions.tsexport async function deleteMessage({ message }) { const supabase = await createServerSupabaseClient(); const { data: { session }, error, } = await supabase.auth.getSession(); if (error || !session.user) { throw new Error("User is not authenticated"); } const { error: updateError } = await supabase .from("message") .update({ ...message, is_deleted: true, }) .eq("id", message.id); if (updateError) { throw new Error("Message error"); } } export async function getMessage(id: string) { if (!id) { return null; } const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("message") .select("*") .eq("id", Number(id)) .maybeSingle(); if (error) { return null; } return data; }deleteMessage: 메시지 객체를 받아 is_deleted 컬럼의 값을 true 변경getMessage: 모달 창에서 이용할 action message의 id를 받아 해당 id로 message 테이블을 쿼리해 메시지를 반환 /components/chat/chat-delete-button.tsx"use client"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { deleteMessage } from "actions/chat-actions"; import { useRouter } from "next/navigation"; import { useRecoilValue } from "recoil"; import { selectedUserIdState } from "utils/recoil/atoms"; export default function ChatDeleteButton({ message }) { const queryClient = useQueryClient(); const selectedUserId = useRecoilValue(selectedUserIdState); const deleteMessageMutaition = useMutation({ mutationFn: () => deleteMessage({ message }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["messages", selectedUserId] }); }, }); const router = useRouter(); return ( <div className="flex flex-col gap-1 font-bold bg-white border-2 px-6 pt-8 py-4 rounded-md"> <p> message : <span className="underline">{message.message}</span> </p> <p>Are you sure you want to delete this message?</p> <div className="flex gap-2 justify-center py-4"> <button onClick={() => { deleteMessageMutaition.mutate(); router.back(); }} className="w-14 py-1 px-2 bg-red-100 rounded-md border-2" > yes </button> <button onClick={() => router.back()} className="w-14 py-1 px-2 bg-blue-100 rounded-md border-2" > no </button> </div> </div> ); } 모달 창에서 렌더링할 컴포넌트 yes 버튼은 deleteMessageMutation.mutate()를 호출해 deleteMessage action을 호출한다. yes, no 버튼 마지막에 router.back()을 호출해 모달창을 종료한다./components/chat/chat-screen.tsx useEffect(() => { const channel = supabase .channel("message_postgres_changes") .on( "postgres_changes", { event: "INSERT", schema: "public", table: "message" }, (payload) => { if ( payload.eventType === "INSERT" && !payload.errors && !!payload.new ) { getAllMessagesQuery.refetch(); } } ) .on( "postgres_changes", { event: "UPDATE", schema: "public", table: "message" }, (payload) => { if (payload.eventType === "UPDATE" && !payload.errors) { getAllMessagesQuery.refetch(); } } ) .subscribe(); return () => { channel.unsubscribe(); }; }, []);chat-screen.tsx 컴포넌트에 supabase.channel 구독에서 { event: 'UPDATE' }를 추가함으로 message 테이블에 변경사항이 있으면 모든 메시지를 리패치하도록 함   

워밍업 클럽 3기 BE 클린코드&테스트 - 4주차 발자국

회고운이 좋게도 진행중인 프로젝트에서 Mock을 사용하여 테스트 코드 작성을 해야했다.일석이조라 생각하여 즐겁게 강의를 보았지만? 역시 쉽지 않다.기준을 확실하게 잡는 것이 중요하다고 생각된다.어느 부분까지 테스트를 진행해야 할지, 어느 부분까지 stub 처리를 해야 할지.. 테스트를 통해 검증해야 할 부분을 명확하게 선정해야 한다. 테스트는 문서다.테스트 자체만으로 의도가 전달되어야 한다.여러 테스트에서 공통되는 부분을 제거하는 경우, 개별 테스트의 의도를 해치지 않는지 확인해보아야 한다. 어노테이션 사용 시 알고 쓰자.스프링부트 관련 어노테이션인지 아닌지 확실하게 파악해야 한다.그 또한 테스트의 의도가 담겨있다. private 메서드의 테스트가 필요된다고 생각되면 객체 분리의 신호로 생각하자. 스프링 프레임워크 사용 시 익숙하지 않은 라이브러리를 어떻게 학습해야 하나에 대한 고민이 있었는데, 그에 대한 방법을 강의로부터 찾은 것 같다.써보기 전에 역시 알고 써야 한다. 그 이유가 확실해야 한다.  한달간의 스터디가 끝났다.쉽지 않지만? 끝까지 완주했다는 점에서 나에게 칭찬...어차피 계속 공부해야 한다. 즐기면서 하자구~모두 고생하셨습니다. 

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

김보민

[인프런 워밍업 클럽 3기 - 백엔드 프로젝트] 4주차 발자국

워밍업 클럽 마지막 주차가 되었다..!! ✅ 강의4주차에서는 구글 클라우드 플랫폼을 활용해서 지금까지 만들었던 프로젝트를 배포해보았다!도커와 도커파일을 활용해서 프로젝트를 빌드했다.구글 클라우드 플랫폼을 통해서 인스턴스를 생성했다.생성한 인스턴스에서 만들었던 프로젝트의 도커 컨테이너를 실행했다.도메인을 구입하고 연결해보고 HTTPS도 적용하는 방법을 배웠다.  ✅ 미션마지막 미션 두가지는 이번주차 강의 내용을 따라가기만 하면 문제없이 해결할 수 있었다.[미션6] 가상 프로필을 나의 프로필로 바꾸기[미션7] 내 포트폴리오 도메인 공유하기아직 나의 프로필에 쓸 내용을 많이 정리하지 못해서 구색만 갖추게...되었다..😓마지막 포트폴리오 도메인 공유에서는 직접 도메인을 구매할 수도 있었지만 현재 나에게 도메인이 필요한 상황이 많지 않아서 무료 도메인 호스팅해주는 웹사이트(Duck DNS)를 활용했다..!  🍀 마무리아쉬운 점은 미션7을 제 시간안에 제출하지 못한 점이다. 회사일이 바빠서 미션7과...중간점검 2차 참여를 놓치게 되었다....너무 아쉬웠다..🥺미니 프로젝트도 좀 더 디벨롭해보고자 했는데...그래도 워밍업 클럽을 진행하면서 평소라면 초반에 달리다가..용두사미가되어...완강까지 쉽지 않았을..ㅎ 인프런 강의를 한달안에 제대로 완주할 수 있어서 좋았다...!알찬 3월을 보낼 수 있게 이런 좋은 이벤트(?)를 열어준 인프런과 정보근 코치님께 감사드린다는 말을 마지막으로 4주차 발자국을 마치도록 하겠습니다...!🍀

junghlee234

[인프런 워밍업 클럽 스터디 3기 - 프로덕트 디자인 (Figma)] 4주차 발자국

진행 기간: 4주차(20250323-20250330)진행 강의:[UI3 업데이트] 피그마 배리어블을 활용한 디자인 시스템 구축하기 학습 내용네비게이션 컴포넌트, 모드 활용(다크모드, 통합 브랜드 구현, 반응형 디자인, 다중언어)컴포넌트 부분에서는 마지막인 네비게이션 관련 부분을 학습하였습니다.링크, 탭, 바텀 네비게이션, 사이드 네비게이션, 글로벌 네비게이션, 페이지네이션, 케러셀네비게이션 컴포넌트는 어떻게 보면 제일 많이 쓰이는 중요한 요소일 수 있는데, 개발자 입장에서는 제일 구현을 안하는 요소이기도 한 것 같습니다. 전부 라이브러리로 가져다 쓰는 부분이 많아서, 원리만 생각하고 많이 사용했던 것 같습니다. 이번에 네비게이션 컴포넌트를 배우면서 개발 원리를 배우지는 않았지만 "실제로 구현을 할 수 있겠다"는 생각과 라이브러리에만 의존하는 것이 아니라 "차별화 된 디자인적 요소"를 반영 할 수 있다는 생각이 들었습니다.모드 활용 부분을 진행하면서 실제로 개발을 해보지 않고도 많은 시나리오를 미리 계획할 수 있고, 테스트 해 볼 수 생각이 들었습니다.브랜드 통합 부분에서는 기본 UX 요소를 배리어블과 모드를 통해서 쉽게 전환이 가능했습니다.모드를 활용하면 개발 전에 여러 디바이스 들을 대응하기가 매우 용이하였습니다.언어의 경우에도 아직은 많이 부족하지만, 어느 정도는 시나리오를 테스트 할 수 있었습니다.B2B 어드민, B2C 이러닝, 모바일 OTT 앱이 부분은 앞에서 왜 열심히 컴포넌트를 만들고 모드를 활용하는 법을 배웠는지에 대해서 알 수 있는 실습 위주의 파트였습니다. 또한, 실제 피그마를 사용하는 시나리오와 제일 유사한 환경이기도 하였습니다.이 파트는 워밍업 클럽을 하지 않더라도 꼭 진행을 스스로 해봐야 한다는 생각이 드는 파트였습니다.컴포넌트, 파운데이션 등을 미리 정확히 준비해 놓고 화면을 구성하는 것과 아닌 것이 엄청나게 많이 차이났기 때문입니다.강의를 듣기 전에 앱 디자인을 했을 때, 딱 이 강의의 반대로 했기 때문에 UX 구현시에 많은 시간을 소모했었습니다.강의 수강 이후로는 앱을 빠르게 디자인 하고, 다른 부분에 시간을 더 투자할 수 있을 것 같습니다.개인적으로 이 파트는 뒤로 갈수록 개인적으로 난이도가 쉬워졌습니다.여러 페이지를 만들면서 중복된 요소들이 등장하고 만드는 법이나 스스로의 노하우가 늘어난다는 생각이 들었습니다. 4주차 및 강의와 활동을 마치며중간 점검 미팅에서 선생님은 디자이너로서 개발을 해보면 내 프로덕트에서 더 디자인이 나아질 부분이 있다는 것을 발견하신다고 하셨는데, 저는 반대로 UX구현을 좀 더 체계적으로 진행하면서 개발자로서는 부족하지 않지만, 사용자로서는 부족했을 요소들, 실제로는 그렇게 개발하면 안되는 요소들을 많이 찾아 볼 수 있게 되었습니다.모든지 쉽게 할 수 있는 AI 시대에 스스로의 생각으로는 개발자, 기획, 디자인의 끝은 만류귀종으로 "개인의 경쟁력" 하나로 귀결된다는 생각이 들었습니다.미팅에서 말씀하셨던 것처럼 개개인의 주요 전공과 더불어, 노하우와 제품을 보는 눈과 같은 "개인의 경쟁력"을 지속적으로 발전시켜야 한다는 생각이 들었습니다.또한 앱이나 웹 등을 개발하거나 디자인을 하더라도, 나만의 요소(디자인 시스템, 기술적인 구현, 참신한 아이디어 등)에 AI를 적극적으로 도입하여서 스스로의 작은 기획과 테스트를 여러 번 진행 하고 실제 출시 시에는 완성도가 높고 성장성이 높은 프로덕트를 낼 수 있겠다는 생각이 들었고 실제로 그렇게 해보려고 합니다.워밍업 클럽을 하기 전에는 사실 이 많은 부분을 다 할 수 있을까 생각이 많이 들었지만 2가지 요소 덕분에 완강 및 미션을 모두 진행할 수 있었습니다. 강의에서 처음 피그마를 하는 사람도 들을 수 있을 정도로 자세하고 친절하게, 또 실제 구현시에 노하우를 숨김 없이 알려주십니다.워밍업 특성상 타임 리밋이 있다는 점이 집중도를 매우 높일 수 있는 부분이 있습니다.강의를 듣기 전과 들은 후가 스스로에게 있어 많은 차이가 있다고 생각이 듭니다. 특히 전체적인 프로덕트를 보는 눈이 넓어졌고, 개발자지만 막상 앱을 구현하라고 하면 막막한데, 이제는 구현이 가능하다고 자신있게 말할 수 있을 것 같습니다. 특히 개발자의 경우에는 지엽적으로 개발하는 경우가 많고, 전체적인 맥락을 모르고 개발하는 경우가 많은데, 이 강의를 듣고 실습을 실제로 진행한다면 전체 프로세스를 보는 눈이 생길 것이라고 생각하며 꼭 강의를 들어 보시고 실습도 해보시기를 추천 합니다! 

UX/UI인프런_워밍업_클럽UX/UIFigma디자인시스템볼드UX

이수진

[인프런 워밍업 클럽 Full-Stack 3기] 4주차 발자국 - 인스타그램 클론코딩

이번주는 강의 볼륨이 제일 많았던 인스타그램 클론코딩을 진행했다. 로그인, 회원가입을 위한 Supabase Auth와 실시간 채팅을 구현하기 위한 Supabase Realtime Database 배포까지 구현해볼 수 있었다. 이번시간엔 거의 다른 주차에 비해 2배정도 되는? 양인거 같아 월요일부터 틈틈히 해서 겨우 시간을 맞출 수 있었다. 수강 내용Section 6 인스타그램 클론코딩 - Supabase 인증 구현 Part 1이번 챕터에서는 Supabase 인증 시스템을 구현해보는 것이었다. 다음과 같은 기능을 구현했다.이메일 인증을 통한 회원가입OTP 인증을 통한 회원가입로그인(추가) 카카오 소셜 로그인이는 Supabase Auth를 통해 구현했다. 강의에서는 위의 내용들만 구현했지만 Supabase Auth에는 더 다양한 기능들을 제공하고있었다. JWTSession 등을 지원하기도했고, 권한관리 등도 제공했다.그리고 강의를 들으면서 느꼈던 것이지만 React Query를 잘 사용하셔서 React Query의 활용성에 대해서도 한번 더 성장한다는 느낌을 받았다.Section 7 인스타그램 클론코딩 - 인스타 DM 채팅 기능 구현 Part 2인증 기능을 모두 마친 뒤에는 실시간 채팅 기능을 구현했다. 이 시간에 Supabase Realtime Database를 학습할 수 있었다. 사실 그냥 Database랑 어떤 차이인지는.. 잘 모르겠지만 해당 기능을 켰을 때 실시간으로 채팅 기능을 수현할 수 있었다. 이때에도 React Query를 이용해 데이터를 캐싱하고 다시 불러오는 작업을 함으로써 좀 더 효율적으로 Supabase Table 데이터들을 관리할 수 있었다. 그리고 전에도 얘기했던것이지만 Database라서 그런지 메서드 자체가 SQL문을 그대로 가져와서 학부시절 DB를 배워놨었던 부분이 이해하기도, 적응하기에도 좀 더 편했었다.export async function getAllMessages({ chatUserId }: { chatUserId: string }) { const supabase = await createServerSupabaseAdminClient(); const { data, error } = await supabase.auth.getSession(); if (error && !data?.session) { throw new Error("User is not authenticated"); } const { data: messages, error: messagesError } = await supabase .from("message") .select("*") .or(`receiver.eq.${chatUserId},receiver.eq.${data?.session?.user.id}`) .or(`sender.eq.${chatUserId},sender.eq.${data?.session?.user.id}`) .order("created_at", { ascending: true }); if (messagesError) { throw new Error("Failed to get messages"); } return messages; } const { data: messages, error: messagesError, isLoading: messagesLoading, refetch, } = useQuery({ queryKey: ["messages", selectedIndexState], queryFn: async () => { const allMessages = await getAllMessages({ chatUserId: selectedIndexState, }); return allMessages; }, });useEffect(() => { const channel = supabase .channel("message_postgres_changes") .on( "postgres_changes", { event: "INSERT", schema: "public", table: "message", }, (payload) => { console.log(payload); } ) .subscribe(); return () => { channel.unsubscribe(); }; }, []);그리고 메시지를 주고받고 한 다음 채팅을 그냥 보여주기만 하면 실시간으로 동기화가 안된다는 문제점이 있어 (새로고침을 해야 보여진다.) 그 때를 위한 동기화 작업을 supabase channel기능을 사용했다.Section 8 웹사이트 배포하기 - 도메인 등록, Vercel, AWS 배포Vercel 배포같은 경우는 개인적으로 포트폴리오 배포할 때 유용하게 사용했었고 AWS배포도 한번쯤은 해보는것이 좋다고 해서 해본적이 있어서 좀 가볍게 들었다. 그리고 배포 시 빌드 에러를 최소화하기 위해 항상 push하기 전에 build한번 하고 push를 해서 빌드 오류도 없이 무난하게 모든 프로젝트를 배포할 수 있었다.미션4주차 미션은 다음과 같았다.작성한 모든 프로젝트 배포하기채팅 메시지 삭제 기능 구현채팅 읽음, 안읽음 표시 기능 구현삭제기능, 읽음/안읽음 표시 기능을 위해 supabase data table에 is_readis_deleted 항목을 추가하고 각 항목들을 update하는식으로 구현했다. 그리고 동기화를 위해 channel에 update 항목을 추가해주는걸로. 인터넷에서 찾아보니 *을 이요해 모든 이벤트 INSERTDELETEUPDATE 등을 포함할 수 있다고 했지만 그냥 업데이트만 하나 추가해주었다. ui는 그렇다 치고.. 일단 삭제 기능이랑 읽음 기능까지는 구현해봤다.마무리인스타그램 클론코딩을 끝으로 이렇게 4주간의 대장정이 마무리가됬다. Supabase 프로젝트 한번 해보겠다고 들을까 말까 했던 강의였는데 이렇게 다같이 스터디할 수 있는 기회가 생겨서 나 혼자 했으면 흐지부지 되었을텐데 이렇게 끝까지 완강할 수 있어서 개인적으로 뜻깊었던 스터디 기간이었다.강의 내용도 부실하지 않고 적당히 그리고 자세하게 알려주셔서 내용을 이해하기도 쉬웠다. 또한 Supabase 자체에 국한되지 않고 Next.js나 React Query같은 프론트엔드 지식도 함께 쌓아 더 괜찮은 시간이었다. 지금 이 토대를 기반으로 다음에 할 Supabase를 통한 개인 프로젝트도 화이팅해야겠다!

웹 개발웹개발프론트엔드백엔드supabase

제갈진우

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

✅ 핵심 요약성과를 내는 제품은 단순한 기능 구현이 아니라, 가설 수립 → 검증 → 개선이라는 치밀한 과정을 거쳐야 한다는 점을 배움.특히, Value, Usability, Feasibility, Viability 관점을 통해 가설을 세우는 방식이 인상 깊었음.A/B 테스트는 도구일 뿐, 만능은 아님. 문제의 본질에 집중하는 접근이 중요함을 실감.📌 이번 주 배운 점기존에는 아이디어가 먼저 떠오르고, 그걸 어떻게든 테스트로 풀어가는 방식이었다면, 이번 강의에서는 ‘가설을 제대로 세우는 것부터 시작해야 한다’는 기본을 다시 짚게 됐다.가설을 세울 때 단순히 “이 기능이 먹힐까?”가 아니라,“이 기능이 고객에게 어떤 가치를 주는가?”, “기술적으로 실현 가능한가?” 등 네 가지 기준(Value, Usability, Viability, Feasibility)을 명확히 설정하는 법을 배움.특히, 작은 문제부터 명확히 해결하는 것이 결국 전체 성과로 이어진다는 점이 기억에 남는다.복잡한 문제일수록 작게 쪼개어 접근하는 것이 효과적.💡 인상 깊은 개념 정리기회-솔루션 트리: 고객의 Pain Point를 바탕으로 기회를 정의하고, 여기에 맞는 솔루션을 트리 형태로 구조화.북극성 지표: 조직이 장기적으로 지향하는 핵심 성과 지표. 팀의 모든 활동이 이 지표와 연결돼야 방향이 맞다.Growth Lever: Acquisition(유입), Retention(유지), Monetization(수익화)이라는 세 축을 중심으로 그로스를 설계.특히 Retention을 높이기 위한 Fogg Behavior Model은 유저 행동을 유도하는 실질적 도구로 기억할 만하다.🔍 개인 회고솔직히 이번 주는 개념적으로 어렵게 느껴졌다. 특히 기회-솔루션 트리와 북극성 지표의 관계가 처음엔 모호했지만,‘북극성 지표 = 방향, 기회-솔루션 트리 = 그 방향으로 가는 경로’라는 설명을 통해 머릿속에 정리됐다.실습 없이 개념만 배울 땐 흘려듣기 쉽지만, 구글 플레이 스토어 리뷰 분석을 하면서“왜 고객 인터뷰가 필요한지” 몸으로 느끼게 됐다.리뷰는 피상적인 불만만 담겨 있고, 진짜 문제는 인터뷰를 통해서만 들을 수 있다는 걸 체감.✨ 마무리4주 동안 꽤 압축된 학습이었지만, 단순한 ‘강의 시청’이 아니라 제품을 바라보는 시각 자체를 다듬는 시간이었다.특히 튜터님의 Q&A와 커리어 피드백 덕분에 실무와 연결해서 생각해볼 수 있었던 점이 좋았다.다음에 또 이런 워밍업 클럽이 있다면, 실제 제품을 가지고 실습해보는 프로젝트형 강의로 참여하고 싶다.

기획 · PM· POPMPO워밍업클럽인프런

인프런 워밍업 클럽 스터디 3기 - 백엔드 클린 코드, 테스트 코드 4주차 발자국

이번 주차에서는테스트 코드 리뷰더 나은 테스트를 작성하기 위한 구체적 조언 1. 테스트 코드 리뷰 정리 테스트 커버리지코드 커버리지(테스트 커버리지)는 괜찮은 부정 지표지만 동시에 좋지 않은 긍정 지표다. 테스트 커버리지가 너무 낮을 경우 테스트가 충분하지 않다는 좋은 증거가 되지만, 테스트 커버리지가 100%라고 해서 반드시 양질의 테스트 스위트가 보장되지는 않는다.학습 시에는 높은 커버리지를 목표로 하는 경험이 도움이 될 수 있다  테스트 시 사용하는 자원private 메소드라도 상단에 위치시켜 보기 편하도록 한다네이밍에 신경쓰자 (ex : target~ all~)  검증하는 데이터가 변경될 시 테스트가 깨지는 현상은 자연스러운 현상이다데이터 정책 ex passtype이 바뀐다면 이를 사용한 테스트 코드들이 전부 깨질 것으로 예상된다프로덕션 코드가 변경되면 테스트 코드 또한 영향을 받는 것은 당연하다테스트 코드가 실패 하는 것을 보고 영향 범위를 인지 할 수 있다. (프로덕션 코드 변경에 대한 영향을 인지 못하는 것이 더욱 큰 문제)수정해야 하는 비용은 들겠지만, 이는 자연스러운 현상이며, 테스트 코드에 대한 존재 이유이기도 하다  검증해야 하는 테스트 케이스가 너무 많아질 경우 코드 자체의 가독성을 위해 반복문을 선택할 수 있지만 이 또한 코드 이해에 허들이 될 수 있음을 인지해야 한다ex 4개 정도면 그냥 나열하자 or 10개 이상이면 반복문을 돌리자  검증부는 상수로 쓰인 데이터를 하드코딩 해서 검증하자ex EMPTY_SIGN 이 EMPTY_SIGN인지 검증 하는 형태는 항상 통과하게 된다 → 테스트 하는 의미가 없음EMPTY_SIGN 이 ㅁ 문자열 인지 검증 (하드코딩)  간단한 로직의 경우 테스트 해야할까?getter 정도는 테스트 하지 않아도 무방. getter와 거의 동일한 작업을 하는 is~메서드 등한줄이라도 가공 비교 판별 등 비지니스에 직결된다면 테스트 해야 한다  테스트 시 새로운 제약사항이 필요하다고 판단되면 클라이언트에게 역 제안도 가능하다  displayname 에 변경이 일어나기 쉬운 내용 넣지 말자 (ex 파일 경로)f/u 하기 힘듦  2. 더 나은 테스트를 작성하기 위한 구체적 조언 한 문단에 한 주제여러가지 논리 구조(분기문, 반복문)가 들어 가는 것을 피한다 완벽하게 제어하기현재 시간 같은 제어할 수 없는 변수는 쓰는 것을 지양하자. 수행되는 환경 (로컬/배포)에 따라 달라질 가능성이 있다. 테스트 환경의 독립성을 보장하자팩토리 메서드는 프로덕션 코드에서 의도를 가지고 만드는 편 → 테스트 환경에서 사용은 지양하는 것이 좋다대신 순수한 생성자를 가지고 테스트 환경을 위한 given절에서 객체 생성 테스트 간 독립성 보장공유 자원 사용 금지 TestFixture테스트를 위해 원하는 상태로 고정시킨 일련의 객체각 테스트 입장에서 어떻게 구성 되는지 몰라도 내용을 이해하는데 문제가 없을 때수정해도 모든 테스트에 영향을 미치지 않을 때예) 댓글 생성 테스트의 경우 → 댓글 생성에 집중 → 테스트를 위한 게시글 생성 로직, 사용자 생성 로직 등이 @BeforeEach에 위치시켜 TestFixture을 구성할 수 있다. TestFixture클렌징deleteAll의 경우 셀렉트 쿼리, 각 레코드 마다 딜리트 쿼리가 건 단위로 나가게 된다. → 성능 이슈가 생길 수 있음deletAllInBatch가 더욱 효과적으로 생각된다트랜잭션 롤백을 사용하는 전략의 경우 SpringBatch를 사용한 배치 통합 테스트의 경우 사용하기 어렵다. 테스트 환경 통합하기테스트 수행에 드는 시간 또한 잘 관리하여야 한다스프링을 띄우는 환경이 조금이라도 달라지면 테스트 시 새로운 컨택스트를 띄우게 된다동일한 환경에서 띄운다면 시간을 단축 가능하다datajpatest → 데이터 jpa 관련 빈들만 올려서 빠르게 테스트 할 수 있지만, 서비스 에서 @Transactional로 테스트 후 사용하게 된다면 스프링을 새로 띄우게 된다는 걸 알아야함서비스 테스트를 하면서 같이 레포지토리 테스트를 하는 것이 좋은 전략일 수 있다. private 메서드의 테스트테스트 하지 않는다테스트 하고 싶어진다면 객체 분리의 신호일 수 있다. 테스트에서만 필요한 메서드보수적으로 생성한다매우 간단한 메서드 or 추후에 프로덕션 코드에서 사용할 가능성이 있는 경우예시 size(), isEmpty()

백엔드-Code 발자국 4주차

워밍업 클럽에 참여 시작한게 어제같은데,, 어떻게 한달이 지나갔는지 모르겠다.이번주부터 여러 일정들이 시작되면서 학습을 진행하는데 많은 어려움이 있었지만이동하는 지하철에서, 쉬는 시간에 짬짬이 강의를 수강하고 여러번 반복하면서 많은 배움을 얻을 수 있었다!28일 마지막 중간 점검에서 강의와 관련된 내용들뿐만 아니라 비슷한 상황에서 다른 분들이 고민하는 내용,그에 대한 우빈님의 답변을 통해 깨달은 바가 정말 많았다..이번 워밍업클럽을 참여하면서 제대로된 코드 리뷰를 처음 해봤는데 진짜 개발자는 혼자 성장하기보다 "같이 성장하기"가 너무나 중요하다고 느꼈다. 이번 주 강의를 수강하면서 정리한 내용은더 나은 테스트를 작성하기 위해서는 한문단에는 반드시 하나의 주제를 가지고 작성한다.이때 작성한 내용은 독립성을 보장해야한다.이를 위해 테스트 환경의 독립성을 보장하기 위해 노력한다.또한 한눈에 들어오는 Test fixture를 구성하는 것도 방법이다.@Parameterized Test , @DynamicTest를 활용하는 방법도 있따!그 중에서 Test fixture에 대해 좀 더 알아봤따.Test fixture는 특정 테스트를 일관되게 테스트하기 위해 사용하는 장치이다.테스트를 수행하기 전 필요한 환경이나 상태를 설정하고, 동일하거나 유사한 객체가 사용되는 여러 테스트가 있을 경우실제 값을 검증한다. 이 과정에 더 많은 시간을 할애하는 경우도 생길 수 있으므로, 동일한 Fixture를 여러 테스트가 공유할 수 있도록 설정하여 성능 향상과 비용 절감을 얻는다! 한달의 시간동안 처음 듣는 내용도 많았고, 알고 있었지만 실천하지 않았던 부분도 많았다는 것을 알게 된 아주 값진 시간이었다. 학습한 내용을 바탕으로 꾸준히 한 단계씩 올라가기 위해 노력할 동기부여도 받았다.교육을 해주신 우빈님에게 무한한 감사의 말씀을 이렇게나마 전달할 수 있었으면 좋곘따!!

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

4주가 정말 빠르게 지나가서 벌써 스터디의 마지막 주차가 지나갔다. 가독성좋은 코드를 작성하는 것에 대해, 테스트코드에 대해 막연한 관심만 가지고, 알던 사용법과 지식 안에서 머무르던 나였는데 이번 스터디와 우빈님의 강의를 통해 지식과 경험의 확장이 시작된거 같아 만족한다. 또한 우빈님께서 매번 정말 열정적으로 중간점검 라이브를 진행해주셨는데 그 정성과 진심이 감동이었다. 강의 중 주제에 관련된 내용들 뿐만 아니라 스프링관련 이라던가 배경지식으로 필요한 여러 개념들을 설명해주시는 방식들도 좋았고 여러모로 배운 것이 정말 많은 강의였다. 추후에 우빈님의 강의도 스터디도 또 열린다면 참여할 의향이 크다. 스터디 참여를 통해서 혼자 강의를 수강했다면 느슨해져서 집중도 있게 학습하지 못했을 수 있는데, 스터디 참여를 하며 미션 수행, 회고 작성들을 기한내에 하기 위해서라도 학습에 집중도가 높아졌던 거 같고 미션들을 해결하는 과정이 학습에 많은 도움이 되었다. 또한 중간점검 라이브를 통해 강사인 우빈님께서 직접 소통하며 질의응답시간과 코드리뷰 등을 해주셔서 정말 스터디를 안할 이유가 없다고 생각한다. 이번 인프런 스터디는 값진 경험이었고 기대이상이었다. 다음에도 다른 스터디들에도 참여해보려한다. 강의 출처 :Practical Testing: 실용적인 테스트 가이드

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

강의 내용 요약 Product Discovery - 실험, 가설 가정 베팅 Value, Usability, Feasibility 가설의 개념과 검증방법 에시에 대해 배움 Product Discovery - 기회 솔루션 트리 '기회 솔루션 트리'의 개념 Product Discovery - 북극성 지표 '북극성 지표' 의 개념에 대해서 살펴봄. Product Growth Growth Lever가 무엇인지, 그리고 이를 구성하는 Acquisition, Retention, Monetization이 무엇인지 배움. Growth lever- AcquisitionAcquisition이란, 유저를 흭득하는 과정이기 때문에, 유저를 어떻게 모으는지에 대한 전략에 대해서 살펴보았음. 이에 대한 방식은 추천(레퍼럴), 네트워크 효과, UGC, 플러그인 방식, PLG 등이 있다.Growth lever- Retention:Engagement Retention에 영향을 주는 Engagement Level에 대해서 배우고 Engagement Level을 높이는 방법인 'Fogg Behavior Model'에 대해서 배움 Growth level- Monetization프로덕트의 다양한 수익화 전략에 대해서 배움 Growth Model 그로스 모델 & 그로스 모델링에 대해 배움 회고 A/B테스트의 함정에 빠지지 말라는 이야기가 인상적이었다. 사실 이 강의를 듣기 전에는 A/B 테스트는 필수이고 만능인줄 알았다. 개인적으로 기회-솔루션 트리와 북극성 지표에 대해 개념이 많이 헷갈렸다. 찾아보니, 북극성 지표는 조직이나 팀이 장기적으로 지향하는 가장 핵심적인 성과지표를 의미하고, 기회-솔루션 트리는 제품이나 서비스 개선 시 문제 중심 접근법을 시각적으로 표현한 도구이다. 즉, 두 개념의 관계는.. 북극성 지표는 우리가 어디로 가야하는지 보여주는 '방향' 이고, 기회-솔루션 트리는 그 방향으로 가기 위해 어떤 길로 가야할지 설계하는 지표이다. 담당하는 프로덕트가 없다보니 구글 플레이 스토어 리뷰를 바탕으로 분석을 진행해보고 있는데, 코치님께서 왜 기회-솔루션 트리를 설계할 때 고객 인터뷰가 중요하다고 하는지 알 것 같았다. 생각보다 리뷰는 고객 경험을 많이 담지 못한다. 개인적으로 이번에 배운 강의들은 어려운 개념이라고 많이 느껴졌다. 그러나 이 강의를 들으면서 배운 개념은 시작이고, 정리한 내용을 차근차근 정리하면서 다시 공부해나가야겠다고 생각이 들었다.

tikitaka

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

[풀스택 완성] Supabase로 웹사이트 3개 클론하기 (Next.js 14) 4주차 배운 내용 정리Supabase AuthJavaScript: Overview | Supabase Docs 이메일 인증 방식 제공Confirmation URL: 이메일을 통해 인증 링크를 보내 사용자 계정을 활성화6-Digit OTP: 6자리 숫자로 로그인 가능OAuth 로그인 지원Kakao Social Login: 카카오 계정으로 간편 로그인 가능JSON 웹 토큰(JWT) 사용클라이언트가 인증된 사용자인지 검증하는 데 활용Access Token을 활용하여 API 요청 가능SSR 환경에서는 쿠키 기반 인증(세션 관리) 활용 가능cookies()를 통해 서버에서 세션 관리Supabase의 auth.getSession()을 사용하여 인증 상태 확인Refresh Token을 사용하여 토큰 갱신 가능Access Token 만료 시 Refresh Token을 이용해 자동 갱신 가능auth.refreshSession()을 제공하여 토큰 재발급 가능auth.onAuthStateChange()를 활용하여 인증 상태 변화를 감지할 수 있음 RealtimeBroadcast모든 사용자에게 동일한 데이터를 전송하는 방식Presence현재 접속한 사용자의 상태를 추적 및 공유Postgres Changes데이터베이스 변경 사항을 실시간으로 감지한다.INSERT / UPDATE / DELETE 이벤트별로 처리 가능 RLS(Row Level Security) 적용데이터베이스에서 보안 정책을 적용하는 기능클라이언트 사이드에서 직접 데이터 권한을 확인하고 실행 가능테이블별로 RLS 활성화 후, 필요한 정책을 생성하여 특정 사용자에게만 데이터 접근 권한 부여 배포 (Vercel, AWS, 도메인 설정)Vercel: 프론트엔드 빌드 & 배포 자동화AWS: EC2사용 - 인스턴스 생성 및 배포도메인 설정: 도메인 구매 후 Vercel에 연결하여 HTTPS 설정 적용, DNS 설정을 통해 연결 미션 3이제까지 만드신 모든 프로젝트를 배포하신 후 배포된 링크를 업로드 해주세요.(선택사항) Instagram Clone 프로젝트에 아래 기능 중 하나를 선택하여 구현하세요.아래 예시 외에도 "채팅 신고", "유저 차단기능" 등 다른 기능을 추가 구현하셔도 괜찮습니다.1⃣ 채팅 메시지 삭제 기능사용자가 특정 채팅 메시지를 삭제할 수 있도록 구현(선택 사항) 삭제된 메시지 대신 “이 메시지는 삭제되었습니다” 같은 알림 표시2⃣ 채팅 읽음/안 읽음 표시 기능채팅방에서 사용자가 읽지 않은 메시지 개수를 표시상대방이 메시지를 읽었는지 확인할 수 있는 “읽음” 표시 추가Instagram Clone 프로젝트 구현 사항:사용자가 특정 채팅 메시지를 삭제할 수 있도록 구현상대방이 메시지를 읽었는지 확인할 수 있는 “읽음” 표시 추가“읽음” 표시 대신 “1” 표시상대방이 메시지 작성 중인지 타이핑 표시 추가메시지 창 진입시 마지막 채팅으로 스크롤 이동Instagram Clone 프로젝트 Github:https://inf.run/T5DSj모든 프로젝트 배포:Todo-listhttps://inf.run/cvWTCdropbox clonehttps://inf.run/3LF9Knetflix clonehttps://inf.run/73D8kinstagram clonehttps://inf.run/kNhK6 과제 해결 과정배포Vercel을 이용해 GitHub과 연동하여 간편하게 배포할 수 있었다.가비아에서 도메인 구매부터 설정을 완료하는 데까지 10분도 걸리지 않았다.또한, .env 파일에 도메인 환경 변수를 추가하고 로그인 콜백 URL을 수정한 뒤 재배포하니,정상적으로 로그인까지 이루어지는 것을 확인할 수 있었다. SMTP 설정도메인을 구매하여 SMTP 설정도 해주었다.감사하게도 러너분께서 SMTP 설정 관련 블로그를 공유해주신 덕분에 쉽게 설정할 수 있었다.가비아 도메인 설정에서 Resend의 DNS 레코드를 추가하면 간단하게 설정이 완료된다.Supabase Authentication>Emails>SMTP Settings에서 활성화한 후 SMTP를 설정해준다.openGraph 설정openGraph 메타태그를 설정할 때,이미지 경로를 상대경로로 입력했더니 메타태그 자체는 정상적으로 적용되었지만, 이미지는 제대로 불러오지 못했다.검색해보니 Open Graph의 og:image 속성에는 절대경로를 사용해야 한다는 점을 알게 되었다. 상대경로(/images/image.jpg)를 사용하면 Facebook, Twitter, Kakao 등의 플랫폼에서 이미지를 정상적으로 불러오지 못할 수 있기 때문이다.따라서, 이미지 경로를 절대경로로 변경하여 이미지를 불러왔다.export const metadata: Metadata = { title: "Instagram clone", description: "nextjs supabase Instagram clone", openGraph: { images: [ { url: "<https://inflearn-nextjs-supabase-instagram-clone.vercel.app/images/inflearngram.png>", alt: "inflearngram", }, ], }, }; 기능 구현getAllMessages 400 에러 해결과정RLS를 적용한 후, Chat 페이지에 처음 접근할 때 ChatPeopleList와 ChatScreen 두 개의 컴포넌트가 동시에 렌더링되었다.이 과정에서 ChatScreen의 getAllMessages 함수가 호출되었는데, 해당 함수는 선택된 유저의 아이디를 매개변수로 받아야 한다.문제는 페이지에 처음 진입했을 때는 아직 유저가 선택되지 않은 상태이므로, getAllMessages(null)이 호출되면서 400 에러가 발생했다.이를 해결하기 위해, getAllMessages 함수에서 매개변수가 null인 경우 빈 배열을 반환하도록 수정하여 초기 렌더링 시 발생하던 에러를 해결했다. 사용자가 특정 채팅 메시지를 삭제할 수 있도록 구현데이터베이스에서 메시지를 완전히 삭제하는 대신, is_deleted 컬럼을 사용하여 논리적으로 삭제하는 방식으로 처리하도록 구현했다.논리적 삭제(Update) 후 데이터를 다시 불러와 ‘이 메시지는 삭제되었습니다.’로 표시했다. componenets/chat/ChatScreen.tsx[Supabase에서 is_deleted 값 업데이트]export async function deletedMessage(id) { const supabase = createBrowserSupabaseClient(); const { error } = await supabase .from("message") .update({ is_deleted: true }) .eq("id", id); if (error) { throw new Error(error.message); } } 메시지의 id를 받아 해당 메시지의 is_deleted 값을 true로 업데이트한다. [Supabase Realtime 업데이트 감지]useEffect(() => { const channel = supabase.channel("message_postgres_changes") .on( "postgres_changes", { event: "UPDATE", schema: "public", table: "message" }, (payload) => { if (payload.eventType === "UPDATE" && !payload.errors) { getAllMessagesQuery.refetch(); } } ) .subscribe(); return () => { channel.unsubscribe(); }; }, []); UPDATE 이벤트가 발생하면 getAllMessagesQuery.refetch()로 변경된 메시지를 반영한다. [삭제 버튼 Mutation]const deletedMessageMutation = useMutation({ mutationFn: deletedMessage }); 삭제 버튼을 클릭했을 때 해당 메시지를 삭제하는 Mutation을 호출한다. [메시지 컴포넌트 렌더링]{getAllMessagesQuery.data?.map((message) => ( <Message key={message.id} isFromMe={message.receiver === selectedUserID} isDeleted={message.is_deleted} isReadAt={message.read_at} onClickDeleted={() => deletedMessageMutation.mutate(message.id)} message={ message.is_deleted ? "이 메시지는 삭제되었습니다." : message.message } /> ))} 삭제 버튼 클릭 시, onClickDeleted 핸들러가 호출되며 deletedMessageMutation.mutate(message.id)를 통해 삭제를 수행한다. componenets/chat/Message.tsxexport default function Messag({ onClickDeleted, isDeleted, isReadAt, isFromMe, message, }) { const [showButton, setShowButton] = useState(false); return ( <div onMouseEnter={() => setShowButton(true)} onMouseLeave={() => setShowButton(false)} onClick={() => setShowButton(true)} > <div> {isReadAt == null && isFromMe && ( <p>{"1"}</p> )} {!isDeleted && isFromMe && showButton && ( <button onClick={onClickDeleted}><i className="fa fa-times"></i></button> )} <div> <p className={`${isDeleted ? "text-gray-900" : ""}`}>{message}</p> </div> </div> </div> ); } Message 컴포넌트에서는 isDeleted, onClickDeleted, message 등의 props를 받는다. 각 메시지를 렌더링할 때, is_deleted 값에 따라 메시지를 다르게 표시한다. 만약 메시지가 삭제된 상태라면 "이 메시지는 삭제되었습니다."라고 표시하고, 내가 보낸 메시지 중 삭제되지 않은 메시지에는 삭제 버튼을 표시하여 삭제 기능을 수행할 수 있다. 상대방이 메시지를 읽었는지 확인할 수 있는 “읽음” 표시 추가(“읽음” 표시 대신 “1”로 읽지 않음 표시)ChatPeopleList 컴포넌트에서 온라인 상태를 추적하는 Channel을 설정하고, ChatScreen 컴포넌트에서 유저의 입장과 퇴장을 추적하는 Channel을 설정했다.Message 테이블에 read_at 컬럼을 추가하여, 각 메시지가 읽혔는지 여부를 관리하도록 한다. 채팅 화면에서 유저가 입장할 때(즉, 상대방이 채팅방에 들어올 때), Presence 채널의 join 이벤트를 활용하여 해당 유저의 메시지 중 아직 읽지 않은 메시지(read_at이 null인 메시지)를 현재 시각으로 업데이트한다. 상대방이 메시지를 읽었을 때, 메시지의 상태가 업데이트된다. componenets/chat/ChatScreen.tsx[Presence 채널과 Join 이벤트 처리]useEffect(() => { const presenceKey = `${loggedInUser.email?.split("@")?.[0]}-${ selectedUserQuery.data?.email?.split("@")?.[0] }`; const channel = supabase.channel("message_postgres_changes", { config: { presence: { key: presenceKey, }, }, }); channel .on("presence", { event: "join" }, ({ key, newPresences }) => { const newState = newPresences; Object.keys(newState).forEach((key) => { if (key === presenceKey) return; if (!isJoined) { setIsJoined(true); readMessageMutation.mutate(); } }); }) .on("presence", { event: "leave" }, ({ key }) => { if (key === presenceKey) return; setIsJoined(false); }) .subscribe(); return () => { channel.unsubscribe(); }; }, []); join 이벤트가 발생하면 readMessageMutation.mutate()를 호출하여 해당 메시지를 읽은 시점으로 업데이트한다. [읽음 시각 업데이트 Mutation]const readMessageMutation = useMutation({ mutationFn: () => readMessage({ chatUserId: selectedUserID }), }); readMessageMutation은 readMessage를 호출한다. [Supabase에서 read_at 값 업데이트]export async function readMessage({ chatUserId }) { if (chatUserId === null) return; const supabase = createBrowserSupabaseClient(); const { data: { session }, error, } = await supabase.auth.getSession(); if (error || !session.user) { throw new Error("User is not authenticated"); } const { error: readMessagesError } = await supabase .from("message") .update({ read_at: new Date().toISOString() }) .eq("receiver", session.user.id) .eq("sender", chatUserId) .is("read_at", null); if (readMessagesError) { throw new Error(readMessagesError.message); } } readMessage는 읽지 않은 메시지(read_at이 null인 메시지)를 현재 시간으로 업데이트한다.receiver: 로그인된 사용자와 sender: 채팅 상대방이 일치하고, read_at이 null인 메시지에 대해 현재 시각을 read_at에 업데이트한다. [읽음 표시 추가 → “1” 읽지 않음으로 표시]{getAllMessagesQuery.data?.map((message) => ( <Message key={message.id} isFromMe={message.receiver === selectedUserID} isDeleted={message.is_deleted} isReadAt={message.read_at} onClickDeleted={() => deletedMessageMutation.mutate(message.id)} message={ message.is_deleted ? "이 메시지는 삭제되었습니다." : message.message } /> ))} isReadAt 값을 Message 컴포넌트에 전달한다. componenets/chat/Message.tsxexport default function Messag({ onClickDeleted, isDeleted, isReadAt, isFromMe, message, }) { const [showButton, setShowButton] = useState(false); return ( <div onMouseEnter={() => setShowButton(true)} onMouseLeave={() => setShowButton(false)} onClick={() => setShowButton(true)} > <div> {isReadAt == null && isFromMe && ( <p>{"1"}</p> )} {!isDeleted && isFromMe && showButton && ( <button onClick={onClickDeleted}><i className="fa fa-times"></i></button> )} <div> <p className={`${isDeleted ? "text-gray-900" : ""}`}>{message}</p> </div> </div> </div> ); } Message 컴포넌트에서 isReadAt 값이 null인 경우, 즉 상대방이 메시지를 읽지 않았다면 "1"을 표시하여 읽지 않음을 나타낸다. 상대방이 메시지 작성 중인지 타이핑 표시 추가이 기능은 Precence 자료를 찾다가 한 영상을 발견하면서 프로젝트에 넣어봤다.Youtube - Chat app with Nextjs & Supabase | Postgres Changes | Presence | Supbase Realtime Course part 1 [Realtime Subscription 설정]const [typingUsers, setTypingUsers] = useState([]); const channelRef = useRef(null); const realTimeSubscription = () => { const presenceKey = `${loggedInUser.email?.split("@")?.[0]}-${ selectedUserQuery.data?.email?.split("@")?.[0] }`; const channel = supabase.channel("message_postgres_changes", { config: { presence: { key: presenceKey, }, }, }); channel .on("postgres_changes",{ event: "INSERT", schema: "public", table: "message" }, (payload) => { // ... } ) .on("postgres_changes",{ event: "UPDATE", schema: "public", table: "message" }, (payload) => { // ... } ) .on("presence", { event: "sync" }, () => { const newState = channel.presenceState(); let filteredUsers = []; // 타이핑 중인 유저 추적 Object.keys(newState).forEach((key) => { if (key === presenceKey) return; const presences = newState[key]; presences.forEach( (presence: { presence_ref: string; isTyping?: boolean; name?: string; }) => { if (presence.isTyping) { filteredUsers.push(presence.name); // 타이핑 중인 유저 이름 추적 } } ); }); setTypingUsers(filteredUsers); // 타이핑 중인 유저 상태 업데이트 }) .subscribe(async (status) => { if (status !== "SUBSCRIBED") return; // 타이핑 상태를 false로 초기화 await channel.track({ onlineAt: new Date().toISOString(), isTyping: false, name: loggedInUser.email?.split("@")?.[0], }); }); return channel; }; Presence를 통해 사용자의 타이핑 상태를 실시간으로 추적하고 동기화한다.타이핑 상태인 사용자들을 추적하여 filteredUsers 배열에 추가하고, setTypingUsers를 통해 관리한다. useEffect(() => { if (!selectedUserID) return; channelRef.current = realTimeSubscription(); return () => { channelRef.current?.unsubscribe(); }; }, [selectedUserQuery?.data]); useEffect내에서 realTimeSubscription을 호출하고, channelRef로 채널을 관리하여 구독을 설정한다. [타이핑 상태 트래킹]const isTypingRef = useRef(false); // 타이핑 상태 추적 const typingTimeoutRef = useRef(null); // 타이핑 후 시간 지연 처리 const trackTyping = async (status) => { await channelRef.current.track({ isTyping: status, name: loggedInUser.email?.split("@")?.[0], }); }; const handleInputChange = async (e) => { setMessage(e.target.value); // 타이핑이 시작되면 isTyping 상태를 true로 설정 if (!isTypingRef.current) { await trackTyping(true); isTypingRef.current = true; } // 기존 타이머가 있으면 clear if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current); // 2초 뒤 타이핑 종료 typingTimeoutRef.current = setTimeout(async () => { if (!isTypingRef.current) return; await trackTyping(false); isTypingRef.current = false; }, 2000); }; trackTyping 함수는 유저가 타이핑 중인지 여부를 channel.track()로 실시간으로 업데이트한다.handleInputChange 함수는 사용자의 타이핑 상태와 타이핑 종료 타이머를 관리한다. 사용자가 메시지를 입력하기 시작하면 isTyping 상태를 true로 설정하고, 타이핑을 멈추면 2초 후에 isTyping을 false로 설정하여 타이핑 상태 변화를 반영한다. [타이핑 상태 표시] {/* 채팅방 영역 */} <div className="flex flex-col"> {typingUsers.length > 0 && ( <div className="w-full flex items-center space-x-2 text-gray-500"> <span className="text-sm">{typingUsers[0]} is typing...</span> </div> )} <div className="flex"> <input value={message} onChange={handleInputChange} onKeyDown={(e) => { if (e.key === "Enter" && !e.nativeEvent.isComposing) { e.preventDefault(); sendMessageMutation.mutate(); } }} className="p-3 w-full border-2 border-light-blue-600" placeholder="메시지를 입력하세요." /> typingUsers를 통해 타이핑 중인 사용자들의 이름을 업데이트하고, 화면에서 타이핑 상태를 "is typing..." 형태로 표시한다.(etc. 한글 메시지 엔터 이벤트 시 2번 보내지는 오류는 e.nativeEvent.isComposing을 통해 해결했다. 문자 입력중인 상태를 감지하여 입력이 완료된 상태(false)만 전송이 가능하다.) 메시지 창 진입시 최신 채팅으로 스크롤 이동export default function ChatScreen({ loggedInUser }) { const chatRef = useRef<HTMLDivElement>(null); const [scrollOn, setScrollOn] = useState(true); useEffect(() => { if (!chatRef.current || !getAllMessagesQuery.isSuccess) return; if (scrollOn) { chatRef.current.scrollTop = chatRef.current.scrollHeight; } }, [ selectedUserID, getAllMessagesQuery.isSuccess, getAllMessagesQuery.data, scrollOn, ]); return selectedUserQuery.data !== null ? ( // ... {/* 채팅 영역 */} <div ref={chatRef}> // ... </div> // ... ) : ( <div></div> ); } 채팅방에 들어왔을때와 새 메시지가 오면 useRef를 사용하여 최신 메시지로 스크롤을 이동한다.메시지 삭제(Update)시 데이터가 다시 불러와지는데, scrollOn으로 삭제시에는 스크롤이 유지되도록 설정했다.따라서 채팅방에서 최신 메시지를 자동으로 보여주고, 메시지 삭제 시에는 스크롤을 유지한다. 4주차 회고이번 주에는 Supabase의 유용한 기능들을 많이 사용해 볼 수 있었고, 그 과정에서 많은 것을 배웠습니다. 실시간 채팅 구현에서는 WebSocket만 사용할 수 있을 줄 알았는데, Supabase의 기능을 활용할 수 있어서 새로 배워갔습니다. 또한 배포는 Vercel로 간단하게만 해본 경험이 있었지만, 이번에는 도메인 구매부터 SMTP 설정까지 직접 해보며 기억에 남는 경험을 쌓을 수 있었습니다.미션에서는 읽음 표시 기능을 구현했는데, 현재는 presence 채널을 통해 상대방이 입장할 때 메시지의 읽음 상태를 업데이트하고, useState로 입장/퇴장 상태를 관리하며 메시지를 업데이트하고 있습니다. 더 나은 방법이 있을 것 같지만, 구체적인 해결책은 아직 떠오르지 않았습니다. 현업에서는 어떻게 처리하는지 궁금합니다. 또한, 실시간 기능은 더 공부해서 개인 프로젝트에 적용해보고 싶습니다.벌써 끝이라니 믿기지가 않네요. 그만큼 정말 재미있었고, 오랜만에 프로젝트를 만들고 따라가면서 즐겁게 보냈습니다. 처음 시작할 때는 Next.js의 페이지 라우팅만 알고 있었고, Supabase는 부끄럽지만 이름만 들어본 상태여서 잘 따라갈 수 있을지 걱정했었는데, 너무 좋은 강의를 통해 많이 성장한 것 같습니다.Supabase를 쉽고 빠르게 배울 수 있도록 강의를 준비해주신 로펀 강사님께 감사드립니다! 덕분에 어렵지 않게 재밌게 배웠습니다.😊

채널톡 아이콘