<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>찬스맨의 프로그래밍 스토리</title>
    <link>https://chansman.tistory.com/</link>
    <description>안녕하세요! 코딩을 시작한 지 얼마 되지 않은  초보 개발자 찬스맨입니다. 이 블로그는 제 학습 기록을 남기고, 다양한 코딩 실습을 통해 성장하는 과정을 공유하려고 합니다. 초보자의 눈높이에 맞춘 실습과 팁, 그리고 개발하면서 겪은 어려움과 해결 과정을 솔직하게 풀어내려 합니다. 함께 성장하는 개발자 커뮤니티가 되기를 바랍니다.</description>
    <language>ko</language>
    <pubDate>Wed, 17 Jun 2026 01:50:57 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Chansman</managingEditor>
    <image>
      <title>찬스맨의 프로그래밍 스토리</title>
      <url>https://tistory1.daumcdn.net/tistory/7769339/attach/b7812785f6bd48d08e8f1cf12fd119b8</url>
      <link>https://chansman.tistory.com</link>
    </image>
    <item>
      <title>이력서 초안</title>
      <link>https://chansman.tistory.com/1135</link>
      <description>&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/LW4Xr/dJMb9MQeNB0/WubBdvDHIscNWdhpmTC1mk/BE_%EC%9D%B4%EC%83%81%EC%9D%B8%20%EC%9D%B4%EB%A0%A5%EC%84%9C.pdf?attach=1&amp;amp;knm=tfile.pdf&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;BE_이상인 이력서.pdf&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;2.11MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <author>Chansman</author>
      <guid isPermaLink="true">https://chansman.tistory.com/1135</guid>
      <comments>https://chansman.tistory.com/1135#entry1135comment</comments>
      <pubDate>Mon, 25 Aug 2025 15:20:56 +0900</pubDate>
    </item>
    <item>
      <title>이력서 수정안 (강사님 feedback 요함)</title>
      <link>https://chansman.tistory.com/1134</link>
      <description>&lt;h2 data-end=&quot;79&quot; data-start=&quot;69&quot; data-ke-size=&quot;size26&quot;&gt;Profile&lt;/h2&gt;
&lt;p data-end=&quot;494&quot; data-start=&quot;81&quot; data-ke-size=&quot;size16&quot;&gt;한국과 뉴질랜드에서 영업&amp;middot;운영&amp;middot;서비스 등 다양한 업무를 경험하며, 문제를 구조화하고 협업으로 해결하는 방식을 체득해 왔습니다.&lt;/p&gt;
&lt;p data-end=&quot;494&quot; data-start=&quot;81&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;494&quot; data-start=&quot;81&quot; data-ke-size=&quot;size16&quot;&gt;AI 기술이 빠르게 발전하면서 개발에 대한 진입 장벽이 낮아지고, 나도 직접 무언가를 만들어 볼 수 있겠다는 확신이 생겼습니다.&lt;br /&gt;이를 계기로 백엔드 개발자로 전향해, 실제 데이터를 수집&amp;middot;처리&amp;middot;제공하는 서비스를 구현해보며 역량을 키우고 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;494&quot; data-start=&quot;81&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;494&quot; data-start=&quot;81&quot; data-ke-size=&quot;size16&quot;&gt;귀국 후 국가지원 부트캠프 &amp;lsquo;오즈코딩스쿨&amp;rsquo;에서 Django와 FastAPI 기반의 API 개발, 자동화 스케줄링, 크롤링, 배포까지 아우르는 백엔드 실습을 수행하며 개인&amp;middot;팀 프로젝트를 완주했습니다. 4번의 팀프로젝트의경우 3번의 팀장을 맡았으며 1번은 개인프로젝트로 진행하였습니다. 팀프로젝트의 경우 팀원들과의 소통 스케쥴링 이탈자가 발생하지않게 관리하였으며 개인프로젝트를 통하여 백앤드의 전체흐름을 이해하는데 주력하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;494&quot; data-start=&quot;81&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;학습에 관한 내용정리 및 학습 중 마주한 에러와 트러블슈팅 과정을 정리해 공유하기 위해 기술 블로그를 운영 중입니다.&lt;br /&gt;&lt;a href=&quot;https://chansman.tistory.com/&quot;&gt;https://chansman.tistory.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756102069925&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;찬스맨의 프로그래밍 스토리&quot; data-og-description=&quot;안녕하세요! 코딩을 시작한 지 얼마 되지 않은 초보 개발자 찬스맨입니다. 이 블로그는 제 학습 기록을 남기고, 다양한 코딩 실습을 통해 성장하는 과정을 공유하려고 합니다. 초보자의 눈높이&quot; data-og-host=&quot;chansman.tistory.com&quot; data-og-source-url=&quot;https://chansman.tistory.com/&quot; data-og-url=&quot;https://chansman.tistory.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/9zTsJ/hyZC2jtmt9/OAX7Js5fmqy9I6J4pbPU20/img.jpg?width=800&amp;amp;height=599&amp;amp;face=0_0_800_599,https://scrap.kakaocdn.net/dn/YJzCR/hyZCXvG1ET/W5yokDQzxJb9yvgZLbGgl0/img.jpg?width=800&amp;amp;height=599&amp;amp;face=0_0_800_599&quot;&gt;&lt;a href=&quot;https://chansman.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chansman.tistory.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/9zTsJ/hyZC2jtmt9/OAX7Js5fmqy9I6J4pbPU20/img.jpg?width=800&amp;amp;height=599&amp;amp;face=0_0_800_599,https://scrap.kakaocdn.net/dn/YJzCR/hyZCXvG1ET/W5yokDQzxJb9yvgZLbGgl0/img.jpg?width=800&amp;amp;height=599&amp;amp;face=0_0_800_599');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;찬스맨의 프로그래밍 스토리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요! 코딩을 시작한 지 얼마 되지 않은 초보 개발자 찬스맨입니다. 이 블로그는 제 학습 기록을 남기고, 다양한 코딩 실습을 통해 성장하는 과정을 공유하려고 합니다. 초보자의 눈높이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chansman.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;githudb 의 경우 아래와같이 관리되고있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/rainsos&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/rainsos&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;499&quot; data-start=&quot;496&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;522&quot; data-start=&quot;501&quot; data-ke-size=&quot;size26&quot;&gt;Project Experience&lt;/h2&gt;
&lt;p data-end=&quot;754&quot; data-start=&quot;524&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;블로기&lt;/b&gt;는 오즈코딩스쿨에서 팀 프로젝트로 2인으로 진행하였고, 백엔드에서 프론트까지 실제로 모두 구현해보았던 사례입니다.&lt;br /&gt;실제로 배포를 진행하였고 7일간의 블로그 운영을 통해 수익화를 이루어냈으며, AI 콘텐츠 자동화 서비스를 목표로 직접 기획해 진행한 프로젝트입니다.&lt;br /&gt;Django와 FastAPI를 역할별로 분리하고, 스크래핑 &amp;rarr; 생성 &amp;rarr; 검수 &amp;rarr; 게시까지의 전체 백엔드 파이프라인을 구축했습니다.&lt;/p&gt;
&lt;p data-end=&quot;754&quot; data-start=&quot;524&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blogi.store/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blogi.store/&lt;/a&gt;&lt;/p&gt;
&lt;hr data-end=&quot;759&quot; data-start=&quot;756&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;770&quot; data-start=&quot;761&quot; data-ke-size=&quot;size23&quot;&gt;역할 구조&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1007&quot; data-start=&quot;771&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;842&quot; data-start=&quot;771&quot;&gt;&lt;b&gt;Django (B)&lt;/b&gt;: 사용자 인증, 관심 키워드 관리, 키워드 클릭 로그, 생성 콘텐츠 저장 및 관리자 기능 전담&lt;/li&gt;
&lt;li data-end=&quot;934&quot; data-start=&quot;843&quot;&gt;&lt;b&gt;FastAPI (A)&lt;/b&gt;: 기사 수집(Playwright), 이미지 수집(Kakao API), HyperCLOVA X 연동, 내부 API 통한 결과 반환&lt;/li&gt;
&lt;li data-end=&quot;1007&quot; data-start=&quot;935&quot;&gt;&lt;b&gt;공통&lt;/b&gt;: PostgreSQL, Redis, Docker, NCP 서버, Github Actions 기반 배포 및 모니터링&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1012&quot; data-start=&quot;1009&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1026&quot; data-start=&quot;1014&quot; data-ke-size=&quot;size23&quot;&gt;주요 구현 기능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1307&quot; data-start=&quot;1027&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1082&quot; data-start=&quot;1027&quot;&gt;Django: 공개/관리/내부 API로 모듈 분리, 관리자 대시보드와 사용자 결과 UI 제공&lt;/li&gt;
&lt;li data-end=&quot;1148&quot; data-start=&quot;1083&quot;&gt;FastAPI: 키워드 기준 기사 본문 스크래핑, 이미지 수집, AI 글 생성, 프리뷰&amp;middot;PDF&amp;middot;복사 기능 지원&lt;/li&gt;
&lt;li data-end=&quot;1204&quot; data-start=&quot;1149&quot;&gt;Playwright: 키워드 기반 뉴스/블로그 본문 스크래핑, 네이버 API 백업 로직 탑재&lt;/li&gt;
&lt;li data-end=&quot;1258&quot; data-start=&quot;1205&quot;&gt;HyperCLOVA: 프롬프트 구성 및 생성 요청 전송, 실패 로그/리트라이 핸들링 구현&lt;/li&gt;
&lt;li data-end=&quot;1307&quot; data-start=&quot;1259&quot;&gt;배포: Docker Compose + Nginx + Gunicorn으로 NCP 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1312&quot; data-start=&quot;1309&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1355&quot; data-start=&quot;1314&quot; data-ke-size=&quot;size23&quot;&gt;문제 발생 및 해결 사례 &amp;mdash; Playwright CPU/메모리 폭주&lt;/h3&gt;
&lt;p data-end=&quot;1481&quot; data-start=&quot;1357&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 요약&lt;/b&gt;&lt;br /&gt;기사 수집 과정에서 Playwright가 다수의 Chromium 프로세스를 동시에 생성하면서, 일부 프로세스가 종료되지 않고 &lt;b&gt;좀비 상태로 남아 메모리 누수 및 CPU 100% 점유&lt;/b&gt;가 반복됨.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1586&quot; data-start=&quot;1482&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1507&quot; data-start=&quot;1482&quot;&gt;평균 메모리 사용량: 4.3GiB 이상&lt;/li&gt;
&lt;li data-end=&quot;1528&quot; data-start=&quot;1508&quot;&gt;CPU 점유율: 최대 101%&lt;/li&gt;
&lt;li data-end=&quot;1554&quot; data-start=&quot;1529&quot;&gt;크래시 발생: 1회/4회 실행 중 발생&lt;/li&gt;
&lt;li data-end=&quot;1586&quot; data-start=&quot;1555&quot;&gt;컨테이너 장애 &amp;rarr; 결과 생성 실패율 상승(약 30%)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1599&quot; data-start=&quot;1588&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 방안&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1815&quot; data-start=&quot;1600&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1650&quot; data-start=&quot;1600&quot;&gt;Playwright 실행 구조 변경: 브라우저 1개 + 컨텍스트 풀 방식으로 일원화&lt;/li&gt;
&lt;li data-end=&quot;1690&quot; data-start=&quot;1651&quot;&gt;세마포어(최대 동시 실행 2~4개) 적용 &amp;rarr; 동시 생성 수 제한&lt;/li&gt;
&lt;li data-end=&quot;1719&quot; data-start=&quot;1691&quot;&gt;n건마다 브라우저 재기동 &amp;rarr; 세션 누적 방지&lt;/li&gt;
&lt;li data-end=&quot;1772&quot; data-start=&quot;1720&quot;&gt;모든 실행에 try/finally close() 강제 적용 &amp;rarr; 좀비 프로세스 완전 제거&lt;/li&gt;
&lt;li data-end=&quot;1815&quot; data-start=&quot;1773&quot;&gt;docker-compose.yml에 메모리 상한(6GiB) 설정 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1828&quot; data-start=&quot;1817&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과 수치&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1977&quot; data-start=&quot;1829&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1865&quot; data-start=&quot;1829&quot;&gt;CPU 점유율 최대 101% &amp;rarr; 56%로 감소 (-45%)&lt;/li&gt;
&lt;li data-end=&quot;1903&quot; data-start=&quot;1866&quot;&gt;메모리 사용량 4.3GiB &amp;rarr; 평균 2.1GiB로 절반 이하&lt;/li&gt;
&lt;li data-end=&quot;1938&quot; data-start=&quot;1904&quot;&gt;좀비 프로세스 발생 0건, 타임아웃&amp;middot;크래시 7일간 0건&lt;/li&gt;
&lt;li data-end=&quot;1977&quot; data-start=&quot;1939&quot;&gt;실패율 30% &amp;rarr; 0%로 안정화, 기사 수집 성공률 100% 유지&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Chansman</author>
      <guid isPermaLink="true">https://chansman.tistory.com/1134</guid>
      <comments>https://chansman.tistory.com/1134#entry1134comment</comments>
      <pubDate>Mon, 25 Aug 2025 15:09:21 +0900</pubDate>
    </item>
    <item>
      <title>Django/DRF pk vs id 파라미터 불일치로 인한 500 오류 &amp;mdash; 원인과 해결</title>
      <link>https://chansman.tistory.com/1130</link>
      <description>&lt;h1&gt;Django/DRF pk vs id 파라미터 불일치로 인한 500 오류 &amp;mdash; 원인과 해결&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 저장(복사) 버튼 클릭 시 500 발생 &amp;rarr; 원인: URL 파라미터명(pk)과 뷰 시그니처(id) 불일치.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TL;DR&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;증상: 프론트에서 AxiosError: 500, 백엔드에서 TypeError: PostCopyAPIView.post() got an unexpected keyword argument 'pk'.&lt;/li&gt;
&lt;li&gt;원인: urls.py에서 posts/&amp;lt;int:pk&amp;gt;/copy/로 등록 &amp;rarr; resolver가 {'pk': 376}을 전달. 그러나 뷰는 def post(self, request, id)만 받음 &amp;rarr; 이름 불일치(TypeError)로 500.&lt;/li&gt;
&lt;li&gt;해결:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;URL 통일&lt;/b&gt;: posts/&amp;lt;int:id&amp;gt;/copy/로 되돌림.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방어 패치(선택)&lt;/b&gt;: 뷰에서 id/pk 모두 수용(kwargs).&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;검증: resolve/inspect로 최종 매칭 파라미터와 로딩된 시그니처 확인.&lt;/li&gt;
&lt;li&gt;재발 방지: 파라미터명 일관 가이드, 단위 테스트/CI 가드, 중복 경로 제거, 캐시/오토리로드 주의.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경 &amp;amp; 환경&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프론트: 저장(=복사) 버튼 클릭 시 백엔드 /api/posts/&amp;lt;post_id&amp;gt;/copy/ 호출.&lt;/li&gt;
&lt;li&gt;백엔드: Django/DRF, 함수/클래스 혼용. 일부 경로에서 &amp;lt;int:id&amp;gt;/&amp;lt;int:pk&amp;gt;가 혼재.&lt;/li&gt;
&lt;li&gt;관찰 로그(요약):
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 401(비로그인 상태의 POST) &amp;rarr; 로그인 후 재호출은 200.&lt;/li&gt;
&lt;li&gt;복사 시도 시 500: TypeError: ... unexpected keyword argument 'pk'.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 로그 스냅샷&lt;/h2&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;AxiosError: Request failed with status code 500
...
TypeError: PostCopyAPIView.post() got an unexpected keyword argument 'pk'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Django resolve 결과(로컬/배포 동일):&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;resolve('/api/posts/376/copy/').kwargs == {'pk': 376}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 중 뷰 시그니처(초기 상태):&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;PostCopyAPIView.post -&amp;gt; (self, request, id)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인 분석 (한 줄 요약)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;URL은 pk 라벨로 값을 전달&lt;/b&gt; ({'pk': 376})&lt;/li&gt;
&lt;li&gt;&lt;b&gt;뷰는 id 라벨만 받도록 구현&lt;/b&gt; (def post(..., id))&lt;/li&gt;
&lt;li&gt;파라미터 &lt;b&gt;이름 불일치&lt;/b&gt;로 Python이 unexpected keyword argument 'pk' 예외 발생 &amp;rarr; DRF에서 500으로 응답.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &amp;lt;int:...&amp;gt;에서 int는 타입이고, 진짜 중요한 건 **라벨 이름(pk/id)**입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;재현 절차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;urls.py에 다음 경로가 존재:&lt;/li&gt;
&lt;li&gt;path(&quot;posts/&amp;lt;int:pk&amp;gt;/copy/&quot;, PostCopyAPIView.as_view(), name=&quot;post-copy&quot;)&lt;/li&gt;
&lt;li&gt;뷰 시그니처가 다음과 같음:&lt;/li&gt;
&lt;li&gt;def post(self, request, id): ...&lt;/li&gt;
&lt;li&gt;/api/posts/376/copy/ 호출 시 resolver가 {'pk': 376} 전달 &amp;rarr; 뷰는 id만 받음 &amp;rarr; TypeError.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 전략&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;A) URL 통일(권장)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;urls.py 수정:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;- path(&quot;posts/&amp;lt;int:pk&amp;gt;/copy/&quot;, PostCopyAPIView.as_view(), name=&quot;post-copy&quot;),
+ path(&quot;posts/&amp;lt;int:id&amp;gt;/copy/&quot;, PostCopyAPIView.as_view(), name=&quot;post-copy&quot;),
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;B) 뷰 방어 패치(선택, 환경 무관 안정성)&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;class PostCopyAPIView(APIView):
    permission_classes = [IsUser]
    serializer_class = CopyLogSerializer

    def post(self, request, *args, **kwargs):
        post_id = kwargs.get(&quot;id&quot;) or kwargs.get(&quot;pk&quot;) \
                  or request.data.get(&quot;id&quot;) or request.query_params.get(&quot;id&quot;)
        try:
            post_id = int(post_id)
        except (TypeError, ValueError):
            return Response({&quot;detail&quot;: &quot;post id missing&quot;}, status=400)

        user = request.user
        post = get_object_or_404(GeneratedPost, id=post_id)

        # 1회만 복사 로그 생성 (중복 방지하려면 get_or_create 권장)
        copy_log = CopyLog.objects.create(user=user, post=post)

        # DB 레벨에서 copy_count +1
        GeneratedPost.objects.filter(id=post.id).update(copy_count=F(&quot;copy_count&quot;) + 1)

        # 필요 시 serializer.data 반환 가능
        return Response({&quot;message&quot;: &quot;복사 기록이 저장되었습니다.&quot;}, status=200)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 팁: 팀원이 다시 pk로 바꿔도 안 터지도록, 방어 패치는 유지해도 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;적용 코드(최종 상태)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;urls.py&lt;/li&gt;
&lt;li&gt;path(&quot;posts/&amp;lt;int:id&amp;gt;/copy/&quot;, PostCopyAPIView.as_view(), name=&quot;post-copy&quot;),&lt;/li&gt;
&lt;li&gt;views.py (방어형 그대로 유지)&lt;/li&gt;
&lt;li&gt;def post(self, request, *args, **kwargs): post_id = kwargs.get(&quot;id&quot;) or kwargs.get(&quot;pk&quot;) \ or request.data.get(&quot;id&quot;) or request.query_params.get(&quot;id&quot;) ...&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검증 방법&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;파라미터 매칭 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python manage.py shell -c &quot;from django.urls import resolve; print(resolve('/api/posts/376/copy/').kwargs)&quot;
# 기대: {'id': 376}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실행 중 시그니처/소스 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;python manage.py shell -c &quot;import inspect; from django.urls import resolve; VC=resolve('/api/posts/376/copy/').func.view_class; print(inspect.getfile(VC)); print(inspect.signature(VC.post))&quot;
# 기대: (self, request, *args, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;캐시 정리 &amp;amp; 완전 재기동(필수)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;find . -name &quot;__pycache__&quot; -type d -exec rm -rf {} +
find . -name &quot;*.pyc&quot; -delete
# runserver 재시작 또는 컨테이너 재기동
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;엔드포인트 호출 체크(로그인 필요)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;# 예: curl -H &quot;Authorization: Bearer &amp;lt;token&amp;gt;&quot; -X POST http://localhost:8000/api/posts/376/copy/
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 어떤 때는 서버에서 안 터졌나? (FAQ)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;URL 중복/등록 순서&lt;/b&gt;: 동일 패턴이 id/pk 두 버전으로 정의되어 있거나, DRF Router 포함 순서에 따라 먼저 매칭되는 쪽이 달라질 수 있음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐시/오토리로드&lt;/b&gt;: 개발 서버/컨테이너가 이전 아티팩트를 들고 있어 일시적으로 다른 경로가 적용된 상태였을 가능성.&lt;/li&gt;
&lt;li&gt;결론: 구조적으로 잠복 버그였고, 이번에 &lt;b&gt;URL 통일 + 방어 패치&lt;/b&gt;로 영구 해소.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;재발 방지 체크리스트&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;urls.py 전체에서 파라미터명 &lt;b&gt;일관성 유지&lt;/b&gt; (&amp;lt;int:id&amp;gt; 권장)&lt;/li&gt;
&lt;li&gt;DRF Router(detail=True) 사용 시, 뷰에서 *args, **kwargs 패턴으로 수용 후 내부에서 pk/id 정규화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중복 경로 제거&lt;/b&gt; (같은 URL 패턴이 2번 등록되지 않도록)&lt;/li&gt;
&lt;li&gt;단위 테스트 추가&lt;/li&gt;
&lt;li&gt;from django.urls import resolve def test_post_copy_uses_id_kwarg(): match = resolve('/api/posts/123/copy/') assert match.kwargs.get('id') == 123&lt;/li&gt;
&lt;li&gt;CI 가드(선택):&lt;/li&gt;
&lt;li&gt;# pk 버전 사용 금지 가드 예시 grep -R &quot;posts/&amp;lt;int:pk&amp;gt;/copy/&quot; -n django_app &amp;amp;&amp;amp; { echo &quot;Do not use pk for copy URL&quot;; exit 1; } || true&lt;/li&gt;
&lt;li&gt;코드 주석으로 팀 합의 명시: &quot;게시글 라우트 파라미터는 id로 통일&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부록: 관련 커맨드 모음&lt;/h2&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;# 현재 매칭 파라미터 확인
python manage.py shell -c &quot;from django.urls import resolve; print(resolve('/api/posts/376/copy/').kwargs)&quot;

# 실행 중 시그니처/소스 경로 확인
python manage.py shell -c &quot;import inspect; from django.urls import resolve; VC=resolve('/api/posts/376/copy/').func.view_class; print(inspect.getfile(VC)); print(inspect.signature(VC.post))&quot;

# 중복 클래스 탐색
grep -R &quot;class PostCopyAPIView&quot; -n django_app/apps

# 중복 경로 탐색
grep -R &quot;posts/&amp;lt;int:&quot; -n django_app/apps | grep &quot;/copy&quot;

# 캐시 삭제
find . -name &quot;__pycache__&quot; -type d -exec rm -rf {} +
find . -name &quot;*.pyc&quot; -delete
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 이슈는 &lt;b&gt;파라미터 이름 불일치&lt;/b&gt;라는 작지만 치명적인 설정 차이에서 시작됐습니다.&lt;br /&gt;URL 통일 + 방어 패치 + 검증/가드 절차로 동일 유형의 문제를 확실하게 차단할 수 있습니다. 팀 규칙에 &amp;ldquo;게시글 라우트 파라미터는 id로 통일&amp;rdquo;을 명문화해 주세요.&lt;/p&gt;</description>
      <author>Chansman</author>
      <guid isPermaLink="true">https://chansman.tistory.com/1130</guid>
      <comments>https://chansman.tistory.com/1130#entry1130comment</comments>
      <pubDate>Mon, 18 Aug 2025 21:06:31 +0900</pubDate>
    </item>
    <item>
      <title>  NCP 서버 모니터링 안됨 현상</title>
      <link>https://chansman.tistory.com/1129</link>
      <description>&lt;h1&gt;  NCP 서버 모니터링 안됨 현상&amp;nbsp;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 강제 종료 후 다시 켜니, &lt;b&gt;NCP Cloud Insight&lt;/b&gt; 대시보드에서 CPU/메모리 사용량이 표시되지 않는 문제가 발생했습니다.&lt;br /&gt;콘솔에는 아무런 오류 메시지가 없었지만, 지표가 갱신되지 않아 원인 파악이 필요했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 원인 추측&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NCP 고객센터 답변에 따르면, 서버 모니터링 지표는 &lt;b&gt;Cloud Insight Agent&lt;/b&gt;가 동작해야 수집됩니다.&lt;br /&gt;강제 종료 과정에서 이 Agent 프로세스가 비정상 종료되었을 가능성이 크다는 안내를 받았습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 확인 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH로 서버 접속 후, Agent 프로세스 존재 여부를 확인했습니다:&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;ps -ef | grep agent
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  결과: agent.py, agent_updater.py 프로세스가 보이지 않고, grep 명령어만 출력됨 &amp;rarr; &lt;b&gt;Agent 미동작 상태 확인&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 경로(/home1/nbpmon/agent_controller_linux/)로 이동 후 &lt;b&gt;재시작 스크립트&lt;/b&gt; 실행:&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;cd /home1/nbpmon/agent_controller_linux/
./restart_agent.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다시 확인:&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;ps -ef | grep -E &quot;agent.py|agent_updater.py&quot; | grep -v grep
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 결과:&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;/home1/nbpmon/.../agent_updater.py
/home1/nbpmon/.../agent.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Agent가 정상적으로 기동됨을 확인.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하드 리셋 및 설치방법&lt;/p&gt;
&lt;pre id=&quot;code_1755503221353&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 0) 경로 이동
cd /home1/nbpmon/agent_controller_linux

# 1) 완전 중지(실패해도 무시)
chmod +x stop_agent.sh start_agent.sh restart_agent.sh uninstall_agent.sh install_agent.sh || true
./stop_agent.sh || true
pkill -9 -f &quot;agent.py|agent_updater.py&quot; || true

# 2) 찌꺼기(pid/로그) 제거
find install_folders -name &quot;*.pid&quot; -delete 2&amp;gt;/dev/null
rm -rf logs/* 2&amp;gt;/dev/null

# 3) 재설치 &amp;rarr; 기동
./uninstall_agent.sh || true
./install_agent.sh
./start_agent.sh

# 4) 프로세스/로그 즉시 확인
ps -ef | grep -E &quot;agent.py|agent_updater.py&quot; | grep -v grep
tail -n 120 logs/agent.log&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 후속 조치&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;NCP 콘솔 확인&lt;/b&gt;&lt;br /&gt;약 1~3분 후 Cloud Insight 대시보드에서 CPU/메모리 지표가 정상적으로 표시됨.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동 기동 설정&lt;/b&gt;&lt;br /&gt;서버 재부팅 시 자동 실행되도록 systemd 서비스 등록:&lt;/li&gt;
&lt;li&gt;cd /home1/nbpmon/agent_controller_linux cp nsight-agent.service /etc/systemd/system/nsight-agent.service systemctl daemon-reload systemctl enable --now nsight-agent&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그 확인 (문제 발생 시)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;cd /home1/nbpmon/agent_controller_linux/logs tail -n 200 agent.log tail -n 200 agent_updater.log&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문제&lt;/b&gt;: 서버 강제 종료 이후 모니터링 지표 미표시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인&lt;/b&gt;: Cloud Insight Agent 프로세스 종료&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결&lt;/b&gt;: restart_agent.sh 실행 후 프로세스 정상 기동&lt;/li&gt;
&lt;li&gt;&lt;b&gt;후속 조치&lt;/b&gt;: systemd 자동 기동 등록 + 로그 모니터링&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  결론: &lt;b&gt;Cloud Insight 지표가 안 잡히면, 우선 Agent 상태부터 확인하자!&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 같은 문제가 생겨도 위 순서대로 점검하면 빠르게 복구할 수 있습니다  &lt;/p&gt;</description>
      <author>Chansman</author>
      <guid isPermaLink="true">https://chansman.tistory.com/1129</guid>
      <comments>https://chansman.tistory.com/1129#entry1129comment</comments>
      <pubDate>Mon, 18 Aug 2025 16:34:18 +0900</pubDate>
    </item>
    <item>
      <title>  서버에서 Playwright 좀비 프로세스(headless_shell) 확인 및 삭제 방법</title>
      <link>https://chansman.tistory.com/1128</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  서버에서 Playwright 좀비 프로세스(headless_shell) 확인 및 삭제 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI + Playwright 기반 스크래핑 환경에서는 &lt;b&gt;브라우저 컨텍스트 종료 누락&lt;/b&gt;이나 &lt;b&gt;부모 프로세스 미회수&lt;/b&gt;로 인해 headless_shell &amp;lt;defunct&amp;gt; 프로세스가 남아 서버 메모리/CPU를 잠식하는 경우가 발생할 수 있습니다.&lt;br /&gt;이 글에서는 &lt;b&gt;좀비 프로세스 확인 &amp;rarr; 종료 &amp;rarr; 재발 방지 &amp;rarr; 모니터링&lt;/b&gt;까지 정리합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 현재 좀비 프로세스 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀비 프로세스는 상태 코드(STAT)가 Z로 표시됩니다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;ps -eo pid,ppid,stat,cmd | grep 'Z' | grep -v grep
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 출력:&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;170750  169780 Z    [headless_shell] &amp;lt;defunct&amp;gt;
170751  169780 Z    [headless_shell] &amp;lt;defunct&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PID : 좀비 프로세스 ID (kill 불가능, 부모 종료 시 정리됨)&lt;/li&gt;
&lt;li&gt;PPID: 부모 프로세스 ID&lt;/li&gt;
&lt;li&gt;[headless_shell] &amp;lt;defunct&amp;gt; : Playwright 브라우저 잔여 프로세스&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 부모 프로세스 확인&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;ps -p &amp;lt;PPID&amp;gt; -o pid,ppid,stat,cmd
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;ps -p 169780 -o pid,ppid,stat,cmd
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;PID     PPID STAT CMD
169780  169758 Ssl  /usr/local/bin/python3.12 /usr/local/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;부모가 FastAPI(Uvicorn) 프로세스&lt;/b&gt;임을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 좀비 프로세스 제거 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) FastAPI 컨테이너 재시작&lt;/h4&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose restart fastapi
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker restart fastapi
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 부모 프로세스 재시작 시 하위 좀비들이 정리됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 전체 프로세스 종료 후 재기동&lt;/h4&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;docker compose down
docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 메모리/CPU 상태 확인&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실시간 모니터링&lt;/h4&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;htop
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 간단 확인:&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;free -h
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 예:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Mem:           15Gi       4.0Gi       10Gi       0.5Gi       1.2Gi       12Gi
Swap:            0B          0B          0B
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CPU 사용량 상위 프로세스 확인&lt;/h4&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;ps -eo pid,ppid,%cpu,%mem,stat,cmd --sort=-%cpu | head -n 20
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 좀비 프로세스 재발 방지&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;브라우저 컨텍스트 종료 철저&lt;/b&gt;&amp;rarr; 키워드 처리 후 반드시 컨텍스트 닫기&lt;/li&gt;
&lt;li&gt;from app.features.internal.fetch_article.scraper.playwright_browser import close_all_contexts await close_all_contexts()&lt;/li&gt;
&lt;li&gt;&lt;b&gt;브라우저 주기적 재가동&lt;/b&gt;&amp;rarr; N회 처리마다 브라우저 자체를 재시작&lt;/li&gt;
&lt;li&gt;from app.features.internal.fetch_article.scraper.playwright_browser import recycle_browser await recycle_browser()&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FastAPI 종료 이벤트에서 정리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;@app.on_event(&quot;shutdown&quot;) async def _on_shutdown(): await recycle_browser()&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 좀비 감시 스크립트 예시&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
while true; do
    echo &quot;==== $(date) ====&quot;
    ps -eo pid,ppid,stat,cmd | grep 'Z' | grep -v grep
    free -h
    sleep 60
done
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/root/check_zombies.sh 로 저장&lt;/li&gt;
&lt;li&gt;실행: bash /root/check_zombies.sh&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;정리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Z 상태 프로세스 = 좀비 (kill 불가, 부모 종료 필요)&lt;/li&gt;
&lt;li&gt;부모 확인 후 재시작으로 제거&lt;/li&gt;
&lt;li&gt;컨텍스트/브라우저 종료 로직 철저하게 유지&lt;/li&gt;
&lt;li&gt;정기적인 모니터링으로 재발 방지&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Chansman</author>
      <guid isPermaLink="true">https://chansman.tistory.com/1128</guid>
      <comments>https://chansman.tistory.com/1128#entry1128comment</comments>
      <pubDate>Fri, 15 Aug 2025 21:36:24 +0900</pubDate>
    </item>
    <item>
      <title>  서버에서 CPU/메모리 사용량 확인 및 점검 방법</title>
      <link>https://chansman.tistory.com/1126</link>
      <description>&lt;h1&gt;  서버에서 CPU/메모리 사용량 확인 및 점검 방법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 운영 중 CPU나 메모리 사용량이 갑자기 높아지면 성능 저하나 서비스 중단이 발생할 수 있습니다.&lt;br /&gt;아래 방법을 사용하면 SSH로 접속해 &lt;b&gt;실시간 모니터링&lt;/b&gt;과 &lt;b&gt;원인 파악&lt;/b&gt;을 빠르게 할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣ CPU 사용량 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실시간 보기&lt;/h3&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;top -o %CPU
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU 사용률 기준으로 내림차순 정렬됩니다.&lt;/li&gt;
&lt;li&gt;%CPU가 높은 프로세스를 확인하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상위 20개만 보기&lt;/h3&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;ps aux --sort=-%cpu | head -n 20
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU를 많이 쓰는 프로세스만 빠르게 확인할 때 유용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPU + 메모리 같이 보기&lt;/h3&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;ps -eo pid,ppid,cmd,%cpu,%mem --sort=-%cpu | head -n 20
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU와 메모리 점유율을 동시에 확인해 종합적으로 판단할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣ 메모리 사용량 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실시간 보기&lt;/h3&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;top -o %MEM
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 사용량 기준으로 내림차순 정렬됩니다.&lt;/li&gt;
&lt;li&gt;RES(실제 메모리)와 %MEM(비율)을 참고하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상위 20개만 보기&lt;/h3&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;ps aux --sort=-%mem | head -n 20
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리를 많이 쓰는 프로세스만 출력합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPU + 메모리 같이 보기&lt;/h3&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%mem | head -n 20
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리와 CPU 사용량을 한 번에 체크할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3️⃣ Docker 컨테이너별 자원 사용량&lt;/h2&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;docker stats --no-stream
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 컨테이너의 CPU&amp;middot;메모리 사용량을 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4️⃣ 불필요한 프로세스 종료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 후 불필요한 프로세스는 PID를 이용해 종료합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;kill PID
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 강제 종료:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kill -9 PID
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;팁&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU 폭주 원인은 보통 무한 루프나 비정상 대기 중인 스크립트, 크롤러(headless_shell, playwright, python 등)입니다.&lt;/li&gt;
&lt;li&gt;메모리 폭주는 캐시, DB 서버, 또는 종료되지 않은 대형 프로세스가 원인입니다.&lt;/li&gt;
&lt;li&gt;평소에 top과 docker stats 명령을 익혀두면 장애 대응 속도가 크게&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로젝트</category>
      <author>Chansman</author>
      <guid isPermaLink="true">https://chansman.tistory.com/1126</guid>
      <comments>https://chansman.tistory.com/1126#entry1126comment</comments>
      <pubDate>Thu, 14 Aug 2025 19:54:46 +0900</pubDate>
    </item>
    <item>
      <title>  서버에서 Playwright/Chrome 좀비 프로세스 청소하기</title>
      <link>https://chansman.tistory.com/1125</link>
      <description>&lt;h1&gt;  서버에서 Playwright/Chrome 좀비 프로세스 청소하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 환경(리눅스)에서 Playwright/Chrome 기반 크롤링이 오래 돌다 보면, headless_shell 프로세스가 남아서 CPU와 메모리를 계속 점유하는 경우가 있습니다.&lt;br /&gt;이 상태가 지속되면 서버 성능이 저하되고, 다른 서비스에도 영향을 줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 방법을 사용하면 서버 내부에서 &lt;b&gt;좀비 프로세스 정리 + Docker 컨테이너 종료 + 캐시 삭제&lt;/b&gt;까지 한 번에 할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣ headless_shell / Playwright / Chrome / Python 종료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 SSH 접속 후:&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;# 부드럽게 종료
pkill -f headless_shell || true
pkill -f &quot;chrome --headless&quot; || true
pkill -f playwright || true
pkill -f python || true
pkill -f uvicorn || true
pkill -f gunicorn || true
pkill -f &quot;manage.py&quot; || true

# 3초 후 남아있으면 강제 종료
sleep 3
pgrep -f &quot;headless_shell|chrome|playwright|python|uvicorn|gunicorn|manage.py&quot; | xargs -r kill -9
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣ Docker 컨테이너까지 멈추기&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;docker stop $(docker ps -q) 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3️⃣ Playwright 캐시 삭제&lt;/h2&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;rm -rf ~/.cache/ms-playwright/* 2&amp;gt;/dev/null || true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4️⃣ 서버 전용 정리 스크립트 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 쓴다면 아래처럼 스크립트로 저장해 두세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cleanup_server.sh&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
echo &quot;  Killing zombie processes...&quot;
pkill -f &quot;headless_shell|chrome|playwright|python|uvicorn|gunicorn|manage.py&quot; || true
sleep 2
pgrep -f &quot;headless_shell|chrome|playwright|python|uvicorn|gunicorn|manage.py&quot; | xargs -r kill -9

echo &quot;  Stopping Docker containers...&quot;
docker stop $(docker ps -q) 2&amp;gt;/dev/null

echo &quot;  Cleaning Playwright cache...&quot;
rm -rf ~/.cache/ms-playwright/* 2&amp;gt;/dev/null || true

echo &quot;✅ Done.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 권한 부여:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;chmod +x cleanup_server.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요할 때:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;./cleanup_server.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;결론:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에서 headless_shell 같은 좀비 프로세스가 남아 있으면 성능 저하가 발생합니다.&lt;/li&gt;
&lt;li&gt;위 방법으로 정리하면 메모리와 CPU를 회수하고, 서버 안정성을 높일 수 있습니다.&lt;/li&gt;
&lt;li&gt;주기적으로 점검하거나, 크론탭에 등록해서 자동 실행하는 것도 좋은 방법입니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로젝트</category>
      <author>Chansman</author>
      <guid isPermaLink="true">https://chansman.tistory.com/1125</guid>
      <comments>https://chansman.tistory.com/1125#entry1125comment</comments>
      <pubDate>Thu, 14 Aug 2025 19:52:34 +0900</pubDate>
    </item>
    <item>
      <title>  컴퓨터 느려질 때 WSL + Playwright 좀비 프로세스 청소하기</title>
      <link>https://chansman.tistory.com/1124</link>
      <description>&lt;h1&gt;  컴퓨터 느려질 때 WSL + Playwright 좀비 프로세스 청소하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WSL 환경에서 Playwright/Chrome 기반 크롤링을 오래 돌리다 보면, &lt;b&gt;headless_shell&lt;/b&gt; 프로세스가 메모리를 잡아먹고 안 내려가는 경우가 있습니다.&lt;br /&gt;이 상태가 오래 지속되면 Windows에서 &lt;b&gt;vmmemWSL&lt;/b&gt; 프로세스가 수 GB~수십 GB까지 커져 PC 전체가 느려집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 방법을 따라 하면 좀비 프로세스를 정리하고, 필요하면 WSL 전체 메모리 반환까지 할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣ 현재 메모리&amp;middot;CPU 점유 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Windows PowerShell&lt;/b&gt;에서:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# CPU/메모리 많이 쓰는 순서로 보기
Get-Process | Sort-Object CPU -desc | Select-Object -First 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 vmmemWSL이 크면 WSL 내부에서 무언가 메모리를 많이 쓰고 있는 것입니다.&lt;br /&gt;chrome, headless_shell, python 프로세스가 보이면 크롤링/스크래핑 작업이 원인일 확률이 높습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣ WSL 내부 좀비 프로세스 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PowerShell에서 WSL 진입:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;wsl
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WSL 안에서:&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;# 메모리 점유순 보기
top -o %MEM

# 크롤링 관련 프로세스만 보기
ps aux | egrep 'python|playwright|headless_shell|chrome|chromium' | grep -v grep
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3️⃣ 좀비 프로세스 종료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WSL 안에서:&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;# 부드럽게 종료
pkill -f headless_shell || true
pkill -f &quot;chrome --headless&quot; || true
pkill -f playwright || true
pkill -f python || true
pkill -f uvicorn || true
pkill -f gunicorn || true
pkill -f &quot;manage.py&quot; || true

# 3초 후 남아있으면 강제 종료
sleep 3
pgrep -f &quot;headless_shell|chrome|playwright|python|uvicorn|gunicorn|manage.py&quot; | xargs -r kill -9
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4️⃣ Docker 컨테이너도 종료&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;docker stop $(docker ps -q) 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5️⃣ 메모리를 바로 반환하고 싶을 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀비 프로세스를 정리했는데도 vmmemWSL이 큰 상태라면, PowerShell에서:&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;wsl --shutdown
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WSL이 완전히 종료되면서 메모리가 Windows로 반환됩니다.&lt;br /&gt;⚠ &lt;b&gt;주의:&lt;/b&gt; WSL 내부에서 실행 중이던 모든 서버/컨테이너가 중단됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6️⃣ 한 줄 정리 스크립트 (WSL 내부)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 타이핑이 귀찮다면 WSL에 아래 스크립트를 저장해 두세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cleanup_zombies.sh&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
echo &quot;  Killing zombie processes...&quot;
pkill -f &quot;headless_shell|chrome|playwright|python|uvicorn|gunicorn|manage.py&quot; || true
sleep 2
pgrep -f &quot;headless_shell|chrome|playwright|python|uvicorn|gunicorn|manage.py&quot; | xargs -r kill -9

echo &quot;  Stopping Docker containers...&quot;
docker stop $(docker ps -q) 2&amp;gt;/dev/null

echo &quot;  Cleaning Playwright cache...&quot;
rm -rf ~/.cache/ms-playwright/* 2&amp;gt;/dev/null || true

echo &quot;✅ Done.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 권한 부여:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;chmod +x cleanup_zombies.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요할 때:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;./cleanup_zombies.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;결론:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크롤링/스크래핑 작업 중 headless_shell가 좀비처럼 남아 있으면 메모리 폭주가 발생합니다.&lt;/li&gt;
&lt;li&gt;위 방법으로 WSL 내부에서 프로세스를 정리하면 Windows 전체 속도가 개선됩니다.&lt;/li&gt;
&lt;li&gt;정기적으로 실행하거나, 자동화 스크립트로 등록해 두면 더 편합니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Chansman</author>
      <guid isPermaLink="true">https://chansman.tistory.com/1124</guid>
      <comments>https://chansman.tistory.com/1124#entry1124comment</comments>
      <pubDate>Thu, 14 Aug 2025 18:41:07 +0900</pubDate>
    </item>
    <item>
      <title>프론트? 백앤드? react vs django</title>
      <link>https://chansman.tistory.com/1113</link>
      <description>&lt;h2 data-end=&quot;78&quot; data-start=&quot;65&quot; data-ke-size=&quot;size26&quot;&gt;1. &lt;b&gt;DOM&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;244&quot; data-start=&quot;79&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;130&quot; data-start=&quot;79&quot;&gt;&lt;b&gt;React&lt;/b&gt;: 브라우저가 HTML을 객체화한 구조 &amp;rarr; 화면에 보이는 최종 결과물&lt;/li&gt;
&lt;li data-end=&quot;244&quot; data-start=&quot;131&quot;&gt;&lt;b&gt;Django&lt;/b&gt;: 템플릿(.html)이 서버에서 렌더링되어 브라우저에 도착한 상태&lt;br /&gt;  DOM은 백엔드에서 내려준 HTML과 비슷하지만, 프론트에서는 이를 &lt;b&gt;동적으로 계속 변경&lt;/b&gt; 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;249&quot; data-start=&quot;246&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;302&quot; data-start=&quot;251&quot; data-ke-size=&quot;size26&quot;&gt;2. &lt;b&gt;컴포넌트&lt;/b&gt; (React) &amp;harr; &lt;b&gt;템플릿 + include&lt;/b&gt; (Django)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;468&quot; data-start=&quot;303&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;348&quot; data-start=&quot;303&quot;&gt;&lt;b&gt;React 컴포넌트&lt;/b&gt;: 버튼, 카드, 드로어 등 재사용 가능한 UI 조각&lt;/li&gt;
&lt;li data-end=&quot;468&quot; data-start=&quot;349&quot;&gt;&lt;b&gt;Django&lt;/b&gt;: {% include 'header.html' %}처럼 재사용 가능한 템플릿 조각&lt;br /&gt;  차이: Django 템플릿은 &lt;b&gt;서버에서 조립&lt;/b&gt;, React 컴포넌트는 &lt;b&gt;브라우저에서 조립&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;473&quot; data-start=&quot;470&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;523&quot; data-start=&quot;475&quot; data-ke-size=&quot;size26&quot;&gt;3. &lt;b&gt;훅(Hook)&lt;/b&gt; &amp;harr; &lt;b&gt;서비스/헬퍼 함수(or View 로직 일부)&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;749&quot; data-start=&quot;524&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;578&quot; data-start=&quot;524&quot;&gt;&lt;b&gt;React 훅&lt;/b&gt;: 화면 없이 로직&amp;middot;데이터&amp;middot;상태를 관리 (useGenerateFlow)&lt;/li&gt;
&lt;li data-end=&quot;749&quot; data-start=&quot;579&quot;&gt;&lt;b&gt;Django&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;749&quot; data-start=&quot;595&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;622&quot; data-start=&quot;595&quot;&gt;services.py에 만든 비즈니스 로직&lt;/li&gt;
&lt;li data-end=&quot;749&quot; data-start=&quot;625&quot;&gt;또는 View 함수/클래스 내부의 데이터 처리 부분&lt;br /&gt;  공통점: 직접 UI를 만들지 않고 데이터&amp;middot;동작만 준비&lt;br /&gt;  차이: Django는 &lt;b&gt;서버&lt;/b&gt;에서 실행, React 훅은 &lt;b&gt;브라우저&lt;/b&gt;(클라이언트)에서 실행.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;754&quot; data-start=&quot;751&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;792&quot; data-start=&quot;756&quot; data-ke-size=&quot;size26&quot;&gt;4. &lt;b&gt;Pages&lt;/b&gt; &amp;harr; &lt;b&gt;View + URLconf&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1043&quot; data-start=&quot;793&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;853&quot; data-start=&quot;793&quot;&gt;&lt;b&gt;React Pages&lt;/b&gt;: Home.js, About.js 등 URL에 따라 전체 화면 렌더링&lt;/li&gt;
&lt;li data-end=&quot;1043&quot; data-start=&quot;854&quot;&gt;&lt;b&gt;Django&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1043&quot; data-start=&quot;870&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;910&quot; data-start=&quot;870&quot;&gt;View 함수/클래스(home_view, about_view)&lt;/li&gt;
&lt;li data-end=&quot;1043&quot; data-start=&quot;913&quot;&gt;urls.py로 URL 매핑&lt;br /&gt;  공통점: URL 요청 시 어떤 화면/응답을 보여줄지 결정&lt;br /&gt;  차이: Django View는 &lt;b&gt;서버에서 HTML/JSON 응답&lt;/b&gt;, React Page는 &lt;b&gt;브라우저에서 UI 렌더링&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1048&quot; data-start=&quot;1045&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1090&quot; data-start=&quot;1050&quot; data-ke-size=&quot;size26&quot;&gt;5. &lt;b&gt;API 클라이언트&lt;/b&gt; &amp;harr; &lt;b&gt;Django APIView&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1554&quot; data-start=&quot;1091&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1330&quot; data-start=&quot;1091&quot;&gt;&lt;b&gt;React API 파일 (src/api/*.js)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1330&quot; data-start=&quot;1132&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1181&quot; data-start=&quot;1132&quot;&gt;generatePost(keywordId) &amp;rarr; POST /generate/&lt;/li&gt;
&lt;li data-end=&quot;1231&quot; data-start=&quot;1184&quot;&gt;getPostDetail(postId) &amp;rarr; GET /posts/:id/&lt;/li&gt;
&lt;li data-end=&quot;1282&quot; data-start=&quot;1234&quot;&gt;copyPost(postId) &amp;rarr; POST /posts/:id/copy/&lt;/li&gt;
&lt;li data-end=&quot;1330&quot; data-start=&quot;1285&quot;&gt;getPdfUrl(postId) &amp;rarr; GET /posts/:id/pdf/&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1554&quot; data-start=&quot;1331&quot;&gt;&lt;b&gt;Django&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1554&quot; data-start=&quot;1349&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1389&quot; data-start=&quot;1349&quot;&gt;/generate/ &amp;rarr; GeneratePostAPIView&lt;/li&gt;
&lt;li data-end=&quot;1432&quot; data-start=&quot;1392&quot;&gt;/posts/&amp;lt;id&amp;gt;/ &amp;rarr; PostDetailAPIView&lt;/li&gt;
&lt;li data-end=&quot;1478&quot; data-start=&quot;1435&quot;&gt;/posts/&amp;lt;id&amp;gt;/copy/ &amp;rarr; PostCopyAPIView&lt;/li&gt;
&lt;li data-end=&quot;1554&quot; data-start=&quot;1481&quot;&gt;/posts/&amp;lt;id&amp;gt;/pdf/ &amp;rarr; PostPdfAPIView&lt;br /&gt;  역할: 프론트와 백엔드의 &lt;b&gt;HTTP 연결 포인트&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1559&quot; data-start=&quot;1556&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1580&quot; data-start=&quot;1561&quot; data-ke-size=&quot;size26&quot;&gt;6. &lt;b&gt;전체 구조 비교표&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;React (프론트)Django (백엔드)역할
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1883&quot; data-start=&quot;1582&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;1883&quot; data-start=&quot;1632&quot;&gt;
&lt;tr data-end=&quot;1667&quot; data-start=&quot;1632&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1642&quot; data-start=&quot;1632&quot;&gt;&lt;b&gt;DOM&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1657&quot; data-start=&quot;1642&quot;&gt;렌더링된 HTML 응답&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1667&quot; data-start=&quot;1657&quot;&gt;최종 결과물&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1720&quot; data-start=&quot;1668&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1679&quot; data-start=&quot;1668&quot;&gt;&lt;b&gt;컴포넌트&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;1706&quot; data-start=&quot;1679&quot; data-col-size=&quot;sm&quot;&gt;템플릿 조각 ({% include %})&lt;/td&gt;
&lt;td data-end=&quot;1720&quot; data-start=&quot;1706&quot; data-col-size=&quot;sm&quot;&gt;재사용 가능한 UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1778&quot; data-start=&quot;1721&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1735&quot; data-start=&quot;1721&quot;&gt;&lt;b&gt;훅(Hook)&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1761&quot; data-start=&quot;1735&quot;&gt;Service 함수 / View 로직 일부&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1778&quot; data-start=&quot;1761&quot;&gt;공통 로직, 데이터 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1831&quot; data-start=&quot;1779&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1791&quot; data-start=&quot;1779&quot;&gt;&lt;b&gt;Pages&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1815&quot; data-start=&quot;1791&quot;&gt;View 함수/클래스 + URLconf&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1831&quot; data-start=&quot;1815&quot;&gt;URL 단위 화면/응답&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1883&quot; data-start=&quot;1832&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1845&quot; data-start=&quot;1832&quot;&gt;&lt;b&gt;API 파일&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1865&quot; data-start=&quot;1845&quot;&gt;APIView + urls.py&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1883&quot; data-start=&quot;1865&quot;&gt;HTTP 통신 연결 포인트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;1888&quot; data-start=&quot;1885&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1907&quot; data-start=&quot;1890&quot; data-ke-size=&quot;size26&quot;&gt;  블로기 프로젝트 예시&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2218&quot; data-start=&quot;1908&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1976&quot; data-start=&quot;1908&quot;&gt;&lt;b&gt;useGenerateFlow&lt;/b&gt; = Django의 services/post_service.py (비즈니스 로직)&lt;/li&gt;
&lt;li data-end=&quot;2031&quot; data-start=&quot;1977&quot;&gt;&lt;b&gt;GenerateResultDrawer&lt;/b&gt; = Django 템플릿(drawer.html)&lt;/li&gt;
&lt;li data-end=&quot;2081&quot; data-start=&quot;2032&quot;&gt;&lt;b&gt;Home.js&lt;/b&gt; = Django의 home_view + home.html&lt;/li&gt;
&lt;li data-end=&quot;2117&quot; data-start=&quot;2082&quot;&gt;&lt;b&gt;DOM&lt;/b&gt; = home.html이 최종 렌더링된 모습&lt;/li&gt;
&lt;li data-end=&quot;2218&quot; data-start=&quot;2118&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기술블로그</category>
      <author>Chansman</author>
      <guid isPermaLink="true">https://chansman.tistory.com/1113</guid>
      <comments>https://chansman.tistory.com/1113#entry1113comment</comments>
      <pubDate>Sat, 9 Aug 2025 19:26:15 +0900</pubDate>
    </item>
    <item>
      <title>✅ FastAPI vs Django 구조 비교</title>
      <link>https://chansman.tistory.com/1087</link>
      <description>&lt;h2 data-end=&quot;128&quot; data-start=&quot;100&quot; data-ke-size=&quot;size26&quot;&gt;✅ FastAPI vs Django 구조 비교&lt;/h2&gt;
&lt;div&gt;&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;928&quot; data-start=&quot;130&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;역할 구분&lt;/td&gt;
&lt;td&gt;Django 기준&lt;/td&gt;
&lt;td&gt;FastAPI 기준&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;464&quot; data-start=&quot;344&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;359&quot; data-start=&quot;344&quot;&gt;URL 매핑&lt;/td&gt;
&lt;td data-end=&quot;403&quot; data-start=&quot;359&quot; data-col-size=&quot;sm&quot;&gt;urls.py&lt;/td&gt;
&lt;td data-end=&quot;443&quot; data-start=&quot;403&quot; data-col-size=&quot;sm&quot;&gt;router.py 또는 routes/*.py&lt;/td&gt;
&lt;td data-end=&quot;464&quot; data-start=&quot;443&quot; data-col-size=&quot;sm&quot;&gt;어떤 경로로 요청을 받을지 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;574&quot; data-start=&quot;465&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;482&quot; data-start=&quot;465&quot;&gt;View / Logic&lt;/td&gt;
&lt;td data-end=&quot;523&quot; data-start=&quot;482&quot; data-col-size=&quot;sm&quot;&gt;views.py (함수형 or APIView)&lt;/td&gt;
&lt;td data-end=&quot;558&quot; data-start=&quot;523&quot; data-col-size=&quot;sm&quot;&gt;router.py에서 직접 또는 service.py&lt;/td&gt;
&lt;td data-end=&quot;574&quot; data-start=&quot;558&quot; data-col-size=&quot;sm&quot;&gt;요청 처리 로직을 담당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;697&quot; data-start=&quot;575&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;589&quot; data-start=&quot;575&quot;&gt;로직 분리&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;629&quot; data-start=&quot;589&quot;&gt;views.py 내부 함수 / class&lt;/td&gt;
&lt;td data-end=&quot;672&quot; data-start=&quot;629&quot; data-col-size=&quot;sm&quot;&gt;service.py&lt;/td&gt;
&lt;td data-end=&quot;697&quot; data-start=&quot;672&quot; data-col-size=&quot;sm&quot;&gt;비즈니스 로직 분리 (가독성/유지보수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;817&quot; data-start=&quot;698&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;715&quot; data-start=&quot;698&quot;&gt;Serializer&lt;/td&gt;
&lt;td data-end=&quot;759&quot; data-start=&quot;715&quot; data-col-size=&quot;sm&quot;&gt;serializers.py&lt;/td&gt;
&lt;td data-end=&quot;802&quot; data-start=&quot;759&quot; data-col-size=&quot;sm&quot;&gt;schema.py&lt;/td&gt;
&lt;td data-end=&quot;817&quot; data-start=&quot;802&quot; data-col-size=&quot;sm&quot;&gt;요청/응답 구조 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;928&quot; data-start=&quot;818&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;835&quot; data-start=&quot;818&quot;&gt;Model&lt;/td&gt;
&lt;td data-end=&quot;879&quot; data-start=&quot;835&quot; data-col-size=&quot;sm&quot;&gt;models.py&lt;/td&gt;
&lt;td data-end=&quot;916&quot; data-start=&quot;879&quot; data-col-size=&quot;sm&quot;&gt;models.py (ORM에 따라 다름)&lt;/td&gt;
&lt;td data-end=&quot;928&quot; data-start=&quot;916&quot; data-col-size=&quot;sm&quot;&gt;DB 모델 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;933&quot; data-start=&quot;930&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;957&quot; data-start=&quot;935&quot; data-ke-size=&quot;size26&quot;&gt;✅ 예시로 보면 더 쉽게 이해돼요:&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1371&quot; data-start=&quot;959&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1140&quot; data-start=&quot;959&quot;&gt;Django에서는:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1753856579374&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# urls.py
path('articles/', ArticleListView.as_view())

# views.py
class ArticleListView(APIView):
    def get(self, request):
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1371&quot; data-start=&quot;959&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1371&quot; data-start=&quot;1142&quot;&gt;FastAPI에서는:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1753856590671&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# router.py
@router.get(&quot;/articles&quot;, response_model=ArticleListSchema)
async def list_articles():
    return await get_articles()

# service.py
async def get_articles():
    # DB에서 조회&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-end=&quot;1376&quot; data-start=&quot;1373&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1403&quot; data-start=&quot;1378&quot; data-ke-size=&quot;size26&quot;&gt;  실무에서는 보통 이렇게 분리합니다:&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1600&quot; data-start=&quot;1405&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1428&quot; data-start=&quot;1405&quot;&gt;schema.py: 요청/응답 구조&lt;/li&gt;
&lt;li data-end=&quot;1475&quot; data-start=&quot;1429&quot;&gt;router.py: API 정의 (@router.get/post/...)&lt;/li&gt;
&lt;li data-end=&quot;1510&quot; data-start=&quot;1476&quot;&gt;service.py: DB 조회, API 호출 등 로직&lt;/li&gt;
&lt;li data-end=&quot;1553&quot; data-start=&quot;1511&quot;&gt;client.py: 외부 API 연동 (예: Bing 이미지 API)&lt;/li&gt;
&lt;li data-end=&quot;1600&quot; data-start=&quot;1554&quot;&gt;model.py: ORM 모델 (Tortoise / SQLAlchemy 등)&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Chansman</author>
      <guid isPermaLink="true">https://chansman.tistory.com/1087</guid>
      <comments>https://chansman.tistory.com/1087#entry1087comment</comments>
      <pubDate>Wed, 30 Jul 2025 15:23:39 +0900</pubDate>
    </item>
  </channel>
</rss>