<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>2star</title>
        <link>https://velog.io/</link>
        <description>안녕하세요.</description>
        <lastBuildDate>Mon, 03 Feb 2025 10:24:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>2star</title>
            <url>https://velog.velcdn.com/images/gyu_p/profile/b7234c4d-a6b9-4870-9227-66d6f0bad797/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 2star. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/gyu_p" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[모의 면접 질문 리스트 정리]]></title>
            <link>https://velog.io/@gyu_p/%EB%AA%A8%EC%9D%98-%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@gyu_p/%EB%AA%A8%EC%9D%98-%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 03 Feb 2025 10:24:21 GMT</pubDate>
            <description><![CDATA[<h1 id="모의-면접-질문-리스트">모의 면접 질문 리스트</h1>
<h2 id="프로젝트-소개">프로젝트 소개</h2>
<ul>
<li>어떤 기능을 하는 프로젝트인가요?</li>
<li>타 비슷한 프로젝트에 비해서 장점이 무엇인가요?</li>
<li>프로젝트의 핵심 목표는 무엇인가요?</li>
</ul>
<h2 id="추천-로직">추천 로직</h2>
<ul>
<li>추천 로직이 어떻게 되나요? (홈 + 챗봇)</li>
<li>추천 시스템에서 사용한 알고리즘은 무엇인가요?</li>
<li>추천 결과의 정확도를 어떻게 평가했나요?</li>
</ul>
<h2 id="갈등-해결">갈등 해결</h2>
<ul>
<li>프로젝트 진행 중 갈등이 발생한 적이 있나요?</li>
<li>갈등을 어떻게 해결했나요?</li>
<li>팀원 간 의견 충돌이 있을 때 어떻게 조율했나요?</li>
</ul>
<h2 id="맡은-역할">맡은 역할</h2>
<ul>
<li>프로젝트에서 맡은 역할은 무엇인가요?</li>
<li>맡은 역할에서 가장 중점을 둔 부분은 무엇인가요?</li>
<li>프로젝트를 진행하면서 배운 점은 무엇인가요?</li>
</ul>
<h2 id="데이터베이스-설계-erd">데이터베이스 설계 (ERD)</h2>
<ul>
<li>ERD를 설계할 때 고려한 사항은 무엇인가요?</li>
<li>주요 테이블과 그 관계에 대해 설명해주세요.</li>
<li>성능 최적화를 위해 어떤 노력을 했나요?</li>
</ul>
<h2 id="기술-스택">기술 스택</h2>
<ul>
<li>어떤 기술 스택을 사용했나요?</li>
<li>각 기술을 선택한 이유는 무엇인가요?</li>
<li>프로젝트에서 사용한 주요 라이브러리나 프레임워크는 무엇인가요?</li>
</ul>
<h2 id="aws-서버-배포">AWS 서버 배포</h2>
<ul>
<li>AWS 배포 과정은 어떻게 진행했나요?</li>
<li>사용한 AWS 서비스는 무엇인가요? (EC2, S3, RDS 등)</li>
<li>서버 모니터링과 유지보수는 어떻게 했나요?</li>
</ul>
<h2 id="docker-활용">Docker 활용</h2>
<ul>
<li>Docker를 사용했나요? 사용했다면 어떻게 적용했나요?</li>
<li>Docker Compose를 활용했나요?</li>
<li>Docker 이미지 빌드 및 배포 과정에 대해 설명해주세요.</li>
</ul>
<h2 id="프로젝트-버전-관리">프로젝트 버전 관리</h2>
<ul>
<li>프로젝트에서 git 컨벤션을 어떻게 정했나요?</li>
<li>PR 규칙 및 브랜치 전략은 어떻게 운영했나요?</li>
<li>프로젝트 협업 과정에서 git을 어떻게 활용했나요?</li>
</ul>
<h2 id="코드-리뷰">코드 리뷰</h2>
<ul>
<li>코드 리뷰를 진행했나요?</li>
<li>코드 리뷰에서 중요하게 생각한 부분은 무엇인가요?</li>
<li>코드 리뷰를 통해 개선된 사례가 있다면 설명해주세요.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로세스 기초]]></title>
            <link>https://velog.io/@gyu_p/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@gyu_p/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Sun, 02 Feb 2025 07:53:19 GMT</pubDate>
            <description><![CDATA[<h3 id="프로세스-관리">프로세스 관리</h3>
<h4 id="1-프로세스의-개념과-구성-요소">1. 프로세스의 개념과 구성 요소</h4>
<ul>
<li><strong>프로세스 정의</strong>  <ul>
<li>실행 중인 프로그램을 의미 각 프로세스는 독립된 메모리 공간과 실행 상태를 가짐</li>
</ul>
</li>
<li><strong>프로세스 제어 블록(PCB)</strong>  <ul>
<li>프로세스의 상태, 프로그램 카운터, 레지스터, 메모리 관리 정보, I/O 상태 등의 정보를 저장하는 자료구조</li>
<li>PCB는 운영체제가 프로세스를 관리하고 스케줄링할 때 핵심적인 역할을 함</li>
</ul>
</li>
</ul>
<h4 id="2-프로세스-생성과-종료">2. 프로세스 생성과 종료</h4>
<ul>
<li><strong>프로세스 생성</strong>  <ul>
<li><strong>fork:</strong> 기존 프로세스를 복제하여 새로운 프로세스를 생성하는 시스템 호출. 부모와 자식 프로세스는 독립적으로 실행. 자식은 부모의 메모리 상태를 복사함</li>
<li><strong>exec:</strong> 생성된 프로세스가 다른 프로그램을 실행하도록 덮어쓰는 방식으로 사용. fork와 함께 자주 사용됨</li>
</ul>
</li>
<li><strong>프로세스 종료</strong>  <ul>
<li>프로세스가 실행을 마치거나 오류 발생 시 종료. 운영체제는 종료된 프로세스의 PCB를 해제하고, 관련 자원을 반환</li>
</ul>
</li>
</ul>
<h4 id="3-프로세스-상태와-전이">3. 프로세스 상태와 전이</h4>
<ul>
<li><strong>주요 프로세스 상태</strong>  <ul>
<li><strong>생성(New):</strong> 프로세스가 생성되어 준비되는 상태</li>
<li><strong>준비(Ready):</strong> CPU 할당을 기다리는 상태. 여러 프로세스가 이 상태에서 대기</li>
<li><strong>실행(Running):</strong> CPU를 할당받아 명령어를 실행 중인 상태</li>
<li><strong>대기(Waiting):</strong> I/O 요청이나 특정 이벤트를 기다리는 상태</li>
<li><strong>종료(Terminated):</strong> 프로세스 실행이 완료되거나 강제 종료된 상태</li>
</ul>
</li>
<li><strong>상태 전이 과정</strong>  <ul>
<li>프로세스는 실행 중 I/O 요청이나 이벤트 대기로 인해 준비 상태에서 대기 상태로, 이벤트가 완료되면 다시 준비 상태로 돌아감 즉 상태 간의 전이가 이루어짐</li>
</ul>
</li>
</ul>
<h4 id="4-cpu-스케줄링과-알고리즘">4. CPU 스케줄링과 알고리즘</h4>
<ul>
<li><strong>스케줄링의 목적</strong>  <ul>
<li>CPU를 여러 프로세스 간에 효율적으로 분배하여 시스템 자원 활용도를 높이고, 응답 시간을 최소화함</li>
</ul>
</li>
<li><strong>주요 스케줄링 알고리즘</strong>  <ul>
<li><strong>FCFS(First-Come, First-Served):</strong>  <ul>
<li>도착 순서대로 프로세스에 CPU 할당. 단순하지만 평균 대기 시간이 길어질 수 있음</li>
</ul>
</li>
<li><strong>SJF(Shortest Job First):</strong>  <ul>
<li>실행 시간이 짧은 프로세스 우선. 평균 대기 시간이 짧아지지만, 예측이 어려울 경우 부적합</li>
</ul>
</li>
<li><strong>라운드 로빈(Round Robin):</strong>  <ul>
<li>각 프로세스에 고정된 시간(타임 슬라이스)을 할당하며 순환적으로 CPU를 분배. 공정성이 높지만, 타임 슬라이스의 크기에 따라 성능이 좌우됨</li>
</ul>
</li>
<li><strong>우선순위 스케줄링:</strong>  <ul>
<li>프로세스마다 우선순위를 부여하여 높은 우선순위를 가진 프로세스에 CPU를 우선 할당. 우선순위 역전 문제 등이 발생할 수 있음</li>
</ul>
</li>
<li><strong>멀티 레벨 큐 스케줄링:</strong>  <ul>
<li>프로세스들을 여러 큐로 분류한 후, 각 큐에 다른 스케줄링 알고리즘을 적용하여 관리. 서로 다른 특성을 가진 프로세스들을 효율적으로 처리</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="5-문맥-교환context-switching">5. 문맥 교환(Context Switching)</h4>
<ul>
<li><strong>문맥 교환의 정의</strong>  <ul>
<li>CPU가 한 프로세스에서 다른 프로세스로 전환할 때 현재 프로세스의 상태(레지스터, 프로그램 카운터 등)를 저장하고 새로운 프로세스의 상태를 불러오는 과정</li>
</ul>
</li>
<li><strong>문맥 교환의 오버헤드</strong>  <ul>
<li>빈번한 문맥 교환은 CPU 시간을 소모. 스케줄링 알고리즘 설계 시 오버헤드를 최소화하는 것이 중요함</li>
</ul>
</li>
</ul>
<h4 id="6-인터프로세스-커뮤니케이션ipc">6. 인터프로세스 커뮤니케이션(IPC)</h4>
<ul>
<li><strong>IPC의 필요성</strong>  <ul>
<li>여러 프로세스가 서로 데이터를 교환하거나 협력 작업을 수행하기 위해 필요</li>
</ul>
</li>
<li><strong>IPC 기법들</strong>  <ul>
<li><strong>파이프(Pipe):</strong> 한 프로세스의 출력이 다른 프로세스의 입력으로 연결되는 방식</li>
<li><strong>메시지 큐:</strong> 프로세스 간에 메시지를 보내고 받을 수 있도록 지원하는 큐 구조</li>
<li><strong>공유 메모리:</strong> 여러 프로세스가 동일한 메모리 공간을 공유하며 빠른 통신을 가능하게 함</li>
<li><strong>소켓:</strong> 네트워크를 통한 프로세스 간 통신에 사용.</li>
</ul>
</li>
</ul>
<h4 id="7-동기화와-경쟁-조건-문제">7. 동기화와 경쟁 조건 문제</h4>
<ul>
<li><strong>동기화 필요성</strong>  <ul>
<li>여러 프로세스나 스레드가 동시에 공유 자원에 접근할 때 데이터 무결성을 보장하기 위한 방법</li>
</ul>
</li>
<li><strong>주요 동기화 기법</strong>  <ul>
<li><strong>뮤텍스(Mutex):</strong> 상호 배제를 통해 한 번에 한 프로세스만 공유 자원에 접근하도록 제한</li>
<li><strong>세마포어(Semaphore):</strong> 프로세스 동기화와 자원 관리를 위한 카운터 기반의 기법</li>
<li><strong>모니터(Monitor):</strong> 데이터와 그 데이터를 조작하는 절차를 하나로 묶어 동기화를 자동으로 처리</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제]]></title>
            <link>https://velog.io/@gyu_p/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C</link>
            <guid>https://velog.io/@gyu_p/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C</guid>
            <pubDate>Sun, 02 Feb 2025 07:50:48 GMT</pubDate>
            <description><![CDATA[<h3 id="운영체제-기초">운영체제 기초</h3>
<h4 id="1-운영체제의-개념과-역할">1. 운영체제의 개념과 역할</h4>
<ul>
<li><strong>운영체제(OS)의 정의</strong>  <ul>
<li>컴퓨터 하드웨어와 응용 프로그램 사이에서 자원을 효율적으로 관리하는 소프트웨어  </li>
<li>사용자와 하드웨어 간의 인터페이스 역할 수행</li>
</ul>
</li>
<li><strong>주요 역할</strong>  <ul>
<li>프로세스, 메모리, 파일, 입출력 장치 등 시스템 자원 관리  </li>
<li>시스템의 안정성과 효율성 보장</li>
</ul>
</li>
</ul>
<h4 id="2-프로세스-관리">2. 프로세스 관리</h4>
<ul>
<li><strong>프로세스와 스레드</strong>  <ul>
<li><strong>프로세스:</strong> 실행 중인 프로그램으로, 독립적인 메모리 공간을 가짐  </li>
<li><strong>스레드:</strong> 프로세스 내에서 실행되는 작업 단위, 같은 프로세스 내 여러 스레드는 메모리 공간을 공유</li>
</ul>
</li>
<li><strong>프로세스 상태</strong>  <ul>
<li>생성, 준비, 실행, 대기, 종료 상태로 구분되며, 각 상태 전환 과정을 통해 프로세스가 운영됨</li>
</ul>
</li>
<li><strong>스케줄링</strong>  <ul>
<li>CPU 할당을 위한 알고리즘: FCFS(선입선출), SJF(최단 작업 우선), 우선순위, 라운드 로빈 등이 있음  </li>
<li>각 알고리즘은 작업의 종류와 시스템 환경에 따라 장단점이 있음</li>
</ul>
</li>
</ul>
<h4 id="3-메모리-관리">3. 메모리 관리</h4>
<ul>
<li><strong>주기억장치와 보조기억장치</strong>  <ul>
<li><strong>주기억장치:</strong> RAM, 휘발성 메모리로서 프로세스가 실행되는 동안 데이터를 저장  </li>
<li><strong>보조기억장치:</strong> HDD, SSD 등 비휘발성 저장 장치로 데이터를 영구 보관</li>
</ul>
</li>
<li><strong>메모리 할당 방식</strong>  <ul>
<li><strong>연속 할당:</strong> 단순하지만 외부 단편화 문제 발생  </li>
<li><strong>비연속 할당:</strong> 페이징과 세그멘테이션 기법을 통해 메모리 효율성 증대</li>
</ul>
</li>
<li><strong>가상 메모리</strong>  <ul>
<li>실제 메모리보다 큰 주소 공간을 제공하여, 디스크의 일부를 메모리처럼 사용하는 기술  </li>
<li>페이지 교체 알고리즘(예: LRU, FIFO)을 통해 효율적인 메모리 활용</li>
</ul>
</li>
</ul>
<h4 id="4-파일-시스템">4. 파일 시스템</h4>
<ul>
<li><strong>파일의 개념과 관리</strong>  <ul>
<li>데이터를 논리적 단위(파일)로 관리하며, 디렉토리 구조를 통해 조직화  </li>
<li>파일 접근 권한 및 보안을 위한 관리 필요</li>
</ul>
</li>
<li><strong>파일 시스템의 종류</strong>  <ul>
<li>FAT, NTFS, ext4 등 각 운영체제에 맞는 파일 시스템이 존재  </li>
<li>각 시스템은 성능, 안정성, 보안 측면에서 차이가 있음</li>
</ul>
</li>
<li><strong>파일 입출력</strong>  <ul>
<li>파일 생성, 읽기, 쓰기, 삭제 등의 기본 작업 수행  </li>
<li>버퍼링, 캐싱 등 성능 최적화 기법도 학습 필요</li>
</ul>
</li>
</ul>
<h4 id="5-입출력-장치와-장치-관리">5. 입출력 장치와 장치 관리</h4>
<ul>
<li><strong>I/O 장치 관리</strong>  <ul>
<li>다양한 입출력 장치(키보드, 마우스, 프린터, 네트워크 장비 등)를 효율적으로 제어  </li>
<li>장치 드라이버를 통해 하드웨어와 소프트웨어 간 통신을 담당</li>
</ul>
</li>
<li><strong>입출력 방식</strong>  <ul>
<li>인터럽트 기반, DMA(Direct Memory Access) 등 하드웨어 효율성을 높이는 방식 적용</li>
</ul>
</li>
</ul>
<h4 id="6-동시성-제어와-데드락">6. 동시성 제어와 데드락</h4>
<ul>
<li><strong>동기화 문제</strong>  <ul>
<li>여러 프로세스나 스레드가 동시에 공유 자원에 접근할 때 발생하는 문제 해결  </li>
<li>뮤텍스, 세마포어, 모니터 등의 기법을 사용하여 충돌 방지</li>
</ul>
</li>
<li><strong>데드락(Deadlock)</strong>  <ul>
<li>두 개 이상의 프로세스가 서로 자원을 기다리며 무한 대기 상태에 빠지는 현상  </li>
<li>예방, 회피, 탐지 및 회복 기법으로 관리</li>
</ul>
</li>
</ul>
<h4 id="7-보안과-접근-제어">7. 보안과 접근 제어</h4>
<ul>
<li><strong>사용자 인증 및 권한 관리</strong>  <ul>
<li>시스템 자원에 대한 접근을 제어하여, 무단 접근이나 악의적 행위를 방지  </li>
<li>파일 권한 설정, 암호화, 방화벽 등 다양한 보안 기법 적용</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트 발표 회고]]></title>
            <link>https://velog.io/@gyu_p/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%9C%ED%91%9C-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@gyu_p/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%9C%ED%91%9C-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 31 Jan 2025 13:00:23 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-발표-회고">프로젝트 발표 회고</h2>
<h3 id="1">1.</h3>
<p>다른 조들의 발표를 경청하면서 모두가 정말 열심히 준비했다는 걸 느꼈다. 각 팀이 각자의 방식으로 고민하고, 개발하고, 완성해가는 과정이 고스란히 발표에 담겨 있어서 감탄스러웠다. 다만 A, B 발표장이 나뉘어 있어서 다른 발표장의 조들이 어떤 발표를 했는지 직접 듣지 못한 점은 조금 아쉬웠다. 다른 팀들의 발표도 직접 보고 싶었는데, 녹화 영상이 있다면 꼭 챙겨봐야겠다고 생각했다.</p>
<h3 id="2">2.</h3>
<p>우리 팀의 발표도 무사히 끝났다. 20여 분이라는 짧은 시간이었지만, 프로젝트의 핵심 내용을 전달하는 데에는 큰 무리가 없었던 것 같다. 사실, 개발하는 동안 다양한 기능을 추가하고 싶었지만, 발표 시간 내에 모든 걸 보여주기는 어려웠다. 하지만 핵심적인 기능과 프로젝트의 방향성을 효과적으로 전달했다고 생각하니 뿌듯했다.</p>
<p>또한, 협력사 분들께서 질의응답 시간에 피드백을 주셨다는 점이 인상적이었다. 실무에서 바라보는 시각과 우리가 개발하면서 놓쳤던 부분들을 짚어주셔서 정말 큰 도움이 되었다. &quot;이 기능을 더 발전시키면 이런 활용이 가능할 것 같다&quot;는 말이 기억에 남는다. 앞으로 추가적인 기능을 고민할 때 참고해야겠다고 생각했다.</p>
<h3 id="3">3.</h3>
<p>발표가 끝나고 저녁 시간에는 다양한 분들이 다가와 프로젝트에 대한 이야기를 나누었다. 몇몇 튜터님들과 외부 손님들도 프로젝트에 대한 질의응답을 해 주셨고, 피드백을 남겨주셨다. 심지어 추가적인 멘토링 기회를 얻게 된 것도 있었다. 이 과정에서 느낀 것은, 우리가 만든 프로젝트가 단순한 학습 과제를 넘어서 실제로 유용한 가치를 가질 수도 있겠다는 점이었다.</p>
<p>특히 &quot;이런 기능이 추가되면 정말 좋겠다&quot;, &quot;서비스로 발전시키면 사용자들이 관심을 가질 것 같다&quot;는 피드백을 들었을 때, 단순한 팀 프로젝트로 끝내기엔 아깝다는 생각이 들었다. 우리가 만든 기능이 사용자들에게 실제로 어떤 가치를 줄 수 있을지 더 깊이 고민해봐야겠다는 다짐을 하게 되었다.</p>
<h3 id="4">4.</h3>
<p>1월 한 달 동안 열정적인 팀원들과 함께 프로젝트를 하면서 정말 많은 걸 배웠다. 단순히 코드를 짜는 것이 아니라, 협업의 과정에서 커뮤니케이션이 얼마나 중요한지, 예상치 못한 이슈를 해결하는 과정에서 어떤 태도를 가져야 하는지를 배울 수 있었다.</p>
<p>물론 힘든 순간도 많았다. 일정이 촉박한 상황에서 예상보다 오래 걸리는 기능 구현, 예상치 못한 버그, 그리고 팀원들 간의 의견 조율까지. 하지만 이러한 과정이 있었기에 더 성장할 수 있었고, 마지막에 프로젝트가 완성되었을 때의 희열은 오랫동안 남아 있을 듯 하다 !</p>
<h3 id="5">5.</h3>
<p>마지막으로...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django 쿼리 최적화]]></title>
            <link>https://velog.io/@gyu_p/Django-%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@gyu_p/Django-%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Wed, 29 Jan 2025 09:22:19 GMT</pubDate>
            <description><![CDATA[<h3 id="django-쿼리-최적화">Django 쿼리 최적화</h3>
<h3 id="🔹-django-공식문서-기반-쿼리-최적화">🔹 Django 공식문서 기반 쿼리 최적화</h3>
<p>Django는 ORM을 사용하여 SQL 쿼리를 자동 생성하지만, 잘못된 사용은 성능 저하를 유발할 수 있음.<br>Django 공식 문서에서 추천하는 쿼리 최적화 방법을 정리.</p>
<h4 id="✅-1-select_related-사용-join-최적화">✅ 1. <code>select_related()</code> 사용 (JOIN 최적화)</h4>
<ul>
<li><strong>ForeignKey, OneToOneField 관련 데이터를 함께 조회하여 N+1 문제 방지</strong><pre><code class="language-python"># N+1 문제 발생 (비효율적)
games = Game.objects.all()
for game in games:
  print(game.genre.name)  # 매번 genre를 조회하는 추가 쿼리 발생
</code></pre>
</li>
</ul>
<h1 id="최적화된-코드">최적화된 코드</h1>
<p>games = Game.objects.select_related(&quot;genre&quot;)  # JOIN 수행하여 한 번에 조회</p>
<pre><code>
#### ✅ 2. `prefetch_related()` 사용 (ManyToMany, ForeignKey 역참조 최적화)
- **ManyToMany, OneToMany 관계에서 여러 개의 추가 쿼리를 미리 가져옴**
```python
# N+1 문제 발생 (비효율적)
games = Game.objects.all()
for game in games:
    for tag in game.tags.all():
        print(tag.name)  # 여러 개의 쿼리 발생

# 최적화된 코드
games = Game.objects.prefetch_related(&quot;tags&quot;)  # 미리 가져와서 쿼리 수 감소</code></pre><h4 id="✅-3-only와-defer로-불필요한-필드-로딩-방지">✅ 3. <code>only()</code>와 <code>defer()</code>로 불필요한 필드 로딩 방지</h4>
<ul>
<li><strong>필요한 필드만 가져와 성능 최적화</strong><pre><code class="language-python"># 모든 필드 조회 (비효율적)
game = Game.objects.get(id=1)
</code></pre>
</li>
</ul>
<h1 id="특정-필드만-조회-최적화">특정 필드만 조회 (최적화)</h1>
<p>game = Game.objects.only(&quot;id&quot;, &quot;name&quot;)  # 나머지 필드는 필요할 때까지 조회되지 않음
game = Game.objects.defer(&quot;large_text_field&quot;)  # 특정 필드 제외하여 조회</p>
<pre><code>
#### ✅ 4. `exists()`를 사용하여 불필요한 데이터 로딩 방지
- **데이터 존재 여부만 확인할 때 `count()` 대신 `exists()` 사용**
```python
# 비효율적인 방식
if Game.objects.filter(name=&quot;Cyberpunk 2077&quot;).count() &gt; 0:
    print(&quot;게임이 존재합니다.&quot;)

# 최적화된 코드
if Game.objects.filter(name=&quot;Cyberpunk 2077&quot;).exists():
    print(&quot;게임이 존재합니다.&quot;)  # 더 빠르고 메모리 절약 가능</code></pre><h4 id="✅-5-bulk_create와-update-사용하여-다수의-데이터-처리-최적화">✅ 5. <code>bulk_create()</code>와 <code>update()</code> 사용하여 다수의 데이터 처리 최적화</h4>
<pre><code class="language-python"># 여러 개의 객체를 한 번에 저장
games = [
    Game(name=&quot;The Witcher 3&quot;),
    Game(name=&quot;Elden Ring&quot;),
]
Game.objects.bulk_create(games)  # 여러 INSERT 쿼리를 하나로 합침</code></pre>
<h4 id="✅-6-annotate를-활용한-집계-연산-최적화">✅ 6. <code>annotate()</code>를 활용한 집계 연산 최적화</h4>
<pre><code class="language-python">from django.db.models import Count

# 각 게임이 가진 리뷰 개수 가져오기 (JOIN + GROUP BY)
games = Game.objects.annotate(review_count=Count(&quot;reviews&quot;))
for game in games:
    print(f&quot;{game.name} - {game.review_count}개 리뷰&quot;)</code></pre>
<h4 id="✅-7-iterator를-사용하여-대량-데이터-효율적으로-처리">✅ 7. <code>iterator()</code>를 사용하여 대량 데이터 효율적으로 처리</h4>
<ul>
<li>대량 데이터 조회 시 메모리 사용량을 줄이기 위해 <code>iterator()</code> 사용<pre><code class="language-python">for game in Game.objects.iterator():
  print(game.name)  # 메모리 최적화됨</code></pre>
</li>
</ul>
<hr>
<h3 id="🔹-정리">🔹 정리</h3>
<ol>
<li><strong><code>select_related()</code>, <code>prefetch_related()</code></strong> → N+1 문제 해결</li>
<li><strong><code>only()</code>, <code>defer()</code></strong> → 불필요한 필드 로딩 방지</li>
<li><strong><code>exists()</code> 사용</strong> → <code>count()</code> 대신 존재 여부만 체크할 때 사용</li>
<li><strong><code>bulk_create()</code></strong> → 다량의 데이터 저장 시 성능 최적화</li>
<li><strong><code>annotate()</code> 사용</strong> → ORM에서 직접 집계 연산 수행</li>
<li><strong><code>iterator()</code> 활용</strong> → 대량 데이터 메모리 절약</li>
</ol>
<p>🚀 <strong>Django ORM을 잘 활용하면 성능 최적화가 가능하며, 불필요한 SQL 쿼리 실행을 줄일 수 있음!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django verbose_name]]></title>
            <link>https://velog.io/@gyu_p/Django-verbosename</link>
            <guid>https://velog.io/@gyu_p/Django-verbosename</guid>
            <pubDate>Wed, 29 Jan 2025 09:21:37 GMT</pubDate>
            <description><![CDATA[<h3 id="django-verbose_name">Django <code>verbose_name</code></h3>
<hr>
<h3 id="🔹-verbose_name이란">🔹 <code>verbose_name</code>이란?</h3>
<ul>
<li><code>verbose_name</code>은 Django 모델 필드의 사람이 읽기 쉬운 이름을 정의하는 옵션.</li>
<li>기본적으로 필드명이 <code>snake_case</code>일 경우, Django는 자동으로 <code>_</code>을 공백으로 변환하여 UI에서 표시.</li>
<li>하지만, 보다 직관적인 필드명을 제공하기 위해 직접 <code>verbose_name</code>을 지정하는 것이 일반적.</li>
</ul>
<h4 id="✅-verbose_name의-장점">✅ <code>verbose_name</code>의 장점</h4>
<ol>
<li><strong>관리자(admin) UI 개선</strong>  <ul>
<li>Django Admin에서 필드명을 보기 좋게 표시</li>
</ul>
</li>
<li><strong>Form 및 ModelSerializer에서 명확한 라벨 제공</strong>  <ul>
<li>API 응답, Django Form에서 더 직관적인 필드명 제공</li>
</ul>
</li>
<li><strong>다국어 지원</strong>  <ul>
<li><code>gettext_lazy</code>를 사용하면 다국어(Localization) 처리 가능</li>
</ul>
</li>
</ol>
<h4 id="🔹-예제">🔹 예제</h4>
<pre><code class="language-python">from django.db import models
from django.utils.translation import gettext_lazy as _

class Game(models.Model):
    name = models.CharField(max_length=255, verbose_name=_(&quot;게임 이름&quot;))
    release_date = models.DateField(verbose_name=&quot;출시일&quot;)</code></pre>
<ul>
<li><code>verbose_name=_(&quot;게임 이름&quot;)</code> 을 사용하면 다국어 지원 가능  </li>
<li>Django Admin에서 <code>release_date</code> 필드가 &quot;출시일&quot;로 표시됨</li>
</ul>
<hr>
<h4 id="더보기-"><a href="https://docs.djangoproject.com/en/4.2/ref/models/fields/#verbose-name">더보기 !</a></h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python PEP 8 요약]]></title>
            <link>https://velog.io/@gyu_p/Python-PEP-8-%EC%9A%94%EC%95%BD</link>
            <guid>https://velog.io/@gyu_p/Python-PEP-8-%EC%9A%94%EC%95%BD</guid>
            <pubDate>Wed, 29 Jan 2025 09:17:53 GMT</pubDate>
            <description><![CDATA[<h2 id="python-pep-8-요약">Python PEP 8 요약</h2>
<h4 id="1️⃣-코드-레이아웃">1️⃣ 코드 레이아웃</h4>
<ul>
<li><strong>들여쓰기</strong>: 4칸의 스페이스(space) 사용, 탭(tab) 사용 금지</li>
<li><strong>라인 길이</strong>: 최대 79자 (긴 줄은 <code>\</code> 또는 괄호로 감싸서 줄바꿈)</li>
<li><strong>빈 줄</strong>: 최상위 함수·클래스 앞에 2줄, 메서드 간 1줄</li>
</ul>
<h4 id="2️⃣-임포트-스타일">2️⃣ 임포트 스타일</h4>
<ul>
<li><p><strong>순서</strong>: 표준 라이브러리 → 서드파티 → 로컬 모듈 (각 블록 사이 한 줄 띄움)</p>
</li>
<li><p><strong>형태</strong>:</p>
<pre><code class="language-python">import os
import sys

from django.conf import settings
from my_project.module import MyClass</code></pre>
</li>
<li><p><strong>와일드카드 임포트 금지</strong>: <code>from module import *</code>는 지양</p>
</li>
</ul>
<h4 id="3️⃣-네이밍-컨벤션">3️⃣ 네이밍 컨벤션</h4>
<ul>
<li><strong>함수, 변수, 속성</strong>: <code>snake_case</code> (소문자+언더스코어)</li>
<li><strong>클래스</strong>: <code>PascalCase</code> (대문자로 시작)</li>
<li><strong>상수</strong>: <code>ALL_CAPS</code> (대문자+언더스코어)</li>
<li><strong>비공개</strong>: <code>_private_variable</code> (접두어 <code>_</code> 사용)</li>
</ul>
<h4 id="4️⃣-공백-사용-규칙">4️⃣ 공백 사용 규칙</h4>
<ul>
<li><strong>괄호 안 공백</strong>: <code>spam(ham[1], {eggs: 2})</code> ✅ (<code>spam( ham[1] , { eggs: 2 } )</code> ❌)</li>
<li><strong>쉼표 후 공백</strong>: <code>a, b = 1, 2</code> ✅ (<code>a,b=1,2</code> ❌)</li>
<li><strong>연산자 주위 공백</strong>: <code>x = a + b</code> ✅ (<code>x=a+b</code> ❌)</li>
</ul>
<h4 id="5️⃣-문서화-docstrings">5️⃣ 문서화 (Docstrings)</h4>
<ul>
<li><strong>모든 모듈, 클래스, 함수에 docstring 추가</strong></li>
<li><strong>예시</strong><pre><code class="language-python">def add(x, y):
    &quot;&quot;&quot;두 수의 합을 반환합니다.&quot;&quot;&quot;
    return x + y</code></pre>
</li>
</ul>
<h4 id="6️⃣-조건문-루프">6️⃣ 조건문, 루프</h4>
<ul>
<li><strong>한 줄 if문 지양</strong><pre><code class="language-python">if x == 42:
    print(&quot;정답입니다.&quot;)  # ✅</code></pre>
</li>
<li><strong>불필요한 비교 연산 지양</strong><pre><code class="language-python">if my_list:  # ✅ 빈 리스트 체크 (`if len(my_list) &gt; 0:` ❌)</code></pre>
</li>
</ul>
<h4 id="7️⃣-예외-처리">7️⃣ 예외 처리</h4>
<ul>
<li><code>try-except</code> 사용 시 특정 예외를 잡도록 권장<pre><code class="language-python">try:
    value = my_dict[&quot;key&quot;]
except KeyError:
    value = &quot;default&quot;</code></pre>
</li>
</ul>
<p><strong>🔗 더 보기</strong>: <a href="https://peps.python.org/pep-0008/">PEP 8 공식 문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트 리팩토링 피드백]]></title>
            <link>https://velog.io/@gyu_p/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%ED%94%BC%EB%93%9C%EB%B0%B1</link>
            <guid>https://velog.io/@gyu_p/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%ED%94%BC%EB%93%9C%EB%B0%B1</guid>
            <pubDate>Mon, 27 Jan 2025 10:39:19 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-리팩토링-피드백">프로젝트 리팩토링 피드백</h2>
<h3 id="1-사용하지-않는-파일-삭제">1. <strong>사용하지 않는 파일 삭제</strong></h3>
<ul>
<li>프로젝트에 사용하지 않는 파일이 깃허브에 업로드되어 있어 관리 효율성이 떨어짐</li>
<li><strong>조치:</strong> 사용하지 않는 파일과 코드를 확인 후 삭제</li>
</ul>
<hr>
<h3 id="2-모델-필드-설명-방식-개선">2. <strong>모델 필드 설명 방식 개선</strong></h3>
<ul>
<li>모델의 주석 대신 <code>verbose_name</code> 속성을 사용하여 각 필드의 역할을 명확히 설명하는 Django 권장 방식을 따름</li>
<li><code>verbose_name</code>은 모델 필드에 대한 가독성을 높이고 관리자를 포함한 여러 UI에서 더 직관적인 필드명을 제공</li>
</ul>
<hr>
<h3 id="3-view를-가볍게-유지하기-위한-리팩토링">3. <strong>View를 가볍게 유지하기 위한 리팩토링</strong></h3>
<ul>
<li>&quot;Fat Model, Thin View&quot; 원칙을 적용하여 뷰를 최대한 단순화</li>
<li><strong>조치 방향:</strong><ul>
<li>복잡한 비즈니스 로직을 모델의 메서드 또는 별도의 유틸리티 함수로 이동</li>
<li>예: <code>ReviewAPIView</code>에서 <code>annotate</code>와 관련된 정렬 로직을 모델 메서드로 추출
<img src="https://velog.velcdn.com/images/gyu_p/post/d1f2942c-1c87-4664-99d2-bc40b8936fc2/image.png" alt=""></li>
</ul>
</li>
</ul>
<hr>
<h3 id="4-db-성능-최적화를-위한-db_index-활용">4. <strong>DB 성능 최적화를 위한 <code>db_index</code> 활용</strong></h3>
<ul>
<li>검색 및 필터링이 자주 사용되는 필드에 <code>db_index=True</code>를 설정</li>
<li><strong>예시:</strong><ul>
<li><code>Review</code> 모델의 <code>app_id</code> 필드</li>
<li><code>Game</code> 모델의 <code>name</code> 필드</li>
</ul>
</li>
</ul>
<hr>
<h3 id="5-django-서브쿼리-활용">5. <strong>Django 서브쿼리 활용</strong></h3>
<ul>
<li>View에서 반복적으로 호출되는 쿼리를 최적화하기 위해 Django ORM의 서브쿼리를 사용</li>
<li><strong>적용 대상:</strong><ul>
<li><code>GameDetailAPIView</code>에서 <code>my_review</code> 필드 로직</li>
</ul>
</li>
</ul>
<hr>
<h3 id="6-pep8파이썬-스타일-가이드-준수">6. <strong>PEP8(파이썬 스타일 가이드) 준수</strong></h3>
<ul>
<li><code>import</code> 순서를 정리하고, 그룹별로 나누어 가독성을 향상</li>
<li><strong>조치:</strong><ul>
<li>표준 라이브러리, 서드파티 라이브러리, 로컬 앱 순서로 구분</li>
</ul>
</li>
</ul>
<hr>
<h3 id="7-에러-메시지-커스터마이징">7. <strong>에러 메시지 커스터마이징</strong></h3>
<ul>
<li><a href="https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling">에러 메시지 커스터마이징</a></li>
<li><strong>API의 에러 응답(error response) 스타일을 내맘대로 핸들링</strong></li>
</ul>
<hr>
<h3 id="8-소프트-삭제-도입">8. <strong>소프트 삭제 도입</strong></h3>
<ul>
<li>데이터 삭제 시 실제로 데이터베이스에서 제거하지 않고, Boolean 필드를 사용하여 삭제 상태를 관리</li>
<li><strong>적용 이유:</strong><ul>
<li>삭제된 데이터를 복구하거나 로그를 추적할 수 있도록 보장</li>
</ul>
</li>
<li><strong>구현 방향:</strong><ul>
<li><code>is_deleted = models.BooleanField(default=False)</code> 필드를 추가하고, 쿼리셋에서 기본적으로 필터링</li>
</ul>
</li>
</ul>
<hr>
<hr>
<h4 id="참고-자료">참고 자료</h4>
<ul>
<li><a href="https://docs.djangoproject.com/en/4.2/ref/models/fields/#verbose-name">Django 문서: verbose_name</a></li>
<li><a href="https://docs.djangoproject.com/en/4.2/topics/db/optimization/">Django 문서: 데이터베이스 최적화</a></li>
<li><a href="https://peps.python.org/pep-0008/">PEP 8 스타일 가이드</a></li>
<li><a href="https://resilient-923.tistory.com/419">소프트 삭제 vs 하드 삭제</a></li>
<li><a href="https://eunjin3786.tistory.com/270">에러응답 커스터마이징</a></li>
<li><a href="https://www.softkraft.co/django-best-practises/">Fat model 추천</a></li>
<li><a href="https://docs.djangoproject.com/en/5.1/ref/models/options/">모델 리펙토리시 참고: Meta</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[steam_data 로직 수정]]></title>
            <link>https://velog.io/@gyu_p/steamdata-%EB%A1%9C%EC%A7%81-%EC%88%98%EC%A0%95</link>
            <guid>https://velog.io/@gyu_p/steamdata-%EB%A1%9C%EC%A7%81-%EC%88%98%EC%A0%95</guid>
            <pubDate>Sun, 26 Jan 2025 05:54:51 GMT</pubDate>
            <description><![CDATA[<p>기존에 Selenium을 사용하던 부분을 전부 제거하고, <code>requests</code>와 <code>BeautifulSoup</code>로 대체했다.
<code>steamId</code>가 <strong>항상 64비트</strong>라고 가정하여, 커스텀 URL 변환(<code>resolve_vanity_url</code>)이나 <code>steam_id_str.isdigit()</code> 같은 로직도 모두 뺐다.<br>또한 -게임 목록 공개 여부(<code>check_owned_games_public</code>)- 검사도 제거했다.</p>
<h2 id="변경-코드">변경 코드</h2>
<pre><code class="language-python">import environ
import requests
from django.core.management.base import BaseCommand
from django.db import transaction
from accounts.models import (
    Account,
    SteamProfile,
    SteamReview,
    SteamPlaytime,
)

from bs4 import BeautifulSoup


class Command(BaseCommand):
    help = &quot;Steam 프로필/리뷰/플레이타임 정보를 가져와 DB에 반영&quot;

    def handle(self, *args, **options):
        # 1) 환경 변수에서 API 키 불러오기
        env = environ.Env()
        api_key = env(&quot;STEAM_API_KEY&quot;)  # .env에 STEAM_API_KEY=... 설정

        # 2) steamId가 비어있지 않은 Account를 모두 조회
        accounts = Account.objects.exclude(steamId=&quot;&quot;)

        for account in accounts:
            steam_id_str = account.steamId.strip()

            # 3) 전체 프로필 공개 여부 API로 확인
            visibility_public = self.check_profile_public(api_key, steam_id_str)

            # 일단 기본값 False
            is_review = False
            is_playtime = False

            if visibility_public:
                # (A) 리뷰 크롤링 (최대 3개) - BeautifulSoup 이용
                review_data = self.fetch_top3_reviews(steam_id_str)
                if review_data:
                    is_review = True

                # (B) API로 플레이타임 조회 (상위 2개)
                playtime_data = self.fetch_top2_playtime_api(api_key, steam_id_str)
                if playtime_data:
                    is_playtime = True

                # (C) DB 저장
                with transaction.atomic():
                    sp, _ = SteamProfile.objects.get_or_create(account=account)
                    sp.is_review = is_review
                    sp.is_playtime = is_playtime
                    sp.save()

                    # 기존 리뷰/플레이타임 삭제 후 재생성 (중복 방지)
                    SteamReview.objects.filter(account=account).delete()
                    SteamPlaytime.objects.filter(account=account).delete()

                    if is_review:
                        for rd in review_data:
                            SteamReview.objects.create(
                                account=account,
                                app_id=rd[&quot;app_id&quot;]
                            )
                    if is_playtime:
                        for pd in playtime_data:
                            SteamPlaytime.objects.create(
                                account=account,
                                app_id=pd[&quot;app_id&quot;]
                            )
            else:
                # 프로필이 Private -&gt; is_review=False, is_playtime=False
                with transaction.atomic():
                    sp, _ = SteamProfile.objects.get_or_create(account=account)
                    sp.is_review = False
                    sp.is_playtime = False
                    sp.save()

                    # 기존 리뷰/플레이타임 삭제
                    SteamReview.objects.filter(account=account).delete()
                    SteamPlaytime.objects.filter(account=account).delete()

        self.stdout.write(self.style.SUCCESS(&quot;Steam 데이터 처리 완료.&quot;))

    # -------------------------------------------------------
    # 1) 스팀 프로필이 Public인지 판별 (communityvisibilitystate == 3)
    # -------------------------------------------------------
    def check_profile_public(self, api_key, steam_id_str):
        &quot;&quot;&quot;
        64비트 steam_id_str이 공개 상태인지 확인.
        &quot;&quot;&quot;
        url = &quot;https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/&quot;
        params = {&quot;key&quot;: api_key, &quot;steamids&quot;: steam_id_str}
        try:
            resp = requests.get(url, params=params).json()
        except Exception as e:
            print(f&quot;Error checking profile public: {e}&quot;)
            return False

        players = resp.get(&quot;response&quot;, {}).get(&quot;players&quot;, [])
        if not players:
            return False

        player = players[0]
        vis_state = player.get(&quot;communityvisibilitystate&quot;, 1)
        return (vis_state == 3)

    # -------------------------------------------------------
    # 2) 리뷰 페이지 크롤링 (상위 3개 &#39;Recommended&#39;)
    # -------------------------------------------------------
    def fetch_top3_reviews(self, steam_id_str):
        &quot;&quot;&quot;
        Selenium 없이 HTML만 파싱하여 상위 3개의 추천 리뷰(app_id) 가져오기.
        &quot;&quot;&quot;
        url = f&quot;https://steamcommunity.com/profiles/{steam_id_str}/recommended&quot;
        headers = {
            &quot;User-Agent&quot;: (
                &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) &quot;
                &quot;AppleWebKit/537.36 (KHTML, like Gecko) &quot;
                &quot;Chrome/87.0.4280.66 Safari/537.36&quot;
            )
        }

        try:
            response = requests.get(url, headers=headers, timeout=10)
        except Exception as e:
            print(f&quot;Error fetching reviews page: {e}&quot;)
            return []

        # 상태 코드 확인 (비공개거나 오류 발생 시)
        if response.status_code != 200:
            return []

        soup = BeautifulSoup(response.text, &quot;html.parser&quot;)
        review_boxes = soup.select(&quot;.review_box&quot;)

        recommended = []
        for box in review_boxes:
            try:
                # &#39;추천&#39; 리뷰만 필터링
                title_elem = box.select_one(&quot;.vote_header .title &gt; a&quot;)
                if not title_elem or &quot;Recommended&quot; not in title_elem.text:
                    continue

                href = title_elem.get(&quot;href&quot;, &quot;&quot;)
                if &quot;/recommended/&quot; not in href:
                    continue

                app_id = href.split(&quot;/recommended/&quot;)[1].split(&quot;/&quot;)[0]
                recommended.append({&quot;app_id&quot;: app_id})

                if len(recommended) &gt;= 3:
                    break
            except Exception as e:
                print(f&quot;리뷰 박스 처리 중 오류: {e}&quot;)
                continue

        return recommended

    # -------------------------------------------------------
    # 3) 플레이 타임 조회 (상위 2개)
    # -------------------------------------------------------
    def fetch_top2_playtime_api(self, api_key, steam_id_str):
        &quot;&quot;&quot;
        GetOwnedGames API로 플레이타임이 가장 많은 상위 2개 게임(app_id) 반환.
        &quot;&quot;&quot;
        url = &quot;https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/&quot;
        params = {
            &quot;key&quot;: api_key,
            &quot;steamid&quot;: steam_id_str,
        }
        try:
            resp = requests.get(url, params=params).json()
        except Exception as e:
            print(&quot;Error fetching owned games:&quot;, e)
            return []

        games = resp.get(&quot;response&quot;, {}).get(&quot;games&quot;, [])
        if not games:
            return []

        game_data = []
        for g in games:
            app_id = g.get(&quot;appid&quot;)
            pt_min = g.get(&quot;playtime_forever&quot;, 0)
            pt_hr = round(pt_min / 60.0, 2)  # 시간 단위 환산 (소수점 둘째 자리)
            game_data.append({&quot;app_id&quot;: str(app_id), &quot;playtime&quot;: pt_hr})

        # 플레이타임 내림차순 정렬 후 상위 2개
        sorted_data = sorted(game_data, key=lambda x: x[&quot;playtime&quot;], reverse=True)
        return sorted_data[:2]</code></pre>
<hr>
<h2 id="주요-변경-사항">주요 변경 사항</h2>
<ol>
<li><p><strong>Selenium 전면 제거</strong>  </p>
<ul>
<li><code>webdriver.ChromeOptions()</code>, <code>driver.get()</code>, <code>WebDriverWait</code> 등 모든 관련 코드를 삭제했다.</li>
<li>헤드리스 크롬을 구동하지 않으므로 CPU 부담이 크게 줄어든다.</li>
</ul>
</li>
<li><p><strong>리뷰 크롤링 로직(<code>fetch_top3_reviews</code>)</strong>  </p>
<ul>
<li><code>requests</code>와 <code>BeautifulSoup</code>만 사용한다.  </li>
<li>받은 HTML에서 <code>.review_box</code>를 찾아 “Recommended” 텍스트가 있는 리뷰만 걸러내고, 최대 3개까지 <code>app_id</code>를 추출한다.</li>
</ul>
</li>
<li><p><strong>커스텀 URL 변환 로직 제거</strong>  </p>
<ul>
<li><code>steam_id_str</code>가 항상 64비트 ID라고 가정하므로 <code>resolve_vanity_url</code> 함수는 완전히 삭제했다.</li>
<li><code>check_profile_public</code> 등에서 <code>steam_id_str.isdigit()</code> 관련 분기나 <code>vanityurl</code> 호출도 제거</li>
</ul>
</li>
<li><p><strong>게임 목록 공개 여부(<code>check_owned_games_public</code>) 제거</strong>  </p>
<ul>
<li>필요 없다고 했으므로 해당 함수를 삭제했다.</li>
</ul>
</li>
<li><p><strong>DB 저장 로직 동일</strong>  </p>
<ul>
<li><code>SteamProfile</code>의 <code>is_review</code>, <code>is_playtime</code> 여부를 갱신하고  </li>
<li>기존 <code>SteamReview</code>, <code>SteamPlaytime</code> 전부 삭제 후 새로 생성한다(중복 방지)</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블 슈팅]셀레니움 방식 크롤링 수정]]></title>
            <link>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EC%85%80%EB%A0%88%EB%8B%88%EC%9B%80-%EB%B0%A9%EC%8B%9D-%ED%81%AC%EB%A1%A4%EB%A7%81-%EC%88%98%EC%A0%95</link>
            <guid>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EC%85%80%EB%A0%88%EB%8B%88%EC%9B%80-%EB%B0%A9%EC%8B%9D-%ED%81%AC%EB%A1%A4%EB%A7%81-%EC%88%98%EC%A0%95</guid>
            <pubDate>Sat, 25 Jan 2025 13:50:26 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제">1. 문제</h2>
<ul>
<li>AWS 서버에서 Selenium을 이용해 리뷰를 크롤링하는 코드를 실행했더니 CPU 사용량이 100%에 근접하는 현상이 발생하였다.  </li>
<li>EC2 같이 스펙이 낮은 서버에서 헤드리스 크롬(Chromedriver)을 구동할 경우, 브라우저 엔진이 동작하면서 리소스 사용량이 크게 늘어날 수 있다.  </li>
</ul>
<hr>
<h2 id="2-문제-원인">2. 문제 원인</h2>
<ol>
<li><strong>Selenium 자체의 무거운 동작 방식</strong>  <ul>
<li>Selenium은 실제 브라우저(크롬 등)를 띄워서 페이지 로딩, JavaScript 실행 등을 처리한다.  </li>
<li>이 과정에서 CPU와 메모리를 많이 사용하게 된다.</li>
</ul>
</li>
<li><strong>EC2 인스턴스 환경</strong>  <ul>
<li>일반적으로 작은 EC2 인스턴스는 CPU 자원이 제한적이라, 브라우저를 다수 실행하거나 연속 크롤링 시 CPU 사용량이 급증하기 쉽다</li>
</ul>
</li>
<li><strong>페이지 내용이 이미 HTML에 포함</strong>  <ul>
<li>Steam의 “Recommended” 리뷰는 자바스크립트 없이도 정적 HTML로 되어 있는 경우가 많다</li>
<li>즉, 굳이 실제 브라우저로 렌더링하지 않아도 <code>requests</code> + <code>BeautifulSoup</code> 같은 간단한 방식으로 충분히 파싱 가능하다</li>
</ul>
</li>
</ol>
<hr>
<h2 id="3-해결-방안">3. 해결 방안</h2>
<h3 id="31-selenium-제거-및-beautifulsoup으로-전환">3.1. Selenium 제거 및 BeautifulSoup으로 전환</h3>
<ul>
<li><strong>requests</strong>와 <strong>BeautifulSoup</strong>만 사용해 HTML을 가져오고, 필요한 요소를 파싱한다.</li>
<li>브라우저 엔진을 구동하지 않으므로 CPU 사용량이 매우 낮아진다.</li>
</ul>
<h4 id="수정-전-핵심-흐름">수정 전 핵심 흐름</h4>
<ol>
<li><code>webdriver.Chrome</code>으로 헤드리스 브라우저 실행  </li>
<li><code>driver.get(url)</code>로 페이지 열기  </li>
<li>필요한 셀렉터(<code>.review_box</code>)로 요소 찾기  </li>
<li><code>driver.quit()</code>로 브라우저 종료  </li>
</ol>
<h4 id="수정-후-핵심-흐름">수정 후 핵심 흐름</h4>
<ol>
<li><code>requests.get(url)</code>로 HTML을 텍스트 형태로 받아오기  </li>
<li><code>BeautifulSoup(html, &quot;html.parser&quot;)</code>로 DOM 파싱  </li>
<li><code>.select(&quot;.review_box&quot;)</code> 등 원하는 요소에 접근  </li>
<li>HTML 내 리뷰가 보이는지 확인 후 <code>app_id</code> 등을 추출  </li>
</ol>
<h3 id="32-코드">3.2. 코드</h3>
<pre><code class="language-python">import requests
from bs4 import BeautifulSoup

def fetch_top3_reviews(steam_id_str):
    url = f&quot;https://steamcommunity.com/profiles/{steam_id_str}/recommended&quot;
    headers = {
        &quot;User-Agent&quot;: (
            &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) &quot;
            &quot;AppleWebKit/537.36 (KHTML, like Gecko) &quot;
            &quot;Chrome/87.0.4280.66 Safari/537.36&quot;
        )
    }

    # HTML 가져오기
    response = requests.get(url, headers=headers, timeout=10)
    if response.status_code != 200:
        return []

    # 파싱
    soup = BeautifulSoup(response.text, &quot;html.parser&quot;)
    boxes = soup.select(&quot;.review_box&quot;)

    recommended = []
    for box in boxes:
        try:
            title_elem = box.select_one(&quot;.vote_header .title &gt; a&quot;)
            if not title_elem or &quot;Recommended&quot; not in title_elem.text:
                continue

            href = title_elem.get(&quot;href&quot;, &quot;&quot;)
            if &quot;/recommended/&quot; not in href:
                continue

            app_id = href.split(&quot;/recommended/&quot;)[1].split(&quot;/&quot;)[0]
            recommended.append({&quot;app_id&quot;: app_id})

            if len(recommended) &gt;= 3:
                break
        except Exception as e:
            # 에러 발생 시 로그 남기고 다음 박스로 진행
            print(f&quot;리뷰 박스 처리 중 오류: {e}&quot;)
            continue

    return recommended</code></pre>
<ul>
<li>기존에 <code>WebDriverWait</code>, <code>driver.find_elements</code> 등을 사용하던 부분이 사라졌고, 대신 <code>BeautifulSoup</code>을 통한 단순 HTML 파싱으로 변경되었다.</li>
<li>최대 3개까지만 리뷰 데이터를 가져오며, 각 리뷰에서 <code>app_id</code>를 추출한다.</li>
</ul>
<h3 id="33-적용-효과">3.3. 적용 효과</h3>
<ul>
<li><strong>CPU 사용률 감소</strong>: 브라우저 엔진을 구동하지 않으므로 EC2에서 CPU 부하가 크게 줄어든다.</li>
</ul>
<hr>
<h2 id="4-트러블슈팅-요약">4. 트러블슈팅 요약</h2>
<ul>
<li><strong>문제</strong>: Selenium으로 인한 브라우저 구동 → CPU 사용량 100% 근접  </li>
<li><strong>원인</strong>: 실제 브라우저 엔진이 동작하면서 리소스 사용량 급증  </li>
<li><strong>해결</strong>: 정적 HTML 파싱 가능하므로 <code>requests</code> + <code>BeautifulSoup</code> 도입  </li>
<li><strong>결과</strong>: CPU 부하 감소, 코딩 구조 단순화</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[배포단계 문제]]></title>
            <link>https://velog.io/@gyu_p/%EB%B0%B0%ED%8F%AC%EB%8B%A8%EA%B3%84-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@gyu_p/%EB%B0%B0%ED%8F%AC%EB%8B%A8%EA%B3%84-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Fri, 24 Jan 2025 10:04:01 GMT</pubDate>
            <description><![CDATA[<p>팀원이 작성한 트러블 문서를 정리했습니다.</p>
<h3 id="1-스팀-계정-연동-시-서버-cpu-사용률-증가-트러블">1. 스팀 계정 연동 시 서버 CPU 사용률 증가 트러블</h3>
<p><img src="https://velog.velcdn.com/images/gyu_p/post/64639ba7-7319-4003-8779-a5e0d01256fc/image.png" alt=""></p>
<p><strong>문제 상황:</strong></p>
<ul>
<li><strong><code>Selenium</code>을 사용한</strong> 크롤링 과정에서 <strong>헤드리스</strong> 브라우저가 예상보다 많은 메모리를 사용했습니다.</li>
<li>동시 접속 시 <strong>CPU 사용률</strong>이 100%까지 치솟아 서버가 불안정했습니다.</li>
</ul>
<p><strong>해결 방법:</strong></p>
<ol>
<li><strong>RAM 스왑 설정</strong>:<pre><code> - 하드디스크의 일부를 RAM처럼 사용하도록 스왑 파일을 **생성 및 활성화**했습니다.
 - 2GB 크기의 스왑 파일로 메모리 부족 문제를 **완화**했습니다.</code></pre></li>
</ol>
<pre><code class="language-python">    # 스왑 파일 생성
    sudo dd if=/dev/zero of=/swapfile bs=1M count=2048

    # 스왑 파일 권한 설정
    sudo chmod 600 /swapfile

    # 스왑 영역 설정
    sudo mkswap /swapfile

    # 스왑 활성화
    sudo swapon /swapfile</code></pre>
<h3 id="2-로컬에서-프로덕션-서버로-데이터베이스-마이그레이션-실패">2. 로컬에서 프로덕션 서버로 데이터베이스 마이그레이션 실패</h3>
<p><strong>문제 상황:</strong></p>
<ul>
<li>Django 기본 마이그레이션 도구로 데이터 이전 중 PostgreSQL 버전 차이와 외래 키 제약 조건 문제가 발생했습니다.</li>
<li>복잡한 데이터 구조로 인해 마이그레이션 도중 데이터 손실이 발생했습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/gyu_p/post/ce0f3c64-2085-423e-aea5-4bfd9058ecf7/image.png" alt=""></p>
<p><strong>해결 방법:</strong></p>
<ul>
<li><strong>DBeaver 도구 활용</strong>:</li>
<li>로컬 DB 데이터를 CSV 파일로 추출 후, 프로덕션 서버에 직접 임포트했습니다.</li>
<li>복잡한 외래 키 구조를 임시로 비활성화 후, 데이터 로드 완료 시 다시 활성화를 했습니다.</li>
</ul>
<h3 id="3-환경-변수-관리-및-cors-문제">3. 환경 변수 관리 및 CORS 문제</h3>
<p><strong>문제 상황:</strong></p>
<ul>
<li><code>.env</code> 환경 변수 관리가 배포 환경에서 꼬이는 문제가 발생했습니다.</li>
<li>API 통신 중 CORS 에러가 빈번하게 발생하며, 프론트엔드와의 연결이 실패했습니다.</li>
</ul>
<p><strong>해결 방법:</strong></p>
<ol>
<li><strong>Django CORS 설정</strong>:</li>
</ol>
<ul>
<li><code>CORS_ALLOWED_ORIGINS</code>에 개발 및 배포 환경의 도메인 추가:</li>
</ul>
<pre><code class="language-python">    CORS_ALLOWED_ORIGINS = [
      &quot;http://ai-watson.com&quot;,
      &quot;http://52.78.197.80:8000&quot;,  # AWS IP 추가
    &quot;http://127.0.0.1:5173&quot;,  # localhost에서 발생하는 요청도 허용&quot;
    ]</code></pre>
<ol start="2">
<li><strong>환경 변수 관리 개선</strong></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[페이지네이션 구현과 코드 리팩토링]]></title>
            <link>https://velog.io/@gyu_p/%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84%EA%B3%BC-%EC%BD%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</link>
            <guid>https://velog.io/@gyu_p/%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84%EA%B3%BC-%EC%BD%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</guid>
            <pubDate>Thu, 23 Jan 2025 09:50:46 GMT</pubDate>
            <description><![CDATA[<p>기존 코드에서 페이지네이션이 없었던 <code>ReviewSearchAPIView</code>를 개선하여, 페이지네이션 기능을 추가하고, 클라이언트가 요청한 페이지에 따라 데이터를 효율적으로 반환하도록 수정했습니다.</p>
<hr>
<h3 id="1-기존-코드의-문제점">1. 기존 코드의 문제점</h3>
<ul>
<li><strong>모든 데이터 반환</strong>: 검색 결과를 한 번에 모두 반환하여, 결과가 많아질 경우 성능 문제가 발생할 가능성이 있었습니다.</li>
<li><strong>페이징 정보 부족</strong>: 클라이언트가 데이터를 어떻게 나눠서 보여줄지 판단할 수 있는 정보가 부족했습니다.<ul>
<li>다음 페이지가 존재하는지(<code>has_next</code>) 여부나 현재 페이지 번호(<code>current_page</code>) 같은 메타데이터가 없었습니다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="2-개선된-코드의-핵심-변경-사항">2. 개선된 코드의 핵심 변경 사항</h3>
<h4 id="a-django-paginator-도입">a. <strong>Django Paginator 도입</strong></h4>
<ul>
<li>Django의 <code>Paginator</code>를 사용해 페이지네이션을 간단하고 효율적으로 구현.</li>
<li><code>page</code>와 <code>limit</code>를 기반으로 특정 페이지의 데이터를 반환.</li>
</ul>
<h4 id="b-메타데이터-추가">b. <strong>메타데이터 추가</strong></h4>
<ul>
<li>클라이언트가 페이지를 탐색하기 쉽게 응답에 다음 정보를 추가:<ul>
<li><strong><code>current_page</code></strong>: 현재 페이지 번호</li>
<li><strong><code>has_next</code></strong>: 다음 페이지 존재 여부</li>
</ul>
</li>
</ul>
<h4 id="c-불필요한-예외-처리-제거">c. <strong>불필요한 예외 처리 제거</strong></h4>
<ul>
<li><code>EmptyPage</code> 예외 처리를 제거하고, <code>Paginator</code>의 기본 동작으로 페이지 요청을 처리</li>
<li>검색 결과가 없을 경우 <code>404 Not Found</code>를 반환하도록 간결하게 수정</li>
</ul>
<hr>
<h3 id="3-개선된-코드-구조">3. 개선된 코드 구조</h3>
<pre><code class="language-python">from django.core.paginator import Paginator

class ReviewSearchAPIView(APIView):
    &quot;&quot;&quot;
    리뷰 검색 API
    &quot;&quot;&quot;

    def get(self, request):
        &quot;&quot;&quot;리뷰 검색&quot;&quot;&quot;
        keyword = request.query_params.get(&quot;keyword&quot;, &quot;&quot;).strip()
        if not keyword:
            return Response(
                {&quot;detail&quot;: &quot;검색어를 입력해주세요.&quot;}, status=status.HTTP_400_BAD_REQUEST
            )

        # 검색 결과 필터링
        game_ids = Game.objects.filter(name__icontains=keyword).values_list(
            &quot;appID&quot;, flat=True
        )
        reviews = Review.objects.filter(
            Q(content__icontains=keyword)
            | Q(categories__icontains=keyword)
            | Q(app_id__in=game_ids)
        ).distinct()

        if not reviews.exists():
            return Response(
                {&quot;detail&quot;: &quot;검색 결과가 없습니다.&quot;}, status=status.HTTP_404_NOT_FOUND
            )

        # 페이지네이션 처리
        page = int(request.query_params.get(&quot;page&quot;, 1))
        paginator = Paginator(reviews, 10)
        page_obj = paginator.page(page)

        # 응답 생성
        serializer = ReviewSerializer(page_obj, many=True)
        return Response(
            {
                &quot;reviews&quot;: serializer.data,
                &quot;has_next&quot;: page_obj.has_next(),
                &quot;current_page&quot;: page_obj.number,
            },
            status=status.HTTP_200_OK,
        )</code></pre>
<hr>
<h3 id="4-응답-구조-변화">4. 응답 구조 변화</h3>
<h4 id="기존-응답">기존 응답</h4>
<p>모든 데이터를 한꺼번에 반환:</p>
<pre><code class="language-json">[
    {
        &quot;id&quot;: 1,
        &quot;content&quot;: &quot;리뷰 내용 1&quot;,
        &quot;categories&quot;: &quot;액션&quot;,
        &quot;app_id&quot;: 12345
    },
    ...
]</code></pre>
<h4 id="개선된-응답">개선된 응답</h4>
<p>페이지네이션 메타데이터를 포함:</p>
<pre><code class="language-json">{
    &quot;reviews&quot;: [
        {
            &quot;id&quot;: 1,
            &quot;content&quot;: &quot;리뷰 내용 1&quot;,
            &quot;categories&quot;: &quot;액션&quot;,
            &quot;app_id&quot;: 12345
        },
        ...
    ],
    &quot;has_next&quot;: true,
    &quot;current_page&quot;: 1
}</code></pre>
<hr>
<h3 id="5-장점">5. 장점</h3>
<ul>
<li><strong>효율적인 데이터 로드</strong>: 요청마다 필요한 데이터만 반환하여 서버 부담을 줄이고 클라이언트의 로딩 시간을 단축</li>
<li><strong>명확한 탐색 가능</strong>: 페이지 정보(<code>current_page</code>, <code>has_next</code>)를 제공하여 클라이언트가 다음 페이지 여부를 쉽게 판단</li>
<li><strong>데이터 과다로 인한 성능 문제 방지</strong>: 대량의 데이터를 한꺼번에 보내는 일을 방지하여 서버와 클라이언트의 성능 최적화</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블 슈팅] 게임 상세 페이지에서 클릭한 리뷰 강조]]></title>
            <link>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EA%B2%8C%EC%9E%84-%EC%83%81%EC%84%B8-%ED%8E%98%EC%9D%B4%EC%A7%80%EC%97%90%EC%84%9C-%ED%81%B4%EB%A6%AD%ED%95%9C-%EB%A6%AC%EB%B7%B0-%EA%B0%95%EC%A1%B0</link>
            <guid>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EA%B2%8C%EC%9E%84-%EC%83%81%EC%84%B8-%ED%8E%98%EC%9D%B4%EC%A7%80%EC%97%90%EC%84%9C-%ED%81%B4%EB%A6%AD%ED%95%9C-%EB%A6%AC%EB%B7%B0-%EA%B0%95%EC%A1%B0</guid>
            <pubDate>Tue, 21 Jan 2025 06:26:32 GMT</pubDate>
            <description><![CDATA[<h3 id="문제">문제</h3>
<ul>
<li><strong>요구사항</strong>: 리뷰 목록에서 특정 리뷰를 클릭했을 때, 게임 상세 페이지에서 해당 리뷰를 눈에 띄게 표시하고 나머지 리뷰와 구분하여 관리하고 싶었습니다.</li>
<li><strong>현상</strong>: 기존 API 구조가 이를 지원하지 않았습니다. <code>review_id</code>를 통한 클릭한 리뷰의 강조가 필요하다고 생각했습니다.</li>
</ul>
<hr>
<h3 id="해결">해결</h3>
<ol>
<li><p><strong>API 뷰 수정</strong></p>
<ul>
<li>기존 뷰를 수정하여 <code>review_id</code>를 쿼리 파라미터로 받아 해당 리뷰를 강조(<code>clicked_review</code>)로 분리하여 동작하고자 설계했습니다.</li>
<li>나머지 리뷰 목록에서 클릭한 리뷰를 제외하여 중복을 방지했습니다.</li>
</ul>
</li>
<li><p><strong>URL 수정</strong></p>
<ul>
<li>URL 패턴에서 <code>game_id</code>를 쿼리 파라미터로 처리하도록 변경했습니다.</li>
<li>쿼리 형식: <code>/api/game/?game_id={game_id}&amp;review_id={review_id}</code></li>
</ul>
</li>
</ol>
<hr>
<h3 id="기존-코드">기존 코드</h3>
<pre><code class="language-python">
class GameDetailAPIView(APIView):
    &quot;&quot;&quot;
    Game 상세 페이지 API
    &quot;&quot;&quot;
    permission_classes = [AllowAny]

    def get(self, request, app_id):
        # Game 객체 가져오기
        game = get_object_or_404(Game, appID=app_id)

        # 리뷰 가져오기
        reviews = Review.objects.filter(app_id=app_id)
        my_review = None  # 기본값 설정

        # 사용자가 인증된 경우에만 자신의 리뷰 필터링
        if request.user.is_authenticated:
            my_review = reviews.filter(user_id=request.user.id).first()  # user_id로 매칭
            reviews = reviews.exclude(user_id=request.user.id)

        # 직렬화
        game_serializer = GameSerializer(game)
        my_review_serializer = ReviewSerializer(my_review) if my_review else None
        other_reviews_serializer = ReviewSerializer(reviews, many=True)

        return Response({
            &quot;game&quot;: game_serializer.data,
            &quot;my_review&quot;: my_review_serializer.data if my_review else None,
            &quot;reviews&quot;: other_reviews_serializer.data
        })</code></pre>
<h3 id="수정된-코드">수정된 코드</h3>
<h4 id="1-api-뷰-viewspy">1. API 뷰 (<code>views.py</code>)</h4>
<pre><code class="language-python">class GameDetailAPIView(APIView):
    &quot;&quot;&quot;
    Game 상세 페이지 API
    &quot;&quot;&quot;

    permission_classes = [AllowAny]

    def get(self, request):
        # 쿼리 파라미터에서 game_id와 review_id 가져오기
        game_id = request.query_params.get(&quot;game_id&quot;)
        review_id = request.query_params.get(&quot;review_id&quot;)

        # game_id가 없을 경우 에러 반환
        if not game_id:
            return Response({&quot;error&quot;: &quot;game_id is required.&quot;}, status=400)

        # Game 객체 가져오기
        game = get_object_or_404(Game, appID=game_id)

        # 리뷰 가져오기
        reviews = Review.objects.filter(app_id=game_id)
        my_review = None
        clicked_review = None

        # 사용자가 인증된 경우, 자신의 리뷰 필터링
        if request.user.is_authenticated:
            my_review = reviews.filter(user_id=request.user.id).first()
            reviews = reviews.exclude(user_id=request.user.id)

        # review_id가 있는 경우, 해당 리뷰를 clicked_review로 설정
        if review_id:
            try:
                clicked_review = reviews.get(id=review_id)
                reviews = reviews.exclude(id=review_id)
            except Review.DoesNotExist:
                clicked_review = None

        # 직렬화
        game_serializer = GameSerializer(game)
        my_review_serializer = ReviewSerializer(my_review) if my_review else None
        clicked_review_serializer = (
            ReviewSerializer(clicked_review) if clicked_review else None
        )
        other_reviews_serializer = ReviewSerializer(reviews, many=True)

        return Response(
            {
                &quot;game&quot;: game_serializer.data,
                &quot;my_review&quot;: my_review_serializer.data if my_review else None,
                &quot;clicked_review&quot;: clicked_review_serializer.data
                if clicked_review
                else None,
                &quot;reviews&quot;: other_reviews_serializer.data,
            }
        )</code></pre>
<h4 id="2-url-패턴-urlspy">2. URL 패턴 (<code>urls.py</code>)</h4>
<pre><code class="language-python">path(&quot;game/&quot;, views.GameDetailAPIView.as_view(), name=&quot;game_detail&quot;),</code></pre>
<hr>
<h3 id="테스트">테스트</h3>
<h4 id="요청-예시-1-클릭한-리뷰가-있는-경우">요청 예시 1: 클릭한 리뷰가 있는 경우</h4>
<ul>
<li>URL: <code>/api/game/?game_id=12345&amp;review_id=67890</code></li>
</ul>
<p><strong>응답 데이터</strong>:</p>
<pre><code class="language-json">{
    &quot;game&quot;: {
        &quot;id&quot;: 12345,
        &quot;name&quot;: &quot;Game Title&quot;,
        &quot;description&quot;: &quot;Game description...&quot;
    },
    &quot;my_review&quot;: {
        &quot;id&quot;: 111,
        &quot;content&quot;: &quot;My review content...&quot;,
        &quot;rating&quot;: 4
    },
    &quot;clicked_review&quot;: {
        &quot;id&quot;: 67890,
        &quot;content&quot;: &quot;This is the clicked review...&quot;,
        &quot;rating&quot;: 5
    },
    &quot;reviews&quot;: [
        {
            &quot;id&quot;: 22222,
            &quot;content&quot;: &quot;Another review...&quot;,
            &quot;rating&quot;: 3
        },
        {
            &quot;id&quot;: 33333,
            &quot;content&quot;: &quot;Yet another review...&quot;,
            &quot;rating&quot;: 4
        }
    ]
}</code></pre>
<h4 id="요청-예시-2-클릭한-리뷰가-없는-경우">요청 예시 2: 클릭한 리뷰가 없는 경우</h4>
<ul>
<li>URL: <code>/api/game/?game_id=12345</code></li>
</ul>
<p><strong>응답 데이터</strong>:</p>
<pre><code class="language-json">{
    &quot;game&quot;: {
        &quot;id&quot;: 12345,
        &quot;name&quot;: &quot;Game Title&quot;,
        &quot;description&quot;: &quot;Game description...&quot;
    },
    &quot;my_review&quot;: {
        &quot;id&quot;: 111,
        &quot;content&quot;: &quot;My review content...&quot;,
        &quot;rating&quot;: 4
    },
    &quot;clicked_review&quot;: null,
    &quot;reviews&quot;: [
        {
            &quot;id&quot;: 67890,
            &quot;content&quot;: &quot;Another review...&quot;,
            &quot;rating&quot;: 5
        },
        {
            &quot;id&quot;: 33333,
            &quot;content&quot;: &quot;Yet another review...&quot;,
            &quot;rating&quot;: 4
        }
    ]
}</code></pre>
<hr>
<h3 id="요약-표">요약 표</h3>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>문제</strong></td>
<td>리뷰 목록에서 게임 상세 페이지로 이동 시 클릭한 리뷰를 눈에 띄게 하고 나머지 리뷰와 분리 필요</td>
</tr>
<tr>
<td><strong>해결 방법</strong></td>
<td><code>GameDetailAPIView</code> 수정: <code>review_id</code>를 받아 클릭한 리뷰(<code>clicked_review</code>)를 강조 및 분리</td>
</tr>
<tr>
<td><strong>URL 변경</strong></td>
<td><code>/api/game/?game_id={game_id}&amp;review_id={review_id}</code>로 변경</td>
</tr>
<tr>
<td><strong>테스트 결과</strong></td>
<td>클릭한 리뷰(<code>clicked_review</code>)가 응답에서 별도로 제공되며, 나머지 리뷰(<code>reviews</code>)와 구분됨</td>
</tr>
</tbody></table>
<hr>
<p><a href="https://www.notion.so/teamsparta/d649ad93962144d196b7758328b9a8e1">API 명세서 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블 슈팅] 신규 유저 Steam_data 즉시 반영 필요]]></title>
            <link>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EC%8B%A0%EA%B7%9C-%EC%9C%A0%EC%A0%80-Steamdata-%EC%A6%89%EC%8B%9C-%EB%B0%98%EC%98%81-%ED%95%84%EC%9A%94</link>
            <guid>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EC%8B%A0%EA%B7%9C-%EC%9C%A0%EC%A0%80-Steamdata-%EC%A6%89%EC%8B%9C-%EB%B0%98%EC%98%81-%ED%95%84%EC%9A%94</guid>
            <pubDate>Tue, 21 Jan 2025 04:25:54 GMT</pubDate>
            <description><![CDATA[<h2 id="기존-로직">기존 로직</h2>
<pre><code class="language-python"># steam_data.py 

import environ
import requests
from django.core.management.base import BaseCommand
from accounts.models import Account, SteamProfile, SteamReview, SteamPlaytime
from selenium import webdriver

class Command(BaseCommand):
    help = &quot;전체 DB의 스팀 정보를 일괄 갱신&quot;

    def handle(self, *args, **options):
        env = environ.Env()
        api_key = env(&quot;STEAM_API_KEY&quot;)
        accounts = Account.objects.exclude(steamId=&quot;&quot;)

        # WebDriver 준비
        driver = webdriver.Chrome()

        for account in accounts:
            # (A) 프로필 공개 여부 확인
            # (B) 리뷰, 플레이타임 조회
            # (C) SteamProfile/SteamReview/SteamPlaytime 저장
            # ...

        driver.quit()
        self.stdout.write(self.style.SUCCESS(&quot;Steam 데이터 처리 완료.&quot;))</code></pre>
<p><strong>기존 로직</strong>(전체 DB를 일괄 업데이트하는 커맨드)으로는 <strong>새로 가입한 유저가 스팀을 연동했을 때</strong> 즉각적으로 DB에 반영되지 않는 문제가 있었다.
이를 해결하기 위해 <strong>신규 유저가 스팀 계정을 연결하자마자 DB에 정보를 저장</strong>할 수 있도록 <strong>별도의 모듈 함수</strong>를 만들고 기존 회원가입 로직과 스팀콜백 로직에 반영하여 해결했다.</p>
<hr>
<h2 id="문제-상황-trouble">문제 상황 (Trouble)</h2>
<ol>
<li><p><strong>전체 일괄 업데이트 로직</strong>:  </p>
<ul>
<li>기존에는 <code>steam_data.py</code>에 있는 <code>Django Management Command</code>를 통해, DB에 등록된 <code>steamid</code>가 필드에 존재하는 모든 유저를 한번에 스팀 정보(프로필, 리뷰, 플레이타임)를 갱신했다.  </li>
<li>이는 <code>crontab</code>을 이용한 정기적 작업이나 대규모 업데이트에는 유용하지만, <strong>‘새 유저가 스팀을 방금 연동한 순간’</strong>에는 곧바로 DB에 반영되지 않는 문제가 있었다.  </li>
<li>새로 스팀 연동을 마친 유저 입장에선, 자기 스팀 리뷰나 플레이타임 정보를 즉시 확인하고 싶지만, 전체 작업이 실행될 때까지 기다려야 했다.</li>
</ul>
</li>
<li><p><strong>즉각적 반영 불가</strong>:  </p>
<ul>
<li>새 유저가 스팀 계정을 등록해도, 다음에 해당 커맨드를 수동/자동으로 실행하기 전까지 DB에 갱신된 정보가 들어오지 않는다.  </li>
<li>결과적으로, 사용자 경험(UX)이 떨어지고, <strong>“가입 시점에 곧바로 프로필·리뷰·플레이타임 확인”</strong> 같은 기능이 구현 어려웠다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="해결-방안-solution">해결 방안 (Solution)</h2>
<pre><code class="language-python"># steam_service.py

import environ
import requests
from django.db import transaction
from selenium import webdriver
from accounts.models import Account, SteamProfile, SteamReview, SteamPlaytime

def sync_new_steam_user_data(account):
    &quot;&quot;&quot;
    새 유저(특정 account)에 대해 프로필/리뷰/플레이타임 정보를 즉시 DB에 반영.
    &quot;&quot;&quot;
    # 1) Steam API 키 불러오기
    env = environ.Env()
    api_key = env(&quot;STEAM_API_KEY&quot;)

    # 2) 프로필 공개 여부 확인
    if not check_profile_public(api_key, account.steamId):
        # 비공개면 is_review=False, is_playtime=False
        sp, _ = SteamProfile.objects.get_or_create(account=account)
        sp.is_review = False
        sp.is_playtime = False
        sp.save()
        return

    # 3) 리뷰/플레이타임 조회
    # ...
    # 4) DB에 즉시 저장
    # ...
</code></pre>
<ol>
<li><strong>단일 유저 대상 함수 <code>sync_new_steam_user_data</code></strong> 생성:  <ul>
<li><code>steam_service.py</code>로 별도 모듈을 만들고, <code>sync_new_steam_user_data(account)</code> 함수를 정의했다.</li>
<li>이 함수는 <strong>단 한 명</strong>의 <code>Account</code>(특히 스팀 ID 보유)를 입력받아, 리뷰/플레이타임/프로필 공개 여부를 크롤링/조회 후 DB에 즉시 반영한다.  </li>
<li>기존 “전체 유저 갱신” 로직을 재활용해서, <strong>개별 유저</strong>만 빠르게 처리 가능하도록 최적화했다.</li>
</ul>
</li>
</ol>
<pre><code class="language-python">@api_view([&quot;POST&quot;])
def signup(request):
            #...
            steam_id = request.data.get(&quot;steam_id&quot;)
            if steam_id:
                # DB에 리뷰공개 여부/플레이타임/리뷰 데이터 동기화 다음에 하기 할 때 제외하는 로직
                sync_new_steam_user_data(user)

            return

@api_view([&quot;POST&quot;])
def steam_callback(request):
            #...
            # DB에 리뷰공개 여부/플레이타임/리뷰 데이터 동기화
            sync_new_steam_user_data(account)

            return</code></pre>
<ol start="2">
<li><p><strong>View 에서 새 유저 연동 시점에 호출</strong>:  </p>
<ul>
<li>새 유저가 회원가입 완료 후, 스팀 로그인 콜백 등을 통해 <code>steamId</code>를 등록하는 즉시, <code>sync_new_steam_user_data(account)</code>를 호출한다.  </li>
<li>결과: 스팀 프로필이 Public이라면, 즉시 리뷰·플레이타임을 DB에 저장. Private이면 <code>is_review=False, is_playtime=False</code>로 처리.  </li>
<li>사용자 입장에선, <strong>연동 직후</strong> 리뷰 목록이나 플레이타임 정보를 볼 수 있게 된다.</li>
</ul>
</li>
<li><p><strong>기존 커맨드(<code>steam_data.py</code>)는 유지</strong>:  </p>
<ul>
<li>배치 작업 또는 정기 갱신 시에는 여전히 <strong>전체</strong> DB를 순회할 수 있는 기존 커맨드를 사용.  </li>
<li>새 모듈(<code>steam_service.py</code>)과 커맨드에 있는 로직이 중복되지 않도록, <strong>공통 함수</strong>를 활용하거나 로직 구조를 최대한 비슷하게 맞췄다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="요약">요약</h2>
<ul>
<li><strong>트러블</strong>: 기존엔 전체 DB 단위 배치만 존재해서, 새 유저가 스팀 연동해도 즉시 DB 저장이 불가능했다.  </li>
<li><strong>해결</strong>: 단일 유저용 함수를 만들어, 스팀 연동 순간 DB에 곧바로 반영하도록 개선했다.  </li>
</ul>
<p>수정 함으로써 신규 유저가 스팀을 연동하는 시점에 <strong>즉각적인 리뷰·플레이타임 DB 등록</strong>이 가능해졌다. 기존 배치 로직(전체 일괄 갱신)은 그대로 유지하되, <strong>새로 가입한 유저</strong>에 대해서만 개별로 빠르게 동기화하는 최적화를 시행했다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>기존 (<code>steam_data.py</code>)</th>
<th>개선 (<code>steam_service.py</code>)</th>
</tr>
</thead>
<tbody><tr>
<td>목적</td>
<td>모든 계정(steamId 보유)을 <strong>일괄 갱신</strong></td>
<td>특정 유저(신규 스팀 연동 등)만 <strong>즉각 갱신</strong></td>
</tr>
<tr>
<td>실행 시점</td>
<td>정기적 or 필요 시(수동 명령)</td>
<td>회원가입/로그인/스팀 연동 완료 시, 실시간(개별 호출)</td>
</tr>
<tr>
<td>단점 (기존)</td>
<td>새로 가입한 유저가 <strong>갱신 대기</strong> 필요</td>
<td>-</td>
</tr>
<tr>
<td>장점 (개선)</td>
<td>많은 유저를 한 번에 갱신 가능</td>
<td>신규 유저 연동 시 <strong>즉시 DB 반영</strong>, 사용자 경험 크게 향상</td>
</tr>
<tr>
<td>기존 리뷰/플레이타임 처리</td>
<td>(코드에 따라) 전체 삭제 후 새로 생성 가능</td>
<td><strong>삭제 없이</strong> <code>get_or_create</code>로 중복만 방지</td>
</tr>
</tbody></table>
<p>즉, <strong>전체 업데이트용 커맨드</strong>와 <strong>신규 유저 즉시 반영용 서비스 함수</strong>를 동시에 운영함으로써, 기존 DB 갱신과 실시간 반영을 모두 처리할 수 있게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] 리뷰 작성할 때 content 필드에 데이터가 저장되지 않는 문제와 블러 처리 구현]]></title>
            <link>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EB%A6%AC%EB%B7%B0-%EC%9E%91%EC%84%B1%ED%95%A0-%EB%95%8C-content-%ED%95%84%EB%93%9C%EC%97%90-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%A0%80%EC%9E%A5%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C%EC%99%80-%EB%B8%94%EB%9F%AC-%EC%B2%98%EB%A6%AC-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EB%A6%AC%EB%B7%B0-%EC%9E%91%EC%84%B1%ED%95%A0-%EB%95%8C-content-%ED%95%84%EB%93%9C%EC%97%90-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%A0%80%EC%9E%A5%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C%EC%99%80-%EB%B8%94%EB%9F%AC-%EC%B2%98%EB%A6%AC-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 19 Jan 2025 05:19:29 GMT</pubDate>
            <description><![CDATA[<h1 id="content-필드에-데이터가-저장되지-않는-문제와-블러-처리-구현"><code>content</code> 필드에 데이터가 저장되지 않는 문제와 블러 처리 구현</h1>
<hr>
<h4 id="문제">문제</h4>
<p>새 리뷰를 생성할 때, <code>POST</code> 요청에 포함된 <code>content</code> 값이 데이터베이스에 저장되지 않는 문제가 발생했습니다. 이와 동시에 차단된 사용자의 리뷰를 조회할 때 내용을 블러 처리하는 기능을 구현하려고 했습니다. 하지만 두 기능이 충돌하면서 아래와 같은 문제가 발생했습니다:</p>
<ol>
<li><code>content</code> 필드가 <code>SerializerMethodField</code>로 설정되어 있었습니다.<ul>
<li><strong>문제</strong>: <code>SerializerMethodField</code>는 읽기 전용 필드로, 요청 데이터를 받아들이지 않기 때문에 새 리뷰 생성 시 <code>content</code> 값이 무시되었습니다.</li>
</ul>
</li>
<li>블러 처리 로직이 <code>content</code> 필드와 연결되어 리뷰 생성과 조회 기능 간의 혼란이 발생했습니다.<ul>
<li><code>content</code> 필드가 저장되지 않아 블러 처리 이전에 기본 동작부터 실패했습니다.</li>
</ul>
</li>
</ol>
<hr>
<h4 id="원인-분석">원인 분석</h4>
<ol>
<li><p><strong><code>SerializerMethodField</code>의 읽기 전용 특성</strong></p>
<ul>
<li><code>SerializerMethodField</code>는 데이터를 직렬화할 때 <code>get_&lt;field_name&gt;</code> 메서드의 반환값만 사용하며, 모델 필드와 연결되지 않습니다.</li>
<li>결과적으로, <code>POST</code> 요청에서 <code>content</code> 데이터를 저장하지 못했습니다.</li>
</ul>
</li>
<li><p><strong>블러 처리와 데이터 저장의 충돌</strong></p>
<ul>
<li><code>content</code> 필드를 블러 처리 로직과 동일하게 처리하려다 보니, 읽기와 쓰기가 충돌했습니다.</li>
<li>블러 처리 로직은 데이터 조회 시 적용되어야 하지만, 데이터 저장 로직에 영향을 미쳤습니다.</li>
</ul>
</li>
</ol>
<hr>
<h4 id="해결-방안">해결 방안</h4>
<ol>
<li><p><strong>읽기와 쓰기를 분리</strong></p>
<ul>
<li><code>content</code> 필드는 데이터 저장용으로 유지하고, 차단된 사용자 리뷰를 숨김 처리하는 블러 처리 로직은 <code>content_display</code>라는 추가 필드로 분리했습니다.</li>
<li><code>content</code>: 원본 리뷰 데이터를 저장하고 반환.</li>
<li><code>content_display</code>: 차단된 사용자의 리뷰 내용을 숨김 처리하여 반환.</li>
</ul>
</li>
<li><p><strong>직렬화 및 로직 구현</strong></p>
<ul>
<li><code>content_display</code>는 <code>SerializerMethodField</code>를 사용하여 읽기 전용으로 설정.</li>
<li><code>content</code>는 모델 필드와 연결하여 리뷰 생성 시 데이터를 저장.</li>
</ul>
</li>
</ol>
<hr>
<h4 id="수정된-코드">수정된 코드</h4>
<p><strong>1. <code>ReviewSerializer</code></strong></p>
<pre><code class="language-python">class ReviewSerializer(serializers.ModelSerializer):
    &quot;&quot;&quot;Review 모델 직렬화&quot;&quot;&quot;
    nickname = serializers.SerializerMethodField()  # 사용자 닉네임 반환
    content_display = serializers.SerializerMethodField()  # 차단된 사용자 콘텐츠 처리
    comments = ReviewCommentSerializer(many=True, read_only=True)  # 연결된 댓글들
    total_likes = serializers.IntegerField(read_only=True)  # annotate로 계산된 값
    total_dislikes = serializers.IntegerField(read_only=True)  # annotate로 계산된 값
    game_name = serializers.CharField(read_only=True)
    header_image = serializers.CharField(read_only=True)


    class Meta:
        model = Review
        fields = [
            &#39;id&#39;, &#39;user&#39;, &#39;nickname&#39;, &#39;content&#39;, &#39;content_display&#39;, &#39;app_id&#39;, &#39;game_name&#39;, &#39;header_image&#39;, &#39;score&#39;, &#39;categories&#39;,
            &#39;created_at&#39;, &#39;updated_at&#39;, &#39;comments&#39;, &#39;total_likes&#39;, &#39;total_dislikes&#39;
        ]
        read_only_fields = [
            &#39;id&#39;, &#39;created_at&#39;, &#39;updated_at&#39;, &#39;comments&#39;, &#39;total_likes&#39;, &#39;total_dislikes&#39;, &#39;game_name&#39;, &#39;header_image&#39;, &#39;categories&#39;, &#39;content_display&#39;
        ]

    def get_nickname(self, obj):
        &quot;&quot;&quot;유저 닉네임 반환 (유저가 없으면 &#39;알수없음&#39;)&quot;&quot;&quot;
        return obj.user.nickname if obj.user else &quot;알수없음&quot;

    def get_content_display(self, obj):
        blocked_users = self.context.get(&#39;blocked_users&#39;, [])
        if obj.user and obj.user.id in blocked_users:
            return &quot;이 사용자의 리뷰는 차단되어 표시되지 않습니다.&quot;
        return obj.content</code></pre>
<p><strong>2. <code>ReviewDetailAPIView</code></strong></p>
<pre><code class="language-python">class ReviewDetailAPIView(APIView):
    def get(self, request, pk):
        &quot;&quot;&quot;특정 리뷰 조회&quot;&quot;&quot;
        review = get_object_or_404(Review, pk=pk)
        blocked_users = Block.objects.filter(blocker=request.user).values_list(&#39;blocked_user&#39;, flat=True) if request.user.is_authenticated else []

        serializer = ReviewSerializer(review, context={&#39;blocked_users&#39;: list(blocked_users)})
        return Response(serializer.data, status=status.HTTP_200_OK)</code></pre>
<hr>
<h4 id="해결-결과">해결 결과</h4>
<ol>
<li><p><strong>리뷰 생성 시 <code>content</code> 저장 정상화</strong></p>
<ul>
<li><code>POST</code> 요청 시 <code>content</code> 데이터가 <code>Review</code> 모델에 정상적으로 저장됩니다.</li>
</ul>
</li>
<li><p><strong>블러 처리 기능 분리</strong></p>
<ul>
<li><code>content_display</code> 필드가 차단된 사용자의 리뷰를 숨김 처리하여 반환합니다.</li>
<li>원본 데이터(<code>content</code>)는 변경되지 않고, 필요할 때 블러 처리 로직만 동작합니다.</li>
</ul>
</li>
<li><p><strong>코드의 명확성과 확장성 증가</strong></p>
<ul>
<li>데이터 저장과 블러 처리 로직이 분리되어 각 기능이 독립적으로 유지됩니다.</li>
<li>블러 처리 로직을 유저별로 다르게 설정하거나 조건을 추가하는 등의 확장이 용이해졌습니다.</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블 슈팅]알림 모델 개선]]></title>
            <link>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EC%95%8C%EB%A6%BC-%EB%AA%A8%EB%8D%B8-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EC%95%8C%EB%A6%BC-%EB%AA%A8%EB%8D%B8-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Sun, 19 Jan 2025 01:58:47 GMT</pubDate>
            <description><![CDATA[<h4 id="문제-정의"><strong>문제 정의</strong></h4>
<ol>
<li><p><strong>알림 유형 구분의 어려움</strong>:</p>
<ul>
<li>알림 유형을 숫자(<code>int</code>)로 관리하다 보니, 코드에서 의미를 직관적으로 파악하기 어려움</li>
<li>예: <code>type=1</code>이 무엇을 의미하는지 명확하지 않음</li>
</ul>
</li>
<li><p><strong>알림 상태 관리의 불편함</strong>:</p>
<ul>
<li>알림의 읽음 여부(<code>is_read</code>)를 별도로 관리하지 않아, 사용자가 읽은 알림을 처리하는 로직이 복잡해질 가능성이 있음</li>
</ul>
</li>
<li><p><strong>확장성 부족</strong>:</p>
<ul>
<li>새로운 알림 유형을 추가하려면, 기존 코드의 모든 <code>type</code> 관련 부분을 수정해야 하는 구조적 문제가 있음</li>
</ul>
</li>
</ol>
<hr>
<h4 id="해결-방안"><strong>해결 방안</strong></h4>
<ol>
<li><p><strong><code>choices</code> 필드를 활용한 유형 정의</strong>:</p>
<ul>
<li><code>TYPE_CHOICES</code>를 도입해 알림 유형의 의미를 명확히 하고, 새 유형 추가 시 수정 범위를 최소화</li>
<li>각 알림 유형에 대해 상수(<code>TYPE_GENERAL</code>, <code>TYPE_FRIEND_REQUEST</code> 등)를 정의해 코드 가독성을 높임</li>
</ul>
</li>
<li><p><strong>읽음 여부 관리</strong>:</p>
<ul>
<li><code>is_read</code> 필드를 추가해 알림의 상태를 관리</li>
<li>사용자가 알림을 읽었는지 여부를 직관적으로 확인하고 처리할 수 있도록 개선</li>
</ul>
</li>
<li><p><strong>모델 기본값 및 정렬</strong>:</p>
<ul>
<li><code>type</code>의 기본값을 지정하고, 생성일 기준 정렬을 기본으로 설정해 데이터 조회 성능 향상</li>
</ul>
</li>
</ol>
<hr>
<h4 id="최종-수정-모델"><strong>최종 수정 모델</strong></h4>
<pre><code class="language-python">class Notice(models.Model):
    user_id = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name=&quot;accounts&quot;
    )
    TYPE_GENERAL = 1
    TYPE_FRIEND_REQUEST = 2
    TYPE_COMMENT = 3

    TYPE_CHOICES = [
        (TYPE_GENERAL, &quot;일반 알림&quot;),
        (TYPE_FRIEND_REQUEST, &quot;친구 요청 알림&quot;),
        (TYPE_COMMENT, &quot;댓글 알림&quot;),
    ]

    type = models.IntegerField(choices=TYPE_CHOICES, default=0)
    content = models.CharField(max_length=50)
    is_read = models.BooleanField(default=False) # 읽음 여부
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)</code></pre>
<hr>
<h4 id="변경"><strong>변경</strong></h4>
<ol>
<li><p><strong><code>TYPE_CHOICES</code>로 알림 유형 관리</strong>:</p>
<ul>
<li>숫자 값 대신 상수(<code>TYPE_FRIEND_REQUEST</code> 등)를 사용해 가독성을 높이고, <code>choices</code>를 통해 유효성을 보장.</li>
</ul>
</li>
<li><p><strong><code>is_read</code> 필드 추가</strong>:</p>
<ul>
<li>알림의 읽음 여부를 쉽게 확인하고, 클라이언트와 서버 간 상태 동기화를 용이하게 만듦.</li>
</ul>
</li>
</ol>
<hr>
<h4 id="모델-변경에-따른-영향"><strong>모델 변경에 따른 영향</strong></h4>
<ol>
<li><p><strong>직렬화 코드 변경</strong>:</p>
<ul>
<li><p><code>NoticeSerializer</code>에서 추가적인 변경 없이 동작 가능:</p>
<pre><code class="language-python">class NoticeSerializer(serializers.ModelSerializer):
    class Meta:
        model = Notice
        fields = [&#39;id&#39;, &#39;type&#39;, &#39;content&#39;, &#39;is_read&#39;, &#39;created_at&#39;, &#39;updated_at&#39;]</code></pre>
</li>
<li><p><code>get_type_display()</code>를 활용하려면 추가 필드를 정의 가능:</p>
<pre><code class="language-python">class NoticeSerializer(serializers.ModelSerializer):
    type_display = serializers.CharField(source=&quot;get_type_display&quot;, read_only=True)

    class Meta:
        model = Notice
        fields = [&#39;id&#39;, &#39;type&#39;, &#39;type_display&#39;, &#39;content&#39;, &#39;is_read&#39;, &#39;created_at&#39;, &#39;updated_at&#39;]</code></pre>
</li>
</ul>
</li>
<li><p><strong>조회 및 업데이트 로직 변경</strong>:</p>
<ul>
<li><p>알림을 읽음 처리하거나 삭제하는 API를 간단하게 구현 가능:</p>
<pre><code class="language-python"># 읽음 처리
notice = get_object_or_404(Notice, id=notice_id, user_id=request.user)
notice.is_read = True
notice.save()</code></pre>
</li>
<li><p>읽은 알림 삭제:</p>
<pre><code class="language-python">Notice.objects.filter(user_id=request.user, is_read=True).delete()</code></pre>
</li>
</ul>
</li>
<li><p><strong>확장성 향상</strong>:</p>
<ul>
<li>새로운 알림 유형 추가 시, <code>TYPE_CHOICES</code>에 값을 추가하기만 하면 쉽게 확장 가능:<pre><code class="language-python">TYPE_LIKE = 4
TYPE_CHOICES.append((TYPE_LIKE, &quot;좋아요 알림&quot;))</code></pre>
</li>
</ul>
</li>
</ol>
<hr>
<h4 id="피드백"><strong>피드백</strong></h4>
<ol>
<li><p><strong>가독성과 유지보수성</strong>:</p>
<ul>
<li><code>choices</code>와 상수를 활용하면 코드를 더 직관적이고 관리하기 쉽게 만들 수 있다</li>
</ul>
</li>
<li><p><strong>데이터 상태 관리</strong>:</p>
<ul>
<li>상태 필드(<code>is_read</code>)를 추가해 클라이언트와 서버 간의 데이터 동기화가 원활해짐</li>
</ul>
</li>
<li><p><strong>확장 가능성</strong>:</p>
<ul>
<li>구조적으로 개선된 모델은 새로운 요구사항이 추가되더라도 최소한의 수정만으로 대응할 수 있다</li>
</ul>
</li>
</ol>
<hr>
<h4 id="앞으로의-계획"><strong>앞으로의 계획</strong></h4>
<p><strong>실시간 알림 지원</strong>:</p>
<ul>
<li>WebSocket이나 Django Channels를 통해 알림을 실시간으로 전달하는 구조를 검토할 예정</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블 슈팅] 친구 요청 및 친구 관리 기능 구현]]></title>
            <link>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EC%B9%9C%EA%B5%AC-%EC%9A%94%EC%B2%AD-%EB%B0%8F-%EC%B9%9C%EA%B5%AC-%EA%B4%80%EB%A6%AC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@gyu_p/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EC%B9%9C%EA%B5%AC-%EC%9A%94%EC%B2%AD-%EB%B0%8F-%EC%B9%9C%EA%B5%AC-%EA%B4%80%EB%A6%AC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sat, 18 Jan 2025 09:27:10 GMT</pubDate>
            <description><![CDATA[<h4 id="1-문제-친구-요청-상태-관리-방식의-혼란">1. <strong>문제: 친구 요청 상태 관리 방식의 혼란</strong></h4>
<ul>
<li><p><strong>상황</strong>: 초기에는 <code>type</code> 필드를 활용하여 요청 상태를 <code>0</code>(대기), <code>1</code>(수락), <code>-1</code>(거절)로 관리하려 했습니다. 그러나 데이터베이스에 거절된 요청을 계속 유지하는 방식은 복잡성을 증가시키고, 성능 저하 가능성이 있었습니다</p>
</li>
<li><p><strong>해결</strong>:</p>
<ol>
<li>거절된 요청은 데이터베이스에서 삭제</li>
<li>요청 수락 시, 요청을 삭제하면서 <code>Friend</code> 모델에 친구 관계를 저장</li>
</ol>
</li>
<li><p><strong>변경 후 코드</strong>:</p>
<pre><code class="language-python">if new_type == 1:  # 수락
    Friend.objects.create(user_id=request.user, friend_id=friend_request.user_id)
    Friend.objects.create(user_id=friend_request.user_id, friend_id=request.user)
    friend_request.delete()  # 요청 삭제
elif new_type == -1:  # 거절
    friend_request.delete()  # 요청 삭제</code></pre>
</li>
</ul>
<hr>
<h4 id="2-문제-중복-요청-처리의-혼란">2. <strong>문제: 중복 요청 처리의 혼란</strong></h4>
<ul>
<li><p><strong>상황</strong>: 기존 요청이 거절(<code>type=-1</code>)된 상태에서 새로운 요청을 처리할 때, <code>type</code> 상태를 업데이트하는 방식이 프론트엔드와의 통신에 복잡함을 초래</p>
</li>
<li><p><strong>해결</strong>:</p>
<ol>
<li>모든 요청은 수락 또는 거절 시 삭제</li>
<li>새로운 요청은 언제든지 새롭게 생성 가능하도록 설계</li>
</ol>
</li>
<li><p><strong>변경 후 코드</strong>:</p>
<pre><code class="language-python">def post(self, request):
    &quot;&quot;&quot;친구 요청 생성&quot;&quot;&quot;
    friend_request, created = FriendRequest.objects.get_or_create(
        user_id=request.user, friend_id=friend,
        defaults={&quot;type&quot;: 0}
    )
    if not created:
        return Response({&quot;message&quot;: &quot;이미 대기 중인 친구 요청이 있습니다.&quot;}, status=status.HTTP_400_BAD_REQUEST)</code></pre>
</li>
</ul>
<hr>
<h4 id="3-문제-요청-id와-사용자-id-혼동">3. <strong>문제: 요청 ID와 사용자 ID 혼동</strong></h4>
<ul>
<li><p><strong>상황</strong>: 프론트엔드에서 친구 요청을 수락/거절할 때 <code>friend_request_id</code>가 아닌 요청 보낸 사람의 <code>user_id</code>를 보내 혼란 발생</p>
</li>
<li><p><strong>해결</strong>:</p>
<ol>
<li><code>friend_request_id</code>를 요청 고유 식별자로 사용</li>
<li>프론트엔드가 <code>GET</code> 요청으로 받은 <code>id</code>를 그대로 <code>PUT</code> 요청에 사용하도록 설계</li>
</ol>
</li>
<li><p><strong>변경 후 코드</strong>:</p>
<pre><code class="language-python">def put(self, request):
    &quot;&quot;&quot;친구 요청 상태 변경&quot;&quot;&quot;
    friend_request = get_object_or_404(FriendRequest, id=request.data.get(&quot;friend_request_id&quot;), friend_id=request.user)
    # 수락 또는 거절 처리</code></pre>
</li>
</ul>
<hr>
<h4 id="4-문제-친구-요청과-친구-관리-로직의-혼합">4. <strong>문제: 친구 요청과 친구 관리 로직의 혼합</strong></h4>
<ul>
<li><p><strong>상황</strong>: 친구 요청(<code>FriendRequest</code>)과 실제 친구 관계(<code>Friend</code>) 로직이 혼재되어 있어 유지보수가 어려움</p>
</li>
<li><p><strong>해결</strong></p>
<ol>
<li><code>FriendRequestAPIView</code>는 요청 생성/조회/상태 변경만 처리</li>
<li><code>FriendAPIView</code>를 별도로 만들어 친구 목록 조회 및 삭제 관리</li>
</ol>
</li>
<li><p><strong>변경 후 구조</strong>:</p>
<pre><code class="language-python">class FriendRequestAPIView(APIView):
    # 친구 요청 생성, 조회, 수락/거절 처리
    ...

class FriendAPIView(APIView):
    def get(self, request):
        &quot;&quot;&quot;친구 목록 조회&quot;&quot;&quot;
        friends = Friend.objects.filter(user_id=request.user)
        ...

    def delete(self, request):
        &quot;&quot;&quot;친구 삭제&quot;&quot;&quot;
        Friend.objects.filter(user_id=request.user, friend_id=request.data.get(&quot;friend_id&quot;)).delete()</code></pre>
</li>
</ul>
<hr>
<h3 id="최종-결과">최종 결과</h3>
<ul>
<li><strong>변경된 로직</strong>은 다음과 같은 특징을 가집니다:<ol>
<li><strong>친구 요청과 친구 관리의 명확한 분리</strong>:<ul>
<li><code>FriendRequestAPIView</code>: 요청 생성, 조회, 상태 변경</li>
<li><code>FriendAPIView</code>: 친구 목록 조회, 친구 삭제</li>
</ul>
</li>
<li><strong>거절된 요청 삭제</strong>:<ul>
<li>데이터베이스가 깔끔하게 관리되며 성능 최적화</li>
</ul>
</li>
</ol>
</li>
</ul>
<hr>
<h3 id="배운-점">배운 점</h3>
<ol>
<li><strong>데이터 설계는 유연하면서도 간단해야 함</strong>:<ul>
<li>복잡한 상태 관리(<code>type=-1</code> 유지)를 피하고 삭제를 통해 단순화.</li>
</ul>
</li>
<li><strong>클라이언트와의 통신은 명확해야 함</strong>:<ul>
<li>고유 ID(<code>friend_request_id</code>)를 활용하여 혼란 방지.</li>
</ul>
</li>
<li><strong>로직 분리는 유지보수의 핵심</strong>:<ul>
<li>친구 요청과 친구 관리를 명확히 분리하여 역할을 구체화.</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[친구 요청 및 친구 관리 기능 구현 과정]]></title>
            <link>https://velog.io/@gyu_p/%EC%B9%9C%EA%B5%AC-%EC%9A%94%EC%B2%AD-%EB%B0%8F-%EC%B9%9C%EA%B5%AC-%EA%B4%80%EB%A6%AC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@gyu_p/%EC%B9%9C%EA%B5%AC-%EC%9A%94%EC%B2%AD-%EB%B0%8F-%EC%B9%9C%EA%B5%AC-%EA%B4%80%EB%A6%AC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Sat, 18 Jan 2025 09:21:12 GMT</pubDate>
            <description><![CDATA[<h4 id="1-프로젝트-요구사항-재정리">1. <strong>프로젝트 요구사항 재정리</strong></h4>
<p>처음에는 <strong>친구 요청과 친구 관리</strong>를 단순하게 처리하려 했으나, 구현 과정에서 여러 경우의 수를 고려해야 했습니다:</p>
<ul>
<li><strong>친구 요청 상태 관리 (<code>type</code> 필드)</strong>:<ul>
<li><code>type=0</code>: 대기 중</li>
<li><code>type=1</code>: 수락(친구 관계 형성)</li>
<li><code>type=-1</code>: 거절</li>
</ul>
</li>
<li><strong>친구 요청과 친구 관리의 분리</strong>:<ul>
<li>친구 요청과 실제 친구 목록을 구분해 설계해야 함을 깨달았습니다.</li>
</ul>
</li>
</ul>
<hr>
<h4 id="2-의사결정-변화">2. <strong>의사결정 변화</strong></h4>
<ol>
<li><p><strong>친구 요청 상태 관리 방식</strong></p>
<ul>
<li><strong>초기 설계</strong>: <code>type=-1</code>로 상태를 유지하고 데이터베이스에 남겨두기.<ul>
<li><strong>문제점</strong>: 거절된 요청이 데이터베이스에 쌓이면서 관리가 복잡해지고, 성능 저하 우려</li>
<li><strong>결정</strong>: <strong>거절된 요청은 삭제</strong>하는 방식으로 변경</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>친구 관계 형성</strong></p>
<ul>
<li><strong>초기 설계</strong>: <code>type=1</code>로 요청 상태를 업데이트한 뒤, 별도로 친구 관계를 처리하지 않음<ul>
<li><strong>문제점</strong>: 요청 상태를 업데이트하는 것만으로는 명확히 친구 관계를 관리하기 어려움</li>
<li><strong>결정</strong>: <strong>요청 수락 시 요청을 삭제하고, 별도의 <code>Friend</code> 모델로 관계 관리.</strong></li>
</ul>
</li>
</ul>
</li>
<li><p><strong>중복 요청 방지</strong></p>
<ul>
<li><strong>초기 설계</strong>: <code>type</code> 상태를 활용해 요청을 중복 생성하지 않도록 함<ul>
<li><strong>문제점</strong>: 같은 요청을 재활용할 경우 클라이언트와의 통신이 복잡해짐</li>
<li><strong>결정</strong>: 요청은 삭제하되, 언제든 <strong>새로운 요청을 생성 가능</strong>하도록 설계</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>프론트엔드 연동 고려</strong></p>
<ul>
<li><strong>초기 설계</strong>: 프론트엔드에서 <code>user_id</code> 기반으로 요청 처리<ul>
<li><strong>문제점</strong>: 여러 요청이 있을 경우 특정 요청을 구분하기 어려움</li>
<li><strong>결정</strong>: 요청 고유 ID(<code>friend_request_id</code>)를 사용해 명확히 처리</li>
</ul>
</li>
</ul>
</li>
</ol>
<hr>
<h4 id="3-최종-설계">3. <strong>최종 설계</strong></h4>
<ol>
<li><p><strong>친구 요청 로직 (<code>FriendRequestAPIView</code>)</strong></p>
<ul>
<li>친구 요청 생성:<ul>
<li>이미 친구이거나 대기 중인 요청이 있는 경우 요청 불가</li>
</ul>
</li>
<li>요청 조회:<ul>
<li>현재 사용자가 받은 대기 중 요청만 반환</li>
</ul>
</li>
<li>요청 상태 변경:<ul>
<li>수락 시:<ul>
<li>요청 삭제.</li>
<li><code>Friend</code> 모델에 관계 생성</li>
</ul>
</li>
<li>거절 시:<ul>
<li>요청 삭제.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>친구 관리 로직 (<code>FriendAPIView</code>)</strong></p>
<ul>
<li>친구 목록 조회:<ul>
<li>현재 사용자의 친구 목록 반환</li>
</ul>
</li>
<li>친구 삭제:<ul>
<li>양방향 관계를 모두 삭제</li>
</ul>
</li>
</ul>
</li>
</ol>
<hr>
<h4 id="4-배운-점">4. <strong>배운 점</strong></h4>
<ol>
<li><p><strong>로직을 명확히 분리해야 유지보수가 쉬움</strong>:</p>
<ul>
<li>친구 요청과 친구 관리를 분리하여 각각의 책임을 명확히 함</li>
</ul>
</li>
<li><p><strong>데이터베이스 정리의 중요성</strong>:</p>
<ul>
<li>거절된 요청을 삭제하여 불필요한 데이터가 쌓이는 것을 방지</li>
<li>삭제를 통해 요청을 재활용할 필요가 없으므로 로직 단순화</li>
</ul>
</li>
<li><p><strong>프론트엔드와의 협업</strong>:</p>
<ul>
<li>고유 ID(<code>friend_request_id</code>)를 사용하여 요청을 명확히 처리함으로써 클라이언트와의 통신 간소화</li>
</ul>
</li>
<li><p><strong>의사결정은 반복적이고 점진적인 과정</strong>:</p>
<ul>
<li>초기 설계에서 부족했던 부분들을 구현과 테스트 과정을 통해 보완</li>
</ul>
</li>
</ol>
<hr>
<h4 id="5-개선-방향">5. <strong>개선 방향</strong></h4>
<ol>
<li><strong>알림 기능 추가</strong>:<ul>
<li>친구 요청 수락/거절 시 알림을 통해 사용자 경험 개선</li>
</ul>
</li>
<li><strong>친구 추천 기능</strong>:<ul>
<li>친구 목록을 기반으로 게임 추천 기능 구현</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[차단한 사용자의 리뷰 블러 처리 구현]]></title>
            <link>https://velog.io/@gyu_p/%EC%B0%A8%EB%8B%A8%ED%95%9C-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%EB%A6%AC%EB%B7%B0-%EB%B8%94%EB%9F%AC-%EC%B2%98%EB%A6%AC-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@gyu_p/%EC%B0%A8%EB%8B%A8%ED%95%9C-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%EB%A6%AC%EB%B7%B0-%EB%B8%94%EB%9F%AC-%EC%B2%98%EB%A6%AC-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Fri, 17 Jan 2025 02:55:38 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>우리 프로젝트의 주요 요구사항 중 하나는 <strong>사용자가 차단한 유저의 리뷰를 블러 처리하는 기능</strong>이었습니다. 이 기능은 사용자 경험을 개선하기 위한 중요한 요소로, 리뷰를 조회할 때 차단된 사용자의 리뷰를 숨기거나 대체 메시지로 표시해야 했습니다.</p>
<h3 id="구현-목표">구현 목표</h3>
<ol>
<li><strong>회원</strong>: 차단한 사용자의 리뷰를 블러 처리.</li>
<li><strong>비회원</strong>: 모든 리뷰를 정상적으로 표시.</li>
<li><strong>일관된 동작</strong>: 모든 리뷰 목록 조회 및 상세 조회 API에서 동일한 블러 처리 로직 적용.</li>
</ol>
<hr>
<h2 id="구현-과정">구현 과정</h2>
<h3 id="1-의사결정-serializer-활용">1. 의사결정: Serializer 활용</h3>
<p>리뷰를 블러 처리하기 위해 선택한 접근 방식은  <strong>Serializer</strong>를 활용하는 것이었습니다. 이 방법을 선택한 이유는 다음과 같습니다:</p>
<ul>
<li><strong>재사용성</strong>: 여러 View에서 동일한 <code>ReviewSerializer</code>를 사용하여 로직의 일관성을 유지할 수 있습니다.</li>
<li><strong>간결성</strong>: Serializer는 데이터를 직렬화하는 과정에서 로직을 포함할 수 있으므로, 블러 처리 로직을 효율적으로 추가할 수 있습니다.</li>
</ul>
<hr>
<h3 id="2-블러-처리-로직-추가">2. 블러 처리 로직 추가</h3>
<p><code>ReviewSerializer</code>에 다음과 같은 로직을 추가했습니다:</p>
<ul>
<li><strong><code>SerializerMethodField</code></strong>:<ul>
<li><code>content</code> 필드를 오버라이드하여 블러 처리 여부를 결정.</li>
</ul>
</li>
<li><strong><code>get_content</code> 메서드</strong>:<ul>
<li>현재 사용자가 차단한 사용자의 리뷰인 경우 <code>&quot;이 사용자의 리뷰는 차단되어 표시되지 않습니다.&quot;</code> 메시지를 반환.</li>
</ul>
</li>
</ul>
<h4 id="reviewserializer-코드"><code>ReviewSerializer</code> 코드</h4>
<pre><code class="language-python">class ReviewSerializer(serializers.ModelSerializer):
    content = serializers.SerializerMethodField()  # 블러 처리된 콘텐츠 반환
    nickname = serializers.SerializerMethodField()  # 사용자 닉네임 반환

    def get_content(self, obj):
        blocked_users = self.context.get(&#39;blocked_users&#39;, [])
        if obj.user and obj.user.id in blocked_users:
            return &quot;이 사용자의 리뷰는 차단되어 표시되지 않습니다.&quot;
        return obj.content</code></pre>
<hr>
<h3 id="3-view에서-context-전달">3. View에서 <code>context</code> 전달</h3>
<p>Serializer가 블러 처리 로직을 실행하려면, <strong>차단된 사용자 목록</strong>(<code>blocked_users</code>)을 <code>context</code>로 전달해야 했습니다. 이를 위해 <code>ReviewAPIView</code>와 <code>ReviewDetailAPIView</code>에서 <code>context</code>를 설정했습니다.</p>
<h4 id="reviewapiview-수정"><code>ReviewAPIView</code> 수정</h4>
<pre><code class="language-python">class ReviewAPIView(APIView):
    def get(self, request):
        blocked_users = Block.objects.filter(blocker=request.user).values_list(&#39;blocked_user&#39;, flat=True) if request.user.is_authenticated else []
        reviews = Review.objects.all()
        serializer = ReviewSerializer(reviews, many=True, context={&#39;request&#39;: request, &#39;blocked_users&#39;: list(blocked_users)})
        return Response(serializer.data, status=status.HTTP_200_OK)</code></pre>
<hr>
<h2 id="트러블-슈팅">트러블 슈팅</h2>
<h3 id="문제-1-블러-처리가-작동하지-않음">문제 1: 블러 처리가 작동하지 않음</h3>
<p><strong>증상</strong>: 차단된 사용자의 리뷰가 여전히 원본 내용으로 표시됨.<br><strong>원인</strong>: <code>SerializerMethodField</code>를 누락하여 <code>get_content</code> 메서드가 호출되지 않음.<br><strong>해결</strong>: <code>content = serializers.SerializerMethodField()</code>를 추가하여 <code>get_content</code> 메서드가 호출되도록 수정.</p>
<hr>
<h3 id="문제-2-비회원의-경우-블러-처리-로직-동작">문제 2: 비회원의 경우 블러 처리 로직 동작</h3>
<p><strong>증상</strong>: 비회원 사용자가 조회했을 때도 블러 처리 로직이 작동.<br><strong>원인</strong>: <code>request.user.is_authenticated</code> 검사를 누락.<br><strong>해결</strong>: <code>request.user.is_authenticated</code>를 조건으로 추가하여 비회원 사용자는 블러 처리 로직을 무시하도록 수정.</p>
<pre><code class="language-python">def get_content(self, obj):
    if not self.context.get(&#39;request&#39;).user.is_authenticated:
        return obj.content
    ...</code></pre>
<hr>
<h2 id="최종-결과">최종 결과</h2>
<ol>
<li><strong>블러 처리 성공</strong>: 차단된 사용자의 리뷰가 <code>&quot;이 사용자의 리뷰는 차단되어 표시되지 않습니다.&quot;</code>로 표시됨.</li>
<li><strong>비회원 접근 처리</strong>: 비회원은 모든 리뷰를 원본 그대로 확인 가능.</li>
<li><strong>일관된 동작</strong>: 모든 View(API)에서 동일한 블러 처리 로직을 적용.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[차단 기능 구현 의사결정 과정]]></title>
            <link>https://velog.io/@gyu_p/%EC%B0%A8%EB%8B%A8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@gyu_p/%EC%B0%A8%EB%8B%A8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Thu, 16 Jan 2025 11:11:28 GMT</pubDate>
            <description><![CDATA[<h2 id="1-목표">1. <strong>목표</strong></h2>
<p><code>Block</code> 기능</p>
<ul>
<li>특정 유저를 차단하거나 차단 해제할 수 있도록 API를 제공</li>
<li>차단된 유저의 리뷰가 블러 처리되거나 제외됨</li>
<li>차단된 유저와의 채팅 제한</li>
</ul>
<hr>
<h2 id="2-의사결정-과정">2. <strong>의사결정 과정</strong></h2>
<h3 id="21-차단-모델-설계">2.1 <strong>차단 모델 설계</strong></h3>
<p>처음에는 Django의 <strong>Many-to-Many 필드</strong>를 사용하려고 했습니다. 하지만 아래와 같은 이유로 <strong>명시적인 <code>Block</code> 모델</strong>을 설계하기로 결정했습니다:</p>
<ol>
<li><strong>추가 메타데이터 저장 필요성</strong>: 차단 시점(<code>created_at</code>)이나 차단 사유(<code>reason</code>)를 저장하려면 Many-to-Many 필드만으로는 한계가 있음</li>
<li><strong>명확한 관계 표현</strong>: <code>blocker</code>(차단자)와 <code>blocked_user</code>(차단된 사용자)의 비대칭적 관계를 명확히 하기 위해 중간 모델이 더 적합</li>
<li><strong>확장성</strong>: 추후 차단 상태 변경 이력 기록, 통계 분석 등의 기능을 고려</li>
</ol>
<h4 id="최종-모델">최종 모델</h4>
<pre><code class="language-python">class Block(models.Model):
    blocker = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name=&quot;blocked_users&quot;
    )
    blocked_user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name=&quot;blocked_by_users&quot;
    )
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = [(&#39;blocker&#39;, &#39;blocked_user&#39;)]
        ordering = [&#39;-created_at&#39;]</code></pre>
<hr>
<h3 id="22-serializer-필요-여부">2.2 <strong>Serializer 필요 여부</strong></h3>
<p>초기에는 요청과 응답 데이터 처리를 위해 <code>Serializer</code>를 추가할지 고민했습니다. 하지만 구현 초기에 Serializer를 생략한 이유는 다음과 같습니다:</p>
<ol>
<li><strong>단순한 데이터 처리</strong>:<ul>
<li>차단/차단 해제 요청은 단일 필드(<code>blocked_user_id</code>)로 이루어짐</li>
<li>유효성 검증은 <code>get_object_or_404</code>로 충분히 처리 가능</li>
</ul>
</li>
<li><strong>직렬화할 데이터가 적음</strong>:<ul>
<li>클라이언트에게 반환하는 데이터는 간단한 상태 메시지와 차단된 유저 목록</li>
</ul>
</li>
</ol>
<p>하지만, 추후 확장성을 고려하여 <code>Serializer</code>를 추가하는 방안을 열어둠</p>
<hr>
<h3 id="23-차단-로직-구현">2.3 <strong>차단 로직 구현</strong></h3>
<p><code>APIView</code>를 기반으로 차단, 차단 해제, 차단된 유저 목록 조회 기능을 구현했습니다.</p>
<h4 id="주요-고려사항">주요 고려사항</h4>
<ol>
<li><strong>중복 차단 방지</strong>:<ul>
<li><code>Block.objects.get_or_create</code>를 사용하여 동일한 유저를 중복 차단하는 경우를 방지</li>
</ul>
</li>
<li><strong>차단 해제 처리</strong>:<ul>
<li><code>Block</code> 레코드 삭제 시 삭제된 레코드가 없으면 적절한 에러 메시지를 반환</li>
</ul>
</li>
<li><strong>목록 조회</strong>:<ul>
<li><code>select_related</code>를 사용해 쿼리 최적화를 고려</li>
</ul>
</li>
</ol>
<h4 id="최종-뷰-코드">최종 뷰 코드</h4>
<pre><code class="language-python">class BlockedUserAPIView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        blocked_users = Block.objects.filter(blocker=request.user).select_related(&#39;blocked_user&#39;)
        data = [{&quot;id&quot;: block.blocked_user.id, &quot;nickname&quot;: block.blocked_user.nickname} for block in blocked_users]
        return Response(data, status=status.HTTP_200_OK)

    def post(self, request):
        blocked_user_id = request.data.get(&quot;blocked_user_id&quot;)
        blocked_user = get_object_or_404(Account, id=blocked_user_id)
        block, created = Block.objects.get_or_create(blocker=request.user, blocked_user=blocked_user)
        if not created:
            return Response({&quot;message&quot;: &quot;이미 차단된 사용자입니다.&quot;}, status=status.HTTP_400_BAD_REQUEST)
        return Response({&quot;message&quot;: &quot;유저를 차단했습니다.&quot;}, status=status.HTTP_201_CREATED)

    def delete(self, request):
        blocked_user_id = request.data.get(&quot;blocked_user_id&quot;)
        blocked_user = get_object_or_404(Account, id=blocked_user_id)
        deleted, _ = Block.objects.filter(blocker=request.user, blocked_user=blocked_user).delete()
        if not deleted:
            return Response({&quot;message&quot;: &quot;차단된 유저가 아닙니다.&quot;}, status=status.HTTP_400_BAD_REQUEST)
        return Response({&quot;message&quot;: &quot;유저 차단을 해제했습니다.&quot;}, status=status.HTTP_204_NO_CONTENT)</code></pre>
<hr>
<h3 id="24-리뷰-차단-처리">2.4 <strong>리뷰 차단 처리</strong></h3>
<p>차단된 유저의 리뷰를 블러 처리하거나 목록에서 제외하는 방안을 결정했습니다.</p>
<h4 id="방법">방법</h4>
<ol>
<li><strong>블러 처리</strong>:<ul>
<li>리뷰 내용 직렬화 시, 차단된 유저의 리뷰라면 <code>&quot;차단된 사용자가 작성한 리뷰입니다.&quot;</code>를 반환</li>
</ul>
</li>
<li><strong>리뷰 제외</strong>:<ul>
<li><code>ReviewAPIView</code>에서 차단된 유저의 리뷰를 필터링</li>
</ul>
</li>
</ol>
<hr>
<h3 id="25-url-구성">2.5 <strong>URL 구성</strong></h3>
<p>API 뷰를 다음 URL에 연결:</p>
<ul>
<li><code>GET /accounts/block/</code> : 차단된 유저 목록</li>
<li><code>POST /accounts/block/</code> : 유저 차단</li>
<li><code>DELETE /accounts/block/</code> : 유저 차단 해제</li>
</ul>
<hr>
<h2 id="3-테스트">3. <strong>테스트</strong></h2>
<p>POSTMAN을 사용하여 다음 시나리오를 테스트:</p>
<ol>
<li>유저 차단: 동일 유저 중복 차단 방지 확인</li>
<li>유저 차단 해제: 차단되지 않은 유저 해제 시 에러 반환</li>
<li>차단된 유저 목록 조회: 올바른 데이터를 반환하는지 확인</li>
</ol>
<hr>
<h2 id="4-느낀-점-및-개선-방향">4. <strong>느낀 점 및 개선 방향</strong></h2>
<ul>
<li><strong>현재의 단순 로직</strong>은 충분히 작동하지만, 차단 사유나 차단된 상태의 리뷰를 확인하는 버튼과 같은 추가 기능을 고민할 필요가 있음</li>
<li>추후 차단 상태를 기준으로 알림, 채팅 제한 기능을 확장할 계획</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>