<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 바람의평온</title>
    <description>The latest articles on DEV Community by 바람의평온 (@kys7442).</description>
    <link>https://dev.to/kys7442</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3926107%2F670389ee-4bfd-49d3-84bc-6c5a90bb64e6.jpg</url>
      <title>DEV Community: 바람의평온</title>
      <link>https://dev.to/kys7442</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kys7442"/>
    <language>en</language>
    <item>
      <title>AI 코딩 에이전트의 시크릿 파일 접근, 훅으로 강제 차단한 경험</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Tue, 21 Jul 2026 00:14:40 +0000</pubDate>
      <link>https://dev.to/kys7442/ai-koding-eijeonteuyi-sikeuris-pail-jeobgeun-hugeuro-gangje-cadanhan-gyeongheom-1887</link>
      <guid>https://dev.to/kys7442/ai-koding-eijeonteuyi-sikeuris-pail-jeobgeun-hugeuro-gangje-cadanhan-gyeongheom-1887</guid>
      <description>&lt;h2&gt;
  
  
  AI 코딩 에이전트의 시크릿 파일 접근, 훅으로 강제 차단한 경험
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuyhamypuewkxxnocga0j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuyhamypuewkxxnocga0j.png" alt="AI 코딩 에이전트의 시크릿 파일 접근, 훅으로 강제 차단한 경험" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI 코딩 에이전트를 개발 작업에 활용하면서 생산성 향상을 체감했습니다. 하지만 이 편리함 뒤에는 간과하기 쉬운 보안 위험이 도사리고 있더군요. 개발 과정에서 AI가 실수로 .env, .p8, .pem 같은 민감한 시크릿 파일을 읽거나, 코드에 하드코딩하여 노출할 가능성이었습니다. 단순한 가이드라인만으로는 부족하다는 것을 깨달았고, 기계적인 강제 장치가 필요하다는 결론에 도달했습니다. 이번 글에서는 AI 에이전트의 시크릿 접근을 원천 차단하기 위해 구성했던 훅(hook)에 대한 경험을 공유합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;대상:&lt;/strong&gt; AI 코딩 도구를 사용하며 보안 강화를 고민하는 개발자&lt;br&gt;
&lt;strong&gt;난이도:&lt;/strong&gt; 중급&lt;/p&gt;

&lt;h3&gt;
  
  
  글의 요점
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AI 코딩 도구 사용 시 발생할 수 있는 시크릿 노출 위험&lt;/li&gt;
&lt;li&gt;AI 에이전트의 파일 시스템 접근 특성과 보안 위협&lt;/li&gt;
&lt;li&gt;파일 읽기 및 셸 호출을 가로채는 secrets-gate 훅 구성 방법&lt;/li&gt;
&lt;li&gt;코드 내 하드코딩된 시크릿 패턴을 검사하는 secrets-scan 훅 적용&lt;/li&gt;
&lt;li&gt;개발자의 실수를 줄이고 프로젝트 보안 수준을 높이는 자동화된 방법&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI 시대의 보안: 강제 장치의 필요성
&lt;/h3&gt;

&lt;p&gt;최근 개발 환경에서 AI 코딩 도구의 역할이 커지고 있습니다. 코드 생성, 디버깅, 문서 작성 등 다양한 영역에서 도움을 주고 있죠. 그러나 AI가 직접 프로젝트 파일 시스템에 접근하고 코드를 생성하는 과정에서 민감 정보 유출의 위험 또한 커졌다는 점을 간과해서는 안 됩니다. 단순히 '시크릿은 노출하지 말 것'과 같은 규칙이나 가이드라인만으로는 AI의 예측 불가능한 동작을 완전히 제어하기 어렵습니다. 개발자가 매번 생성된 코드를 면밀히 검토하는 것도 현실적으로 쉽지 않은 일입니다. 이런 환경에서는 인간의 실수를 넘어선, 기계적인 강제 장치를 마련하는 것이 필수적이라는 교훈을 얻었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  의도치 않은 노출, AI 에이전트의 시크릿 접근
&lt;/h3&gt;

&lt;p&gt;저의 경우, AI 코딩 에이전트가 개발 도중 .env, .p8, .pem과 같은 시크릿 파일을 실수로 읽거나, 생성된 코드에 API 키와 같은 민감 정보를 하드코딩할 위험이 있었습니다. 이러한 파일들은 데이터베이스 연결 정보, 서명 키, 인증서 등 프로젝트의 핵심 보안을 담당하는 요소들입니다. AI가 특정 로직을 구현하기 위해 참조할 파일을 찾다가 의도치 않게 시크릿 파일에 접근할 수 있고, 학습된 패턴에 따라 예시 코드를 생성하는 과정에서 실제 시크릿을 삽입할 수도 있다는 점이 문제였습니다. 이 문제는 개발 흐름에 스며들어 있기 때문에, 인지하지 못하면 그대로 배포될 수도 있는 위험이었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  넓은 접근 권한과 패턴 학습의 이면
&lt;/h3&gt;

&lt;p&gt;AI 코딩 에이전트의 동작 특성을 살펴보니, 문제의 원인이 명확해졌습니다. AI 에이전트는 효율적인 작업을 위해 프로젝트 파일 시스템에 대한 상당히 넓은 접근 권한을 가집니다. 이는 특정 파일을 읽거나, 셸 명령을 실행하는 등의 자유로운 동작을 가능하게 합니다. 또한, AI는 방대한 데이터를 학습하여 코드를 생성하므로, 특정 패턴이나 변수명에 반응하여 시크릿을 유추하거나, 이전에 학습했던 예시 코드를 재사용하는 과정에서 민감 정보가 포함될 수 있습니다. 이러한 특성은 개발의 편의성을 높이지만, 동시에 잠재적인 보안 취약점을 내포하고 있어 주의가 필요하다는 것을 깨달았습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  파일 접근을 막는 secrets-gate 훅 구현
&lt;/h3&gt;

&lt;p&gt;AI 에이전트가 시크릿 파일에 직접 접근하는 것을 막기 위해 'secrets-gate' 훅을 구성했습니다. 이 훅은 파일 읽기(read) 및 셸(bash) 호출 시 특정 시크릿 파일 경로를 탐지하고 접근을 즉시 차단하는 역할을 합니다. 예를 들어, AI가 .env 파일을 읽으려고 시도하면, 훅이 이를 감지하여 에러 메시지와 함께 실행을 중단시키는 방식입니다. 이 과정에서 사용한 스크립트의 핵심 로직은 다음과 같습니다. 특정 파일 확장자를 검사하여 접근을 차단하는 간단한 함수입니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  코드 내 시크릿 패턴 검사, secrets-scan 훅
&lt;/h3&gt;

&lt;p&gt;파일 접근 차단 외에, AI가 코드 자체에 시크릿을 하드코딩하는 것을 막기 위한 방안도 마련했습니다. 'secrets-scan' 훅은 코드를 저장하거나 커밋하기 전에 코드 내에 하드코딩된 API 키나 시크릿 패턴이 있는지 자동으로 검사합니다. 이는 Git의 pre-commit 훅과 같은 형태로 구성하여, 개발자가 의도치 않게 시크릿을 포함한 코드를 커밋하는 것을 방지할 수 있습니다. 예를 들어, &lt;code&gt;YOUR_API_KEY&lt;/code&gt;와 같은 명확한 시크릿 플레이스홀더나 특정 형식의 API 키 패턴을 찾아 경고를 보내거나 커밋을 거부하는 식으로 작동합니다. 실제 운영 환경에서는 더 정교한 정규 표현식을 사용하여 민감 정보를 탐지해야 합니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  설정한 보안 훅의 작동 확인
&lt;/h3&gt;

&lt;p&gt;구성한 두 가지 훅의 작동 여부를 검증했습니다. 먼저 secrets-gate 훅의 경우, AI 에이전트에게 '프로젝트 루트 디렉토리의 .env 파일을 읽어 내용을 요약해달라'고 의도적으로 지시했습니다. 예상대로 AI는 해당 파일에 접근하려 했고, secrets-gate 훅이 이를 감지하여 접근이 차단되었다는 에러 메시지를 반환했습니다. 다음으로 secrets-scan 훅은, 테스트용으로 &lt;code&gt;const API_KEY = 'YOUR_TEST_API_KEY';&lt;/code&gt;와 같이 시크릿 패턴이 포함된 코드를 작성한 후 저장 및 커밋을 시도했습니다. 이 경우 훅이 코드 내 패턴을 성공적으로 감지하고 경고를 표시하며 커밋을 차단했습니다. 이로써 AI 코딩 환경에서 시크릿 노출 위험을 기계적으로 방지할 수 있음을 확인했습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  마무리
&lt;/h3&gt;

&lt;p&gt;AI 코딩 도구는 분명 강력한 조력자이지만, 그 잠재적 위험을 인지하고 선제적으로 대응하는 것이 중요합니다. 특히 보안과 관련된 부분에서는 개발자의 주의를 넘어선 기계적인 강제 장치를 마련하는 것이 필수적입니다. 이번 경험을 통해 얻은 교훈은, 편리함 뒤에 숨은 위험을 항상 경계하고, 시스템적인 안전망을 구축해야 한다는 점입니다. 이는 같은 실수를 반복하지 않고 더 안전한 개발 환경을 만드는 데 도움이 될 것입니다.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>git</category>
      <category>secretsgate</category>
      <category>secretsscan</category>
    </item>
    <item>
      <title>더 좋다는 LLM 모델, 내 작업엔 어땠을까? 실측 비교와 유연한 라우팅 전략</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Sun, 19 Jul 2026 21:01:35 +0000</pubDate>
      <link>https://dev.to/kys7442/deo-johdaneun-llm-model-nae-jageoben-eoddaesseulgga-silceug-bigyowa-yuyeonhan-rauting-jeonryag-bef</link>
      <guid>https://dev.to/kys7442/deo-johdaneun-llm-model-nae-jageoben-eoddaesseulgga-silceug-bigyowa-yuyeonhan-rauting-jeonryag-bef</guid>
      <description>&lt;h2&gt;
  
  
  더 좋다는 LLM 모델, 내 작업엔 어땠을까? 실측 비교와 유연한 라우팅 전략
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopj5c1i0071k9kayztmx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopj5c1i0071k9kayztmx.png" alt="더 좋다는 LLM 모델, 내 작업엔 어땠을까? 실측 비교와 유연한 라우팅 전략" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;주말마다 아들과 공원 벤치에 앉아 블로그 초안을 검토하는 게 저의 작은 낙입니다. 최근 블로그 콘텐츠 자동 생성 스크립트의 핵심인 LLM 모델을 두고 고민이 많았습니다. '유료 상위 모델이 더 좋다'는 이야기는 귀에 못이 박히도록 들었지만, 과연 제 블로그 글쓰기 작업, 특히 제가 고심해서 만든 프롬프트에서는 어떤 결과물을 보여줄지 미지수였거든요. 막연한 기대감만으로 모델을 전환하기에는 왠지 찜찜한 기분이 들었습니다. 그래서 직접 두 모델의 실력을 겨뤄보기로 마음먹었습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;대상:&lt;/strong&gt; LLM 모델 전환을 고려하거나 여러 LLM 모델의 성능을 비교하려는 개발자&lt;br&gt;
&lt;strong&gt;난이도:&lt;/strong&gt; 중급&lt;/p&gt;
&lt;h3&gt;
  
  
  여기서 확인할 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;LLM 모델 선택 시 일반 벤치마크의 한계와 실제 작업 환경 비교의 중요성&lt;/li&gt;
&lt;li&gt;Gemini와 Claude 모델의 실제 콘텐츠 생성 특성 비교&lt;/li&gt;
&lt;li&gt;콘텐츠 길이에 따른 LLM 모델별 출력 차이&lt;/li&gt;
&lt;li&gt;환경변수를 활용한 LLM 모델 라우팅 구현 방법&lt;/li&gt;
&lt;li&gt;비용 효율성을 고려한 LLM 모델 운영 전략&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  과연 상위 모델이 무조건 정답일까요?
&lt;/h3&gt;

&lt;p&gt;많은 분들이 저처럼 '더 좋은 모델'이라는 말에 혹할 때가 있을 겁니다. 저 역시 블로그 글 생성을 위해 사용하던 LLM 모델을 유료 상위 모델로 교체할지 진지하게 고민하기 시작했습니다. 일반적으로 새로 출시된 유료 모델은 기존 모델보다 성능이 우수하다는 인식이 지배적이고, 각종 벤치마크 결과도 이를 뒷받침하는 경우가 많았으니까요. 하지만 제 경험상, 이런 일반적인 평가는 실제 제 작업 환경에서 발목을 잡는 경우가 잦았습니다. 막상 적용해보면 기대했던 만큼의 효율이나 결과가 나오지 않아 실망하는 일도 있었고요.&lt;/p&gt;

&lt;p&gt;특히 블로그 콘텐츠 생성처럼 특정 목적과 스타일이 명확한 작업에서는 더욱 그렇습니다. 제가 사용하는 프롬프트는 오랜 시간 시행착오를 거쳐 최적화된 것이라, 단순히 '더 똑똑한' 모델이 같은 프롬프트에 더 좋은 결과를 내리라는 보장이 없다고 생각했습니다. 산책길에 아들이 주워온 나뭇가지도 특정 놀이에는 요긴하게 쓰이듯, 모델의 '강점'이 제 블로그 글쓰기 '용도'와 잘 맞을지가 중요했죠. 무작정 전환했다가 비용만 더 들고 결과물은 오히려 나빠질 수도 있다는 불안감이 저를 움직이게 했습니다.&lt;/p&gt;
&lt;h3&gt;
  
  
  내 프롬프트로는 어떤 결과가 나올까? 직접 비교하기
&lt;/h3&gt;

&lt;p&gt;그래서 저는 기존에 알려진 벤치마크나 다른 개발자들의 일반적인 평가에 의존하기보다는, 제가 실제로 사용하는 블로그 글쓰기 노트와 프롬프트를 가지고 두 모델의 실력을 직접 비교해보기로 했습니다. 비교 대상은 현재 사용 중인 무료 Gemini 모델과 유료 Claude 모델이었습니다. 실용적인 비교를 위해 블로그 글의 분량을 '짧음', '중간', '김' 세 가지 프로필로 나누어 각각 동일한 프롬프트로 결과물을 생성하고 그 차이를 면밀히 분석했습니다.&lt;/p&gt;

&lt;p&gt;단순히 글자 수만 비교하는 것이 아니라, 글의 논리적인 흐름, 정보의 정확성, 그리고 코드 블록이나 표와 같은 구조적인 요소들이 얼마나 잘 생성되는지를 중점적으로 살펴보았습니다. 결과는 예상보다 흥미로웠습니다. 전반적인 분량 면에서는 Gemini 모델이 평균 4,503자로 Claude 모델의 3,177자보다 훨씬 우세했습니다. 아이와 함께 즐겨 읽는 동화책처럼 술술 읽히는 길고 풍부한 설명을 만드는 데는 Gemini가 강점을 보인 것이죠. 하지만 블로그 글에 자주 들어가는 코드 블록이나 표 같은 구조적인 요소들의 정확성과 완성도는 Claude가 확연히 뛰어났습니다. 마치 정교한 레고 블록을 조립하듯, 필요한 구조를 깔끔하게 만들어내는 재주가 더 좋았달까요. 이처럼 각 모델이 가진 뚜렷한 장단점을 직접 확인하고 나니, 어떤 모델을 선택해야 할지 명확한 그림이 그려지기 시작했습니다.&lt;/p&gt;
&lt;h3&gt;
  
  
  모델의 강점을 살리는 유연한 라우팅 전략 구현
&lt;/h3&gt;

&lt;p&gt;실측 비교를 통해 얻은 인사이트는 '하나의 모델만 고집할 필요가 없다'는 것이었습니다. Gemini는 풍성한 분량과 자연스러운 문체에 강점이 있었고, Claude는 구조적인 정확성과 완성도에서 빛을 발했습니다. 이 두 모델의 장점을 모두 활용하면서도 비용 효율성을 놓치지 않으려면 어떻게 해야 할까 고민하다가, 모델 라우팅 로직을 구현하기로 마음먹었습니다.&lt;/p&gt;

&lt;p&gt;기본적으로는 무료인 Gemini 모델을 유지하되, 특정 구조(코드 블록, 표 등)가 필요한 글을 생성할 때는 환경변수를 통해 Claude 모델로 전환하는 방식입니다. 파이썬 스크립트에서 환경변수를 읽어 모델 인스턴스를 동적으로 결정하도록 구현했습니다. 아래는 그 예시 코드입니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import os

def get_llm_model():
    model_choice = os.getenv('BLOG_LLM_MODEL', 'gemini')
    if model_choice == 'claude':
        return "Claude Model Instance"
    else:
        return "Gemini Model Instance"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;위 코드는 &lt;code&gt;BLOG_LLM_MODEL&lt;/code&gt; 환경변수 값에 따라 적절한 LLM 모델 인스턴스를 반환하는 간단한 함수입니다. 이렇게 하면 스크립트 코드를 변경하지 않고도 외부에서 모델을 제어할 수 있게 됩니다. 실제 블로그 글 생성 스크립트에서는 이 함수를 호출하여 필요한 모델을 가져다 쓰면 됩니다.&lt;/p&gt;

&lt;p&gt;특정 모델을 사용하고 싶을 때는 다음과 같이 환경변수를 설정하면 됩니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export BLOG_LLM_MODEL=claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;기본값인 Gemini 모델을 사용하려면, 다음과 같이 설정하거나 아예 환경변수를 설정하지 않아도 됩니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export BLOG_LLM_MODEL=gemini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이러한 라우팅 로직 덕분에 저희 아들과의 캠핑에서 꼭 필요한 장비만 챙겨가는 것처럼, 필요한 상황에 꼭 맞는 LLM 모델을 선택적으로 사용할 수 있게 되었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  마치며
&lt;/h3&gt;

&lt;p&gt;이번 LLM 모델 실측 비교와 라우팅 로직 구현 경험을 통해, '더 좋다'는 일반적인 평가가 반드시 내 작업 환경에 적용되는 것은 아니라는 사실을 다시금 깨달았습니다. 마치 내구성이 좋다는 등산화도 실제 내 발에는 안 맞을 수 있는 것처럼, LLM 모델도 직접 부딪혀보고 테스트하는 과정이 꼭 필요하더군요. 각 모델의 강점을 파악하고 용도에 따라 유연하게 활용하는 것이 비용 효율성과 결과물의 품질을 동시에 잡는 현명한 방법이라는 결론에 도달했습니다. 여러분도 LLM 모델 전환을 고려하고 있다면, 꼭 여러분의 실제 데이터와 프롬프트로 직접 테스트해보시길 강력히 권합니다.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>gemini</category>
      <category>claude</category>
    </item>
    <item>
      <title>로컬 FastAPI 관리 패널, Cloudflare Tunnel과 Access로 안전하게 외부 노출하고 보안 강화하기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Sat, 18 Jul 2026 06:43:37 +0000</pubDate>
      <link>https://dev.to/kys7442/rokeol-fastapi-gwanri-paeneol-cloudflare-tunnelgwa-accessro-anjeonhage-oebu-noculhago-boan-ganghwahagi-4lj8</link>
      <guid>https://dev.to/kys7442/rokeol-fastapi-gwanri-paeneol-cloudflare-tunnelgwa-accessro-anjeonhage-oebu-noculhago-boan-ganghwahagi-4lj8</guid>
      <description>&lt;h2&gt;
  
  
  로컬 FastAPI 관리 패널, Cloudflare Tunnel과 Access로 안전하게 외부 노출하고 보안 강화하기
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhrdr48l8zxcj1obso2we.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhrdr48l8zxcj1obso2we.png" alt="로컬 FastAPI 관리 패널, Cloudflare Tunnel과 Access로 안전하게 외부 노출하고 보안 강화하기" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;아들과 주말마다 공원이며 캠핑을 다니는 즐거움은 말로 다 표현하기 어렵습니다. 그런데 가끔 집을 비울 때, 로컬에 띄워둔 FastAPI 기반의 관리 패널이 눈에 밟힐 때가 있더군요. 개발 중인 서비스의 상태를 확인하거나 간단한 설정을 변경할 일이 생기면, 꼭 집에 돌아와야만 한다는 점이 아쉽게 느껴졌습니다. 처음에는 간단히 포트 포워딩을 생각해보기도 했습니다. 공유기 설정을 만져 8770 포트를 외부로 열어두면 되겠지 싶었죠. 하지만 민감한 관리 패널을 단순히 포트 포워딩으로 외부에 노출하는 것은 보안상 너무나 위험한 일이었습니다. 고정 IP가 없는 유동 IP 환경에서 접속 주소가 계속 바뀌는 것도 번거로운 일이고요. 괜히 이런 허술한 방법으로 외부 공격에 노출될까 봐 불안한 마음이 들었습니다. 주말에 아들과 즐겁게 시간을 보내면서도, 한편으로는 안전하게 외부에서 로컬 서비스를 관리할 방법에 대한 고민이 머릿속을 떠나지 않았습니다.&lt;/p&gt;

&lt;p&gt;이런 분께 — 로컬에서 개발한 웹 서비스를 안전하게 외부로 노출하고 싶은 개발자, 특히 1인 개발자나 소규모 팀 개발자들에게 유용할 것입니다. · 난이도는 중급 정도&lt;/p&gt;

&lt;h3&gt;
  
  
  이번에 정리한 내용
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;로컬에서 실행되는 웹 서비스를 외부로 안전하게 노출하는 방법&lt;/li&gt;
&lt;li&gt;Cloudflare Tunnel을 활용한 포트 포워딩 없는 외부 접근 설정&lt;/li&gt;
&lt;li&gt;Cloudflare Access로 이메일 OTP 기반의 인증 게이트 구축&lt;/li&gt;
&lt;li&gt;FastAPI 백엔드에서 Cloudflare Access JWT를 검증하여 보안 강화&lt;/li&gt;
&lt;li&gt;민감한 터널 설정 파일을 안전하게 관리하는 팁&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  집 밖에서도 아들 재울 때 관리 패널을 열어볼까?
&lt;/h3&gt;

&lt;p&gt;아들과 주말마다 공원이며 캠핑을 다니는 즐거움은 말로 다 표현하기 어렵습니다. 그런데 가끔 집을 비울 때, 로컬에 띄워둔 FastAPI 기반의 관리 패널이 눈에 밟힐 때가 있더군요. 개발 중인 서비스의 상태를 확인하거나 간단한 설정을 변경할 일이 생기면, 꼭 집에 돌아와야만 한다는 점이 아쉽게 느껴졌습니다. 처음에는 간단히 포트 포워딩을 생각해보기도 했습니다. 공유기 설정을 만져 8770 포트를 외부로 열어두면 되겠지 싶었죠. 하지만 금세 머릿속에서 경고음이 울렸습니다. 민감한 관리 패널을 단순히 포트 포워딩으로 외부에 노출하는 것은 보안상 너무나 위험한 일이었습니다. 고정 IP가 없는 유동 IP 환경에서 접속 주소가 계속 바뀌는 것도 번거로운 일이고요. 괜히 이런 허술한 방법으로 외부 공격에 노출될까 봐 불안한 마음이 들었습니다. 주말에 아들과 즐겁게 시간을 보내면서도, 한편으로는 안전하게 외부에서 로컬 서비스를 관리할 방법에 대한 고민이 머릿속을 떠나지 않았습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  보안과 유연성을 고려하니 Cloudflare Tunnel이 답이더군요
&lt;/h3&gt;

&lt;p&gt;기본적인 포트 포워딩으로는 안 되겠다 싶어 다른 방법을 찾아보기 시작했습니다. VPN을 구축하는 것도 방법이겠지만, 간단한 관리 패널 접근을 위해 VPN 서버를 따로 운영하는 건 과하다고 생각했지요. 그러다 문득 Cloudflare Tunnel이 떠올랐습니다. 이전에 다른 프로젝트에서 잠깐 사용해본 경험이 있었는데, 외부에서 직접 서버 포트를 열 필요 없이 Cloudflare의 엣지 네트워크를 통해 안전하게 로컬 서비스를 노출할 수 있다는 점이 매력적이었습니다. Cloudflare Tunnel은 로컬에서 실행되는 &lt;code&gt;cloudflared&lt;/code&gt; 데몬이 Cloudflare 네트워크와 보안 터널을 생성하고, 이 터널을 통해 외부 요청이 로컬 서비스로 전달되는 방식입니다. 이렇게 하면 외부에서 직접 제 공유기나 서버의 IP 주소를 알 필요도 없고, 특정 포트를 열어둘 필요도 없으니 보안 관점에서 훨씬 유리합니다. 무엇보다 유동 IP 환경에서도 Cloudflare가 제공하는 도메인을 통해 안정적으로 접근할 수 있다는 점이 제 상황에 딱 맞았습니다. 관리의 용이성이나 내구성을 따져봐도 다른 대안들보다 훨씬 실용적이라고 판단했습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;cloudflared&lt;/code&gt;로 로컬 FastAPI 서비스 연결하기
&lt;/h3&gt;

&lt;p&gt;Cloudflare Tunnel을 사용하기로 결정한 후, 가장 먼저 할 일은 &lt;code&gt;cloudflared&lt;/code&gt; 클라이언트를 설치하고 터널을 생성하는 것이었습니다. &lt;code&gt;cloudflared&lt;/code&gt;는 macOS 환경에서 Homebrew로 쉽게 설치할 수 있었습니다. 터널을 생성한 다음, 로컬 FastAPI 서비스가 실행 중인 8770 포트를 Cloudflare 도메인과 연결하는 설정 파일을 만들어야 했습니다. 이 설정 파일은 YAML 형식으로 작성되며, 어떤 도메인으로 들어오는 요청을 로컬의 어떤 서비스로 연결할지 정의합니다. 예를 들어, &lt;code&gt;your-domain.com&lt;/code&gt;으로 들어오는 요청을 &lt;code&gt;http://localhost:8770&lt;/code&gt;으로 보내도록 설정하는 식이지요.&lt;/p&gt;

&lt;p&gt;이렇게 설정 파일을 작성하고 나면, 다음 명령어로 터널을 실행할 수 있습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cloudflared tunnel run YOUR_TUNNEL_NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 명령을 실행하면 &lt;code&gt;cloudflared&lt;/code&gt; 데몬이 백그라운드에서 실행되며, Cloudflare 네트워크와 연결된 보안 터널을 유지합니다. 이때 중요한 점은 이 설정 파일에 터널 ID나 인증 관련 민감 정보가 포함될 수 있으므로, Git 저장소에 올릴 때는 반드시 &lt;code&gt;.gitignore&lt;/code&gt;에 추가하여 버전 관리 대상에서 제외해야 한다는 것입니다. 저는 이 부분을 초기 실수로 놓칠 뻔했다가 아차 싶어 바로 &lt;code&gt;.gitignore&lt;/code&gt;에 추가했습니다. 설정 파일의 내용은 다음과 같이 구성했습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: 0.0
hostname: your-domain.com
tunnel: YOUR_TUNNEL_UUID
metrics: 0.0.0.0:2006

ingress:
  - service: http://localhost:8770
  - service: http_status:404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;여기서 &lt;code&gt;your-domain.com&lt;/code&gt;은 제가 연결하고자 하는 도메인이고, &lt;code&gt;http://localhost:8770&lt;/code&gt;은 로컬 FastAPI 관리 패널이 실행되는 주소입니다. 마지막 &lt;code&gt;http_status:404&lt;/code&gt;는 위의 규칙에 해당하지 않는 모든 요청에 대해 404 에러를 반환하도록 하는 기본 규칙입니다. 이렇게 설정하니 제 도메인으로 접속했을 때 로컬 FastAPI 서비스가 외부에서 잘 보이는 것을 확인할 수 있었습니다. 물론 아직 보안 장치는 없는 상태였죠.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloudflare Access로 보안의 문을 걸어 잠그다
&lt;/h3&gt;

&lt;p&gt;로컬 서비스가 외부로 노출되는 것을 확인했으니, 이제는 접근을 통제할 차례였습니다. 단순 노출만으로는 민감한 관리 패널을 안전하게 사용할 수 없기 때문입니다. Cloudflare Access는 이럴 때 아주 유용한 도구입니다. 특정 도메인에 대한 접근을 제한하고, 사용자 인증을 통과해야만 서비스에 접근할 수 있도록 해주는 보안 게이트웨이 역할을 합니다. 저는 이메일 OTP(One-Time Password) 방식을 사용하여 저만 접근할 수 있도록 설정했습니다.&lt;/p&gt;

&lt;p&gt;Cloudflare 대시보드에서 Access 애플리케이션을 생성하고, 정책을 추가했습니다. 정책 설정은 간단합니다. 특정 도메인(&lt;code&gt;your-domain.com&lt;/code&gt;)에 접근할 때, 미리 지정한 이메일 주소(제 개인 이메일)로 OTP를 보내고, 그 OTP를 입력해야만 통과시키는 방식입니다. 이렇게 설정하고 나니, 이제 제 도메인으로 접속하면 Cloudflare Access의 로그인 페이지가 먼저 나타나더군요. 등록된 이메일 주소를 입력하면 해당 이메일로 OTP가 전송되고, 그 코드를 웹페이지에 입력해야만 비로소 FastAPI 관리 패널에 접근할 수 있게 되었습니다. 이 정도면 웬만한 무단 접근 시도는 막을 수 있겠다는 안도감이 들었습니다. 휴대폰으로 OTP를 확인하고 입력하는 과정이 아주 잠깐 번거롭긴 하지만, 그만큼 얻는 보안의 가치가 훨씬 컸습니다. 공원 벤치에 앉아있다가도, 아들이 잠든 캠핑장 텐트 안에서도 안심하고 관리 패널에 접속할 수 있게 된 것이죠. 혹시 모를 상황에 대비해 든든한 방어막을 친 기분입니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  백엔드에서 JWT를 다시 확인하는 이중 방어
&lt;/h3&gt;

&lt;p&gt;Cloudflare Access로 1차 보안을 구축했지만, 여기서 한 발 더 나아가기로 했습니다. 만약 어떤 이유로든 Cloudflare Tunnel을 우회하거나 Access 정책이 제대로 작동하지 않는 상황이 발생한다면 어떻게 될까요? 직접적인 IP 주소는 숨겨져 있지만, 만약의 사태에 대비한 이중 방어가 필요하다고 생각했습니다. Cloudflare Access는 인증에 성공하면 요청 헤더에 JWT(JSON Web Token)를 추가하여 서비스로 전달해줍니다. 저는 이 JWT를 FastAPI 백엔드에서 다시 한번 검증하는 미들웨어를 추가하여, 터널을 우회한 직접 접근을 원천적으로 차단하기로 했습니다.&lt;/p&gt;

&lt;p&gt;FastAPI 애플리케이션에 미들웨어를 추가하여 모든 요청이 들어올 때마다 Cloudflare Access에서 발급한 JWT 헤더가 유효한지 확인하도록 했습니다. 유효하지 않은 JWT가 발견되면, 즉시 401 Unauthorized 응답을 반환하여 접근을 거부하는 방식입니다. 이렇게 하면 Access를 통과하지 않은 요청은 결코 제 관리 패널에 도달할 수 없게 됩니다. 제가 작성한 미들웨어의 기본적인 구조는 다음과 같습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from fastapi import FastAPI, Request, HTTPException
from fastapi.security import HTTPBearer

app = FastAPI()
security = HTTPBearer()

@app.middleware("http")
async def verify_cloudflare_access_jwt(request: Request, call_next):
    # JWT 검증 로직 구현 (예: JWT 헤더 유효성, 서명 등)
    # 유효하지 않으면 HTTPException(status_code=401, detail="Unauthorized")
    response = await call_next(request)
    return response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;물론 위 코드의 &lt;code&gt;# JWT 검증 로직 구현&lt;/code&gt; 부분에는 실제 JWT의 서명 검증, 만료 시간 확인, 발급자 확인 등 구체적인 로직이 들어가야 합니다. Cloudflare Access에서 제공하는 공개 키를 사용하여 JWT의 유효성을 검증하는 것이 일반적인 방법입니다. 이 과정을 추가함으로써, 만에 하나 Cloudflare Access 자체에 문제가 생기거나, 혹은 터널 설정에 오류가 발생하여 외부에서 직접 로컬 서비스로 요청이 들어오는 상황이 발생하더라도, 백엔드에서 마지막 방어선 역할을 해주게 됩니다. 이렇게 이중으로 보안 장치를 마련하니 훨씬 더 마음이 놓이더군요. 체감상 안정성과 보안성이 크게 향상되었다고 느꼈습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  마무리
&lt;/h3&gt;

&lt;p&gt;로컬에서만 사용하던 FastAPI 기반의 관리 패널을 Cloudflare Tunnel과 Access를 활용하여 안전하게 외부로 노출하고, 심지어 백엔드 단에서 JWT를 검증하는 이중 방어막까지 구축하는 과정을 상세히 기록해 보았습니다. 처음에는 단순히 포트 포워딩을 고민했던 것과 비교하면, 훨씬 견고하고 유연한 접근 제어 시스템을 갖추게 된 셈입니다. 이 덕분에 이제는 집 밖 어디에서든, 아들과 즐거운 시간을 보내는 중에도 안심하고 제 서비스의 상태를 확인하고 관리할 수 있게 되었습니다. 혹시 저처럼 로컬 서비스를 안전하게 외부에 노출해야 하는 분들이 계시다면, 이 방법이 좋은 대안이 될 것이라고 생각합니다. 저의 경험이 여러분의 개발 여정에 작은 도움이 되었기를 바랍니다.&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>cloudflaretunnel</category>
      <category>cloudflareaccess</category>
      <category>jwt</category>
    </item>
    <item>
      <title>첫 출시 앱 심사: 인앱 결제(IAP) 버튼 무반응 문제와 해결 전략</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Wed, 15 Jul 2026 12:14:00 +0000</pubDate>
      <link>https://dev.to/kys7442/ceos-culsi-aeb-simsa-inaeb-gyeoljeiap-beoteun-mubaneung-munjewa-haegyeol-jeonryag-18l7</link>
      <guid>https://dev.to/kys7442/ceos-culsi-aeb-simsa-inaeb-gyeoljeiap-beoteun-mubaneung-munjewa-haegyeol-jeonryag-18l7</guid>
      <description>&lt;h2&gt;
  
  
  첫 출시 앱 심사: 인앱 결제(IAP) 버튼 무반응 문제와 해결 전략
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfxfo6jec3kk8mf4s414.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frfxfo6jec3kk8mf4s414.png" alt="첫 출시 앱 심사: 인앱 결제(IAP) 버튼 무반응 문제와 해결 전략" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;앱의 첫 출시 심사를 준비하면서 인앱 결제(IAP) 기능 때문에 예상치 못한 난관에 부딪혔습니다. 야심 차게 준비한 구매 버튼이 심사 빌드에서 아무런 반응 없이 먹통이 되는 문제를 겪었던 것이죠. 이는 앱 심사 리젝 사유가 될 수 있는 치명적인 상황이었습니다. 결론부터 말씀드리자면, 스토어 백엔드에서 인앱 결제 상품이 아직 활성화되지 않아 발생하는 문제였고, 첫 출시 심사용 빌드에서는 인앱 결제 기능을 의도적으로 비활성화하여 이 문제를 우회했습니다. 이후 앱이 성공적으로 출시된 뒤 별도 업데이트를 통해 IAP 기능을 다시 활성화하는 단계적 접근으로 해결했습니다. 이 경험은 첫 출시 심사 시 인앱 결제 기능 관리의 중요성을 깨닫게 해주었습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;대상:&lt;/strong&gt; Flutter 앱 개발자, 앱 출시 심사 과정에서 인앱 결제 문제로 고민하는 개발자&lt;br&gt;
&lt;strong&gt;난이도:&lt;/strong&gt; 중급&lt;/p&gt;
&lt;h3&gt;
  
  
  이 글에서 다루는 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;앱 첫 출시 심사 시 인앱 결제(IAP) 상품 미로드 문제의 원인&lt;/li&gt;
&lt;li&gt;스토어 활성화 시점과 앱 심사 타이밍 불일치에 대한 이해&lt;/li&gt;
&lt;li&gt;첫 출시 빌드에서 IAP 기능을 안전하게 비활성화하는 방법&lt;/li&gt;
&lt;li&gt;인앱 결제 상품 로드 실패 시 UI/UX를 저해하지 않도록 버튼을 처리하는 로직&lt;/li&gt;
&lt;li&gt;단계적인 인앱 결제 기능 활성화 전략&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  앱 심사 중 만난 인앱 결제(IAP) 버튼 무반응 문제
&lt;/h3&gt;

&lt;p&gt;저희 팀이 야심 차게 준비한 Flutter 앱의 첫 출시 심사 과정에서 예상치 못한 문제가 발생했습니다. 인앱 결제(IAP) 기능을 테스트하는데, 구매 버튼이 아무런 반응도 하지 않는 것이었습니다. 개발 환경에서는 분명히 잘 동작하던 기능이었기에, 심사용 빌드에서만 발생하는 이 현상에 당황스러움을 감출 수 없었죠. 이런 무반응 버튼은 사용자 경험을 해칠 뿐만 아니라, 앱 심사팀으로부터 리젝 사유가 될 가능성이 매우 높았습니다.&lt;/p&gt;

&lt;p&gt;앱 심사팀은 사용자 관점에서 앱을 테스트하기 때문에, 인앱 결제 기능이 정상적으로 동작하지 않으면 앱의 기능적 결함으로 판단하기 십상입니다. 저희는 즉시 로그를 확인하며 문제의 원인을 파악하기 시작했습니다. 로컬 환경과 심사 빌드 환경의 차이를 좁히는 것이 급선무였죠. 이 과정에서 인앱 결제와 관련된 중요한 사실을 깨달았습니다.&lt;/p&gt;

&lt;p&gt;결론적으로, 첫 출시 심사를 위해 제출된 빌드에서는 IAP 기능을 아예 비활성화하는 방식으로 이 문제를 우회하기로 결정했습니다. 기능이 아예 없으면 심사팀에서 테스트할 여지도 없으니, 적어도 리젝 사유는 되지 않을 것이라는 판단이었죠. 이후 앱이 성공적으로 출시되면 별도의 업데이트를 통해 IAP 기능을 다시 활성화하기로 계획을 세웠습니다.&lt;/p&gt;
&lt;h3&gt;
  
  
  스토어 백엔드와의 타이밍 불일치: 상품 미로드의 원인
&lt;/h3&gt;

&lt;p&gt;구매 버튼이 무반응이었던 근본적인 원인은 앱이 스토어 백엔드로부터 인앱 결제 상품 정보를 제대로 로드하지 못했기 때문이었습니다. 첫 출시 심사 시점에는 아직 스토어 콘솔에 등록된 인앱 결제 상품들이 ‘활성화’되지 않은 상태였던 것이죠. 스토어 심사 프로세스와 인앱 상품의 활성화 타이밍이 동기화되지 않는다는 사실을 미처 간과하고 있었던 것입니다.&lt;/p&gt;

&lt;p&gt;저희 앱의 인앱 결제 로직은 상품 정보를 성공적으로 로드했을 때만 구매 버튼을 활성화하도록 구현되어 있었습니다. 만약 상품 목록이 비어 있으면, 즉 &lt;code&gt;_products&lt;/code&gt; 리스트가 비어 있으면 구매 버튼의 &lt;code&gt;onPressed&lt;/code&gt; 콜백이 &lt;code&gt;null&lt;/code&gt;로 설정되어 버튼이 비활성화되거나 아무런 동작을 하지 않도록 되어 있었죠. 이는 평소에는 안전한 구현 방식이지만, 상품 로드 자체가 실패하는 특수한 상황에서는 의도치 않은 문제를 발생시켰습니다.&lt;/p&gt;

&lt;p&gt;결국, 앱은 상품이 없다고 판단했고, 그에 따라 구매 버튼이 무반응 상태가 된 것이었습니다. 이 문제를 해결하기 위해서는 단순히 상품이 로드되지 않았을 때의 UI 처리뿐만 아니라, 첫 출시 심사라는 특수한 상황에서 인앱 결제 상품 활성화 여부를 전략적으로 다룰 필요가 있음을 깨닫게 되었습니다.&lt;/p&gt;
&lt;h3&gt;
  
  
  첫 출시 빌드를 위한 IAP 기능 일시 비활성화 전략
&lt;/h3&gt;

&lt;p&gt;저희는 첫 출시 심사용 빌드에 한해 인앱 결제 기능을 완전히 비활성화하는 전략을 택했습니다. 이를 위해 코드 내부에 간단한 상수를 정의하여 인앱 결제 기능의 활성화 여부를 제어하도록 만들었습니다. 다음과 같은 플래그를 추가했죠.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const bool kEnableIapForFirstRelease = false; // 첫 출시 심사용 빌드에서 false로 설정
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 &lt;code&gt;kEnableIapForFirstRelease&lt;/code&gt; 상수는 첫 출시 심사를 위한 빌드를 만들 때만 &lt;code&gt;false&lt;/code&gt;로 설정하고, 앱이 스토어에 출시된 이후에는 &lt;code&gt;true&lt;/code&gt;로 변경하여 업데이트 빌드를 제출하는 방식으로 활용했습니다. 이 플래그를 통해 인앱 결제 초기화 로직을 조건부로 실행하도록 했습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (kEnableIapForFirstRelease) { await InAppPurchase.instance.initialize(); }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;위 코드처럼 &lt;code&gt;initialize()&lt;/code&gt; 호출 자체를 조건부로 감싸면, &lt;code&gt;kEnableIapForFirstRelease&lt;/code&gt;가 &lt;code&gt;false&lt;/code&gt;일 때는 인앱 결제 모듈이 아예 초기화되지 않아 상품 로드를 시도조차 하지 않게 됩니다. 이는 불필요한 네트워크 요청과 에러 발생 가능성을 원천적으로 차단하는 효과를 가져다주었습니다. 덕분에 심사 빌드에서는 인앱 결제 관련 로직이 전혀 실행되지 않아 안정성을 확보할 수 있었지요.&lt;/p&gt;

&lt;h3&gt;
  
  
  사용자 경험을 해치지 않는 구매 버튼 UI 처리
&lt;/h3&gt;

&lt;p&gt;인앱 결제 기능을 일시적으로 비활성화하더라도, 사용자 인터페이스(UI)는 여전히 적절하게 처리되어야 합니다. 단순히 버튼이 사라지거나 오류 메시지만 표시되는 것보다는, 기능이 현재 사용 불가능하다는 것을 명확하게 인지시키는 것이 중요합니다. 저희는 &lt;code&gt;kEnableIapForFirstRelease&lt;/code&gt; 플래그와 함께 &lt;code&gt;_products&lt;/code&gt; 리스트의 상태를 이용하여 구매 버튼의 동작을 제어했습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ElevatedButton(
  onPressed: _products.isNotEmpty &amp;amp;&amp;amp; kEnableIapForFirstRelease ? () =&amp;gt; _buyProduct() : null,
  child: Text('구매하기'),
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;위 코드에서 보듯이, 구매 버튼의 &lt;code&gt;onPressed&lt;/code&gt; 콜백은 두 가지 조건이 모두 충족될 때만 활성화됩니다. 첫째, &lt;code&gt;_products.isNotEmpty&lt;/code&gt;는 인앱 결제 상품 목록이 비어 있지 않아야 한다는 것을 의미합니다. 상품이 로드되지 않았으면 구매 버튼이 활성화될 이유가 없으니까요. 둘째, &lt;code&gt;kEnableIapForFirstRelease&lt;/code&gt;는 인앱 결제 기능 자체가 활성화되어 있어야 함을 나타냅니다. 이 두 조건 중 하나라도 만족하지 않으면 &lt;code&gt;onPressed&lt;/code&gt;는 &lt;code&gt;null&lt;/code&gt;이 되어 버튼은 자동으로 비활성화됩니다.&lt;/p&gt;

&lt;p&gt;이러한 조건부 로직 덕분에 첫 출시 심사 빌드에서는 &lt;code&gt;kEnableIapForFirstRelease&lt;/code&gt;가 &lt;code&gt;false&lt;/code&gt;였으므로, 설령 &lt;code&gt;_products&lt;/code&gt;가 채워지더라도 버튼이 비활성화 상태를 유지했습니다. 이는 심사팀이 무반응 버튼을 발견하고 리젝하는 상황을 효과적으로 방지할 수 있었죠. 버튼이 비활성화되면 사용자에게 명확하게 '지금은 구매할 수 없다'는 메시지를 전달하는 효과도 있습니다. 앱의 기능은 명확하게 전달하되, 심사 과정에서는 불필요한 오해를 사지 않도록 설계한 셈입니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  안정적인 출시와 인앱 결제 기능의 단계적 활성화
&lt;/h3&gt;

&lt;p&gt;인앱 결제 기능이 비활성화된 첫 번째 빌드는 별다른 문제 없이 앱 심사를 통과했습니다. 이로써 우리는 첫 출시의 가장 큰 허들 중 하나를 안전하게 넘길 수 있었죠. 심사 통과 후 앱이 공식적으로 스토어에 출시되었을 때, 인앱 결제 상품들도 스토어 백엔드에서 정상적으로 활성화되는 것을 확인했습니다. 이제는 인앱 결제 기능을 다시 켜야 할 때가 된 것이었습니다.&lt;/p&gt;

&lt;p&gt;저희는 &lt;code&gt;kEnableIapForFirstRelease&lt;/code&gt; 상수를 &lt;code&gt;true&lt;/code&gt;로 변경하고, 인앱 결제 기능을 활성화한 새로운 업데이트 빌드를 제출했습니다. 이 빌드는 다시 한번 스토어 심사를 거쳐야 했지만, 이미 상품들이 활성화된 상태였기 때문에 별다른 이슈 없이 심사를 통과할 수 있었습니다. 이후 앱 내에서 모든 인앱 결제 상품이 정상적으로 로드되고, 구매 프로세스 또한 원활하게 작동하는 것을 최종적으로 확인했습니다.&lt;/p&gt;

&lt;p&gt;이러한 단계적 접근 방식은 첫 출시 심사 과정에서 발생할 수 있는 잠재적인 문제를 효과적으로 회피하며, 안정적인 앱 출시를 가능하게 했습니다. 만약 처음부터 인앱 결제 기능을 완전히 활성화한 채로 심사를 진행했다면, 상품 미로드로 인한 리젝과 그에 따른 출시 지연을 겪었을지도 모릅니다. 저희의 경험은 첫 출시 심사 시 인앱 결제 상품의 스토어 활성화 시점과 앱 심사 타이밍이 항상 일치하지 않을 수 있다는 점을 상기시켜 주었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  마치며
&lt;/h3&gt;

&lt;p&gt;앱의 첫 출시 심사는 여러 변수가 많아 개발자에게 긴장감을 안겨주는 과정입니다. 특히 인앱 결제와 같은 외부 의존성이 큰 기능은 스토어 백엔드와의 동기화 문제로 예상치 못한 난관을 초래할 수 있습니다. 저희는 첫 출시 심사 시 인앱 결제 상품 미로드로 인한 버튼 무반응 문제를 겪었지만, IAP 기능을 일시적으로 비활성화하고 단계적으로 활성화하는 전략을 통해 이를 성공적으로 해결했습니다. 이 경험이 첫 출시를 준비하는 다른 Flutter 개발자분들께도 도움이 되기를 바랍니다.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>iap</category>
    </item>
    <item>
      <title>앱스토어 심사 거부: 계정 삭제 기능 누락과 Firebase 앱 개발자의 해결 기록</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:32:47 +0000</pubDate>
      <link>https://dev.to/kys7442/aebseutoeo-simsa-geobu-gyejeong-sagje-gineung-nuraggwa-firebase-aeb-gaebaljayi-haegyeol-girog-11m1</link>
      <guid>https://dev.to/kys7442/aebseutoeo-simsa-geobu-gyejeong-sagje-gineung-nuraggwa-firebase-aeb-gaebaljayi-haegyeol-girog-11m1</guid>
      <description>&lt;h2&gt;
  
  
  앱스토어 심사 거부: 계정 삭제 기능 누락과 Firebase 앱 개발자의 해결 기록
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu8v3eqy1a4y8vkdv6pxf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu8v3eqy1a4y8vkdv6pxf.png" alt="앱스토어 심사 거부: 계정 삭제 기능 누락과 Firebase 앱 개발자의 해결 기록" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;오랜만에 아이가 좋아하는 게임 앱을 만들어 앱스토어에 제출했는데, 예상치 못한 심사 거부 메일을 받았습니다. 젊은 친구들이야 이런 일을 겪으며 금방 익숙해지겠지만, 저처럼 조금 연륜 있는 개발자에게는 이런 플랫폼 정책 변경이 때론 당황스럽게 다가오기도 합니다. 이번에 제가 마주한 문제는 바로 앱스토어 심사 지침 5.1.1(v) 조항, 즉 '계정 삭제 기능'의 부재였습니다. 사용자 계정 기능을 제공하는 앱은 반드시 앱 내에서 계정을 삭제할 수 있는 기능을 제공해야 한다는 내용이었지요. 처음에는 단순히 계정 생성과 로그인 기능만 잘 작동하면 된다고 생각했던 제 착각이 부른 결과였습니다. 결국 이 문제를 해결하고 무사히 앱을 출시하기까지의 과정을 차분히 기록으로 남겨봅니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;대상:&lt;/strong&gt; App Store에 앱을 제출하려는 Flutter 개발자, Firebase를 사용하는 앱 개발자&lt;br&gt;
&lt;strong&gt;난이도:&lt;/strong&gt; 중급&lt;/p&gt;
&lt;h3&gt;
  
  
  예상치 못했던 앱스토어 심사 거부와 5.1.1(v) 조항
&lt;/h3&gt;

&lt;p&gt;앱스토어 심사 과정은 늘 긴장의 연속입니다만, 이번에는 그 긴장감이 조금 다른 형태로 찾아왔습니다. 열심히 만든 앱이 반려되었다는 메일을 받았을 때, 처음엔 어디서 문제가 생겼는지 감을 잡기 어려웠습니다. 메일에는 'Guideline 5.1.1(v) - Data Collection and Storage'라는 문구가 선명하게 찍혀 있었지요. 내용인즉, 앱이 계정 생성을 지원한다면, 앱 내에서 사용자가 직접 계정을 삭제하고 관련된 모든 데이터를 지울 수 있는 기능을 제공해야 한다는 것이었습니다. 저도 나이가 있다 보니, 이런 최신 지침에 대한 업데이트가 조금 늦었나 하는 생각이 들더군요. 아이들이 '아빠, 이것도 몰랐어?' 할까 봐 괜히 머쓱해지기도 했습니다.&lt;/p&gt;

&lt;p&gt;이 지침은 사용자의 개인 정보 보호와 데이터 주권을 강화하기 위한 애플의 노력이 담겨 있는 것이었습니다. 사용자에게 계정 생성의 자유를 주는 만큼, 삭제의 자유도 보장하라는 의미로 이해했습니다. 문제는 제가 이 부분을 완전히 간과했다는 점입니다. 앱 개발에 몰두하며 기능 구현에만 집중했고, 사용자가 앱을 떠날 때의 절차에 대해서는 깊이 고민하지 못했던 것이지요. 앱 심사팀에서는 앱 내에 계정 삭제 버튼이나 관련 기능이 전혀 없음을 지적하며, 이 부분이 수정되기 전까지는 앱을 승인할 수 없다는 단호한 입장을 전해왔습니다.&lt;/p&gt;

&lt;p&gt;메일을 받고 나서야 부랴부랴 해당 지침을 찾아 자세히 읽어보았습니다. 단순히 계정 삭제 버튼만 추가하는 것을 넘어, 계정 삭제 시 사용자의 모든 관련 데이터가 함께 삭제되어야 한다는 점, 그리고 그 과정이 명확하게 안내되어야 한다는 점을 알게 되었습니다. 단순히 Firebase Auth에서 계정을 지우는 것을 넘어, Firestore나 다른 백엔드 서비스에 저장된 사용자 관련 데이터까지 모두 정리해야 한다는 의미였습니다. 이 지침을 충족시키지 못하면 앱이 영영 출시되지 못할 수도 있다는 생각에 마음이 조급해졌습니다.&lt;/p&gt;
&lt;h3&gt;
  
  
  간과했던 사용자 계정 삭제 기능의 중요성
&lt;/h3&gt;

&lt;p&gt;솔직히 말씀드리자면, 처음 앱을 기획하고 개발할 때는 사용자 계정 삭제 기능의 중요성을 크게 인지하지 못했습니다. Firebase Auth를 이용해 간편하게 계정 생성과 로그인을 구현하고 나면, 나머지는 백엔드에서 알아서 잘 처리해줄 것이라는 막연한 기대가 있었던 것 같습니다. 게임 앱이다 보니, 사용자가 계정을 만들어서 리더보드에 점수를 올리고 친구들과 경쟁하는 재미를 주는 것이 주 목적이었으니까요. 계정 삭제는 사용자가 아주 드물게 요청할 때나 고려할 만한 부수적인 기능이라고 생각했었습니다.&lt;/p&gt;

&lt;p&gt;하지만 앱스토어 심사 지침을 통해 이러한 생각이 얼마나 안일했는지 깨닫게 되었습니다. 사용자 데이터는 단순히 '생성'되는 것이 아니라, '관리'되고 '삭제'될 권리까지 포함한다는 것을 말입니다. 특히 개인 정보 보호에 대한 사회적 인식이 높아지면서, 플랫폼 사업자들도 이 부분을 매우 엄격하게 다루기 시작했다는 것을 몸으로 체감하는 순간이었습니다. Firebase 자체는 계정 관리 기능을 제공하지만, 그 기능을 앱 내에서 사용자에게 직접 제공하는 것은 오롯이 개발자의 몫이더군요.&lt;/p&gt;

&lt;p&gt;제가 개발했던 앱은 Firebase Auth로 사용자 계정을 관리하고, Firestore에는 게임 점수나 프로필 같은 연관 데이터를 저장하고 있었습니다. 앱 내에 계정 삭제 기능이 없었으니, 사용자가 계정을 지우고 싶어도 직접 할 수 있는 방법이 전혀 없었습니다. 고객센터로 문의하거나 개발자에게 직접 연락해야만 가능한 구조였던 것이지요. 이는 명백히 앱스토어의 5.1.1(v) 지침에 위배되는 상황이었습니다. 이제 와서 생각해 보니, 계정 생성 기능을 만들 때부터 삭제 기능도 함께 고려했어야 했다는 뒤늦은 후회가 밀려오더군요.&lt;/p&gt;
&lt;h3&gt;
  
  
  앱 내 계정 삭제 기능 구현의 첫걸음과 재인증
&lt;/h3&gt;

&lt;p&gt;문제를 확인했으니, 이제 해결에 나설 차례였습니다. 가장 먼저 해야 할 일은 앱 내에 사용자가 직접 계정을 삭제할 수 있는 UI를 만드는 것이었습니다. 설정 화면 어딘가에 '계정 삭제' 버튼을 눈에 띄게 배치하고, 이 버튼을 누르면 일련의 절차를 거쳐 계정이 삭제되도록 해야 했습니다. 단순히 버튼만 만들면 되는 것이 아니었습니다. 사용자에게 계정 삭제의 영향을 명확히 안내하고, 정말 삭제할 것인지 최종 확인하는 과정을 거치는 것이 중요하다고 판단했습니다. 실수로 누르거나 오해로 인해 소중한 데이터를 잃는 일이 없도록 말이지요.&lt;/p&gt;

&lt;p&gt;여기서 중요한 부분이 바로 '재인증'입니다. Firebase Auth는 보안상의 이유로 계정을 삭제하거나 민감한 정보를 변경할 때 사용자의 재인증을 요구하는 경우가 많습니다. 특히 일정 시간 이상 로그인 상태가 유지되었을 경우, 계정 삭제와 같은 중요한 작업은 다시 한번 비밀번호를 입력하거나 다른 인증 방법을 통해 본인임을 확인해야 합니다. 이는 악의적인 접근으로부터 사용자 계정을 보호하기 위한 필수적인 절차이므로, 앱 내 계정 삭제 흐름에 반드시 포함해야 했습니다.&lt;/p&gt;

&lt;p&gt;따라서 저는 계정 삭제 버튼을 누르면 사용자에게 '정말 계정을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.'와 같은 경고 메시지를 보여주고, 이어서 현재 로그인 방식에 맞는 재인증 절차를 거치도록 구현했습니다. 예를 들어 비밀번호 기반 로그인 사용자라면 비밀번호를 다시 입력하게 하고, 구글이나 애플 로그인 사용자라면 해당 소셜 로그인 절차를 다시 한번 거치게 하는 방식입니다. 이 과정을 통해 사용자가 충분히 인지하고 동의한 상태에서 계정 삭제가 진행되도록 안전장치를 마련한 것입니다.&lt;/p&gt;
&lt;h3&gt;
  
  
  Firebase 연동 및 연관 데이터 정리 상세 과정
&lt;/h3&gt;

&lt;p&gt;재인증 과정을 거쳐 사용자의 의사를 최종 확인했다면, 이제 실제 Firebase에서 계정을 삭제하고 연관 데이터를 정리하는 작업을 수행해야 합니다. Firebase를 사용한다면 &lt;code&gt;firebase_auth&lt;/code&gt; 패키지를 통해 현재 로그인된 사용자 객체에 접근할 수 있습니다. 먼저 Flutter 앱에서 &lt;code&gt;firebase_auth&lt;/code&gt; 패키지를 임포트해야 합니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import 'package:firebase_auth/firebase_auth.dart';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이후 실제 계정을 삭제하는 코드는 다음과 같습니다. &lt;code&gt;currentUser&lt;/code&gt; 객체를 통해 현재 로그인된 사용자 정보를 가져온 다음, &lt;code&gt;delete()&lt;/code&gt; 메서드를 호출하는 방식입니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final user = FirebaseAuth.instance.currentUser;
if (user != null) {
  // 재인증이 필요한 경우 (예: 비밀번호 기반 로그인)
  // await user.reauthenticateWithCredential(credential);
  await user.delete().then((_) {
    // 계정 삭제 성공 후 추가 데이터 정리 로직 구현 (예: Firestore 문서 삭제)
  }).catchError((error) {
    // 에러 처리
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;주석으로 표시된 것처럼, &lt;code&gt;reauthenticateWithCredential()&lt;/code&gt; 메서드를 통해 재인증을 먼저 수행해야 할 수 있습니다. 이 과정이 성공하면 &lt;code&gt;user.delete()&lt;/code&gt;를 호출하여 Firebase Auth의 사용자 계정을 삭제하게 됩니다. 여기서 중요한 것은 계정 삭제만으로 끝나는 것이 아니라는 점입니다. Firestore 같은 다른 Firebase 서비스나 자체 백엔드에 저장된 사용자의 개인 데이터, 예를 들어 게임 점수, 프로필 정보, 친구 목록 등도 함께 삭제해야 합니다. 저는 &lt;code&gt;delete()&lt;/code&gt; 호출이 성공한 후, Firestore에서 해당 &lt;code&gt;userId&lt;/code&gt;를 기준으로 저장된 모든 문서를 찾아 삭제하는 로직을 추가했습니다. 이 과정에서 한두 개의 문서만 지우는 것이 아니라, 사용자와 연결된 모든 컬렉션과 문서들을 재귀적으로 삭제하는 방식을 고민했습니다. 혹시라도 남아있는 데이터가 발생할까 봐 여러 번 확인하며 꼼꼼하게 처리했습니다. 이 부분이 바로 5.1.1(v) 지침을 완전히 준수하는 핵심이었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  사용자 안내를 위한 웹 페이지 마련
&lt;/h3&gt;

&lt;p&gt;앱 내에서 계정 삭제 기능을 구현하는 것 외에도, 앱스토어 심사 지침은 사용자가 계정 삭제 정책에 대해 명확하게 인지할 수 있도록 하는 것을 요구합니다. 단순히 '계정 삭제' 버튼만 달랑 두는 것이 아니라, 어떤 데이터가 삭제되는지, 삭제 후에는 어떤 변화가 생기는지 등을 상세히 안내하는 페이지를 제공하는 것이 좋습니다. 이를 위해 저는 별도의 웹 페이지를 만들어 앱 내에서 해당 페이지로 연결하는 방식을 택했습니다. 앱 내에서 모든 정책을 장황하게 설명하기보다는, 외부 웹 페이지를 통해 더 풍부하고 업데이트 가능한 정보를 제공하는 것이 효율적이라고 판단했습니다.&lt;/p&gt;

&lt;p&gt;이 웹 페이지에는 계정 삭제 시 어떤 정보가 영구적으로 삭제되는지, 삭제 후에는 앱 서비스 이용이 불가능해지는지, 그리고 재가입 시 기존 데이터는 복구되지 않는다는 점 등을 명확하게 기재했습니다. 또한, 삭제 과정에서 발생할 수 있는 문의 사항에 대한 FAQ도 포함하여 사용자의 궁금증을 해소하고자 노력했습니다. 이러한 정보 제공은 사용자의 알 권리를 충족시키는 동시에, 심사 과정에서도 애플 측에 우리가 사용자 데이터 관리에 충분히 신경 쓰고 있음을 보여주는 좋은 근거가 됩니다.&lt;/p&gt;

&lt;p&gt;해당 웹 페이지의 URL은 앱 내 '계정 삭제' 버튼 주변이나 설정 메뉴의 '개인정보처리방침' 섹션 등에서 접근할 수 있도록 링크를 연결했습니다. 예를 들어, 다음과 같은 형태로 웹 페이지 주소를 만들 수 있습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-domain.com/account-deletion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 웹 페이지는 앱의 업데이트 없이도 내용을 수정할 수 있다는 장점도 있습니다. 앞으로도 개인 정보 관련 정책이나 플랫폼 지침이 변경될 경우, 이 페이지를 통해 빠르게 사용자에게 최신 정보를 제공할 수 있게 되었습니다. 단순히 심사 통과를 위한 것이 아니라, 사용자와의 신뢰를 구축하는 데에도 중요한 역할을 한다고 생각합니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  재심사 통과와 얻게 된 교훈
&lt;/h3&gt;

&lt;p&gt;계정 삭제 기능 구현과 웹 페이지 마련까지 모든 작업을 마친 후, 저는 업데이트된 앱을 다시 앱스토어에 제출했습니다. 솔직히 한 번 반려되었던 경험 때문에 이번에도 혹시나 하는 불안감이 없지 않았습니다. 심사팀에서 또 다른 문제를 지적할까 봐 노심초사하며 기다렸던 기억이 생생합니다. 다행히 며칠 후, 앱이 성공적으로 심사를 통과하여 앱스토어에 게시되었다는 기쁜 소식을 받을 수 있었습니다. 그제야 비로소 안도의 한숨을 내쉬었지요. 이 과정을 통해 저는 여러 가지 중요한 교훈을 얻을 수 있었습니다.&lt;/p&gt;

&lt;p&gt;가장 큰 교훈은 앱 개발 시 단순히 기능 구현에만 집중할 것이 아니라, 플랫폼별 심사 지침과 사용자 개인 정보 보호 규정을 사전에 충분히 검토해야 한다는 점입니다. 특히 앱스토어와 같은 주요 플랫폼의 지침은 주기적으로 변경되고 강화되므로, 개발자는 항상 최신 정보를 파악하고 반영하려는 노력이 필요하다는 것을 깨달았습니다. 젊은 개발자 친구들은 이런 변화에 더 민감하게 반응하고 빠르게 적용하는 것 같더군요. 저도 이제는 '옛날 방식'에만 머물지 않고, 꾸준히 새로운 정보를 습득해야겠다고 다짐했습니다.&lt;/p&gt;

&lt;p&gt;이번 경험은 저에게 단순한 기술적 문제를 넘어, 사용자의 데이터를 대하는 개발자의 자세에 대해 다시 한번 생각하게 만드는 계기가 되었습니다. 사용자의 계정은 단순히 앱 이용을 위한 수단이 아니라, 소중한 개인 정보의 집합이라는 것을 잊지 말아야겠습니다. 앞으로는 새로운 앱을 기획할 때부터 계정 생성과 함께 삭제 기능을 필수적으로 고려하고, 관련 정책 안내에도 더욱 신경 쓸 생각입니다. 한 번의 시행착오를 통해 더 나은 개발자가 될 수 있었다는 점에서, 이번 경험은 저에게 값진 기록으로 남을 것입니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  이번에 정리한 내용
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;App Store 심사 지침 5.1.1(v)의 중요성&lt;/li&gt;
&lt;li&gt;Firebase Auth 환경에서 사용자 계정 삭제 기능 구현 방법&lt;/li&gt;
&lt;li&gt;계정 삭제 시 사용자 데이터 정리 및 재인증 처리&lt;/li&gt;
&lt;li&gt;계정 삭제 정책 안내 웹 페이지의 필요성과 구성&lt;/li&gt;
&lt;li&gt;플랫폼별 심사 지침을 사전에 검토해야 하는 이유&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  마치며
&lt;/h3&gt;

&lt;p&gt;이번 앱스토어 심사 거부와 계정 삭제 기능 구현 경험은 저에게 중요한 이정표가 되었습니다. 기술적인 구현 자체는 어렵지 않았지만, 그 배경에 깔린 플랫폼 정책과 사용자 개인 정보 보호라는 더 큰 맥락을 이해하는 데 시간이 필요했네요. 앞으로는 어떤 앱을 만들더라도, 사용자 경험의 시작부터 끝까지, 심지어는 앱을 떠나는 순간까지도 개발자가 책임감을 가지고 설계해야 한다는 것을 잊지 않겠습니다. 최신 트렌드와 규정을 늘 확인하며 개발에 임하는 것이 저 같은 연륜 있는 개발자에게도 꼭 필요한 자세라는 것을 다시 한번 깨달았습니다.&lt;/p&gt;

</description>
      <category>firebaseauth</category>
      <category>flutter</category>
      <category>511v</category>
    </item>
    <item>
      <title>Fail2ban, Nginx 가상호스트 공격을 놓치던 사각지대 해결기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Tue, 14 Jul 2026 05:37:11 +0000</pubDate>
      <link>https://dev.to/kys7442/fail2ban-nginx-gasanghoseuteu-gonggyeogeul-nohcideon-sagagjidae-haegyeolgi-2loc</link>
      <guid>https://dev.to/kys7442/fail2ban-nginx-gasanghoseuteu-gonggyeogeul-nohcideon-sagagjidae-haegyeolgi-2loc</guid>
      <description>&lt;h2&gt;
  
  
  Fail2ban, Nginx 가상호스트 공격을 놓치던 사각지대 해결기
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo23f6hzspgwf07b6h5v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo23f6hzspgwf07b6h5v.png" alt="Fail2ban, Nginx 가상호스트 공격을 놓치던 사각지대 해결기" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;밤낮없이 서버를 지키는 파수꾼 fail2ban이 어느 날부터인가 왠지 모르게 뚫린 것 같은 찜찜함을 안겨주더군요. WordPress 사이트에 계속해서 wp-login 무차별 대입 공격 시도가 감지되는데, 정작 fail2ban은 아무런 차단 조치도 취하지 않고 있었습니다. 로그 파일에는 분명히 공격 흔적이 선명하게 남아있었지만, fail2ban은 마치 눈뜬장님처럼 그 공격들을 흘려보내고 있었죠. 처음엔 필터 문제인가 싶어 몇 번을 들여다봤는지 모릅니다. 하지만 원인은 예상외로 단순한 곳에 있었습니다. 바로 Nginx 가상호스트별로 분리된 로그 경로 때문이었죠. 이 글에서는 fail2ban이 Nginx 가상호스트 환경에서 발생한 공격을 놓치고 있었던 이유와, 이를 해결하기 위해 어떤 과정을 거쳤는지 제가 직접 부딪히고 해결한 경험을 공유해 드리고자 합니다. 서버를 운영하면서 의외의 사각지대가 발생할 수 있다는 것을 다시 한번 깨달았던 소중한 경험이었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  이 글에서 짚는 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Nginx 가상호스트 환경에서 fail2ban이 로그를 놓치는 원인&lt;/li&gt;
&lt;li&gt;fail2ban WordPress jail의 logpath 설정의 중요성&lt;/li&gt;
&lt;li&gt;가상호스트별 로그 파일 경로를 fail2ban에 정확히 지정하는 방법&lt;/li&gt;
&lt;li&gt;서버 보안 설정 시 로그 경로 확인의 중요성&lt;/li&gt;
&lt;li&gt;실제 공격 차단 결과를 통해 fail2ban 설정 검증하기&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이런 분께 — Nginx와 fail2ban을 사용하여 WordPress 사이트를 운영하는 개발자 및 서버 관리자 · 난이도는 중급 정도&lt;/p&gt;

&lt;h3&gt;
  
  
  문제의 시작: Nginx 가상호스트와 fail2ban의 사각지대
&lt;/h3&gt;

&lt;p&gt;어느 날 갑자기 WordPress 관리자 페이지에 무차별 대입 공격이 쏟아지고 있다는 알림을 받게 되었습니다. 서버 로그를 확인해보니 wp-login.php로의 접근 시도가 계속해서 기록되고 있었죠. 하지만 이상하게도, 이런 공격들을 막아줘야 할 fail2ban은 전혀 작동하지 않는 듯했습니다. 평소 같으면 몇 번의 시도 후에 IP가 차단되었다는 메시지가 fail2ban 로그에 남아야 하는데, 그 어떤 기록도 찾을 수 없더군요. 공격은 계속되는데, 방어 시스템은 침묵하고 있는 상황이었습니다.&lt;/p&gt;

&lt;p&gt;처음에는 fail2ban 필터 규칙이 너무 느슨한가 싶어 &lt;code&gt;maxretry&lt;/code&gt;나 &lt;code&gt;findtime&lt;/code&gt; 같은 설정을 조정해보려고 했습니다. WordPress용 필터인 &lt;code&gt;wordpress.conf&lt;/code&gt;나 &lt;code&gt;wordpress-hard.conf&lt;/code&gt;의 내용도 꼼꼼히 살펴보았죠. 혹시 패턴 매칭에 문제가 있나 싶어 공격 로그를 직접 복사해서 &lt;code&gt;fail2ban-regex&lt;/code&gt; 명령어로 테스트까지 해보았습니다. 하지만 필터 자체는 정상적으로 공격 패턴을 인식하는 것으로 나타났습니다. 그렇다면 대체 무엇이 문제일까, 한참을 헤매게 되더군요.&lt;/p&gt;

&lt;p&gt;공격 시도는 실시간으로 Nginx의 &lt;code&gt;access.log&lt;/code&gt;에 기록되고 있었지만, fail2ban은 그 로그들을 전혀 보지 못하는 것 같았습니다. 이 문제는 단순히 WordPress 로그인 공격뿐만 아니라, 다른 가상호스트에 대한 잠재적인 SQL 인젝션(SQLi)이나 파일 포함(LFI) 공격 시도까지도 fail2ban이 감지하지 못할 수 있다는 불안감을 안겨주었습니다. 이는 서버 전체 보안에 심각한 구멍이 될 수 있는 상황이었죠. 당장이라도 원인을 찾아 해결해야 한다는 압박감을 느꼈습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  로그 파일 추적: 공격 기록은 어디에 숨어있었나?
&lt;/h3&gt;

&lt;p&gt;fail2ban 필터가 정상 작동하는 것을 확인한 후, 다음으로 의심한 것은 바로 로그 파일의 위치였습니다. fail2ban이 감시해야 할 로그를 제대로 들여다보고 있지 못하는 것이 아닐까 하는 가설을 세웠죠. 제가 운영하는 서버는 Nginx를 사용하고 있었고, 여러 WordPress 사이트를 가상호스트(Virtual Host)로 운영하고 있었습니다. Nginx 설정 파일을 다시 한번 꼼꼼하게 살펴보게 되었네요.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nginx.conf&lt;/code&gt;의 기본 설정과 각 가상호스트 설정 파일을 하나씩 열어보았습니다. 예상대로, Nginx는 기본 &lt;code&gt;access.log&lt;/code&gt; 외에 각 가상호스트 설정 블록 내에서 별도의 &lt;code&gt;access_log&lt;/code&gt; 지시어를 사용하여 특정 도메인에 대한 로그를 별도 파일로 기록하고 있었습니다. 예를 들어, &lt;code&gt;your-domain.com&lt;/code&gt;이라는 가상호스트의 로그는 &lt;code&gt;/var/log/nginx/your-domain.com_access.log&lt;/code&gt;와 같은 경로에 저장되고 있었던 것이죠. 이 사실을 확인하고 나서야 문제가 명확해지기 시작했습니다. 공격 시도는 분명히 특정 가상호스트로 들어왔고, 그 기록은 해당 가상호스트의 전용 로그 파일에 남았던 것입니다.&lt;/p&gt;

&lt;p&gt;이러한 Nginx의 가상호스트별 로그 분리 방식은 서버 관리자 입장에서는 트래픽 분석이나 문제 해결에 훨씬 효율적입니다. 하지만 fail2ban과 같은 로그 기반 보안 도구를 설정할 때는 자칫 사각지대를 만들 수 있는 요인이 되기도 합니다. fail2ban이 어떤 로그 파일을 감시하고 있는지를 정확히 파악하는 것이 무엇보다 중요하겠다는 것을 깨닫는 순간이었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  WordPress Jail의 초기 설정과 오해
&lt;/h3&gt;

&lt;p&gt;문제가 발생했던 당시 fail2ban의 WordPress jail 설정은 대부분 기본값에 가까웠습니다. &lt;code&gt;/etc/fail2ban/jail.d/wordpress.conf&lt;/code&gt; 파일에는 다음과 같은 내용이 포함되어 있었죠. 여기서 핵심은 &lt;code&gt;logpath&lt;/code&gt; 설정이었습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/fail2ban/jail.d/wordpress.conf
[wordpress]
enabled = true
port = http,https
filter = wordpress
logpath = /var/log/nginx/access.log
maxretry = 3
bantime = 86400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;보시다시피 &lt;code&gt;logpath&lt;/code&gt;는 &lt;code&gt;/var/log/nginx/access.log&lt;/code&gt; 하나만을 바라보고 있었습니다. 저는 Nginx 서버를 운영하면서 이 경로가 모든 웹 트래픽 로그를 담고 있을 것이라고 막연히 생각하고 있었던 겁니다. 대부분의 설치 가이드나 기본 설정이 이렇게 되어 있기 때문에, 별다른 의심 없이 사용하고 있었던 것이죠. 하지만 앞서 말씀드렸듯이 Nginx는 가상호스트별로 로그를 분리해서 기록하고 있었고, 실제로 wp-login 공격 시도는 &lt;code&gt;/var/log/nginx/your-domain.com_access.log&lt;/code&gt; 같은 개별 로그 파일에 쌓이고 있었습니다.&lt;/p&gt;

&lt;p&gt;이런 오해는 저뿐만 아니라 많은 개발자나 서버 관리자들이 흔히 겪을 수 있는 부분이라고 생각합니다. 기본 설정이 모든 환경에 최적화되어 있을 것이라는 가정에서 비롯되는 것이죠. 특히 여러 도메인을 하나의 서버에서 운영하는 Nginx 가상호스트 환경에서는 기본 &lt;code&gt;access.log&lt;/code&gt; 외에 추가적인 로그 파일이 생성될 수 있다는 점을 항상 염두에 두어야 합니다. fail2ban이 특정 공격을 감지하지 못한다면, 가장 먼저 &lt;code&gt;logpath&lt;/code&gt; 설정과 실제 로그 파일의 위치를 대조해보는 것이 현명한 접근법임을 다시 한번 확인하는 계기가 되었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  원인 분석: 가상호스트별 로그와 fail2ban의 불일치
&lt;/h3&gt;

&lt;p&gt;이제 문제는 명확해졌습니다. fail2ban은 &lt;code&gt;/var/log/nginx/access.log&lt;/code&gt; 파일만 열심히 들여다보고 있었고, 실제로 공격 로그가 기록되고 있던 &lt;code&gt;/var/log/nginx/your-domain.com_access.log&lt;/code&gt; 파일은 아예 감시 대상에서 제외되어 있었던 것이죠. 이는 Nginx의 기본 설정과 fail2ban의 WordPress jail 설정 간의 근본적인 불일치 때문에 발생한 것이었습니다. 마치 도서관의 특정 코너에만 관심을 가지고 다른 중요한 코너는 아예 존재하지 않는다고 생각한 것과 비슷하다고 할까요?&lt;/p&gt;

&lt;p&gt;이러한 불일치는 단순한 설정 오류를 넘어, 서버 보안에 심각한 공백을 초래할 수 있습니다. fail2ban이 감지하지 못하는 공격들은 계속해서 서버 자원을 소모하고, 잠재적으로 더 심각한 보안 취약점으로 이어질 수 있기 때문입니다. 특히 무차별 대입 공격은 성공하면 계정 탈취로 직결될 수 있으므로, 제때 차단하는 것이 매우 중요합니다. 다른 가상호스트에서 SQLi나 LFI 같은 공격이 발생했더라도, 로그 경로가 지정되어 있지 않았다면 이 역시 감지되지 않고 넘어갈 수 있었을 겁니다.&lt;/p&gt;

&lt;p&gt;가장 중요한 교훈은 '기본 설정이 모든 환경을 커버한다고 가정하지 말라'는 것이었습니다. 특히 서버 환경이 복잡해질수록, 각 컴포넌트(Nginx, fail2ban 등)의 설정이 실제 운영 환경과 어떻게 상호작용하는지 세심하게 검토해야 합니다. 이 문제를 해결하기 위해서는 fail2ban에게 '네가 봐야 할 로그 파일이 하나 더 있어!'라고 알려주는 작업이 필요했습니다. 결국, &lt;code&gt;logpath&lt;/code&gt; 설정에 감시해야 할 모든 로그 파일 경로를 명시적으로 추가해주는 것이 핵심 해결책이었죠.&lt;/p&gt;

&lt;h3&gt;
  
  
  해결 과정: &lt;code&gt;logpath&lt;/code&gt; 확장과 시스템 안정화
&lt;/h3&gt;

&lt;p&gt;문제의 원인을 파악한 뒤에는 해결책을 적용하는 것은 비교적 간단했습니다. fail2ban의 WordPress jail 설정 파일인 &lt;code&gt;/etc/fail2ban/jail.d/wordpress.conf&lt;/code&gt;를 열어 &lt;code&gt;logpath&lt;/code&gt; 항목에 공격 로그가 기록되고 있던 가상호스트별 로그 파일 경로를 추가해주었습니다. 여러 로그 파일을 감시하려면 공백으로 구분하여 나열해주면 됩니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/fail2ban/jail.d/wordpress.conf
[wordpress]
enabled = true
port = http,https
filter = wordpress
logpath = /var/log/nginx/access.log /var/log/nginx/your-domain.com_access.log
maxretry = 3
bantime = 86400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;여기서 &lt;code&gt;/var/log/nginx/your-domain.com_access.log&lt;/code&gt;는 실제 운영 중인 가상호스트의 로그 파일 경로로 바꾸어 적용했습니다. 만약 다른 가상호스트가 있다면 그 경로들도 함께 추가해주어야 합니다. 이 변경을 통해 fail2ban이 이제 모든 관련 로그 파일을 감시할 수 있게 된 것이죠. 이 외에도, 점검 과정에서 &lt;code&gt;logrotate&lt;/code&gt;가 &lt;code&gt;.bak&lt;/code&gt; 중복 파일 때문에 간헐적으로 실패하는 것을 발견하여 해당 문제도 함께 처리했습니다. 또한, 컨테이너 환경의 메모리 할당량이 부족해 보이던 부분도 상향 조정하여 전반적인 서버 안정성을 높이는 기회로 삼았습니다.&lt;/p&gt;

&lt;p&gt;설정을 변경한 후에는 fail2ban 서비스를 재시작하여 변경 사항을 적용해야 합니다. 이 과정이 제대로 이루어지지 않으면 아무리 설정을 바꿔도 효과를 볼 수 없으니 주의해야 합니다. 다음 명령어를 사용해서 서비스를 다시 시작했습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemctl restart fail2ban
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;서비스 재시작 후에는 fail2ban이 로그 파일을 다시 읽고 새로운 설정을 적용하게 됩니다. 이제 제대로 작동할 것이라는 기대를 가지고 다음 단계를 진행했습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  즉각적인 효과와 검증: 141개의 차단 기록
&lt;/h3&gt;

&lt;p&gt;fail2ban 서비스를 재시작하고 나서 얼마 지나지 않아, 저는 놀라운 결과를 확인하게 되었습니다. fail2ban이 재시작되면서 과거 로그들을 소급하여 다시 분석했는데, 무려 141건의 과거 공격 시도를 즉시 차단 처리했다는 기록이 로그에 나타났기 때문입니다. 그동안 fail2ban이 얼마나 많은 공격을 놓치고 있었는지 한눈에 알 수 있는 대목이었죠. 이 숫자를 보고 나니, 제대로 된 설정 하나가 얼마나 중요한지 새삼 깨닫게 되더군요.&lt;/p&gt;

&lt;p&gt;재시작 이후 새로운 wp-login 무차별 대입 공격 시도가 들어왔을 때도, fail2ban은 지체 없이 해당 IP를 차단하는 것을 확인했습니다. 더 이상 공격 시도가 감지되는데도 아무런 조치도 취해지지 않는 답답한 상황은 벌어지지 않았습니다. Nginx 로그에는 공격 기록이 남았지만, fail2ban 로그에서는 해당 IP가 즉시 차단되었다는 메시지를 확인할 수 있었죠. 이는 &lt;code&gt;logpath&lt;/code&gt; 설정 변경이 정확히 문제를 해결했다는 확실한 증거였습니다.&lt;/p&gt;

&lt;p&gt;또한, 이번 해결 과정은 단순히 WordPress 로그인 공격 방어를 넘어, 다른 가상호스트에 대한 잠재적인 SQLi나 LFI 공격 시도까지도 이제는 fail2ban이 감지하고 차단할 수 있을 것이라는 기대감을 주었습니다. 모든 로그 파일을 감시하도록 설정했으니, 이제는 어떤 유형의 공격이든 fail2ban의 눈을 피하기는 어려울 것입니다. 이처럼 직접 문제를 해결하고 그 효과를 바로 확인하는 과정은 개발자에게 큰 만족감을 안겨주는 것 같습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  마무리: 서버 보안 설정의 꼼꼼함은 기본
&lt;/h3&gt;

&lt;p&gt;이번 fail2ban 설정 오류 경험은 서버 보안에 있어 '꼼꼼함'이 얼마나 중요한지를 다시 한번 일깨워주었습니다. 특히 여러 서비스를 한 서버에서 운영하는 가상호스트 환경에서는 기본 설정이 모든 상황을 커버한다고 안일하게 생각해서는 안 된다는 것을 뼈저리게 느꼈습니다. 항상 실제 로그가 어디에 기록되는지, 그리고 보안 도구가 그 로그를 제대로 감시하고 있는지 확인하는 습관을 들여야 합니다.&lt;/p&gt;

&lt;p&gt;단순히 'fail2ban이 설치되어 있으니 안전할 거야'라고 생각하기보다는, 주기적으로 fail2ban 로그를 확인하고, 실제로 공격이 발생했을 때 제대로 작동하는지 검증하는 과정이 필요합니다. 저처럼 &lt;code&gt;logpath&lt;/code&gt; 하나 때문에 중요한 공격들을 놓치는 일이 없도록, 여러분의 서버 환경도 한번 점검해보시길 권해드립니다. 불필요한 비용을 들여 비싼 보안 솔루션을 도입하기 전에, 현재 운영 중인 시스템의 기본 설정을 똑똑하게 최적화하는 것이 먼저라는 생각이 드네요. 작은 설정 변경 하나가 서버 보안에 큰 차이를 만들 수 있음을 기억해주세요.&lt;/p&gt;

&lt;h3&gt;
  
  
  끝으로
&lt;/h3&gt;

&lt;p&gt;fail2ban이 Nginx 가상호스트의 공격을 놓치고 있었던 문제는 결국 &lt;code&gt;logpath&lt;/code&gt; 설정의 사각지대에서 비롯되었습니다. 이번 경험을 통해 서버 환경의 복잡성에 비례하여 보안 설정도 더욱 세심하게 관리해야 한다는 교훈을 얻었습니다. 이 글이 같은 문제로 고민하는 분들에게 작은 실마리가 되기를 바랍니다.&lt;/p&gt;

</description>
      <category>fail2ban</category>
      <category>nginx</category>
      <category>wordpress</category>
      <category>logpath</category>
    </item>
    <item>
      <title>Wow Cup Baby Tritan Snow Straw Cup: A Good First Cup for Your Child?</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Mon, 13 Jul 2026 20:39:57 +0000</pubDate>
      <link>https://dev.to/kys7442/wow-cup-baby-tritan-snow-straw-cup-a-good-first-cup-for-your-child-1c13</link>
      <guid>https://dev.to/kys7442/wow-cup-baby-tritan-snow-straw-cup-a-good-first-cup-for-your-child-1c13</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7ndysgfsdgi62p5rt6c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7ndysgfsdgi62p5rt6c.jpg" alt="Wow Cup Baby Tritan Snow Straw Cup, 207ml, 1pc, Pink" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking at social media feeds of young parents, there are many baby products that seem great.&lt;/p&gt;

&lt;p&gt;However, when I took a closer look a bit later, I found some different aspects. Today, for parents considering their child's first straw cup, I'd like to explore the Wow Cup Baby Tritan Snow Straw Cup. Especially if you're concerned about your child getting their clothes or the floor wet every time they drink water, let's see how this product might help.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Secret to Spill-Free Drinking: 360-Degree Valve System
&lt;/h3&gt;

&lt;p&gt;For parents, the process of children drinking water often feels like a constant state of tension. A moment's inattention can lead to wet clothes or a soaked floor. The Wow Cup Baby Tritan Snow Straw Cup claims to have a special technology in this regard. It features a patented 360-degree valve system, designed to prevent spills even when the child tilts the cup. This seems to be a great help for children learning to hold and drink from a cup independently. The spill-proof function is particularly useful when going out or traveling in a car. However, I realized that an adjustment period might be necessary depending on the child's experience and habits with straw cups. If your child is already accustomed to regular straw cups, they might find it a bit awkward at first, so it's good to keep that in mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe Material and Convenient Cleaning
&lt;/h3&gt;

&lt;p&gt;As a product for children, material safety is one of the most important considerations. This Wow Cup is made from Tritan material, which is free from endocrine disruptors. Tritan has the advantage of being durable and safe for hot water sterilization, making it a good choice for parents who prioritize hygiene. Many reviews praise its convenient cleaning, thanks to its 3-part detachable structure: lid, valve, and cup body. Thorough cleaning is directly related to children's health, so such a design will be an important factor in reducing parents' efforts.&lt;/p&gt;

&lt;p&gt;However, it's necessary to check if there's a risk of losing parts due to the complex structure, or if small components are within a child's reach...&lt;/p&gt;

&lt;h3&gt;
  
  
  207ml Capacity: Where Does It Fit?
&lt;/h3&gt;

&lt;p&gt;The Wow Cup Baby Tritan Snow Straw Cup has a capacity of 207ml. This volume is suitable for young children to drink at once and is a manageable size for portability. It's convenient to carry in a bag when going out, and when used at home, it's not too large, making it appropriate for a child to hold and drink from directly. It can be a good choice for a child's first straw cup just after their first birthday, or for those looking for a lightweight straw cup for outings. However, if your child drinks a lot of water, or if parents prefer a larger capacity, this volume might feel a bit insufficient. In such cases, it might be wise to consider other products with larger capacities.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7ndysgfsdgi62p5rt6c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7ndysgfsdgi62p5rt6c.jpg" alt="Wow Cup Baby Tritan Snow Straw Cup, 207ml, 1pc, Pink" width="800" height="800"&gt;&lt;/a&gt;Actual product appearance&lt;/p&gt;

&lt;h3&gt;
  
  
  Price
&lt;/h3&gt;

&lt;p&gt;The Wow Cup Baby Tritan Snow Straw Cup is sold for 18,970 KRW. Considering its patented spill-proof feature, safe Tritan material, and convenient cleaning structure, it seems to be a reasonable price point...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/shorts/H7SjpvmcTYo" rel="noopener noreferrer"&gt;Watch on YouTube Shorts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About the Author&lt;/strong&gt;&lt;br&gt;
I'm a dad raising two kids, and I personally test and choose the products we need.&lt;br&gt;
My reviews are honest, based on my actual experience, and prioritize 'would I buy this again?' over advertising fees.&lt;/p&gt;

</description>
      <category>babyproducts</category>
      <category>parenting</category>
      <category>productreview</category>
      <category>babycup</category>
    </item>
    <item>
      <title>분산형 SSH 공격, fail2ban recidive로 막아낸 이야기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Mon, 13 Jul 2026 07:22:23 +0000</pubDate>
      <link>https://dev.to/kys7442/bunsanhyeong-ssh-gonggyeog-fail2ban-recidivero-maganaen-iyagi-6ao</link>
      <guid>https://dev.to/kys7442/bunsanhyeong-ssh-gonggyeog-fail2ban-recidivero-maganaen-iyagi-6ao</guid>
      <description>&lt;h2&gt;
  
  
  분산형 SSH 공격, fail2ban recidive로 막아낸 이야기
&lt;/h2&gt;

&lt;p&gt;언제부터인가 서버에 잊을만하면 SSH 경고 메일이 계속 들어왔습니다. 분명 fail2ban이 잘 돌고 있고, SSH 포트도 기본이 아닌 다른 걸로 바꿔뒀는데도 말이죠. 첫 아이 키우면서 뭘 해도 처음인 아빠 마음처럼, 서버 운영도 늘 새로운 문제투성이네요. 이번엔 잊을만하면 찾아오는 SSH 무차별 대입 공격에 제대로 대처한 경험을 기록해봅니다. 특히 일반적인 SSH jail로는 잡기 어려운 '분산형 공격'에 어떻게 대응했는지 이야기해보려고 해요. 기존의 fail2ban 설정으로는 반복적인 공격이 막히지 않아 고민이 많았는데, 결국 recidive jail의 설정을 바꿔서 효과적으로 대응할 수 있었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  여기서 확인할 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;fail2ban recidive jail의 작동 원리를 이해합니다.&lt;/li&gt;
&lt;li&gt;분산형 SSH 무차별 대입 공격의 특징을 알아봅니다.&lt;/li&gt;
&lt;li&gt;recidive jail 설정을 변경하여 장기적인 공격을 차단하는 방법을 배웁니다.&lt;/li&gt;
&lt;li&gt;jail.local 파일을 이용한 fail2ban 설정 변경 방법을 확인합니다.&lt;/li&gt;
&lt;li&gt;통신사 동적 IP 환경에서 오탐을 줄이며 방어하는 노하우를 공유합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  이상하게 계속되던 SSH 로그인 시도들
&lt;/h3&gt;

&lt;p&gt;처음엔 fail2ban이 잘 막아주고 있다고 생각했습니다. SSH 포트도 기본이 아닌 다른 걸로 바꿔뒀고, fail2ban SSH jail도 당연히 켜 놨었거든요. 그런데 어느 날부터인가 서버 로그를 보면 잊을만하면 SSH 로그인 시도들이 계속 눈에 띄는 겁니다. 보통 이런 공격들은 특정 IP에서 여러 번 시도하다가 밴(ban) 당하고 잠잠해지는 게 일반적이었는데, 이건 마치 꼬리 자르듯 IP를 계속 바꿔가며 들어오는 느낌이 들었어요. 공격이 들어오는 IP들을 자세히 살펴보니, 대부분 국내 통신사의 동적 IP 대역을 쓰는 것 같더군요.&lt;br&gt;
처음에는 '내 서버가 유명해졌나?' 하고 잠시 어깨를 으쓱하기도 했지만, 이내 '이러다 정말 뚫리면 어쩌지' 하는 불안감이 더 커졌습니다. fail2ban의 상태를 확인해보니, 개별 SSH jail은 열심히 IP들을 차단하고 있었지만, 어딘가 허술한 느낌을 지울 수 없었습니다. 이상한 건, 재범(recidive) 격리, 그러니까 여러 번 밴당한 IP를 장기적으로 막는 jail은 한 번도 발동한 적이 없다는 사실이었습니다. 이건 뭔가 잘못되어가고 있다는 신호 같았어요.&lt;/p&gt;

&lt;h3&gt;
  
  
  재범 격리(recidive)가 조용했던 이유
&lt;/h3&gt;

&lt;p&gt;왜 recidive jail이 침묵하고 있었을까 고민이 많았습니다. 제가 찾아본 바로는, fail2ban의 recidive jail은 '이미 여러 번 다른 jail에서 차단당한 이력이 있는 IP'를 대상으로 더 길게 차단해서 반복 공격을 막는 상위 방어 개념이었습니다. 그런데 저희 서버에 들어오는 공격 패턴은 일반적인 recidive jail의 기본 설정과는 좀 달랐던 거죠.&lt;br&gt;
공격자들은 한 IP에서 로그인 시도를 5번 이상 하지 않고, 2~3번 정도 시도하다가 IP를 바꾸는 방식으로 계속 들어오더군요. fail2ban의 기본 SSH jail 설정은 보통 5회 시도에 1일 관찰 기간(findtime)을 가지는데, 공격자들이 교묘하게 이 임계치를 피해서 공격을 하는 겁니다. 예를 들어, 한 IP에서 3번 시도하고 다른 IP로 바꾸는 식으로 분산해서 공격하니, 개별 IP는 밴당하기 전에 이미 사라지거나, 다시 나타나더라도 '재범'으로 인식될 만큼 충분히 많은 이력을 쌓지 못했던 거죠. 결국, recidive jail이 발동할 조건 자체가 충족되지 않았던 겁니다. 이건 마치 넓은 창으로만 세상을 보다가 정작 코앞의 작은 변화를 놓친 기분이었어요.&lt;/p&gt;

&lt;h3&gt;
  
  
  분산형 공격에 맞춘 recidive 강화 설정
&lt;/h3&gt;

&lt;p&gt;이 문제를 해결하기 위해 가장 중요하다고 생각한 부분은 recidive jail의 설정을 현실적인 공격 패턴에 맞추는 것이었습니다. 단기적인 시도 횟수보다는 '장기적으로 이 IP가 꾸준히 공격을 시도했는가'를 봐야 한다고 판단했죠. 그래서 recidive jail의 findtime(관찰 기간)을 30일로, maxretry(그 기간 안에 밴당해야 하는 횟수)를 3회로 강화했습니다. 즉, 30일이라는 긴 시간 동안 3번만 밴 이력이 생기면 무조건 장기 차단하겠다는 의미였습니다.&lt;br&gt;
설정은 /etc/fail2ban/jail.local 파일의 [recidive] 섹션에 추가하거나 수정해야 합니다. 저도 처음엔 jail.conf를 건드리는 건가 했는데, jail.local이 jail.conf를 덮어쓰는 방식이더군요. 그래서 꼭 jail.local에 반영하는 것이 중요했습니다.&lt;/p&gt;

&lt;p&gt;[recidive]&lt;br&gt;
enabled  = true&lt;br&gt;
findtime = 30d      ; 30일 관찰 창&lt;br&gt;
maxretry = 3        ; 그 안에 3번 밴당하면 장기 격리&lt;br&gt;
bantime  = 1w       ; (환경에 맞춰 조정)&lt;/p&gt;

&lt;p&gt;위 코드는 recidive jail을 활성화하고, 30일 동안 3번 이상 차단된 IP를 일주일간 장기 차단하도록 설정하는 내용입니다. 이렇게 설정하고 나니 왠지 모르게 마음이 좀 놓이는 기분이 들었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  강화된 방어막, 실제 작동 확인하기
&lt;/h3&gt;

&lt;p&gt;설정을 바꾸고 나서 가장 궁금했던 건 '과연 이게 실제로 작동할까?' 였습니다. 단순히 설정만 바꿨다고 끝나는 게 아니니까요. 저는 fail2ban-client 명령어를 통해 recidive jail의 상태를 주기적으로 확인했습니다. 처음엔 여전히 'Currently banned'가 0이었지만, 며칠이 지나자 비로소 숫자가 늘어나기 시작하더군요.&lt;/p&gt;

&lt;p&gt;fail2ban-client status recidive   # Currently banned 가 0에서 늘어나는지&lt;/p&gt;

&lt;p&gt;이 명령어를 입력했을 때, 이전에 항상 0이던 Currently banned 항목에 숫자가 표시되는 것을 보고 정말 안심했습니다. 드디어 이 끈질긴 분산 공격자들이 재범으로 분류되어 장기 격리되기 시작한 것이죠. 동시에 혹시나 정상적인 사용자가 통신사 동적 IP를 할당받아 오탐으로 차단되는 일은 없는지 로그를 주시했습니다. 다행히 제가 관찰한 기간 동안에는 그런 문제는 발생하지 않았습니다. 통신사 동적 IP를 무작정 대량으로 차단하는 대신, 재범 기준을 강화하는 방식으로 접근한 것이 오탐을 줄이는 데 도움이 된 것 같네요.&lt;/p&gt;

&lt;h3&gt;
  
  
  마치며
&lt;/h3&gt;

&lt;p&gt;이번에 겪은 fail2ban recidive jail 튜닝 경험은 '보통 이렇다'고 알려진 기본 설정만으로는 모든 공격 패턴에 대응하기 어렵다는 걸 다시 한번 깨닫게 해주었습니다. 특히 통신사 동적 IP를 활용하는 분산형 공격은 단기적인 임계치로는 잡기 어렵다는 것을 알게 되었네요. 혹시 여러분의 서버에도 끈질기게 들어오는 SSH 공격이 있다면, recidive jail의 findtime을 길게, maxretry를 현실적으로 조정해보는 것을 추천합니다. 그리고 무엇보다 중요한 건, 설정을 바꾼 후에는 반드시 실제로 작동하는지, 그리고 예상치 못한 부작용은 없는지 꼼꼼히 확인하는 과정이라고 생각합니다.&lt;/p&gt;

</description>
      <category>fail2ban</category>
      <category>ssh</category>
      <category>recidive</category>
      <category>jaillocal</category>
    </item>
    <item>
      <title>ROSE11 Newborn Baby Head Shaping Pillow: A Smart Choice Guide</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Sun, 12 Jul 2026 20:48:15 +0000</pubDate>
      <link>https://dev.to/kys7442/rose11-newborn-baby-head-shaping-pillow-a-smart-choice-guide-56km</link>
      <guid>https://dev.to/kys7442/rose11-newborn-baby-head-shaping-pillow-a-smart-choice-guide-56km</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1ulec44l5hvm7euoe2b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1ulec44l5hvm7euoe2b.png" alt="ROSE11 Newborn Baby Head Shaping Pillow" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lately, I've been seeing a lot of products related to baby head shaping on social media. Wondering if my baby might need one, I looked into a few options.&lt;/p&gt;

&lt;p&gt;Among them, I took a closer look at the ROSE11 Newborn Baby Head Shaping Pillow, which has garnered a lot of attention from parents. Today, I'd like to discuss its features and the criteria for choosing a baby head shaping pillow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do babies need head shaping pillows?
&lt;/h3&gt;

&lt;p&gt;In the early stages of life, a baby's skull bones are quite flexible. If they lie in one position for too long, their head shape can become asymmetrical. This is sometimes referred to as 'plagiocephaly' or 'brachycephaly'. To help promote a well-rounded head shape, various baby head shaping pillows are available.&lt;/p&gt;

&lt;p&gt;The ROSE11 Newborn Baby Head Shaping Pillow is designed for parents with these concerns. It is said to help babies maintain a stable posture and promote an even head shape. Especially during the newborn period, when babies spend a lot of time lying down, the role of the pillow becomes even more crucial.&lt;/p&gt;

&lt;h3&gt;
  
  
  ROSE11 Pillow's Ergonomic Design and Material
&lt;/h3&gt;

&lt;p&gt;The ROSE11 Newborn Baby Head Shaping Pillow features an ergonomic design that considers both baby's comfort and head shaping. The center of the pillow is softly designed for the baby's head to rest comfortably, while the surrounding area provides stable support for the baby's neck, aiding in sound sleep. Looking at the specifications, it's clear that the focus is on using breathable materials to prevent sweating on the baby's head and provide a comfortable sleeping environment. Newborns often sweat a lot on their heads due to their immature thermoregulation, so this breathability is one of the aspects parents consider important.&lt;/p&gt;

&lt;p&gt;The ease of washing also adds convenience for hygiene management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Considerations for Choosing a Smart Baby Head Shaping Pillow
&lt;/h3&gt;

&lt;p&gt;When choosing a baby head shaping pillow, there are a few important criteria to consider. First, check if the size is appropriate for your baby's age in months. Newborn pillows should be designed to safely support a baby's small head and neck.&lt;/p&gt;

&lt;p&gt;The ROSE11 pillow is explicitly labeled for 'newborns', indicating it has a design suitable for that age group. Next, it's good to check the safety of the material and the ease of hygiene management. As it's a product that directly touches the baby's skin, ensure it's non-irritating and easy to wash. Finally, the price point is also an important factor, haha. The ROSE11 Newborn Baby Head Shaping Pillow is priced reasonably at 25,800 KRW, making it an affordable option for parents considering their first baby head shaping pillow. Of course, since each baby's growth rate and physique differ, it's important to compare the specifications of various products and find the one that best suits your baby.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1ulec44l5hvm7euoe2b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1ulec44l5hvm7euoe2b.png" alt="ROSE11 Newborn Baby Head Shaping Pillow" width="800" height="800"&gt;&lt;/a&gt;Reference Image&lt;/p&gt;

&lt;p&gt;The ROSE11 Newborn Baby Head Shaping Pillow is currently available on Coupang for 25,800 KRW with Rocket Delivery. This will be a good option for parents who prefer fast shipping.&lt;/p&gt;

&lt;p&gt;I've also summarized it in a video → &lt;a href="https://www.youtube.com/shorts/NeBDOM4p-x0" rel="noopener noreferrer"&gt;This Product Short&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About the Author&lt;/strong&gt;&lt;br&gt;
I'm a dad raising two kids, and I personally test and choose necessary items. &lt;br&gt;
My criteria are 'would I actually buy this again?' rather than advertising fees, and I honestly write about what I've experienced.&lt;/p&gt;

</description>
      <category>babyproducts</category>
      <category>newborncare</category>
      <category>parentingtips</category>
      <category>productreview</category>
    </item>
    <item>
      <title>애드센스 고지 페이지, 자동 발행과 수동 확인 사이의 균형 잡기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Sat, 11 Jul 2026 22:30:12 +0000</pubDate>
      <link>https://dev.to/kys7442/aedeusenseu-goji-peiji-jadong-balhaenggwa-sudong-hwagin-saiyi-gyunhyeong-jabgi-1c7o</link>
      <guid>https://dev.to/kys7442/aedeusenseu-goji-peiji-jadong-balhaenggwa-sudong-hwagin-saiyi-gyunhyeong-jabgi-1c7o</guid>
      <description>&lt;h2&gt;
  
  
  애드센스 고지 페이지, 자동 발행과 수동 확인 사이의 균형 잡기
&lt;/h2&gt;

&lt;p&gt;애드센스 재심사 과정에서 '저가치 콘텐츠'라는 피드백을 받고 한동안 블로그 운영에 애를 먹었던 적이 있습니다. 콘텐츠 품질을 높이는 것도 중요했지만, 블로그의 신뢰도를 높이는 기본 페이지들이 부족하다는 점도 문제로 지적되더군요. 소개, 개인정보처리방침, 연락처 같은 페이지들이죠. 일반 글이야 AI로 빠르게 생성하고 자동 발행할 수 있었지만, 이런 고지 페이지들은 성격이 완전히 달랐습니다. 한 글자라도 틀리면 법적인 문제가 생기거나 심사에 악영향을 줄 수 있으니, 무턱대고 자동화할 수는 없는 노릇이었죠. 이 문제에 부딪히면서 자동 발행 시스템에 '수동 확인' 단계를 어떻게 유연하게 녹여낼지 깊이 고민하게 되었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  이 글에서 짚는 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;애드센스 재심사 시 신뢰 페이지의 중요성을 이해합니다.&lt;/li&gt;
&lt;li&gt;일반 콘텐츠와 고지 페이지 자동 발행 전략의 차이를 파악합니다.&lt;/li&gt;
&lt;li&gt;완전 자동화가 아닌, '반수동' 발행 파이프라인 설계 방법을 배웁니다.&lt;/li&gt;
&lt;li&gt;Python과 Playwright 기반에서 고정 HTML 콘텐츠를 안전하게 발행하는 방법을 알아봅니다.&lt;/li&gt;
&lt;li&gt;발행 전 내용을 확인하는 dry-run과 특정 페이지만 발행하는 옵션 활용법을 익힙니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;대상:&lt;/strong&gt; 블로그 자동 발행 파이프라인을 운영하며 고지 페이지와 같은 중요한 콘텐츠를 안전하게 관리하고 싶은 개발자 및 블로그 운영자입니다. 자동화와 수동 확인의 적절한 경계를 고민하는 분들에게 실질적인 사례가 될 것입니다.&lt;br&gt;
&lt;strong&gt;난이도:&lt;/strong&gt; 중급&lt;/p&gt;

&lt;h3&gt;
  
  
  애드센스 재심사, 신뢰 페이지의 부재가 발목을 잡던 날
&lt;/h3&gt;

&lt;p&gt;애드센스 심사에서 '저가치 콘텐츠'라는 피드백을 받았을 때 처음에는 단순히 글의 양이나 질 문제라고 생각했습니다. 하지만 여러 자료를 찾아보니, 블로그의 신뢰도를 보여주는 고정 페이지들, 이를테면 '블로그 소개', '개인정보처리방침', '연락처' 같은 페이지들이 없으면 심사에 부정적인 영향을 줄 수 있다는 사실을 알게 되었습니다. 마치 식당을 열면서 메뉴판이나 원산지 표시 같은 기본 정보를 제대로 갖추지 않은 것과 같았죠. 그동안 콘텐츠 발행에만 집중하느라 이런 기본적인 부분은 놓치고 있었더군요. 블로그의 얼굴과도 같은 페이지들이니, 서둘러 만들어 발행해야 했습니다. 그런데 여기서 한 가지 문제가 생겼습니다. 기존에 사용하던 자동 발행 파이프라인으로는 이런 페이지들을 올리기가 애매하다는 점이었죠. 일반적인 포스팅은 AI로 내용을 재가공하고 발행하는 방식이었는데, 고지 페이지는 한 글자도 틀리면 안 되는 민감한 문서들이었으니까요. 이대로 자동화 시스템에 맡겼다가는 오히려 일을 그르칠 것 같다는 불안감이 엄습했습니다. 정확성만큼은 타협할 수 없는 영역이었습니다. 그래서 기존의 발행 시스템을 어떻게 수정해야 할지, 고민이 시작되었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  고지 페이지, 일반 포스팅과는 다른 접근이 필요하다는 깨달음
&lt;/h3&gt;

&lt;p&gt;블로그 콘텐츠를 자동으로 생성하고 발행하는 파이프라인은 정말 편리합니다. 수많은 글을 빠르게 생산해낼 수 있으니, 블로그를 성장시키는 데 큰 도움이 되었죠. 하지만 '개인정보처리방침' 같은 고지 문서는 이야기가 달랐습니다. 이 문서는 법적인 효력을 가질 수도 있고, 애드센스 심사에서는 그 내용의 정확성이 매우 중요합니다. AI가 내용을 요약하거나 윤문하는 과정에서 단어 하나라도 바뀌면 원래 의도와 다른 뜻이 되거나 법적 근거가 흔들릴 수 있습니다. 이는 오히려 독이 될 수 있는 부분이었죠. 게다가 티스토리 발행 과정에서 때때로 캡차(CAPTCHA)나 추가 검수 단계가 불쑥 나타날 때가 있더군요. 완전 자동화 스크립트는 이런 예상치 못한 상황에서 막히거나, 심지어는 잘못된 버튼을 눌러버리는 사고를 칠 수도 있었습니다. 이 모든 상황을 고려해보니, 이 페이지들만큼은 '발행' 버튼을 누르는 순간만이라도 사람의 눈으로 직접 확인하는 과정이 필요하다는 결론에 도달했습니다. 모든 것을 자동으로 처리하는 것이 능사가 아니라는 것을 절감하는 순간이었죠.&lt;/p&gt;

&lt;h3&gt;
  
  
  정확성을 위한 본문 보존, 그리고 반수동 발행 전략
&lt;/h3&gt;

&lt;p&gt;고민 끝에 두 가지 핵심 원칙을 세웠습니다. 첫째, 본문 내용은 절대로 AI를 통한 재가공 없이 HTML 원문 그대로 발행해야 한다는 것이었습니다. 고지 페이지의 생명은 '원문 그대로의 정확성'에 있었기 때문에, 요약이나 윤문은 철저히 배제해야 했습니다. 이를 위해 발행 스크립트에서 해당 페이지들을 처리할 때는 AI 재가공 로직을 건너뛰도록 명시적으로 설정했습니다. 둘째, '발행' 버튼만큼은 사람이 직접 누르도록 했습니다. 스크립트가 제목과 본문을 완벽하게 채워 넣고, 마지막 발행 버튼 앞에서 멈춰 서서 사용자에게 최종 확인을 맡기는 방식이었죠. 저는 이것을 '반수동 발행'이라고 부르게 되었습니다. 물론 &lt;code&gt;--auto&lt;/code&gt; 같은 옵션을 주면 완전 자동 발행도 가능하게 설계했지만, 기본값은 언제나 수동 발행으로 두어 안전성을 최우선으로 확보했습니다. 이렇게 하니 정확성을 지키면서도 발행 과정의 대부분은 자동화의 편리함을 누릴 수 있게 되었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  스크립트로 구현한 발행 파이프라인의 유연성
&lt;/h3&gt;

&lt;p&gt;티스토리 발행 엔진은 Python과 Playwright를 기반으로 하고 있었습니다. 이 엔진에 고정 HTML 파일을 읽어와 티스토리 편집기에 붙여 넣는 로직을 추가했습니다. 가장 중요한 것은 앞서 언급한 '본문 원문 보존'과 '반수동 발행'을 스크립트 상에서 어떻게 구현하느냐였습니다. 저는 &lt;code&gt;manual_publish=True&lt;/code&gt;라는 인자를 기본값으로 두어, 스크립트가 모든 내용을 채운 뒤 발행 버튼을 누르지 않고 대기하도록 만들었습니다. 사용자가 직접 브라우저를 확인하고 발행 버튼을 누르면 되는 방식이죠. 이외에도 몇 가지 유용한 옵션을 추가했습니다. 발행 전 어떤 페이지가 어떻게 올라갈지 미리 확인해볼 수 있는 &lt;code&gt;--dry-run&lt;/code&gt; 옵션, 그리고 세 페이지 중 특정 페이지만 선택해서 발행하고 싶을 때 사용할 수 있는 &lt;code&gt;--only&lt;/code&gt; 옵션이 그것입니다. 이렇게 유연하게 옵션을 제공함으로써, 다양한 상황에 대처할 수 있게 되었어요. 아래는 제가 사용한 명령어 예시입니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  명령어와 함께 확인하는 발행 과정
&lt;/h3&gt;

&lt;p&gt;실제 스크립트를 실행할 때 사용했던 명령어는 다음과 같습니다. 이 명령어들을 통해 제가 설계한 반수동 발행 시스템을 효과적으로 활용할 수 있었죠. uv run python scripts/publish_trust_pages.py 이 명령어는 &lt;code&gt;manual_publish=True&lt;/code&gt;가 기본값이므로, 소개, 개인정보처리방침, 연락처 세 페이지의 제목과 본문을 자동으로 채워 넣은 뒤 발행 버튼 앞에서 대기합니다. 사용자가 직접 브라우저에서 최종 확인 후 발행하면 됩니다. uv run python scripts/publish_trust_pages.py --only about 특정 페이지만 발행하고 싶을 때는 &lt;code&gt;--only&lt;/code&gt; 옵션을 사용했습니다. 예를 들어, '소개' 페이지만 다시 올리고 싶을 때 유용합니다. uv run python scripts/publish_trust_pages.py --dry-run 발행 전에 내용이 제대로 준비되었는지 확인하는 &lt;code&gt;--dry-run&lt;/code&gt; 옵션은 정말 유용했습니다. 이 옵션은 실제 발행은 하지 않고, 어떤 제목과 본문 길이로 콘텐츠가 준비될지 콘솔에 출력해줍니다. 덕분에 불필요한 발행 시도를 줄이고, 문제점을 미리 파악할 수 있었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  꼼꼼한 검증으로 얻은 안심, 그리고 애드센스 재심사 통과
&lt;/h3&gt;

&lt;p&gt;스크립트를 완성하고 나서 가장 먼저 &lt;code&gt;--dry-run&lt;/code&gt; 옵션으로 세 페이지의 제목과 본문 길이가 의도대로 나오는지 확인했습니다. 단순히 글자 수뿐만 아니라, 예상되는 본문 내용이 원문 그대로 정확하게 파싱되는지도 육안으로 점검했죠. 이후 실제 발행 테스트를 진행했습니다. 스크립트가 본문을 원문 그대로(AI 재가공 없이) 티스토리 편집기에 입력하는지, 그리고 가장 중요한 '발행' 버튼 직전까지만 자동으로 채워지고 멈추는지 꼼꼼하게 점검했습니다. 예상대로 스크립트는 모든 내용을 채워 넣은 뒤 제 승인을 기다리고 있더군요. 브라우저 창에서 마지막 내용을 한 번 더 확인하고 발행 버튼을 눌렀습니다. 이 과정을 거쳐 발행된 세 페이지는 마침내 애드센스 재심사에 필요한 신뢰 페이지로서 제 역할을 해주었고, 얼마 지나지 않아 애드센스 승인이라는 기쁜 소식을 받을 수 있었습니다. 자동화의 편리함과 수동 검증의 안전성을 적절히 조합한 덕분이라고 생각합니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  정리하며
&lt;/h3&gt;

&lt;p&gt;이번 경험을 통해 모든 것을 자동화하는 것이 항상 최선의 답은 아니라는 것을 다시 한번 깨달았습니다. 특히 개인정보처리방침처럼 문구의 정확성이 생명인 고지 페이지의 경우, '본문 채우기까지는 자동, 발행 버튼은 사람'이라는 반수동 전략이 가장 안전하고 효율적인 방법이었습니다. 무작정 AI로 요약하거나 윤문하려 했다면 오히려 법적 문구를 훼손하여 심사에 독이 되었을 것입니다. 자동화 시스템을 설계할 때는 이처럼 중요한 '경계'를 설정하는 지혜가 필요하다는 것을 배울 수 있었습니다. 이 경험이 여러분의 블로그 운영과 자동화 파이프라인 구축에 작은 도움이 되기를 바랍니다.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>python</category>
    </item>
    <item>
      <title>Holi 1.8L Electric Kettle: A Must-Have for New Parents</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Sat, 11 Jul 2026 22:03:03 +0000</pubDate>
      <link>https://dev.to/kys7442/holi-18l-electric-kettle-a-must-have-for-new-parents-11jl</link>
      <guid>https://dev.to/kys7442/holi-18l-electric-kettle-a-must-have-for-new-parents-11jl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqqxq3jm1hyr4urdq7ov0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqqxq3jm1hyr4urdq7ov0.png" alt="Holi 1.8L Electric Kettle, White WLY-002" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a weekend morning, while preparing breakfast for the kids, I found myself organizing some thoughts that came to mind.&lt;/p&gt;

&lt;p&gt;If I don't jot down the information I find right away, I quickly forget it, so I've been trying to get into the habit of leaving notes on this blog. Today, I looked at a product that might be particularly useful for parents raising newborns: the 'Holi 1.8L Electric Kettle'...&lt;/p&gt;

&lt;p&gt;As the saying goes, 'parenting is all about the gear,' and a convenient tool can significantly improve the quality of parenting. This product seems like a smart item that can reduce the hassle of preparing formula.&lt;/p&gt;

&lt;h3&gt;
  
  
  Temperature Hold Function Reduces Fatigue from Night Feedings
&lt;/h3&gt;

&lt;p&gt;When a baby wakes up crying at night, parents rub their sleepy eyes and head to the kitchen. Especially during early morning feedings, it's quite challenging to get the water to the exact right temperature while half-asleep, haha. The Holi electric kettle allows for precise temperature settings from 40 to 95 degrees Celsius, in 5-degree increments, and can maintain this temperature for up to 24 hours. If you heat the water in advance and set it to the desired temperature, you can prepare formula immediately when the baby wakes up and needs it, saving a lot of time.&lt;/p&gt;

&lt;p&gt;Having water at the right temperature always ready, without the need to quickly boil and cool it, is expected to significantly reduce the fatigue of early morning feedings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Material Selection for Hygiene and Safety
&lt;/h3&gt;

&lt;p&gt;Since this product holds water for a baby, hygiene and safety are likely the most important considerations for parents. The Holi electric kettle is reportedly made with hygienic 304 stainless steel and heat-resistant glass for the interior parts that come into direct contact with water. Stainless steel 304 is widely used in kitchenware because it doesn't rust and is easy to keep clean, while heat-resistant glass offers durability, providing peace of mind even when holding hot water.&lt;/p&gt;

&lt;p&gt;Considering the nature of a formula kettle, which continuously holds hot water, this choice of materials seems to be an important factor in alleviating parents' concerns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generous Capacity and Intuitive Ease of Use
&lt;/h3&gt;

&lt;p&gt;The capacity of a formula kettle is important for reducing the hassle of refilling water multiple times a day. The Holi electric kettle has a generous 1.8-liter capacity, which seems sufficient for several formula feedings once filled.&lt;/p&gt;

&lt;p&gt;Reducing the frequency of water refills can ease the small burdens of parenting.&lt;/p&gt;

&lt;p&gt;Furthermore, the intuitive touch control on the front of the product is a major advantage. Without complicated buttons, you can easily select and set the desired functions, making it stress-free to use even during busy parenting moments. In particular, the ease of operation during sleepy early morning hours is practical.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqqxq3jm1hyr4urdq7ov0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqqxq3jm1hyr4urdq7ov0.png" alt="Holi 1.8L Electric Kettle, White WLY-002" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Versatile Use: More Than Just a Formula Kettle
&lt;/h3&gt;

&lt;p&gt;This product isn't just limited to its formula kettle function; as its name 'electric kettle' suggests, it can be used for various purposes.&lt;/p&gt;

&lt;p&gt;Even after a baby is weaned from formula, situations requiring boiling water continue to arise. For example, it can be useful for blanching ingredients for baby food, making warm tea, or preparing simple meals like instant noodles. With multiple temperature settings from 40 to 95 degrees Celsius, it can be conveniently used for preparing various beverages or dishes that require specific temperatures. This makes it a practical appliance that can continue to occupy a spot in the kitchen even after the child grows up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/shorts/1Ocay371yyw" rel="noopener noreferrer"&gt;Shorts Page&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About the Author&lt;/strong&gt;&lt;br&gt;
I'm a dad raising two kids, and I personally test and choose the products we need.&lt;br&gt;
My reviews are honest, based on my actual experience, and prioritize 'would I buy this again?' over advertising fees.&lt;/p&gt;

</description>
      <category>parentingtech</category>
      <category>babyproducts</category>
      <category>smarthome</category>
      <category>kitchenappliances</category>
    </item>
    <item>
      <title>Pril Secret of Baking Soda Dish Soap: Fresh Orange Scent Review</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Fri, 10 Jul 2026 20:48:28 +0000</pubDate>
      <link>https://dev.to/kys7442/pril-secret-of-baking-soda-dish-soap-fresh-orange-scent-review-1p55</link>
      <guid>https://dev.to/kys7442/pril-secret-of-baking-soda-dish-soap-fresh-orange-scent-review-1p55</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv44cdfj9kabw9493q555.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv44cdfj9kabw9493q555.png" alt="프릴 시크릿오브 베이킹소다 주방세제 상큼한 오렌지향" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As the mid-summer heat keeps us indoors longer,&lt;/p&gt;

&lt;p&gt;I naturally started paying more attention to organizing every corner of the house. Especially as a father raising children, the hygiene and organization of daily kitchen items are always a concern. While searching for a dish soap that could safely and thoroughly clean everything from children's dishes to the family's plates, Pril Secret of Baking Soda Dish Soap with its refreshing orange scent caught my eye. I decided to take a closer look at its features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe Ingredients for Washing Children's Dishes
&lt;/h3&gt;

&lt;p&gt;I believe children's dishes require a bit more special care.&lt;/p&gt;

&lt;p&gt;Upon researching, I found that this product contains baking soda, which can help effectively remove grease and stains.&lt;/p&gt;

&lt;p&gt;Baking soda has long been a familiar ingredient used in various ways for cleaning and deodorizing, so I feel more at ease using it for my children's dishes. Beyond just cleanliness, the ability to worry less about the ingredients can positively influence parents' choices.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Refreshing Scent Fills the Kitchen While Washing Dishes
&lt;/h3&gt;

&lt;p&gt;Washing dishes is a daily chore, but sometimes the pile of dishes or food odors can make it feel daunting. This dish soap contains a refreshing orange scent, which is said to add a pleasant aroma to the kitchen while washing dishes. It seems like a big advantage that it can reduce unpleasant odors that might linger after cleaning greasy foods, and instead provide a fresh feeling. Though a small detail, it can be a factor that makes dishwashing time a bit more enjoyable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Powerful Cleaning and Rich Lather for a Sparkling Clean
&lt;/h3&gt;

&lt;p&gt;The essence of dish soap, in my opinion, lies in its cleaning power. This product is known to generate a rich lather with just a small amount, effectively removing grease and promising a spotless clean.&lt;/p&gt;

&lt;p&gt;Especially when cleaning greasy cooking utensils or dishes, if there isn't enough lather, you often end up using more detergent. This product seems to reduce such hassle. Efficient cleaning power can contribute to easing kitchen chores in a busy daily life.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv44cdfj9kabw9493q555.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv44cdfj9kabw9493q555.png" alt="프릴 시크릿오브 베이킹소다 주방세제 상큼한 오렌지향" width="800" height="800"&gt;&lt;/a&gt;Actual product appearance&lt;/p&gt;

&lt;h3&gt;
  
  
  Price and Value
&lt;/h3&gt;

&lt;p&gt;Considering these advantages, the price of 10,200 won seems like a reasonable choice. The ability to receive it quickly via Rocket Delivery is also a big advantage for busy parents.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/shorts/Vn04nc7h68E" rel="noopener noreferrer"&gt;Watch on YouTube Shorts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About the Author&lt;/strong&gt;&lt;br&gt;
I'm a dad raising two kids, and I personally try and choose products we need.&lt;br&gt;
My reviews are honest reflections of my experiences, based on whether I would actually buy the product again, rather than advertising fees.&lt;/p&gt;

</description>
      <category>productreview</category>
      <category>homeessentials</category>
      <category>dishsoap</category>
      <category>parenting</category>
    </item>
  </channel>
</rss>
