<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Content Ranking · 인재덕</title>
    <link>https://jared.lynskey.co.nz/ko/tags/content-ranking/</link>
    <description>소프트웨어 엔지니어링과 개발 조직 리더십에 대한 이야기. 성과 리뷰, 채용과 팀 확장, CI/CD, 데브옵스 모니터링, 에이전틱 AI. 서울에서 씁니다.</description>
    <generator>Hugo</generator>
    <language>ko</language>
    <copyright>© 2026 인재덕</copyright>
    <lastBuildDate>Wed, 28 Oct 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ko/tags/content-ranking/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>콘텐츠 랭킹 알고리즘이란: 조회수와 좋아요 너머의 신호들</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2023/2023-10-06-reinenforcement-learning/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2023/2023-10-06-reinenforcement-learning/</guid>
      <pubDate>Fri, 06 Oct 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>content ranking</category>
      <category>metrics</category>
      <category>SEO</category>
      <category>machine learning</category>
      <category>user engagement</category>
      <category>content strategy</category>
      <description>콘텐츠 랭킹 알고리즘이 하는 일, 조회수와 좋아요만으로 순위를 매기면 인기 있는 것만 더 인기 있어지는 이유, 그리고 하나의 점수에 섞어 넣을 만한 신호들을 정리했습니다.</description>
      <content:encoded><![CDATA[<p>콘텐츠 랭킹 알고리즘은 피드나 검색 결과, &lsquo;인기 글&rsquo; 목록에 무엇을 어떤 순서로 보여줄지 정하는 장치입니다. 항목마다 여러 신호(사람들이 어떻게 반응하는지, 콘텐츠 자체가 어떤지, 얼마나 새롭고 관련성이 있는지)로 점수를 매기고, 그 점수대로 정렬하죠. 그런데 조회수와 좋아요만으로 순위를 매기면 &lsquo;부익부&rsquo; 루프가 생깁니다. 위에 있는 글이 눈에 띄니 조회수와 좋아요가 더 쌓이고, 그래서 계속 위에 머무는 식입니다.</p>
<p>이게 조회수와 좋아요의 뻔한 결함입니다. 이미 인기 있는 것에만 보상을 준다는 거죠. 새 콘텐츠는 피드 바닥에서 굶고, 플랫폼은 서서히 재방송 채널이 되어 갑니다. 피드 랭킹은 지금 다니는 회사에서도 은근히 어려운 문제라서, 어떤 다른 신호를 믿을 만한지 꽤 오래 고민해 왔습니다.</p>

<h3 class="relative group">클릭 말고 행동을 본다
    <div id="클릭-말고-행동을-본다" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ed%81%b4%eb%a6%ad-%eb%a7%90%ea%b3%a0-%ed%96%89%eb%8f%99%ec%9d%84-%eb%b3%b8%eb%8b%a4" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>명시적 피드백은 양도 적고 편향돼 있습니다. 반면 행동 데이터는 넘칩니다. 체류 시간과 스크롤 깊이를 보면 실제로 읽었는지, 첫 문단에서 이탈했는지 알 수 있죠. 재방문은 더 강력합니다. 같은 글에 다시 돌아온다는 건 어떤 좋아요보다 확실한 지지입니다. 반대로 빠른 이탈은 유용한 부정 신호입니다. 콘텐츠가 약속한 걸 독자가 받지 못했다는 뜻이니까요.</p>

<h3 class="relative group">콘텐츠 자체에 박혀 있는 신호
    <div id="콘텐츠-자체에-박혀-있는-신호" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%bd%98%ed%85%90%ec%b8%a0-%ec%9e%90%ec%b2%b4%ec%97%90-%eb%b0%95%ed%98%80-%ec%9e%88%eb%8a%94-%ec%8b%a0%ed%98%b8" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>좋은 콘텐츠가 처음부터 좋아요를 쓸어 담는 건 아니지만, 품질은 다른 방식으로 드러납니다. 하나는 새로움입니다. 플랫폼에서 아직 다뤄지지 않은 영역을 다루고 있는가. 또 하나는 구조인데, 깔끔한 포맷과 멀쩡한 문법, 명료함은 인정하고 싶지 않을 만큼 품질과 상관관계가 높습니다. 작성자의 이력도 괜찮은 사전 정보가 됩니다. 다만 좋아요 수가 아니라 과거 글이 독자를 얼마나 붙잡았는지로 판단해야 공정하겠죠. 초기 댓글도 도움이 됩니다. 진심 어린 긍정 댓글 세 개가 반사적인 추천 서른 개를 이깁니다. 여기에 의미 분석을 곁들이면 플랫폼에 비어 있던 구멍을 메워 주는 콘텐츠도 찾아낼 수 있습니다.</p>

<h3 class="relative group">바깥에서 신호를 빌려온다
    <div id="바깥에서-신호를-빌려온다" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%b0%94%ea%b9%a5%ec%97%90%ec%84%9c-%ec%8b%a0%ed%98%b8%eb%a5%bc-%eb%b9%8c%eb%a0%a4%ec%98%a8%eb%8b%a4" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>지금 무엇이 통하는지는 바깥 인터넷이 알려줍니다. 검색 엔진의 트렌딩 키워드와 맞아떨어지는 콘텐츠는 태생적으로 시의성이 있고, 뜨는 주제를 빠르게 다룬 글에는 부스트를 줄 가치가 있습니다. 트위터나 레딧에서 화제인 것은 우리 사용자들이 다음에 원할 것의 예고편인 경우가 많아서, 예측을 조금만 얹으면 파도가 오기 전에 해당하는 새 콘텐츠를 밀어줄 수 있습니다. 자체 검색창도 저평가돼 있습니다. 사용자들이 어떤 주제를 계속 검색하고 있거나, 어떤 글이 검색 엔진에서 꾸준히 유입을 끌어오고 있다면, 그건 추측할 필요도 없는 시의성의 증거입니다.</p>

<h3 class="relative group">신호를 하나의 점수로 섞기
    <div id="신호를-하나의-점수로-섞기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%a0%ed%98%b8%eb%a5%bc-%ed%95%98%eb%82%98%ec%9d%98-%ec%a0%90%ec%88%98%eb%a1%9c-%ec%84%9e%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>아이디어를 파이썬 몇 줄로 옮기면 이렇습니다. 설명을 위한 예시일 뿐, 실제 운영 시스템은 아닙니다. 각 신호를 0~1 범위로 정규화해서 원래 숫자가 큰 신호가 나머지를 묻어 버리지 않게 하고, 가중합으로 점수 하나를 만들고, 시간 감쇠로 새 글도 지난달 인기 글과 겨뤄 볼 기회를 줍니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">WEIGHTS</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;read_depth&#34;</span><span class="p">:</span> <span class="mf">0.3</span><span class="p">,</span> <span class="s2">&#34;revisits&#34;</span><span class="p">:</span> <span class="mf">0.25</span><span class="p">,</span> <span class="s2">&#34;quality&#34;</span><span class="p">:</span> <span class="mf">0.25</span><span class="p">,</span> <span class="s2">&#34;early_comments&#34;</span><span class="p">:</span> <span class="mf">0.2</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">HALF_LIFE_HOURS</span> <span class="o">=</span> <span class="mi">48</span>  <span class="c1"># a post loses half its score every two days</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalise</span><span class="p">(</span><span class="n">values</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">lo</span><span class="p">,</span> <span class="n">hi</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">values</span><span class="p">),</span> <span class="nb">max</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[(</span><span class="n">v</span> <span class="o">-</span> <span class="n">lo</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">hi</span> <span class="o">-</span> <span class="n">lo</span><span class="p">)</span> <span class="k">if</span> <span class="n">hi</span> <span class="o">&gt;</span> <span class="n">lo</span> <span class="k">else</span> <span class="mf">0.0</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">rank</span><span class="p">(</span><span class="n">items</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;items: dicts holding raw signal values plus &#34;age_hours&#34;.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">norm</span> <span class="o">=</span> <span class="p">{</span><span class="n">name</span><span class="p">:</span> <span class="n">normalise</span><span class="p">([</span><span class="n">item</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">items</span><span class="p">])</span> <span class="k">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">WEIGHTS</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">scores</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">item</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">items</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">base</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">weight</span> <span class="o">*</span> <span class="n">norm</span><span class="p">[</span><span class="n">name</span><span class="p">][</span><span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">weight</span> <span class="ow">in</span> <span class="n">WEIGHTS</span><span class="o">.</span><span class="n">items</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">scores</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">base</span> <span class="o">*</span> <span class="mf">0.5</span> <span class="o">**</span> <span class="p">(</span><span class="n">item</span><span class="p">[</span><span class="s2">&#34;age_hours&#34;</span><span class="p">]</span> <span class="o">/</span> <span class="n">HALF_LIFE_HOURS</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">order</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">items</span><span class="p">)),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">i</span><span class="p">:</span> <span class="n">scores</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="n">items</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">order</span><span class="p">]</span></span></span></code></pre></div></div>
<p>가중치는 아래에서 말할 테스트로 확인하기 전까지는 그냥 짐작입니다. 조회수와 좋아요를 넣어도 괜찮습니다. 점수의 전부가 아니라 여러 입력 중 하나가 될 뿐이니까요. 반감기는 오래된 글이 얼마나 빨리 가라앉을지를 정하는데, 이건 그 자체로 큰 주제라서 <a href="https://jared.lynskey.co.nz/ko/posts/2023/2023-09-27-simulating-views/" >피드 랭킹 알고리즘은 오래된 글을 언제 묻어버릴까</a>에 따로 정리해 뒀습니다. 실제 시스템은 가중치를 손으로 정하기보다 모델로 학습시키는 경우가 많지만, 모양은 같습니다. 신호 여러 개를 넣고, 숫자 하나를 뽑고, 정렬하는 거죠.</p>

<h3 class="relative group">마지막엔 정직하게 테스트
    <div id="마지막엔-정직하게-테스트" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a7%88%ec%a7%80%eb%a7%89%ec%97%94-%ec%a0%95%ec%a7%81%ed%95%98%ea%b2%8c-%ed%85%8c%ec%8a%a4%ed%8a%b8" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>신호가 아무리 영리해도 A/B 테스트는 필수입니다. 새 콘텐츠를 일부 사용자에게 노출하고 기존 콘텐츠와 인게이지먼트를 비교하는 거죠. 그리고 퍼지는 모양을 관찰합니다. 플랫폼 안팎의 공유, 언급, 임베드는 조회수 카운터보다 훨씬 많은 걸 말해 줍니다.</p>
<p>현실을 견뎌내는 단일 지표는 없습니다. 실제로 작동하는 랭킹은 행동, 콘텐츠 자체의 품질, 시의성, 정직한 실험을 섞은 것이고, 어느 신호에게도 최종 결정권을 주지 않습니다.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-10-06-reinenforcement-learning/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>피드 랭킹 알고리즘은 오래된 글을 언제 묻어버릴까</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2023/2023-09-27-simulating-views/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2023/2023-09-27-simulating-views/</guid>
      <pubDate>Wed, 27 Sep 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Content Ranking</category>
      <category>Decay</category>
      <category>Freshness</category>
      <category>Engagement</category>
      <description>피드 랭킹 알고리즘이 감쇠 함수로 신선도와 참여도를 저울질하는 방식을, 콘텐츠 피드를 만드는 게 일인 사람 입장에서 풀어 봤습니다.</description>
      <content:encoded><![CDATA[<p>콘텐츠 플랫폼에서 일하다 보니, 사용자 눈에는 절대 보이지 않는 질문 하나를 남들보다 오래 붙잡고 삽니다. 피드는 언제쯤 이 콘텐츠를 그만 보여줘야 할까?</p>
<p>콘텐츠는 낡습니다. 사람들은 새로운 게 뭔지 보려고 앱을 열고, 피드는 거기에 답해야 하죠. 그렇다고 지난달의 훌륭한 글이 단지 새롭지 않다는 이유로 사라져야 하는 것도 아닙니다. 이 트레이드오프를 모델링하는 표준적인 방법이 감쇠 함수인데, 이름에서 풍기는 것보다 훨씬 단순합니다.</p>

<h4 class="relative group">신선도는 중요하다, 어디서나는 아니지만
    <div id="신선도는-중요하다-어디서나는-아니지만" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%8b%a0%ec%84%a0%eb%8f%84%eb%8a%94-%ec%a4%91%ec%9a%94%ed%95%98%eb%8b%a4-%ec%96%b4%eb%94%94%ec%84%9c%eb%82%98%eb%8a%94-%ec%95%84%eb%8b%88%ec%a7%80%eb%a7%8c" aria-label="앵커">#</a>
    </span>
    
</h4>
<p>뉴스 사이트든 소셜 피드든, 열어 볼 때 우리가 찾는 건 대개 가장 최신 소식입니다. 뉴스나 금융 시장처럼 빠르게 움직이는 영역에서 지난주 콘텐츠는 그냥 짐이라, 그런 곳의 랭킹은 최신성에 크게 기울어 있습니다.</p>
<p>하지만 모든 영역이 그렇지는 않습니다. 학술 자료나 롱폼 저널리즘은 시간이 지나도 가치가 유지되고, 오히려 올라가기도 하죠. 콘텐츠의 나이는 대부분의 랭킹 시스템에서 의미가 있지만, 얼마나 큰 가중치를 줘야 하는지는 전적으로 무엇을 랭킹하느냐에 달렸습니다.</p>

<h4 class="relative group">지수 감쇠로 퇴색을 모델링하기
    <div id="지수-감쇠로-퇴색을-모델링하기" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%a7%80%ec%88%98-%ea%b0%90%ec%87%a0%eb%a1%9c-%ed%87%b4%ec%83%89%ec%9d%84-%eb%aa%a8%eb%8d%b8%eb%a7%81%ed%95%98%ea%b8%b0" aria-label="앵커">#</a>
    </span>
    
</h4>
<p>관련성이 흐려지는 과정을 모델링하는 정석은 지수 감쇠입니다. 오래될수록 체감 가치가 가속도가 붙어 떨어진다는 거죠. 가장 단순한 형태는 이렇습니다.</p>
<p><code>relevance = e^(-decay_rate × age)</code></p>
<p>여기서 relevance는 콘텐츠의 현재 가치, decay_rate(감쇠율)는 가치를 잃는 속도, age는 보통 게시 후 경과 일수입니다.</p>
<p>이 모델이 마음에 드는 이유는 조절할 값이 하나뿐이라는 점입니다. 감쇠율만 조절하면 콘텐츠의 유효기간을 몇 시간으로 할지, 며칠로 할지, 몇 주로 할지 정할 수 있습니다. 설정할 게 그게 전부예요.</p>

<h4 class="relative group">좋은 콘텐츠를 살려두는 건 참여도
    <div id="좋은-콘텐츠를-살려두는-건-참여도" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%a2%8b%ec%9d%80-%ec%bd%98%ed%85%90%ec%b8%a0%eb%a5%bc-%ec%82%b4%eb%a0%a4%eb%91%90%eb%8a%94-%ea%b1%b4-%ec%b0%b8%ec%97%ac%eb%8f%84" aria-label="앵커">#</a>
    </span>
    
</h4>
<p>나이는 그림의 절반일 뿐입니다. 조회수, 좋아요, 댓글은 사람들이 실제로 관심 있어 하는지를 알려주는 신호고, 이걸 무시하는 랭킹은 좋은 글까지 나쁜 글과 함께 묻어버립니다. 해결책은 단순하게 둘을 곱하는 겁니다.</p>
<p><code>score = engagement × e^(-decay_rate × age)</code></p>
<p>이렇게 하면 참여도가 높은 오래된 글이 평범한 새 글보다 위에 있을 수 있습니다. 다만 감쇠 항이 밑에서 계속 줄어들기 때문에, 자리를 지키려면 점점 더 많은 참여가 필요해지죠.</p>
<p>실제 라이프사이클은 이렇게 흘러갑니다. 새 글은 신선도 부스트를 받아 노출될 기회를 얻습니다. 사람들이 보고, 좋아요를 누르고, 댓글을 달면 점수가 오릅니다. 날이 지날수록 감쇠가 점수를 갉아먹고, 참여가 꾸준히 이어지는 글만 상위에 남습니다. 새 콘텐츠는 기회를 얻고, 정말 좋은 콘텐츠는 제 힘으로 자리를 지키는 구조입니다.</p>
<p>솔직히 이게 전부입니다. 지수 함수 하나, 참여도 항 하나, 조절할 값 하나. 어려운 건 우리 콘텐츠에 맞는 감쇠율을 정하는 일인데, 그 부분은 순전히 판단의 영역입니다.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-09-27-simulating-views/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
