<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <author>
    <name>어썸블로그</name>
  </author>
  <id>국내의 좋은 블로그 글들을 매일 배달해줍니다.</id>
  <title>개발자 어썸블로그</title>
  <updated>2026-10-08T14:42:01+09:00</updated>
  <entry>
    <author>
      <name>김재호</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;지난 주에 DHH는 Rails 컨퍼런스에서 큰 논란을 불러일으키는 키노트를 발표했습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;나는 더 이상 코드를 읽지 않는다.&lt;br&gt;
프로그래머로서 은퇴하고 메이커의 길을 걷는다.&lt;br&gt;
러스트를 사랑한다. (코드를 읽을 필요가 없다면 못생긴 코드가 뭔 상관이란 말이냐)&lt;br&gt;
요즘 Omarchy 를 만드는 것에 미쳐서 산다. (레일즈와 전혀 상관없는 프로젝트)&lt;br&gt;
이제 코드를 짜는 시대는 끝났다. 너희도 인정해라. 옛 추억이나 하면서 패배자가 되지 마라.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;다른 장소도 아니고 레일즈 컨퍼런스의 오프닝에서 이런 소리를 했으니 기다려왔던 레일즈 팬들의 분노와 황당함이 어땠을지 짐작이 갑니다.&lt;/p&gt;

&lt;p&gt;저는 인상적으로 들었습니다.&lt;br&gt;
대부분의 내용이 공감이 되었거든요.&lt;/p&gt;

&lt;p&gt;아래 내용을 볼 때는 약간 오싹하기까지 했습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://jeho.page/assets/img/explosion.png" alt="뭔가가 폭발되고 있음을 잘 보여주는 슬라이드"&gt;&lt;br&gt;
&lt;em&gt;지난 20개월 간 21년 경력 동안 짠 코드의 절반 이상을 짰다고&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;지금 분명히 뭔가가 폭발되고 있는데…&lt;br&gt;
그게 과연 뭘까 하던 찜찜함을 깨부셔 주던 순간이었습니다.&lt;/p&gt;

&lt;p&gt;GitHub에 들어가 제 최근 1년 커밋을 살펴봤습니다. 약 4,700개.&lt;br&gt;
&lt;img src="https://jeho.page/assets/img/2026-9.png" alt="최근 1년의 커밋"&gt;&lt;/p&gt;

&lt;p&gt;지금까지 한 해 동안 가장 많은 커밋을 한 것은 분명한데, 회사 다닐 때 열심히 일했던 해와 비교해보면 크게 차이가 나진 않습니다.
2013~2015년에 매년 약 3,000개를 했던 걸로 기억하거든요.&lt;/p&gt;

&lt;p&gt;DHH와 나의 차이는 어디서 오는 걸까?&lt;br&gt;
나는 아직 AI를 너무 조심스럽게 쓰고 있는 건 아닐까?&lt;/p&gt;

&lt;p&gt;알아차리지 못하면 죽는다. 도망쳐도 죽는다.&lt;br&gt;
누군가는 엄청난 속도로 달리기 시작했는데, 나는 걸어가고 있는 것 아닌가?&lt;br&gt;
이런 생각에 두렵기도 하고 떨리기도 했습니다.&lt;/p&gt;

&lt;p&gt;하지만 DHH는 마지막에 자신감을 주는 말도 들려주었습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;쫄지마라. 경쟁하는 게 무섭냐? 개발자 아닌 모든 사람들과 경쟁하게 되어서?&lt;br&gt;
무서울 게 뭐가 있어? 너희는 레일즈 개발자잖아. 최고 중에 최고라고.&lt;br&gt;
한 번 즐겨봐. 분명 좋은 일이 있을거야.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;그래, 한 번 해보자. 쫄지 말고 공격적으로.&lt;br&gt;
그동안 보수적으로 다뤄왔던 AI도 좀 더 과감하게 사용해보고,&lt;br&gt;
낯간지러워서 잘 하지 않았던 서비스 홍보나 SNS도 적극적으로 해보자.&lt;br&gt;
무섭고 쫄리긴 하지만… 쫄 것 뭐있어.&lt;/p&gt;

&lt;p&gt;개발이라는 게 그렇게 쉬운 일이 아닙니다.&lt;br&gt;
공부해야 할 것이 무진장 많거든요.&lt;br&gt;
의사 변호사 하는 똑똑한 사람들도 컴퓨터 앞에만 서면 작아지는 걸 수없이 봐왔습니다.&lt;/p&gt;

&lt;p&gt;이미 그 길을 거쳐온 우리가 쫄 것 뭐 있어?&lt;br&gt;
원래 경쟁이란 죽을 때까지 해야하는 것. 재밌고 즐겁게 경쟁하면 되지.&lt;/p&gt;

&lt;p&gt;DHH의 영상을 아래에 첨부했습니다.&lt;br&gt;
독자분들에게도 용기가 되었으면 합니다.&lt;/p&gt;

&lt;div class="iframe-container"&gt;
    &lt;iframe src="https://www.youtube.com/embed/vDjW_dRyKXY?si=GxIHcbccov1BzHSd" allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;div class="section-divider"&gt;&lt;span class="ornament"&gt;✔️&lt;/span&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;함께 읽으면 좋은 글:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://jeho.page/essay/2026/03/19/single-thread-programmer.html"&gt;싱글 스레드 프로그래머&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://jeho.page/essay/2025/08/11/solo-developer.html"&gt;진짜 1인 개발자 전성시대&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://jeho.page/essay/2025/02/14/ai-revolution.html"&gt;다시 한 번 산업혁명&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://jeho.page/essay/2026/10/05/dont-hold-back.html</id>
    <link href="https://jeho.page/essay/2026/10/05/dont-hold-back.html"/>
    <summary type="html">지난 주에 DHH는 Rails 컨퍼런스에서 큰 논란을 불러일으키는 키노트를 발표했습니다.</summary>
    <title>쫄지 말고 과감하게</title>
    <updated>2026-10-05T11:56:05+09:00</updated>
    <dc:date>2026-10-05T11:56:05+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>contact@roboco.io (ROBOCO)</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;blockquote&gt;
&lt;p&gt;일머리는 모호한 일을 실행 가능한 일로 바꾸고, 결과까지 책임지는 능력이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure&gt;&lt;img src="https://roboco.io/posts/images/Dohyun.png"&gt;&lt;figcaption&gt;
      &lt;h4&gt;정도현 - 로보코 수석 컨설턴트&lt;/h4&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;hr&gt;
&lt;p&gt;최근 동료와 바이브 코딩 도입 이후 실무자들의 생산성에 차이를 만드는 요소들에 관해 이야기했다. 같은 AI 도구를 활용해도 왜 사람마다 만들어내는 결과가 다를까. 그 차이를 하나씩 짚어가다 보니, 결국 ‘&lt;strong&gt;일머리&lt;/strong&gt;’라는 단어로 설명할 수 있다는 결론에 다다랐다.&lt;/p&gt;
&lt;p&gt;그런데 막상 일머리가 무엇인지 정의해보니 흥미로운 점을 발견했다. 업무의 목적과 맥락을 파악하고, 모호한 문제를 구조화하고, 우선순위를 정해 실행한 뒤, 결과를 확인하고 개선하는 능력. 개발자들이 소프트웨어를 만들며 해오던 일과 정확히 맞아떨어졌다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://roboco.io/posts/ai-era-hiring-strategy/</id>
    <link href="https://roboco.io/posts/ai-era-hiring-strategy/"/>
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;일머리는 모호한 일을 실행 가능한 일로 바꾸고, 결과까지 책임지는 능력이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure&gt;&lt;img src="https://roboco.io/posts/images/Dohyun.png"&gt;&lt;figcaption&gt;
      &lt;h4&gt;정도현 - 로보코 수석 컨설턴트&lt;/h4&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;hr&gt;
&lt;p&gt;최근 동료와 바이브 코딩 도입 이후 실무자들의 생산성에 차이를 만드는 요소들에 관해 이야기했다. 같은 AI 도구를 활용해도 왜 사람마다 만들어내는 결과가 다를까. 그 차이를 하나씩 짚어가다 보니, 결국 ‘&lt;strong&gt;일머리&lt;/strong&gt;’라는 단어로 설명할 수 있다는 결론에 다다랐다.&lt;/p&gt;
&lt;p&gt;그런데 막상 일머리가 무엇인지 정의해보니 흥미로운 점을 발견했다. 업무의 목적과 맥락을 파악하고, 모호한 문제를 구조화하고, 우선순위를 정해 실행한 뒤, 결과를 확인하고 개선하는 능력. 개발자들이 소프트웨어를 만들며 해오던 일과 정확히 맞아떨어졌다.&lt;/p&gt;</summary>
    <title>AI 네이티브 시대의 ‘일머리’ 있는 사람을 뽑는 법</title>
    <updated>2026-10-03T10:00:00+09:00</updated>
    <dc:date>2026-10-03T10:00:00+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>mytory</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;Codex 사용량이 Plus 사용자에겐 좀 모자랄 때가 있습니다. Claude Code는 사용량이 리셋되면 알아서 재개를 해주는데 Codex는 아직 그런 기능이 없습니다. Codex CLI에서 간단한 bash 프로그램으로 시간에 맞춰서 작업을 계속하게 할 수 있습니다. codex 실행과 함께 프롬프트를 주입하는 인터페이스가 있기 때문이죠. 실행하면 아래처럼 됩니다. (하이라이트한 부분이 사용자가 입력하는 부분입니다.) “계속 진행하자” 하고 프롬프트를 입력한 다음에 두 […]&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://mytory.net/archives/18757</id>
    <link href="https://mytory.net/archives/18757"/>
    <summary type="html">Codex 사용량이 Plus 사용자에겐 좀 모자랄 때가 있습니다. Claude Code는 사용량이 리셋되면 알아서 재개를 해주는데 Codex는 아직 그런 기능이 없습니다. Codex CLI에서 간단한 bash 프로그램으로 시간에 맞춰서 작업을 계속하게 할 수 있습니다. codex 실행과 함께 프롬프트를 주입하는 인터페이스가 있기 때문이죠. 실행하면 아래처럼 됩니다. (하이라이트한 부분이 사용자가 입력하는 부분입니다.) &amp;#8220;계속 진행하자&amp;#8221; 하고 프롬프트를 입력한 다음에 두 [&amp;#8230;]</summary>
    <title>Codex CLI 사용량 끝났을 때 한도가 리셋되면 자동으로 계속하게 하기</title>
    <updated>2026-10-06T00:44:25+09:00</updated>
    <dc:date>2026-10-06T00:44:25+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>mytory</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;1. 생성형 인공지능에 대한 기본적인 이해 이 제품이 어떤 원리로 동작하는지 알아야 더 잘 쓸 수 있는 부분이 있습니다. 예컨대, 전자레인지를 이용할 때 전자파가 어떤 원리로 작동하는지는 몰라도 되는데요. 전자파로 음식물 내 수분을 끓여 온도를 올린다는 정도는 알면 좋습니다. 그러면 전자레인지로는 음식을 태우기 힘들다는 걸 알 수 있습니다. 이처럼 기본적으로 이해하고 있어야 제대로 사용할 수 […]&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://mytory.net/archives/18723</id>
    <link href="https://mytory.net/archives/18723"/>
    <summary type="html">1. 생성형 인공지능에 대한 기본적인 이해 이 제품이 어떤 원리로 동작하는지 알아야 더 잘 쓸 수 있는 부분이 있습니다. 예컨대, 전자레인지를 이용할 때 전자파가 어떤 원리로 작동하는지는 몰라도 되는데요. 전자파로 음식물 내 수분을 끓여 온도를 올린다는 정도는 알면 좋습니다. 그러면 전자레인지로는 음식을 태우기 힘들다는 걸 알 수 있습니다. 이처럼 기본적으로 이해하고 있어야 제대로 사용할 수 [&amp;#8230;]</summary>
    <title>LLM 이해하기</title>
    <updated>2026-10-04T17:45:39+09:00</updated>
    <dc:date>2026-10-04T17:45:39+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>애드사이드</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;머신러닝 베스트셀러 &amp;lt;핸즈온 머신러닝 with 사이킷런, 파이토치&amp;gt;의 인프런 완독 챌린지가 시작되었습니다! 이 완독 챌린지에서는 19주간 1주씩 19개의 장을 완독합니다. 스터디 지원을 위해 동영상 강의를 무상으로 제공합니다. 챌린지를 수료하시면 한빛미디어 홈페이지에서 현금처럼 사용할 수 있는 마일리지를 드리며, 얼리버드 가입자 10분에게는 커피 쿠폰도 쏩니다! 그외에도 인프런에 개설된 다양한 강의를 할인받을 수 있습니다. 자세한 내용은 &amp;lt;핸즈온 머신러닝 with […]&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://tensorflow.blog/2026/10/08/%ed%95%b8%ec%a6%88%ec%98%a8-%eb%a8%b8%ec%8b%a0%eb%9f%ac%eb%8b%9d-with-%ec%82%ac%ec%9d%b4%ed%82%b7%eb%9f%b0-%ed%8c%8c%ec%9d%b4%ed%86%a0%ec%b9%98-%ec%99%84%eb%8f%85-%ec%b1%8c%eb%a6%b0%ec%a7%80/</id>
    <link href="https://tensorflow.blog/2026/10/08/%ed%95%b8%ec%a6%88%ec%98%a8-%eb%a8%b8%ec%8b%a0%eb%9f%ac%eb%8b%9d-with-%ec%82%ac%ec%9d%b4%ed%82%b7%eb%9f%b0-%ed%8c%8c%ec%9d%b4%ed%86%a0%ec%b9%98-%ec%99%84%eb%8f%85-%ec%b1%8c%eb%a6%b0%ec%a7%80/"/>
    <summary type="html">머신러닝 베스트셀러 &amp;#60;핸즈온 머신러닝 with 사이킷런, 파이토치&gt;의 인프런 완독 챌린지가 시작되었습니다! 이 완독 챌린지에서는 19주간 1주씩 19개의 장을 완독합니다. 스터디 지원을 위해 동영상 강의를 무상으로 제공합니다. 챌린지를 수료하시면 한빛미디어 홈페이지에서 현금처럼 사용할 수 있는 마일리지를 드리며, 얼리버드 가입자 10분에게는 커피 쿠폰도 쏩니다! 그외에도 인프런에 개설된 다양한 강의를 할인받을 수 있습니다. 자세한 내용은 &amp;#60;핸즈온 머신러닝 with [&amp;#8230;]</summary>
    <title>‘핸즈온 머신러닝 with 사이킷런, 파이토치’ 완독 챌린지가 오픈되었습니다!</title>
    <updated>2026-10-08T14:42:01+09:00</updated>
    <dc:date>2026-10-08T14:42:01+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>햄햄</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;케이타운포유에 재직하면서 내가 맡은 가장 큰 프로젝트라 하면 &lt;strong&gt;장바구니와 주문 개선&lt;/strong&gt;이 가장 먼저 떠오른다. &lt;strong&gt;이커머스의 핵심 도메인&lt;/strong&gt;에 깊게 관여할 수 있었고, 뛰어난 개발자들과 함께 복잡한 레거시를 해체하면서 많은 것을 배울 수 있었기 때문이다.&lt;/p&gt;
&lt;h2 id="발목잡는-레거시"&gt;발목잡는 레거시&lt;/h2&gt;
&lt;h3 id="비즈니스-상황"&gt;비즈니스 상황&lt;/h3&gt;
&lt;p&gt;케이타운포유는 해외 배송 비중이 큰 케이팝 이커머스 플랫폼으로, &lt;strong&gt;배송 정책&lt;/strong&gt;이 사업의 중요한 부분을 차지한다. 그리고 케이팝 굿즈 특성상 입고 자체가 매우 늦을 때가 있다. 어떤 사용자가 상품 A, B를 샀는데 상품 A는 재고가 있지만 상품B는 한달 후에 입고되어 한달이 지난 뒤에나 배송을 받는 상황이 종종 있었다. 이런 상황에서 &lt;strong&gt;사용자들의 만족감&lt;/strong&gt;을 높이고 &lt;strong&gt;이윤&lt;/strong&gt;을 높이기 위해서 &lt;strong&gt;합배송, 분할배송 도입&lt;/strong&gt;이 중요했으며 물류 시스템을 개선하기 위한 &lt;strong&gt;상품 구조의 개선&lt;/strong&gt;도 필요했다. 또한 주문 워크플로우 자체가 10년이 넘어 사용자 경험 개선 또한 필요한 상태였다.&lt;/p&gt;
&lt;h3 id="시스템-상황"&gt;시스템 상황&lt;/h3&gt;
&lt;p&gt;그런데 &lt;strong&gt;레거시 장바구니 쿼리가 장애물&lt;/strong&gt;이 되었다. 레거시 장바구니 쿼리는 2,000줄로 이루어져 있으며 그 안에 장바구니, 상품, 주문, 배송 로직이 뒤섞여 있었다. 가격 계산, 상품 구조, 판매 상태 결정 등 비즈니스 로직이 SQL에 섞여 있었으며, &lt;strong&gt;장바구니 뿐 아니라 주문서&lt;/strong&gt;에서도 이 쿼리를 쓰고 있었다. 오랜 세월동안 누덕누덕 기워진 쿼리는 당연히 &lt;strong&gt;변경하기 어려웠고, 자동화된 테스트 또한 거의 불가능&lt;/strong&gt;했다. 성능 역시 매우 느렸는데, 장바구니 조회 API의 레이턴시는 P95 기준 2.83초였다. 상품 개수가 많을수록 급격히 느려져 장바구니를 비워달라는 CS도 종종 들어왔었다. 상품 주문에 대해 매끄러운 사용자 경험을 제공하고, 다양한 상품 유형과 효율적인 물류로 수익을 극대화하고자 하는 케타포의 미션을 달성하기 위해서 이 쿼리는 무조건 제거되어야 했다.&lt;/p&gt;
&lt;h2 id="그래서-어떻게-제거할-것인가"&gt;그래서 어떻게 제거할 것인가?&lt;/h2&gt;
&lt;p&gt;레거시 쿼리는 거대한 만큼 &lt;strong&gt;다양한 유즈케이스에 사용&lt;/strong&gt;되고 있어 쉽게 제거할 수는 없었다. 한번에 레거시 쿼리를 걷어내는 것을 프로젝트 완료 기준으로 삼으면, 장바구니와 주문을 &lt;strong&gt;한꺼번에 개선&lt;/strong&gt;해야 했다. 그런데 우리 팀은 한번에 모든 것을 바꾸는 개선보다는 &lt;strong&gt;점진적으로 새로운 시스템으로 바꿔가는 전략&lt;/strong&gt;을 선호했다. 비즈니스 가치를 더 빨리 전달할 수 있고, 리스크를 최소화하며, 피드백도 더 빨리 받을 수 있기 때문이다. 따라서 &lt;strong&gt;Strangler Fig&lt;/strong&gt; 방식으로 레거시 쿼리를 제거하고자 했다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://velog.velcdn.com/images/hyemin916/post/18f536c0-c6ba-401b-a8da-1c53b643ecfe/image.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Strangler Fig는 마틴 파울러가 사용하기 시작한 용어로 신규 서비스와 레거시 서비스를 공존시키면서 신규 서비스가 레거시 서비스를 점차 대체해나가는 전략을 말한다. 이와 같은 점진적은 전환을 위해서는 독립적으로 이전이 가능한 &lt;strong&gt;경계&lt;/strong&gt;를 만드는 것이 무엇보다 중요했다. 우리는 우리가 개선해야 할 시스템을 분해하여 경계를 정의하고 이를 기준으로 마일스톤을 세웠다. 프로젝트의 마일스톤은 다음과 같았다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;신규 장바구니 개발 및 레거시 장바구니 Dual Write&lt;/li&gt;
&lt;li&gt;주문서 개선 및 레거시 장바구니 삭제&lt;/li&gt;
&lt;li&gt;주문 Dual Write&lt;/li&gt;
&lt;li&gt;신규 주문 출시&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="점진적이고-안전한-장바구니-전환"&gt;점진적이고 안전한 장바구니 전환&lt;/h2&gt;
&lt;p&gt;첫번째 과제는 &lt;strong&gt;신규 장바구니를 개발&lt;/strong&gt;하고 레거시 장바구니에서 신규 장바구니로 &lt;strong&gt;안전하게 이전&lt;/strong&gt;하는 것이었다. 이를 위해 우리는 &lt;strong&gt;Feature Flag&lt;/strong&gt;와 &lt;strong&gt;Canary Release&lt;/strong&gt;를 활용했다. 먼저 클라이언트와 서버 사이에 파사드 레이어를 두고, 파사드 레이어에서 일정 비율의 사용자는 신규 장바구니를, 그 외의 사용자는 레거시 장바구니를 사용하도록 만든다. 이 또한 &lt;strong&gt;Strangler Fig에서 아이디어를 얻은 전략&lt;/strong&gt;이다. 케타포는 국가별로 다른 상점을 운영했는데, 초기 버전의 영향 범위를 최소화하기 위해 사용자가 가장 적은 상점에 먼저 배포했다. 그리고 처음에는 1%의 작은 비율로 사용자들이 신규 장바구니를 사용하도록 만든다. 그리고 &lt;strong&gt;신규 장바구니 API를 호출&lt;/strong&gt;할 때마다 신규 장바구니와 레거시 장바구니를 &lt;strong&gt;Dual Write하고&lt;/strong&gt; 두 장바구니의 데이터를 비교해서, &lt;strong&gt;불일치가 발생&lt;/strong&gt;하면 해당 유저의 &lt;strong&gt;Feature Flag를 끈다&lt;/strong&gt;. Feature Flag가 꺼진 유저는 다시 레거시 장바구니 페이지로 리다이렉트되어 기존 장바구니를 사용하게 된다. 로직에 대한 플로우 차트를 아래 그림에서 확인할 수 있다. &lt;/p&gt;
&lt;p&gt;&lt;img src="https://velog.velcdn.com/images/hyemin916/post/31bd49bf-d569-4ce1-b277-7e81ae46a53f/image.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Feature Flag 구현에는 생각보다 복잡한 로직이 많이 필요했다. 먼저, &lt;strong&gt;사용자의 Feature Flag를 켜는 기준&lt;/strong&gt;을 세우는 것이 까다로웠다. 특정 확률로 사용자를 선별해서 Feature Flag를 켜되, 장바구니 불일치로 인해 &lt;strong&gt;Feature Flag가 꺼진 사용자도 다시 신규 장바구니를 사용&lt;/strong&gt;할 수 있어야 했다. 따라서 &lt;strong&gt;레거시 장바구니가 비워져 있는 모든 사용자&lt;/strong&gt;를 후보로 허용했다. 우리 서비스는 일주일 단위로 장바구니가 비워졌기 때문에, 시스템에 의해 Feature Flag가 강제적으로 꺼진 사용자도 일주일이 지나 레거시 장바구니가 비워지면 다시 신규 장바구니를 이용할 수 있었다. 또한 불일치의 원인을 추적하기 위해 불일치가 발생할 경우 이를 &lt;strong&gt;로그로 출력&lt;/strong&gt;도 했다.&lt;/p&gt;
&lt;p&gt;또 다른 어려움은 &lt;strong&gt;신규 장바구니와 레거시 장바구니의 행위의 불일치를 제거&lt;/strong&gt;하는 것이었다. 일단, 시스템적 한계로 인해 어떤 불일치는 사실상 일치하는 것으로 여겨져야 했다. 예를 들면 레거시 장바구니에서는 &lt;strong&gt;가격 계산&lt;/strong&gt;에 &lt;strong&gt;Double&lt;/strong&gt; 타입을 쓰고 신규 장바구니에서는 &lt;strong&gt;BigDecimal&lt;/strong&gt; 타입을 썼기 때문에 가격이 환전되면 불일치가 생길 수 밖에 없었다. 따라서 가격은 &lt;strong&gt;0.05의 차이까지는 허용&lt;/strong&gt;해야 했다. 그런데 이와 같이 일정 범위 오차는 일치하는 것으로 여겨도, 신규 시스템과 기존 시스템의 동작을 &lt;strong&gt;100% 일치시키기는 불가능&lt;/strong&gt;했다. 최대한 신규 시스템이 레거시 시스템과 동일하게 동작하도록 수정했지만, 기존 시스템 자체가 가진 버그도 있었기 때문이다. 결국 불일치 케이스가 줄어들수록 남은 케이스를 해결하기는 점점 어려워졌다. 아래 지수함수 그래프처럼, &lt;strong&gt;마지막 5%의 불일치를 없애는 데 막대한 시간&lt;/strong&gt;이 들 것이었다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://velog.velcdn.com/images/hyemin916/post/fde134fb-e87e-4314-b8fb-68d6194e6d33/image.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;따라서 &lt;strong&gt;정량적인 완료 기준(completion threshold)&lt;/strong&gt; 를 세웠다. 불일치 비율을 측정하는 쿼리를 작성하여, 불일치 비율이 5% 이하이면 장바구니 이관이 완료된 것으로 여겼다. 이 덕분에 엣지케이스를 처리하는 데에 너무 많은 시간을 쏟지 않을 수 있었다. 또한 &lt;strong&gt;불일치 케이스를 문서화&lt;/strong&gt;하고 이에 대한 이유를 작성하여 &lt;strong&gt;PM과 공유&lt;/strong&gt;하여, 팀원 모두가 엣지케이스에 대해 인지할 수 있도록 하였다.&lt;/p&gt;
&lt;p&gt;이 전략은 &lt;strong&gt;많은 트레이드 오프를 수반&lt;/strong&gt;했다. 먼저 Feature Flag 로직 자체가 복잡했기 때문에 이에 대한 &lt;strong&gt;추가적인 운영비용&lt;/strong&gt;이 들었다. 그리고 신규 API를 사용하더라도 레거시 장바구니를 Dual Write 했기 때문에 &lt;strong&gt;레이턴시&lt;/strong&gt;는 오히려 나빠졌다. 그럼에도 불구하고 우리는 이 전략이 충분한 가치가 있다고 판단했다. 신규 시스템에 대한 버그가 있더라도 레거시 시스템으로의 폴백이 동작하기 때문에 사용자는 문제없이 서비스를 이용할 수 있었기 때문이다. 덕분에 신규 장바구니를 &lt;strong&gt;안전하게, 마음 편히&lt;/strong&gt; 출시할 수 있었다. 결과적으로 &lt;strong&gt;장애없이 장바구니 시스템을 현대화&lt;/strong&gt;할 수 있었다. 또한 레거시 서비스를 감싼 REST API를 만듦으로서 기존에 모놀리딕으로 개발되어있던 장바구니 페이지를 &lt;strong&gt;FE와 BE로 나눌 수 있었고&lt;/strong&gt;, 따라서 &lt;strong&gt;UI/UX 개선이 가능&lt;/strong&gt;하게 되었다. 또한 &lt;strong&gt;JSP, Java, SQL에 흩어져 있었던 비즈니스 로직&lt;/strong&gt;을 백엔드 코드에 &lt;strong&gt;일원화&lt;/strong&gt;시켜 이전보다 훨씬 &lt;strong&gt;변경과 유지보수가 쉬운 시스템&lt;/strong&gt;으로 만들 수 있었다.&lt;/p&gt;
&lt;h1 id="도메인-객체를-수호하기"&gt;도메인 객체를 수호하기&lt;/h1&gt;
&lt;p&gt;장바구니 시스템을 이관하면서, 가장 중요한 점 중 하나는 &lt;strong&gt;이전의 문제를 반복하지 않는 것&lt;/strong&gt;이었다. 이전에는 비즈니스 로직이 프론트엔드, 백엔드, 인프라스트럭처(SQL)에 전부 흩어져 있어서 변경하기가 매우 까다롭고 자동화된 테스트가 아주 어려웠다. 따라서 새로운 장바구니 프로젝트는 인프라스트럭처와 도메인을 잘 분리하여 &lt;strong&gt;테스트 용이한 코드&lt;/strong&gt;를 작성하는 데에 초점을 맞췄다.&lt;/p&gt;
&lt;p&gt;이전 프로젝트에서는 JPA Entity를 도메인 객체로 활용했었다. 그런데 장점보다는 단점을 더 많이 느꼈다. 먼저, 암시적으로 내포된 Hibernate만의 기능이 너무 많았다. 도메인 객체를 빠르고 안전하게 수정하기 위해서는 Hibernate의 동작을 완벽히 파악하고 있어야 했다. Spring 개발자라면 JPA와 Hibernate 정도는 당연히 잘 알아야 한다고 말할수도 있겠지만, 도메인 로직을 수정하기 위해 프레임워크를 잘 알아야한다는 사실 자체가 &lt;strong&gt;우발적 복잡성과 본질적 복잡성이 분리되지 않았다는 사실&lt;/strong&gt;을 방증한다. 또한 도메인 객체를 불변으로 만들 수 없는 것도 문제였다. 불변을 통한 &lt;strong&gt;스레드 안정성, 예측 가능성, 참조 투명성&lt;/strong&gt;과 같은 이득을 얻을 수 없다.  그래서 우리는 신규 장바구니를 개발할 때 JPA Entity는 인프라스트럭처에 배치하고, 도메인 객체는 POJO로 만들기로 했다. &lt;/p&gt;
&lt;p&gt;도메인 객체를 POJO로 만들고 JPA Entity를 인프라스트럭처에 배치한다면 다음과 같은 단점이 있다. 먼저 &lt;strong&gt;매핑&lt;/strong&gt;(mapping)이 어렵고, 매핑 로직에 &lt;strong&gt;비즈니스 로직이 섞일 가능성&lt;/strong&gt;이 있으며, 장바구니의 프로퍼티가 변경될 때마다 &lt;strong&gt;매핑 로직을 수정&lt;/strong&gt;해야한다는 것이다. &lt;strong&gt;AI를 활용&lt;/strong&gt;한다면 매핑 로직을 작성하는 것은 이전에 비해 훨씬 쉬워졌다. 매핑 로직과 비즈니스 로직이 섞이지 않도록 하는 것에는 &lt;strong&gt;개발자로서의 통찰력&lt;/strong&gt;이 필요하다고 생각한다. 또한 우리 팀은 &lt;strong&gt;주기적인 코드 리뷰&lt;/strong&gt;를 통해 비즈니스 로직의 유출을 감시했다. 도메인 객체에 프로퍼티가 추가될 땐 어쩔 수 없이 수정해야 할 코드가 많았는데, 장바구니 &lt;strong&gt;도메인 객체가 SRP를 준수&lt;/strong&gt;할 수 있도록 프로퍼티를 추가하는 데에 신중을 기했다. 사전에 장바구니의 역할과 책임을 고려하여 객체를 설계하고, 만약에 장바구니에 새로운 필드 추가가 필요하다면 팀원 전원의 합의를 얻도록 했다. 이는 신규 장바구니가 레거시에 대한 의존이 없고, 팀원이 총 4명인 &lt;strong&gt;작은 규모의 팀&lt;/strong&gt;이어서 누릴 수 있는 이득이었다.&lt;/p&gt;
&lt;h1 id="single-source-of-truth인-문서-테스트-코드"&gt;Single Source of Truth인 문서, 테스트 코드&lt;/h1&gt;
&lt;p&gt;우발적 복잡도와 본질적 복잡도를 분리하니 도메인 객체에 대한 &lt;strong&gt;테스트가 매우 쉬워졌다&lt;/strong&gt;. 테스트를 작성하기 쉬워졌으므로, 다양한 테스트 전략을 사용해볼 수 있었다. Test Data Builder, Protocol Driver, Approval Tests 등을 적극적으로 활용해보았다. 결과적으로 가장 유용하다고 느낀 패턴은 &lt;strong&gt;Test Data Builder&lt;/strong&gt;였다. 구현이 쉽고 직관적이었기 때문이었다. 여기서 주의해야할 점은 Test Data Builder를 구현할 때 값을 단순한 setter에 전달하는 것처럼 작성하지 않고, &lt;strong&gt;테스트 코드가 비즈니스 언어처럼 읽히도록 하는 것&lt;/strong&gt;이다. &lt;/p&gt;
&lt;h3 id="단순한-setter로서의-test-data-builder"&gt;단순한 Setter로서의 Test Data Builder&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-java"&gt;anOrder()  
        .withShop(ShopCode.GLOBAL)
        .withDeliveryCode(Country.USA)  
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="비즈니스-문서로서의-test-data-builder"&gt;비즈니스 문서로서의 Test Data Builder&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-java"&gt;anOrder()  
        .globalShop()
        .deliveredToUsa()
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 테스트 코드를 비즈니스 언어와 가깝게 작성한다면 테스트 코드 그 자체가 &lt;strong&gt;Single Source of Truth인 비즈니스 문서&lt;/strong&gt;가 된다. AI 시대의 개발에서는 &lt;strong&gt;'신뢰(trust)'가 병목&lt;/strong&gt;이라고들 한다. 그런데 테스트 코드를 문서처럼 작성한다면 우리는 &lt;strong&gt;신뢰 가능한 스펙&lt;/strong&gt;을 얻게 된다. 물론 모든 유즈케이스에 이 전략이 유효하다고 보지는 않는다. 코드 생산 속도가 기하급수적으로 빨라진 지금, 테스트하기 좋은 도메인 객체를 설계하는 일은 그 자체로 상대적으로 큰 비용이기 때문이다. 하지만 핵심 비즈니스를 담당하는 중요한 도메인 객체라면 이야기가 다르다. 그런 객체에는 테스트 코드를 SSOT 스펙으로 활용해 볼 만하다.&lt;/p&gt;
&lt;h1 id="장바구니-쿼리가-제거된-이후"&gt;장바구니 쿼리가 제거된 이후&lt;/h1&gt;
&lt;p&gt;레거시 쿼리의 완전한 제거는 주문(checkout) 페이지까지 이관한 이후에 가능했다. 레거시 쿼리가 제거된 이후, 장바구니 &lt;strong&gt;레이턴시가 P95 기준 2.83s에서 203ms&lt;/strong&gt;로 줄었다. 덕분에 "장바구니가 조회되지 않는다"는 사용자 불만도 사라졌다. 또한 주문 UX/UI를 개선해, 사용자가 &lt;strong&gt;주문을 완료하기까지 걸리는 시간이 2분 43초에서 1분 21초로 약 50%&lt;/strong&gt; 줄었다. 또한 테스트와 변경이 용이한 구조로 코드를 작성한 덕분에 빠른 기능 변경이 가능해져 비즈니스 손실을 막을 수 있었다. 미국향 배송에 관세가 추가되어 실시간으로 손실을 보고 있는 상황에서 &lt;strong&gt;영업일 7일만에 관세 기능&lt;/strong&gt;을 빠르게 추가할 수 있었다. 그 외에도 2000줄의 SQL 쿼리에 강결합 되어있던 &lt;strong&gt;상품, 배송 도메인&lt;/strong&gt;에 대한 제어권이 유연한 구조의 어플리케이션 로직으로 넘어오면서 &lt;strong&gt;빠르게 변경하고 실험&lt;/strong&gt;할 수 있었다. 개인적으로는 난이도 높고 기간이 긴 프로젝트에 참여함으로서 레거시 코드를 생성하는 것에 대한 비용과 이를 안전하게 제거하는 것에 대한 중요성을 알게 되어 뜻깊었다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://velog.io/@hyemin916/2000%EC%A4%84%EC%9D%98-%EB%A0%88%EA%B1%B0%EC%8B%9C-%EC%BF%BC%EB%A6%AC-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EA%B1%B7%EC%96%B4%EB%82%B4%EA%B8%B0</id>
    <link href="https://velog.io/@hyemin916/2000%EC%A4%84%EC%9D%98-%EB%A0%88%EA%B1%B0%EC%8B%9C-%EC%BF%BC%EB%A6%AC-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EA%B1%B7%EC%96%B4%EB%82%B4%EA%B8%B0"/>
    <summary type="html">&lt;p&gt;케이타운포유에 재직하면서 내가 맡은 가장 큰 프로젝트라 하면 &lt;strong&gt;장바구니와 주문 개선&lt;/strong&gt;이 가장 먼저 떠오른다. &lt;strong&gt;이커머스의 핵심 도메인&lt;/strong&gt;에 깊게 관여할 수 있었고, 뛰어난 개발자들과 함께 복잡한 레거시를 해체하면서 많은 것을 배울 수 있었기 때문이다.&lt;/p&gt;
&lt;h2 id="발목잡는-레거시"&gt;발목잡는 레거시&lt;/h2&gt;
&lt;h3 id="비즈니스-상황"&gt;비즈니스 상황&lt;/h3&gt;
&lt;p&gt;케이타운포유는 해외 배송 비중이 큰 케이팝 이커머스 플랫폼으로, &lt;strong&gt;배송 정책&lt;/strong&gt;이 사업의 중요한 부분을 차지한다. 그리고 케이팝 굿즈 특성상 입고 자체가 매우 늦을 때가 있다. 어떤 사용자가 상품 A, B를 샀는데 상품 A는 재고가 있지만 상품B는 한달 후에 입고되어 한달이 지난 뒤에나 배송을 받는 상황이 종종 있었다. 이런 상황에서 &lt;strong&gt;사용자들의 만족감&lt;/strong&gt;을 높이고 &lt;strong&gt;이윤&lt;/strong&gt;을 높이기 위해서 &lt;strong&gt;합배송, 분할배송 도입&lt;/strong&gt;이 중요했으며 물류 시스템을 개선하기 위한 &lt;strong&gt;상품 구조의 개선&lt;/strong&gt;도 필요했다. 또한 주문 워크플로우 자체가 10년이 넘어 사용자 경험 개선 또한 필요한 상태였다.&lt;/p&gt;
&lt;h3 id="시스템-상황"&gt;시스템 상황&lt;/h3&gt;
&lt;p&gt;그런데 &lt;strong&gt;레거시 장바구니 쿼리가 장애물&lt;/strong&gt;이 되었다. 레거시 장바구니 쿼리는 2,000줄로 이루어져 있으며 그 안에 장바구니, 상품, 주문, 배송 로직이 뒤섞여 있었다. 가격 계산, 상품 구조, 판매 상태 결정 등 비즈니스 로직이 SQL에 섞여 있었으며, &lt;strong&gt;장바구니 뿐 아니라 주문서&lt;/strong&gt;에서도 이 쿼리를 쓰고 있었다. 오랜 세월동안 누덕누덕 기워진 쿼리는 당연히 &lt;strong&gt;변경하기 어려웠고, 자동화된 테스트 또한 거의 불가능&lt;/strong&gt;했다. 성능 역시 매우 느렸는데, 장바구니 조회 API의 레이턴시는 P95 기준 2.83초였다. 상품 개수가 많을수록 급격히 느려져 장바구니를 비워달라는 CS도 종종 들어왔었다. 상품 주문에 대해 매끄러운 사용자 경험을 제공하고, 다양한 상품 유형과 효율적인 물류로 수익을 극대화하고자 하는 케타포의 미션을 달성하기 위해서 이 쿼리는 무조건 제거되어야 했다.&lt;/p&gt;
&lt;h2 id="그래서-어떻게-제거할-것인가"&gt;그래서 어떻게 제거할 것인가?&lt;/h2&gt;
&lt;p&gt;레거시 쿼리는 거대한 만큼 &lt;strong&gt;다양한 유즈케이스에 사용&lt;/strong&gt;되고 있어 쉽게 제거할 수는 없었다. 한번에 레거시 쿼리를 걷어내는 것을 프로젝트 완료 기준으로 삼으면, 장바구니와 주문을 &lt;strong&gt;한꺼번에 개선&lt;/strong&gt;해야 했다. 그런데 우리 팀은 한번에 모든 것을 바꾸는 개선보다는 &lt;strong&gt;점진적으로 새로운 시스템으로 바꿔가는 전략&lt;/strong&gt;을 선호했다. 비즈니스 가치를 더 빨리 전달할 수 있고, 리스크를 최소화하며, 피드백도 더 빨리 받을 수 있기 때문이다. 따라서 &lt;strong&gt;Strangler Fig&lt;/strong&gt; 방식으로 레거시 쿼리를 제거하고자 했다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://velog.velcdn.com/images/hyemin916/post/18f536c0-c6ba-401b-a8da-1c53b643ecfe/image.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Strangler Fig는 마틴 파울러가 사용하기 시작한 용어로 신규 서비스와 레거시 서비스를 공존시키면서 신규 서비스가 레거시 서비스를 점차 대체해나가는 전략을 말한다. 이와 같은 점진적은 전환을 위해서는 독립적으로 이전이 가능한 &lt;strong&gt;경계&lt;/strong&gt;를 만드는 것이 무엇보다 중요했다. 우리는 우리가 개선해야 할 시스템을 분해하여 경계를 정의하고 이를 기준으로 마일스톤을 세웠다. 프로젝트의 마일스톤은 다음과 같았다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;신규 장바구니 개발 및 레거시 장바구니 Dual Write&lt;/li&gt;
&lt;li&gt;주문서 개선 및 레거시 장바구니 삭제&lt;/li&gt;
&lt;li&gt;주문 Dual Write&lt;/li&gt;
&lt;li&gt;신규 주문 출시&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="점진적이고-안전한-장바구니-전환"&gt;점진적이고 안전한 장바구니 전환&lt;/h2&gt;
&lt;p&gt;첫번째 과제는 &lt;strong&gt;신규 장바구니를 개발&lt;/strong&gt;하고 레거시 장바구니에서 신규 장바구니로 &lt;strong&gt;안전하게 이전&lt;/strong&gt;하는 것이었다. 이를 위해 우리는 &lt;strong&gt;Feature Flag&lt;/strong&gt;와 &lt;strong&gt;Canary Release&lt;/strong&gt;를 활용했다. 먼저 클라이언트와 서버 사이에 파사드 레이어를 두고, 파사드 레이어에서 일정 비율의 사용자는 신규 장바구니를, 그 외의 사용자는 레거시 장바구니를 사용하도록 만든다. 이 또한 &lt;strong&gt;Strangler Fig에서 아이디어를 얻은 전략&lt;/strong&gt;이다. 케타포는 국가별로 다른 상점을 운영했는데, 초기 버전의 영향 범위를 최소화하기 위해 사용자가 가장 적은 상점에 먼저 배포했다. 그리고 처음에는 1%의 작은 비율로 사용자들이 신규 장바구니를 사용하도록 만든다. 그리고 &lt;strong&gt;신규 장바구니 API를 호출&lt;/strong&gt;할 때마다 신규 장바구니와 레거시 장바구니를 &lt;strong&gt;Dual Write하고&lt;/strong&gt; 두 장바구니의 데이터를 비교해서, &lt;strong&gt;불일치가 발생&lt;/strong&gt;하면 해당 유저의 &lt;strong&gt;Feature Flag를 끈다&lt;/strong&gt;. Feature Flag가 꺼진 유저는 다시 레거시 장바구니 페이지로 리다이렉트되어 기존 장바구니를 사용하게 된다. 로직에 대한 플로우 차트를 아래 그림에서 확인할 수 있다. &lt;/p&gt;
&lt;p&gt;&lt;img src="https://velog.velcdn.com/images/hyemin916/post/31bd49bf-d569-4ce1-b277-7e81ae46a53f/image.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Feature Flag 구현에는 생각보다 복잡한 로직이 많이 필요했다. 먼저, &lt;strong&gt;사용자의 Feature Flag를 켜는 기준&lt;/strong&gt;을 세우는 것이 까다로웠다. 특정 확률로 사용자를 선별해서 Feature Flag를 켜되, 장바구니 불일치로 인해 &lt;strong&gt;Feature Flag가 꺼진 사용자도 다시 신규 장바구니를 사용&lt;/strong&gt;할 수 있어야 했다. 따라서 &lt;strong&gt;레거시 장바구니가 비워져 있는 모든 사용자&lt;/strong&gt;를 후보로 허용했다. 우리 서비스는 일주일 단위로 장바구니가 비워졌기 때문에, 시스템에 의해 Feature Flag가 강제적으로 꺼진 사용자도 일주일이 지나 레거시 장바구니가 비워지면 다시 신규 장바구니를 이용할 수 있었다. 또한 불일치의 원인을 추적하기 위해 불일치가 발생할 경우 이를 &lt;strong&gt;로그로 출력&lt;/strong&gt;도 했다.&lt;/p&gt;
&lt;p&gt;또 다른 어려움은 &lt;strong&gt;신규 장바구니와 레거시 장바구니의 행위의 불일치를 제거&lt;/strong&gt;하는 것이었다. 일단, 시스템적 한계로 인해 어떤 불일치는 사실상 일치하는 것으로 여겨져야 했다. 예를 들면 레거시 장바구니에서는 &lt;strong&gt;가격 계산&lt;/strong&gt;에 &lt;strong&gt;Double&lt;/strong&gt; 타입을 쓰고 신규 장바구니에서는 &lt;strong&gt;BigDecimal&lt;/strong&gt; 타입을 썼기 때문에 가격이 환전되면 불일치가 생길 수 밖에 없었다. 따라서 가격은 &lt;strong&gt;0.05의 차이까지는 허용&lt;/strong&gt;해야 했다. 그런데 이와 같이 일정 범위 오차는 일치하는 것으로 여겨도, 신규 시스템과 기존 시스템의 동작을 &lt;strong&gt;100% 일치시키기는 불가능&lt;/strong&gt;했다. 최대한 신규 시스템이 레거시 시스템과 동일하게 동작하도록 수정했지만, 기존 시스템 자체가 가진 버그도 있었기 때문이다. 결국 불일치 케이스가 줄어들수록 남은 케이스를 해결하기는 점점 어려워졌다. 아래 지수함수 그래프처럼, &lt;strong&gt;마지막 5%의 불일치를 없애는 데 막대한 시간&lt;/strong&gt;이 들 것이었다.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://velog.velcdn.com/images/hyemin916/post/fde134fb-e87e-4314-b8fb-68d6194e6d33/image.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;따라서 &lt;strong&gt;정량적인 완료 기준(completion threshold)&lt;/strong&gt; 를 세웠다. 불일치 비율을 측정하는 쿼리를 작성하여, 불일치 비율이 5% 이하이면 장바구니 이관이 완료된 것으로 여겼다. 이 덕분에 엣지케이스를 처리하는 데에 너무 많은 시간을 쏟지 않을 수 있었다. 또한 &lt;strong&gt;불일치 케이스를 문서화&lt;/strong&gt;하고 이에 대한 이유를 작성하여 &lt;strong&gt;PM과 공유&lt;/strong&gt;하여, 팀원 모두가 엣지케이스에 대해 인지할 수 있도록 하였다.&lt;/p&gt;
&lt;p&gt;이 전략은 &lt;strong&gt;많은 트레이드 오프를 수반&lt;/strong&gt;했다. 먼저 Feature Flag 로직 자체가 복잡했기 때문에 이에 대한 &lt;strong&gt;추가적인 운영비용&lt;/strong&gt;이 들었다. 그리고 신규 API를 사용하더라도 레거시 장바구니를 Dual Write 했기 때문에 &lt;strong&gt;레이턴시&lt;/strong&gt;는 오히려 나빠졌다. 그럼에도 불구하고 우리는 이 전략이 충분한 가치가 있다고 판단했다. 신규 시스템에 대한 버그가 있더라도 레거시 시스템으로의 폴백이 동작하기 때문에 사용자는 문제없이 서비스를 이용할 수 있었기 때문이다. 덕분에 신규 장바구니를 &lt;strong&gt;안전하게, 마음 편히&lt;/strong&gt; 출시할 수 있었다. 결과적으로 &lt;strong&gt;장애없이 장바구니 시스템을 현대화&lt;/strong&gt;할 수 있었다. 또한 레거시 서비스를 감싼 REST API를 만듦으로서 기존에 모놀리딕으로 개발되어있던 장바구니 페이지를 &lt;strong&gt;FE와 BE로 나눌 수 있었고&lt;/strong&gt;, 따라서 &lt;strong&gt;UI/UX 개선이 가능&lt;/strong&gt;하게 되었다. 또한 &lt;strong&gt;JSP, Java, SQL에 흩어져 있었던 비즈니스 로직&lt;/strong&gt;을 백엔드 코드에 &lt;strong&gt;일원화&lt;/strong&gt;시켜 이전보다 훨씬 &lt;strong&gt;변경과 유지보수가 쉬운 시스템&lt;/strong&gt;으로 만들 수 있었다.&lt;/p&gt;
&lt;h1 id="도메인-객체를-수호하기"&gt;도메인 객체를 수호하기&lt;/h1&gt;
&lt;p&gt;장바구니 시스템을 이관하면서, 가장 중요한 점 중 하나는 &lt;strong&gt;이전의 문제를 반복하지 않는 것&lt;/strong&gt;이었다. 이전에는 비즈니스 로직이 프론트엔드, 백엔드, 인프라스트럭처(SQL)에 전부 흩어져 있어서 변경하기가 매우 까다롭고 자동화된 테스트가 아주 어려웠다. 따라서 새로운 장바구니 프로젝트는 인프라스트럭처와 도메인을 잘 분리하여 &lt;strong&gt;테스트 용이한 코드&lt;/strong&gt;를 작성하는 데에 초점을 맞췄다.&lt;/p&gt;
&lt;p&gt;이전 프로젝트에서는 JPA Entity를 도메인 객체로 활용했었다. 그런데 장점보다는 단점을 더 많이 느꼈다. 먼저, 암시적으로 내포된 Hibernate만의 기능이 너무 많았다. 도메인 객체를 빠르고 안전하게 수정하기 위해서는 Hibernate의 동작을 완벽히 파악하고 있어야 했다. Spring 개발자라면 JPA와 Hibernate 정도는 당연히 잘 알아야 한다고 말할수도 있겠지만, 도메인 로직을 수정하기 위해 프레임워크를 잘 알아야한다는 사실 자체가 &lt;strong&gt;우발적 복잡성과 본질적 복잡성이 분리되지 않았다는 사실&lt;/strong&gt;을 방증한다. 또한 도메인 객체를 불변으로 만들 수 없는 것도 문제였다. 불변을 통한 &lt;strong&gt;스레드 안정성, 예측 가능성, 참조 투명성&lt;/strong&gt;과 같은 이득을 얻을 수 없다.  그래서 우리는 신규 장바구니를 개발할 때 JPA Entity는 인프라스트럭처에 배치하고, 도메인 객체는 POJO로 만들기로 했다. &lt;/p&gt;
&lt;p&gt;도메인 객체를 POJO로 만들고 JPA Entity를 인프라스트럭처에 배치한다면 다음과 같은 단점이 있다. 먼저 &lt;strong&gt;매핑&lt;/strong&gt;(mapping)이 어렵고, 매핑 로직에 &lt;strong&gt;비즈니스 로직이 섞일 가능성&lt;/strong&gt;이 있으며, 장바구니의 프로퍼티가 변경될 때마다 &lt;strong&gt;매핑 로직을 수정&lt;/strong&gt;해야한다는 것이다. &lt;strong&gt;AI를 활용&lt;/strong&gt;한다면 매핑 로직을 작성하는 것은 이전에 비해 훨씬 쉬워졌다. 매핑 로직과 비즈니스 로직이 섞이지 않도록 하는 것에는 &lt;strong&gt;개발자로서의 통찰력&lt;/strong&gt;이 필요하다고 생각한다. 또한 우리 팀은 &lt;strong&gt;주기적인 코드 리뷰&lt;/strong&gt;를 통해 비즈니스 로직의 유출을 감시했다. 도메인 객체에 프로퍼티가 추가될 땐 어쩔 수 없이 수정해야 할 코드가 많았는데, 장바구니 &lt;strong&gt;도메인 객체가 SRP를 준수&lt;/strong&gt;할 수 있도록 프로퍼티를 추가하는 데에 신중을 기했다. 사전에 장바구니의 역할과 책임을 고려하여 객체를 설계하고, 만약에 장바구니에 새로운 필드 추가가 필요하다면 팀원 전원의 합의를 얻도록 했다. 이는 신규 장바구니가 레거시에 대한 의존이 없고, 팀원이 총 4명인 &lt;strong&gt;작은 규모의 팀&lt;/strong&gt;이어서 누릴 수 있는 이득이었다.&lt;/p&gt;
&lt;h1 id="single-source-of-truth인-문서-테스트-코드"&gt;Single Source of Truth인 문서, 테스트 코드&lt;/h1&gt;
&lt;p&gt;우발적 복잡도와 본질적 복잡도를 분리하니 도메인 객체에 대한 &lt;strong&gt;테스트가 매우 쉬워졌다&lt;/strong&gt;. 테스트를 작성하기 쉬워졌으므로, 다양한 테스트 전략을 사용해볼 수 있었다. Test Data Builder, Protocol Driver, Approval Tests 등을 적극적으로 활용해보았다. 결과적으로 가장 유용하다고 느낀 패턴은 &lt;strong&gt;Test Data Builder&lt;/strong&gt;였다. 구현이 쉽고 직관적이었기 때문이었다. 여기서 주의해야할 점은 Test Data Builder를 구현할 때 값을 단순한 setter에 전달하는 것처럼 작성하지 않고, &lt;strong&gt;테스트 코드가 비즈니스 언어처럼 읽히도록 하는 것&lt;/strong&gt;이다. &lt;/p&gt;
&lt;h3 id="단순한-setter로서의-test-data-builder"&gt;단순한 Setter로서의 Test Data Builder&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-java"&gt;anOrder()  
        .withShop(ShopCode.GLOBAL)
        .withDeliveryCode(Country.USA)  
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="비즈니스-문서로서의-test-data-builder"&gt;비즈니스 문서로서의 Test Data Builder&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-java"&gt;anOrder()  
        .globalShop()
        .deliveredToUsa()
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위와 같이 테스트 코드를 비즈니스 언어와 가깝게 작성한다면 테스트 코드 그 자체가 &lt;strong&gt;Single Source of Truth인 비즈니스 문서&lt;/strong&gt;가 된다. AI 시대의 개발에서는 &lt;strong&gt;&amp;#39;신뢰(trust)&amp;#39;가 병목&lt;/strong&gt;이라고들 한다. 그런데 테스트 코드를 문서처럼 작성한다면 우리는 &lt;strong&gt;신뢰 가능한 스펙&lt;/strong&gt;을 얻게 된다. 물론 모든 유즈케이스에 이 전략이 유효하다고 보지는 않는다. 코드 생산 속도가 기하급수적으로 빨라진 지금, 테스트하기 좋은 도메인 객체를 설계하는 일은 그 자체로 상대적으로 큰 비용이기 때문이다. 하지만 핵심 비즈니스를 담당하는 중요한 도메인 객체라면 이야기가 다르다. 그런 객체에는 테스트 코드를 SSOT 스펙으로 활용해 볼 만하다.&lt;/p&gt;
&lt;h1 id="장바구니-쿼리가-제거된-이후"&gt;장바구니 쿼리가 제거된 이후&lt;/h1&gt;
&lt;p&gt;레거시 쿼리의 완전한 제거는 주문(checkout) 페이지까지 이관한 이후에 가능했다. 레거시 쿼리가 제거된 이후, 장바구니 &lt;strong&gt;레이턴시가 P95 기준 2.83s에서 203ms&lt;/strong&gt;로 줄었다. 덕분에 &amp;quot;장바구니가 조회되지 않는다&amp;quot;는 사용자 불만도 사라졌다. 또한 주문 UX/UI를 개선해, 사용자가 &lt;strong&gt;주문을 완료하기까지 걸리는 시간이 2분 43초에서 1분 21초로 약 50%&lt;/strong&gt; 줄었다. 또한 테스트와 변경이 용이한 구조로 코드를 작성한 덕분에 빠른 기능 변경이 가능해져 비즈니스 손실을 막을 수 있었다. 미국향 배송에 관세가 추가되어 실시간으로 손실을 보고 있는 상황에서 &lt;strong&gt;영업일 7일만에 관세 기능&lt;/strong&gt;을 빠르게 추가할 수 있었다. 그 외에도 2000줄의 SQL 쿼리에 강결합 되어있던 &lt;strong&gt;상품, 배송 도메인&lt;/strong&gt;에 대한 제어권이 유연한 구조의 어플리케이션 로직으로 넘어오면서 &lt;strong&gt;빠르게 변경하고 실험&lt;/strong&gt;할 수 있었다. 개인적으로는 난이도 높고 기간이 긴 프로젝트에 참여함으로서 레거시 코드를 생성하는 것에 대한 비용과 이를 안전하게 제거하는 것에 대한 중요성을 알게 되어 뜻깊었다.&lt;/p&gt;
</summary>
    <title>2000줄의 레거시 쿼리, 안전하게 걷어내기</title>
    <updated>2026-10-06T16:28:54+09:00</updated>
    <dc:date>2026-10-06T16:28:54+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>향로 (기억보단 기록을)</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;이나모리 가즈오 회장님의 "왜 일하는가" 에는 다음과 같은 일화가 나온다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;"성능은 좋을지 몰라도 이 상태로는 아무데도 쓸 수가 없네. 색이 칙칙하지 않은가."&lt;/p&gt;
&lt;p&gt;내 말을 들은 리더의 얼굴색이 순식간에 붉어졌다. 전력을 다해 만들어낸 제품을 성능이 아닌 외견으로 단박에 평가받았으니 그럴 만도 했다. 예상대로 그는 성난 기세로 반박했다.&lt;/p&gt;
&lt;p&gt;"사장님도 기술자이시니 논리적으로 평가해주십시오. 색이 칙칙한 것과 제품의 성능은 관계없지 않습니까?"&lt;/p&gt;
&lt;p&gt;"물론 성능 면에서는 요구를 충족했을지 모르네. 하지만 이건 완성된 제품이 아니야."&lt;/p&gt;
&lt;p&gt;이렇게 단호하게 말하면서 샘플을 퇴짜 놓았다. 훌륭한 성능을 갖춘 제품은 당연히 겉보기에도 깔끔하고 예쁘다. 다만 그렇다고 해도 '모든 면에서 완벽해야 한다'는 점을 잊어서는 안 된다.&lt;br&gt;외견은 '가장 바깥쪽에 있는 내용물'이다. 겉모습까지 완벽한 제품이 되어야 한다. 나는 그 리더에게 이렇게 덧붙여 말했다.&lt;/p&gt;
&lt;p&gt;"세라믹은 본래 순백색이어야 해. 겉모습도 마찬가지로 '만지면 손이 베이는 게 아닐까' 두려워질 정도로 아름다워야 하지. 겉모습이 그 정도로 훌륭하면 성능도 분명 최고인 제품일 걸세."&lt;/p&gt;
&lt;p&gt;그렇게 나는 다시 한 번 '손이 베일 것 같은 제품'을 만들자고 호소했다. 너무나 훌륭하고 완벽해서, 마치 손을 대면 베일 듯한 기분이 드는, 그 정도로 완전무결한 제품을 추구해야 한다는 걸 꼭 전하고 싶었다.&lt;/p&gt;
&lt;p&gt;생각해보니 '손이 베일 듯한'이라는 표현은 어릴 때 부모님이 내게 자주 사용하시던 말이었다.&lt;/p&gt;
&lt;p&gt;"무슨 일이든 손이 베일만큼 하라. 그러지 않으면 제대로 했다고 할 수 없다."&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;이나모리 가즈오 회장님의 "왜 일하는가"를 읽다보니깐,&lt;br&gt;요즘의 AI를 통해 어떻게든 더 쉽고 편하게 일을 끝내려는 방향으로 가는 것에 대해 크게 경고하는 것 같았다.  &lt;/p&gt;
&lt;p&gt;예를 들면, 발표 자료를 만든다고 가정해보자.&lt;br&gt;한참 스티브잡스의 아이폰 프레젠테이션이 세상을 강타했을 때, 많은 사람들과 자료들이 슬라이드 작성 원칙들을 공유했다.  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 장에는 하나의 메세지를 담을 것&lt;/li&gt;
&lt;li&gt;제목과 본문은 짧게 쓸 것&lt;/li&gt;
&lt;li&gt;뒷 자리에서도 읽을 수 있을만큼 글자를 크게 쓸 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 원칙들은 이쁜 장표를 만들기 위함이 아니라, 청중이 더 발표에 몰입할 수 있도록, 이 발표가 전달하는 바가 제대로 전달될 수 있도록 하기 위함이다.  &lt;/p&gt;
&lt;p&gt;근데 요즘 AI를 많이 활용한다는 분들의 발표 자료는 좀 다르다.&lt;br&gt;문장을 하나하나 읽어야 의미를 알 수 있는 장표들이 많다.&lt;br&gt;각 항목의 설명을 읽고, 앞에서 읽은 내용과 비교하고, 그중 무엇이 중요한지 찾아야 한다.&lt;br&gt;그러다 보면 장표를 읽는 동안 발표자의 설명을 놓치게 된다.&lt;/p&gt;
&lt;p&gt;혼자 읽어볼 문서라면 자세한 설명이 도움이 될 것이다.&lt;br&gt;하지만 발표 중에 보여주는 자료라면, 청중이 설명을 들으면서 이해할 수 있는지도 살펴봐야 한다.  &lt;/p&gt;
&lt;p&gt;왜 도구가 충분히 발전했음에도, 결과물이 전달하는 가치는 더 낮아진걸까?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;어느 정도 쓸 만한 초안을 편의상 80점이라고 해보자.&lt;br&gt;AI가 80점까지 빠르게 만들어주면, 나는 그 위에서 부족한 부분을 고쳐 90점, 100점으로 만들면 되지 않을까?&lt;br&gt;초안을 만드는 시간이 줄었으니, 예전보다 완성도를 높일 여유도 생길 것 같았다.  &lt;/p&gt;
&lt;p&gt;그런데 막상 해보면 81점으로 고치는 일이 예전보다 더 어렵다.&lt;br&gt;AI 활용을 잘 못해서 그런것 같아서 AI 도구를 더 잘 사용하기 위해 배우거나 프롬프트를 더 잘 작성하기 위한 노력도 한다.  &lt;/p&gt;
&lt;p&gt;다시 직접 키노트를 초안부터 만들어가보면 느끼게 된다.&lt;br&gt;그런 도구의 숙련도의 문제가 아니였다.  &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;예전에는 80점까지 만드는 동안 무엇을 고쳐야 할지도 함께 알아가고 있었다&lt;/strong&gt;.  &lt;/p&gt;
&lt;p&gt;발표 자료를 직접 만들 때는 처음부터 완성된 장표를 만들지 않았다.&lt;br&gt;각 장에서 전달할 핵심 메세지를 적어 전체 자료를 먼저 만들었다.&lt;br&gt;어떤 순서로 설명할지 살펴보고, 그 위에 표와 그래프, 이미지를 넣었다.&lt;br&gt;그러면서 문장을 줄이거나 강조할 내용을 바꿨다.  &lt;/p&gt;
&lt;p&gt;처음 적었던 장표 메세지를 끝까지 그대로 사용하지도 않는다.&lt;br&gt;한 장으로 표현하려고 했던 것을 두 장으로 나누기도 하고, 두 장으로 나눠진 걸 한장으로 줄이기도 한다.  &lt;/p&gt;
&lt;p&gt;청중의 입장에서 보는 일도 그 과정에서 함께 했다.&lt;br&gt;이 내용을 처음 듣는 사람이 어디를 봐야 할지 알 수 있을까, 설명을 듣는 동안 이만큼의 글을 읽을 수 있을까,&lt;br&gt;앞 장에서 이야기한 내용을 기억하고 있어야만 이해되는 것은 아닐까 등등.  &lt;/p&gt;
&lt;p&gt;그러다 보니 80점쯤 되었을 때는 내가 이 자료로 무슨 말을 하려는지 처음보다 잘 알게 됐다.&lt;br&gt;왜 이 제목을 골랐는지, 어떤 설명을 넣었다가 뺐는지, 어떤 도표를 시도했지만 적절하지 않았는지도 알고 있었다.  &lt;/p&gt;
&lt;p&gt;아직 아쉬운 부분도 대략 알고 있었다.&lt;br&gt;이 장은 설명이 길어서 나누고 싶고, 저 장은 숫자가 많아서 줄이고 싶었다.&lt;br&gt;앞부분을 고쳤으니 뒷부분의 같은 설명은 빼도 되겠다는 판단도 할 수 있었다.  &lt;/p&gt;
&lt;p&gt;그래서 81점, 82점으로 고치는 과정이 자연스러웠다.&lt;br&gt;그런데 1점부터 80점까지를 통째로 AI에게 맡기면, 이 과정이 빠지는 것이였다.&lt;br&gt;80점짜리 결과물이 있다는 것과, 내가 그 결과물을 81점으로 고칠 준비가 되어 있다는 것은 별개였다.  &lt;/p&gt;
&lt;p&gt;처음부터 AI에게 의도를 더 자세히 전달하면 되지 않느냐고 할 수도 있다.&lt;br&gt;하지만 내가 무슨 말을 하고 싶은지 항상 처음부터 분명했던 것은 아니다.&lt;br&gt;비교표를 만들어봐야 비교 기준이 적절하지 않다는 것을 알기도 하고, 여러 장을 나란히 놓고 봐야 같은 이야기를 반복하고 있다는 것을 알기도 한다.&lt;br&gt;한 장에 내용을 배치해본 다음에야 무엇을 빼야 할지 결정할 때도 있다.&lt;br&gt;나는 만든 것을 보면서 생각을 바꿔왔다.&lt;br&gt;아직 만들어보지 않아서 나도 알지 못하는 내용을 처음 요청에 모두 적기는 어려웠다.  &lt;/p&gt;
&lt;p&gt;그러니 “초안 제작 시간을 줄이고, 남은 시간으로 완성도를 높이면 된다”는 계산에는 빠진 것이 있었다.&lt;br&gt;초안을 만드는 시간은 내 의도를 명확하게 정리하는 시간이고 앞으로 완성도를 높이기 위해 무엇을 해야할지 정리하는 시간이였다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;책에는 아래와 같은 내용이 있다.  &lt;/p&gt;
&lt;p&gt;“매일 약간의 창의와 궁리를 더해 어제보다 한 걸음 더 앞서간 오늘을 창조하라.&lt;br&gt;그렇게 앞으로 나아가려는 마음가짐이야말로 일과 인생에 더없이 중요하며, 진정한 창조의 길로 다가가는 비결이다.”&lt;/p&gt;
&lt;p&gt;처음에는 AI로 더 쉽게 일을 끝내려는 태도를 경고하는 말처럼 읽었다.&lt;br&gt;지금은 내가 그동안 어떤 과정에서 이런 궁리를 해왔는지도 살펴보게 된다.  &lt;/p&gt;
&lt;p&gt;모든 일을 100점으로 만들 필요는 없다.&lt;br&gt;정해진 수준으로 정확하게 처리하면 충분한 일도 있고, 빠르게 끝낸 뒤 쉬어도 된다.&lt;/p&gt;
&lt;p&gt;다만 내가 더 잘하고 싶은 일이라면, 결과물을 빨리 받는 것만으로 충분한지 생각해볼 필요가 있다.&lt;br&gt;마지막 완성도는 내가 책임지겠다고 해놓고, 정작 그것을 높이는 데 필요한 판단을 해볼 기회까지 없애고 있지는 않은지 확인해야 한다.&lt;/p&gt;
&lt;p&gt;새로운 도구를 써보니, 그동안 도구가 불편해서 어쩔 수 없이 들인다고 생각했던 시간이 내 생각을 정리하는 데도 도움이 되고 있었다는 걸 알게 됐다.&lt;br&gt;이런 발견도 재미있다.  &lt;/p&gt;
&lt;p&gt;이 과정을 AI의 발전과 함께 어떻게 더 개선할 수 있을지, 앞으로 고민하고 시도해볼 것을 생각하니 설렌다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://jojoldu.tistory.com/888</id>
    <link href="https://jojoldu.tistory.com/888"/>
    <summary type="html">&lt;p&gt;이나모리 가즈오 회장님의 &amp;quot;왜 일하는가&amp;quot; 에는 다음과 같은 일화가 나온다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;&amp;quot;성능은 좋을지 몰라도 이 상태로는 아무데도 쓸 수가 없네. 색이 칙칙하지 않은가.&amp;quot;&lt;/p&gt;
&lt;p&gt;내 말을 들은 리더의 얼굴색이 순식간에 붉어졌다. 전력을 다해 만들어낸 제품을 성능이 아닌 외견으로 단박에 평가받았으니 그럴 만도 했다. 예상대로 그는 성난 기세로 반박했다.&lt;/p&gt;
&lt;p&gt;&amp;quot;사장님도 기술자이시니 논리적으로 평가해주십시오. 색이 칙칙한 것과 제품의 성능은 관계없지 않습니까?&amp;quot;&lt;/p&gt;
&lt;p&gt;&amp;quot;물론 성능 면에서는 요구를 충족했을지 모르네. 하지만 이건 완성된 제품이 아니야.&amp;quot;&lt;/p&gt;
&lt;p&gt;이렇게 단호하게 말하면서 샘플을 퇴짜 놓았다. 훌륭한 성능을 갖춘 제품은 당연히 겉보기에도 깔끔하고 예쁘다. 다만 그렇다고 해도 &amp;#39;모든 면에서 완벽해야 한다&amp;#39;는 점을 잊어서는 안 된다.&lt;br&gt;외견은 &amp;#39;가장 바깥쪽에 있는 내용물&amp;#39;이다. 겉모습까지 완벽한 제품이 되어야 한다. 나는 그 리더에게 이렇게 덧붙여 말했다.&lt;/p&gt;
&lt;p&gt;&amp;quot;세라믹은 본래 순백색이어야 해. 겉모습도 마찬가지로 &amp;#39;만지면 손이 베이는 게 아닐까&amp;#39; 두려워질 정도로 아름다워야 하지. 겉모습이 그 정도로 훌륭하면 성능도 분명 최고인 제품일 걸세.&amp;quot;&lt;/p&gt;
&lt;p&gt;그렇게 나는 다시 한 번 &amp;#39;손이 베일 것 같은 제품&amp;#39;을 만들자고 호소했다. 너무나 훌륭하고 완벽해서, 마치 손을 대면 베일 듯한 기분이 드는, 그 정도로 완전무결한 제품을 추구해야 한다는 걸 꼭 전하고 싶었다.&lt;/p&gt;
&lt;p&gt;생각해보니 &amp;#39;손이 베일 듯한&amp;#39;이라는 표현은 어릴 때 부모님이 내게 자주 사용하시던 말이었다.&lt;/p&gt;
&lt;p&gt;&amp;quot;무슨 일이든 손이 베일만큼 하라. 그러지 않으면 제대로 했다고 할 수 없다.&amp;quot;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이나모리 가즈오 회장님의 &amp;quot;왜 일하는가&amp;quot;를 읽다보니깐,&lt;br&gt;요즘의 AI를 통해 어떻게든 더 쉽고 편하게 일을 끝내려는 방향으로 가는 것에 대해 크게 경고하는 것 같았다.  &lt;/p&gt;
&lt;p&gt;예를 들면, 발표 자료를 만든다고 가정해보자.&lt;br&gt;한참 스티브잡스의 아이폰 프레젠테이션이 세상을 강타했을 때, 많은 사람들과 자료들이 슬라이드 작성 원칙들을 공유했다.  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 장에는 하나의 메세지를 담을 것&lt;/li&gt;
&lt;li&gt;제목과 본문은 짧게 쓸 것&lt;/li&gt;
&lt;li&gt;뒷 자리에서도 읽을 수 있을만큼 글자를 크게 쓸 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 원칙들은 이쁜 장표를 만들기 위함이 아니라, 청중이 더 발표에 몰입할 수 있도록, 이 발표가 전달하는 바가 제대로 전달될 수 있도록 하기 위함이다.  &lt;/p&gt;
&lt;p&gt;근데 요즘 AI를 많이 활용한다는 분들의 발표 자료는 좀 다르다.&lt;br&gt;문장을 하나하나 읽어야 의미를 알 수 있는 장표들이 많다.&lt;br&gt;각 항목의 설명을 읽고, 앞에서 읽은 내용과 비교하고, 그중 무엇이 중요한지 찾아야 한다.&lt;br&gt;그러다 보면 장표를 읽는 동안 발표자의 설명을 놓치게 된다.&lt;/p&gt;
&lt;p&gt;혼자 읽어볼 문서라면 자세한 설명이 도움이 될 것이다.&lt;br&gt;하지만 발표 중에 보여주는 자료라면, 청중이 설명을 들으면서 이해할 수 있는지도 살펴봐야 한다.  &lt;/p&gt;
&lt;p&gt;왜 도구가 충분히 발전했음에도, 결과물이 전달하는 가치는 더 낮아진걸까?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;어느 정도 쓸 만한 초안을 편의상 80점이라고 해보자.&lt;br&gt;AI가 80점까지 빠르게 만들어주면, 나는 그 위에서 부족한 부분을 고쳐 90점, 100점으로 만들면 되지 않을까?&lt;br&gt;초안을 만드는 시간이 줄었으니, 예전보다 완성도를 높일 여유도 생길 것 같았다.  &lt;/p&gt;
&lt;p&gt;그런데 막상 해보면 81점으로 고치는 일이 예전보다 더 어렵다.&lt;br&gt;AI 활용을 잘 못해서 그런것 같아서 AI 도구를 더 잘 사용하기 위해 배우거나 프롬프트를 더 잘 작성하기 위한 노력도 한다.  &lt;/p&gt;
&lt;p&gt;다시 직접 키노트를 초안부터 만들어가보면 느끼게 된다.&lt;br&gt;그런 도구의 숙련도의 문제가 아니였다.  &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;예전에는 80점까지 만드는 동안 무엇을 고쳐야 할지도 함께 알아가고 있었다&lt;/strong&gt;.  &lt;/p&gt;
&lt;p&gt;발표 자료를 직접 만들 때는 처음부터 완성된 장표를 만들지 않았다.&lt;br&gt;각 장에서 전달할 핵심 메세지를 적어 전체 자료를 먼저 만들었다.&lt;br&gt;어떤 순서로 설명할지 살펴보고, 그 위에 표와 그래프, 이미지를 넣었다.&lt;br&gt;그러면서 문장을 줄이거나 강조할 내용을 바꿨다.  &lt;/p&gt;
&lt;p&gt;처음 적었던 장표 메세지를 끝까지 그대로 사용하지도 않는다.&lt;br&gt;한 장으로 표현하려고 했던 것을 두 장으로 나누기도 하고, 두 장으로 나눠진 걸 한장으로 줄이기도 한다.  &lt;/p&gt;
&lt;p&gt;청중의 입장에서 보는 일도 그 과정에서 함께 했다.&lt;br&gt;이 내용을 처음 듣는 사람이 어디를 봐야 할지 알 수 있을까, 설명을 듣는 동안 이만큼의 글을 읽을 수 있을까,&lt;br&gt;앞 장에서 이야기한 내용을 기억하고 있어야만 이해되는 것은 아닐까 등등.  &lt;/p&gt;
&lt;p&gt;그러다 보니 80점쯤 되었을 때는 내가 이 자료로 무슨 말을 하려는지 처음보다 잘 알게 됐다.&lt;br&gt;왜 이 제목을 골랐는지, 어떤 설명을 넣었다가 뺐는지, 어떤 도표를 시도했지만 적절하지 않았는지도 알고 있었다.  &lt;/p&gt;
&lt;p&gt;아직 아쉬운 부분도 대략 알고 있었다.&lt;br&gt;이 장은 설명이 길어서 나누고 싶고, 저 장은 숫자가 많아서 줄이고 싶었다.&lt;br&gt;앞부분을 고쳤으니 뒷부분의 같은 설명은 빼도 되겠다는 판단도 할 수 있었다.  &lt;/p&gt;
&lt;p&gt;그래서 81점, 82점으로 고치는 과정이 자연스러웠다.&lt;br&gt;그런데 1점부터 80점까지를 통째로 AI에게 맡기면, 이 과정이 빠지는 것이였다.&lt;br&gt;80점짜리 결과물이 있다는 것과, 내가 그 결과물을 81점으로 고칠 준비가 되어 있다는 것은 별개였다.  &lt;/p&gt;
&lt;p&gt;처음부터 AI에게 의도를 더 자세히 전달하면 되지 않느냐고 할 수도 있다.&lt;br&gt;하지만 내가 무슨 말을 하고 싶은지 항상 처음부터 분명했던 것은 아니다.&lt;br&gt;비교표를 만들어봐야 비교 기준이 적절하지 않다는 것을 알기도 하고, 여러 장을 나란히 놓고 봐야 같은 이야기를 반복하고 있다는 것을 알기도 한다.&lt;br&gt;한 장에 내용을 배치해본 다음에야 무엇을 빼야 할지 결정할 때도 있다.&lt;br&gt;나는 만든 것을 보면서 생각을 바꿔왔다.&lt;br&gt;아직 만들어보지 않아서 나도 알지 못하는 내용을 처음 요청에 모두 적기는 어려웠다.  &lt;/p&gt;
&lt;p&gt;그러니 “초안 제작 시간을 줄이고, 남은 시간으로 완성도를 높이면 된다”는 계산에는 빠진 것이 있었다.&lt;br&gt;초안을 만드는 시간은 내 의도를 명확하게 정리하는 시간이고 앞으로 완성도를 높이기 위해 무엇을 해야할지 정리하는 시간이였다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;책에는 아래와 같은 내용이 있다.  &lt;/p&gt;
&lt;p&gt;“매일 약간의 창의와 궁리를 더해 어제보다 한 걸음 더 앞서간 오늘을 창조하라.&lt;br&gt;그렇게 앞으로 나아가려는 마음가짐이야말로 일과 인생에 더없이 중요하며, 진정한 창조의 길로 다가가는 비결이다.”&lt;/p&gt;
&lt;p&gt;처음에는 AI로 더 쉽게 일을 끝내려는 태도를 경고하는 말처럼 읽었다.&lt;br&gt;지금은 내가 그동안 어떤 과정에서 이런 궁리를 해왔는지도 살펴보게 된다.  &lt;/p&gt;
&lt;p&gt;모든 일을 100점으로 만들 필요는 없다.&lt;br&gt;정해진 수준으로 정확하게 처리하면 충분한 일도 있고, 빠르게 끝낸 뒤 쉬어도 된다.&lt;/p&gt;
&lt;p&gt;다만 내가 더 잘하고 싶은 일이라면, 결과물을 빨리 받는 것만으로 충분한지 생각해볼 필요가 있다.&lt;br&gt;마지막 완성도는 내가 책임지겠다고 해놓고, 정작 그것을 높이는 데 필요한 판단을 해볼 기회까지 없애고 있지는 않은지 확인해야 한다.&lt;/p&gt;
&lt;p&gt;새로운 도구를 써보니, 그동안 도구가 불편해서 어쩔 수 없이 들인다고 생각했던 시간이 내 생각을 정리하는 데도 도움이 되고 있었다는 걸 알게 됐다.&lt;br&gt;이런 발견도 재미있다.  &lt;/p&gt;
&lt;p&gt;이 과정을 AI의 발전과 함께 어떻게 더 개선할 수 있을지, 앞으로 고민하고 시도해볼 것을 생각하니 설렌다.&lt;/p&gt;</summary>
    <title>왜 일하는가</title>
    <updated>2026-10-06T08:20:52+09:00</updated>
    <dc:date>2026-10-06T08:20:52+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>향로 (기억보단 기록을)</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;AI 덕분에 그동안 미뤄왔던 일들을 하나씩 하고 있다.&lt;br&gt;하루하루 놀랍고, 더 잘 활용하고 싶다.&lt;/p&gt;
&lt;p&gt;근데 요즘 AI 이야기를 보면 얼마나 쉽게 할 수 있는지에 대한 이야기가 많다.&lt;br&gt;그런 이야기를 보다 보면 나도 결과를 얼마나 더 좋게 만들 수 있을지보다, 얼마나 덜 노력할 수 있을지에 먼저 관심이 간다.&lt;/p&gt;
&lt;p&gt;예를 들어 120의 노력을 들여 100짜리 결과물을 만들었다고 해보자.&lt;br&gt;이제는 AI로 30의 노력만 들여도 95를 만들 수 있다.&lt;br&gt;이 정도 차이는 괜찮다고 생각한다.&lt;/p&gt;
&lt;p&gt;결과를 들인 노력으로 나눠보면, 예전에는 100 / 120으로 약 0.83, 이제는 95 / 30으로 약 3.17이다.&lt;br&gt;말하자면 노력의 가성비가 좋아진 셈이다.&lt;/p&gt;
&lt;p&gt;여기서 95를 100 이상으로 높이는 방법을 찾을 수도 있고, 95를 만드는 데 필요한 노력을 더 줄일 수도 있다.&lt;/p&gt;
&lt;p&gt;근데 95에서 100으로 가려면 얼마나 더 노력해야 할지 알기 어렵다.&lt;br&gt;전체 노력이 다시 80, 90까지 늘어날 수도 있다.&lt;br&gt;그러니 30을 20으로 줄이는 쪽에 관심이 가기 쉽다.&lt;/p&gt;
&lt;p&gt;결과는 그대로 유지하면서 노력만 줄일 수 있다면 좋다.&lt;br&gt;문제는 결과가 조금 나빠져도, 노력이 많이 줄었다는 이유로 계속 괜찮다고 생각할 때다.&lt;/p&gt;
&lt;p&gt;20의 노력으로 88을 만드는 방법을 발견한다.&lt;br&gt;95보다는 낮지만 이 정도면 비슷한데 노력은 많이 줄었다고 생각한다.&lt;br&gt;가성비도 4.4로 더 좋아졌다.&lt;/p&gt;
&lt;p&gt;다음에는 10으로 77을 만든다.&lt;br&gt;이번에도 비슷한 결과를 훨씬 적은 노력으로 만들었다고 생각한다.&lt;br&gt;가성비는 7.7이다.&lt;/p&gt;
&lt;p&gt;매번 바로 직전의 결과와 비교하다 보니 기준은 조금씩 낮아진다.&lt;br&gt;가성비는 계속 좋아지는데, 처음의 100은 이제 77이 됐다.&lt;/p&gt;
&lt;p&gt;제임스 클리어의 "아주 작은 습관의 힘" 에는 매일 1%씩 성장하거나 퇴보할 때의 차이를 계산한 예시가 나온다.&lt;br&gt;처음 수준을 1로 놓고 매일 전날보다 1%씩 성장한다고 가정하면, 1년 뒤에는 1.01^365로 약 37.78이 된다.&lt;br&gt;반대로 매일 1%씩 퇴보한다면, 0.99^365로 약 0.03이 된다.&lt;/p&gt;
&lt;p&gt;물론 결과물의 수준이 이 계산대로 달라지는 것은 아니다.&lt;br&gt;다만 '이 정도 차이는 괜찮다' 는 판단을 반복하다 보면, 처음에는 받아들이지 않았을 결과에도 만족하게 될 수 있겠다는 생각이 든다.&lt;/p&gt;
&lt;p&gt;예전에는 120의 노력으로 100짜리 하나를 만들었다면 이제는 같은 노력으로 77짜리 12개를 만들 수 있다.&lt;br&gt;만든 개수는 훨씬 늘었다.&lt;br&gt;그런데 고객에게 필요한 결과물도 그만큼 늘었을까?&lt;/p&gt;
&lt;p&gt;물론 77점짜리라도 고객의 문제를 충분히 해결한다면 좋은 결과물이다.&lt;br&gt;여러 개를 빠르게 만들어 고객의 반응을 확인하는 것도 좋은 방법일 수 있다.&lt;br&gt;반대로 내가 100점이라고 생각한 결과물도 고객에게는 필요하지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;그러니 77점이라는 것 자체가 문제는 아니다.&lt;br&gt;다만 '이 정도면 충분하다'는 판단을 고객의 반응이 아니라, 내가 들인 노력으로 하고 있지는 않을까.&lt;/p&gt;
&lt;p&gt;고객은 내 노력의 가성비를 보고 선택하지 않는다.&lt;br&gt;자기 문제를 해결해 주는지를 보고 선택한다.&lt;/p&gt;
&lt;p&gt;만든 개수와 만드는 속도만 보고 있다면, 팔리지 않는 제품을 더 빠르게 만들고 있는 건지도 모른다.&lt;/p&gt;
&lt;p&gt;AI로 들어가는 노력을 줄일 수 있다는 건 너무 좋다.&lt;br&gt;그러면서 100짜리 결과물을 110, 120, 150으로도 높여 보고 싶다.&lt;/p&gt;
&lt;p&gt;그동안 아무리 노력해도 넘지 못했던 100의 벽을 AI와 함께 돌파하려면 어떻게 해야 할지 계속 고민하고 있다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://jojoldu.tistory.com/887</id>
    <link href="https://jojoldu.tistory.com/887"/>
    <summary type="html">&lt;p&gt;AI 덕분에 그동안 미뤄왔던 일들을 하나씩 하고 있다.&lt;br&gt;하루하루 놀랍고, 더 잘 활용하고 싶다.&lt;/p&gt;
&lt;p&gt;근데 요즘 AI 이야기를 보면 얼마나 쉽게 할 수 있는지에 대한 이야기가 많다.&lt;br&gt;그런 이야기를 보다 보면 나도 결과를 얼마나 더 좋게 만들 수 있을지보다, 얼마나 덜 노력할 수 있을지에 먼저 관심이 간다.&lt;/p&gt;
&lt;p&gt;예를 들어 120의 노력을 들여 100짜리 결과물을 만들었다고 해보자.&lt;br&gt;이제는 AI로 30의 노력만 들여도 95를 만들 수 있다.&lt;br&gt;이 정도 차이는 괜찮다고 생각한다.&lt;/p&gt;
&lt;p&gt;결과를 들인 노력으로 나눠보면, 예전에는 100 / 120으로 약 0.83, 이제는 95 / 30으로 약 3.17이다.&lt;br&gt;말하자면 노력의 가성비가 좋아진 셈이다.&lt;/p&gt;
&lt;p&gt;여기서 95를 100 이상으로 높이는 방법을 찾을 수도 있고, 95를 만드는 데 필요한 노력을 더 줄일 수도 있다.&lt;/p&gt;
&lt;p&gt;근데 95에서 100으로 가려면 얼마나 더 노력해야 할지 알기 어렵다.&lt;br&gt;전체 노력이 다시 80, 90까지 늘어날 수도 있다.&lt;br&gt;그러니 30을 20으로 줄이는 쪽에 관심이 가기 쉽다.&lt;/p&gt;
&lt;p&gt;결과는 그대로 유지하면서 노력만 줄일 수 있다면 좋다.&lt;br&gt;문제는 결과가 조금 나빠져도, 노력이 많이 줄었다는 이유로 계속 괜찮다고 생각할 때다.&lt;/p&gt;
&lt;p&gt;20의 노력으로 88을 만드는 방법을 발견한다.&lt;br&gt;95보다는 낮지만 이 정도면 비슷한데 노력은 많이 줄었다고 생각한다.&lt;br&gt;가성비도 4.4로 더 좋아졌다.&lt;/p&gt;
&lt;p&gt;다음에는 10으로 77을 만든다.&lt;br&gt;이번에도 비슷한 결과를 훨씬 적은 노력으로 만들었다고 생각한다.&lt;br&gt;가성비는 7.7이다.&lt;/p&gt;
&lt;p&gt;매번 바로 직전의 결과와 비교하다 보니 기준은 조금씩 낮아진다.&lt;br&gt;가성비는 계속 좋아지는데, 처음의 100은 이제 77이 됐다.&lt;/p&gt;
&lt;p&gt;제임스 클리어의 &amp;quot;아주 작은 습관의 힘&amp;quot; 에는 매일 1%씩 성장하거나 퇴보할 때의 차이를 계산한 예시가 나온다.&lt;br&gt;처음 수준을 1로 놓고 매일 전날보다 1%씩 성장한다고 가정하면, 1년 뒤에는 1.01^365로 약 37.78이 된다.&lt;br&gt;반대로 매일 1%씩 퇴보한다면, 0.99^365로 약 0.03이 된다.&lt;/p&gt;
&lt;p&gt;물론 결과물의 수준이 이 계산대로 달라지는 것은 아니다.&lt;br&gt;다만 &amp;#39;이 정도 차이는 괜찮다&amp;#39; 는 판단을 반복하다 보면, 처음에는 받아들이지 않았을 결과에도 만족하게 될 수 있겠다는 생각이 든다.&lt;/p&gt;
&lt;p&gt;예전에는 120의 노력으로 100짜리 하나를 만들었다면 이제는 같은 노력으로 77짜리 12개를 만들 수 있다.&lt;br&gt;만든 개수는 훨씬 늘었다.&lt;br&gt;그런데 고객에게 필요한 결과물도 그만큼 늘었을까?&lt;/p&gt;
&lt;p&gt;물론 77점짜리라도 고객의 문제를 충분히 해결한다면 좋은 결과물이다.&lt;br&gt;여러 개를 빠르게 만들어 고객의 반응을 확인하는 것도 좋은 방법일 수 있다.&lt;br&gt;반대로 내가 100점이라고 생각한 결과물도 고객에게는 필요하지 않을 수 있다.&lt;/p&gt;
&lt;p&gt;그러니 77점이라는 것 자체가 문제는 아니다.&lt;br&gt;다만 &amp;#39;이 정도면 충분하다&amp;#39;는 판단을 고객의 반응이 아니라, 내가 들인 노력으로 하고 있지는 않을까.&lt;/p&gt;
&lt;p&gt;고객은 내 노력의 가성비를 보고 선택하지 않는다.&lt;br&gt;자기 문제를 해결해 주는지를 보고 선택한다.&lt;/p&gt;
&lt;p&gt;만든 개수와 만드는 속도만 보고 있다면, 팔리지 않는 제품을 더 빠르게 만들고 있는 건지도 모른다.&lt;/p&gt;
&lt;p&gt;AI로 들어가는 노력을 줄일 수 있다는 건 너무 좋다.&lt;br&gt;그러면서 100짜리 결과물을 110, 120, 150으로도 높여 보고 싶다.&lt;/p&gt;
&lt;p&gt;그동안 아무리 노력해도 넘지 못했던 100의 벽을 AI와 함께 돌파하려면 어떻게 해야 할지 계속 고민하고 있다.&lt;/p&gt;</summary>
    <title>100점에서 120점으로</title>
    <updated>2026-10-04T19:10:37+09:00</updated>
    <dc:date>2026-10-04T19:10:37+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>이경원</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;석사 때 쓴 공간 데이터베이스 논문을 AI에게 익명으로 평가받고, 11년 뒤의 인용까지 따라가 봤다. 논문 쓰기는 쉬워졌는데 의미 있는 논문 쓰기는 더 어려워진 시대에 대한 기록.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://kyoungwon.me/development/2026/10/06/ai-reviewed-my-12-year-old-paper/</id>
    <link href="https://kyoungwon.me/development/2026/10/06/ai-reviewed-my-12-year-old-paper/"/>
    <summary type="html">석사 때 쓴 공간 데이터베이스 논문을 AI에게 익명으로 평가받고, 11년 뒤의 인용까지 따라가 봤다. 논문 쓰기는 쉬워졌는데 의미 있는 논문 쓰기는 더 어려워진 시대에 대한 기록.</summary>
    <title>AI에게 12년 전 내 논문을 평가받았다</title>
    <updated>2026-10-06T09:00:00+09:00</updated>
    <dc:date>2026-10-06T09:00:00+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>GREEN.1229</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;안녕하세요. &lt;span style="color: #409d00;"&gt;&lt;b&gt;그린&lt;/b&gt;&lt;/span&gt;입니다  &lt;/span&gt;&lt;br&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이번 포스팅에서는 &lt;span style="background-color: #9feec3;"&gt;&lt;b&gt;SwiftUI의 TipKit 프레임워크&lt;/b&gt;&lt;/span&gt;에 대해 애플 공식 문서를 기준으로 정리해보겠습니다  &lt;/span&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-filename="12312.001.jpeg" data-origin-width="400" data-origin-height="400"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/GC2cO/dJMcafByQNM/GCQssw0rzLh8TIGO5RYjJ0/img.jpg" data-phocus="https://blog.kakaocdn.net/dn/GC2cO/dJMcafByQNM/GCQssw0rzLh8TIGO5RYjJ0/img.jpg"&gt;&lt;img src="https://blog.kakaocdn.net/dn/GC2cO/dJMcafByQNM/GCQssw0rzLh8TIGO5RYjJ0/img.jpg" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGC2cO%2FdJMcafByQNM%2FGCQssw0rzLh8TIGO5RYjJ0%2Fimg.jpg" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="400" height="400" data-filename="12312.001.jpeg" data-origin-width="400" data-origin-height="400"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Intro&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;div style="background: #f2f8ee; border-radius: 10px; padding: 16px 20px; margin: 16px 0;"&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;주제:&lt;/b&gt; TipKit&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;프레임워크:&lt;/b&gt; TipKit&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;지원 버전:&lt;/b&gt; iOS/iPadOS 17 · macOS 14 · tvOS 16 · watchOS 10 · visionOS 1 이상&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;공식 문서:&lt;/b&gt; &lt;a style="color: #000000;" href="https://developer.apple.com/documentation/tipkit" target="_blank" rel="noopener"&gt;developer.apple.com/documentation/tipkit&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;TipKit이란&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;공식 문서는 TipKit을 이렇게 소개해요.&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style="border-left: 4px solid #3a8c28; background: #f2f8ee; padding: 8px 16px; margin: 16px 0;" data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Use TipKit to show contextual tips that highlight new, interesting, or unused features people haven't discovered on their own yet.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;즉, &lt;b&gt;사람들이 아직 스스로 발견하지 못한 기능을 가볍게 알려주는 용도&lt;/b&gt;예요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Tip&lt;/code&gt; 프로토콜로 팁의 내용과, 그 팁이 언제 나타나야 하는지의 조건을 정의하고, &lt;code&gt;TipView&lt;/code&gt;로 그 기능에 시선을 끌게 만들어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;공식 문서는 사용 가이드도 명확히 제시해요.&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;사람들을 부담주지 않도록 팁은 아껴서 써야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;스스로 발견하기 어려운, 눈에 잘 안 띄는 기능에만 써야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;앱을 쓸 때마다 매번 팁을 띄우지 말 것 — 불필요하게 나타나면 방해가 됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁으로 앱 사용법을 처음부터 끝까지 안내하거나, 광고/프로모션 목적으로 쓰면 안 됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;디자인 가이드는 &lt;a style="color: #000000;" href="https://developer.apple.com/design/human-interface-guidelines/offering-help" target="_blank" rel="noopener"&gt;Human Interface Guidelines의 Offering Help&lt;/a&gt; 문서를 참고하라고 안내하고 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Tip 프로토콜 — 팁의 내용 정의하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;공식 문서의 첫 예제 코드예요.&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;import SwiftUI
import TipKit

// Define your tip's content.
struct FavoriteLandmarkTip: Tip {
    var title: Text {
        Text("Save as a Favorite")
    }

    var message: Text? {
        Text("Your favorite landmarks always appear at the top of the list.")
    }

    var image: Image? {
        Image(systemName: "star")
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Tip&lt;/code&gt;은 &lt;code&gt;Identifiable&lt;/code&gt;, &lt;code&gt;Sendable&lt;/code&gt;을 채택하는 프로토콜이에요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁이 유효하려면 &lt;code&gt;title&lt;/code&gt;은 반드시 설정해야 하고, &lt;code&gt;message&lt;/code&gt;와 &lt;code&gt;image&lt;/code&gt;는 선택이에요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;rules&lt;/code&gt;와 &lt;code&gt;options&lt;/code&gt; 프로퍼티에 &lt;code&gt;Rule&lt;/code&gt;과 &lt;code&gt;Option&lt;/code&gt; 인스턴스를 전달해서 팁이 언제 나타날지 제어할 수 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;앱 시작 시 팁 로드하기 — Tips.configure(_:)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁을 화면에 띄우기 전에, &lt;b&gt;먼저 앱에 로드해야 해요. &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;공식 문서는 앱 세션마다 한 번, 예를 들어 &lt;code&gt;App&lt;/code&gt;의 &lt;code&gt;init()&lt;/code&gt;에서 &lt;code&gt;configure(_:)&lt;/code&gt;를 호출하는 걸 베스트 프랙티스로 안내해요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;@main
struct TipKitExamples: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }

    init() {
        do {
            // Configure and load all tips in the app.
            try Tips.configure()
        }
        catch {
            print("Error initializing tips: \(error)")
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;configure(_:)&lt;/code&gt;에는 &lt;code&gt;ConfigurationOption&lt;/code&gt; 배열을 전달해서 세부 설정도 가능해요.&lt;/span&gt;&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;displayFrequency(_:)&lt;/code&gt;: 팁이 뜬 뒤 다음 팁이 뜨기까지의 최소 간격을 설정. &lt;code&gt;.immediate&lt;/code&gt;(기본값), &lt;code&gt;.hourly&lt;/code&gt;, &lt;code&gt;.daily&lt;/code&gt;, &lt;code&gt;.weekly&lt;/code&gt;, &lt;code&gt;.monthly&lt;/code&gt; 중 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;datastoreLocation(_:)&lt;/code&gt;: 팁 상태를 저장할 위치를 커스터마이즈&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;cloudKitContainer(_:)&lt;/code&gt;: 팁 동기화에 쓸 CloudKit 컨테이너를 설정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;.monthly&lt;/code&gt;를 예로 들면, 공식 문서는 이렇게 설명해요.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;When you set this, the system shows no more than one tip per month.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁 띄우기 — Inline과 Popover&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁을 보여주는 방법은 두 가지예요. &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;공식 문서는 가능하면 &lt;b&gt;인라인(Inline)&lt;/b&gt; 방식을 먼저 쓰라고 권장해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;사람들이 상호작용하고 싶어할 UI 요소를 가리지 않기 때문이에요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;인라인 팁 — TipView&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct InlineView: View {
    // Create an instance of your tip content.
    let inlineTip = InlineTip()

    var body: some View {
        VStack(spacing: 20) {
            Text("A TipView embeds itself directly in the view.")

            // Place your tip near the feature you want to highlight.
            TipView(inlineTip, arrowEdge: .bottom)
            Button {
                // Invalidate the tip when someone uses the feature.
                inlineTip.invalidate(reason: .actionPerformed)
            } label: {
                Label("Favorite", systemImage: "star")
            }
            Spacer()
        }
        .padding()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;인라인 팁은 그 뷰 안에 직접 끼어들어 자리를 차지해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그래서 팁이 나타나거나 사라질 때 주변 레이아웃이 그에 맞춰 바뀝니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팝오버 팁 — popoverTip(_:arrowEdge:action:)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct PopoverView: View {
    let highlightTip = HighlightTip()
    
    var body: some View {
        Image(systemName: "wand.and.stars")
            .imageScale(.large)
            // Place the tip on the feature to highlight.
            .popoverTip(highlightTip)
            .onTapGesture {
                tip.invalidate(reason: .actionPerformed)
            }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팝오버 방식은 화면 위에 팁을 띄워서 아래 UI를 가려요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;레이아웃을 바꾸고 싶지 않거나, 팁이 가리는 컨트롤과 상호작용할 필요가 없을 때 적합해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팝오버는 이미 기능을 가리키고 있으니, 이미지는 생략하는 걸 고려해보라고도 안내합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁 무효화하기 — invalidate(reason:)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;사람이 그 기능을 실제로 쓰고 나면, 팁을 프로그래밍적으로 무효화할 수 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;InvalidationReason&lt;/code&gt;에는 다음 네 가지 케이스가 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;actionPerformed&lt;/code&gt;: 팁이 설명하는 행동을 사용자가 실제로 수행함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;displayCountExceeded&lt;/code&gt;: 팁이 최대 표시 횟수를 초과함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;displayDurationExceeded&lt;/code&gt;: 팁이 최대 표시 기간을 초과함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;
&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;tipClosed&lt;/code&gt;: 사용자가 팁이 떠 있는 동안 명시적으로 닫기 버튼을 눌러 닫음&lt;br&gt;&lt;/span&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;버튼 추가하기 — actions&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁에 추가 정보나 옵션으로 안내하는 버튼을 넣을 수도 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt; &lt;code&gt;Tip&lt;/code&gt;의 &lt;code&gt;actions&lt;/code&gt; 프로퍼티에 &lt;code&gt;Action&lt;/code&gt; 인스턴스들을 정의하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;pre class="less" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;struct PasswordResetTip: Tip {
    var title: Text {
        Text("Need Help?")
    }

    var message: Text? {
        Text("Do you need help logging in to your account?")
    }

    var image: Image? {
        Image(systemName: "lock.shield")
    }

    var actions: [Action] {
        Action(id: "reset-password", title: "Reset Password")
        Action(id: "faq", title: "View our FAQ")
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;TipView&lt;/code&gt;에 전달한 클로저에서 &lt;code&gt;action.id&lt;/code&gt;를 확인해서 각 버튼을 눌렀을 때의 동작을 분기할 수 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;pre class="reasonml" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;TipView(passwordResetTip, arrowEdge: .bottom) { action in
    if action.id == "reset-password", let url = URL(string: "https://iforgot.apple.com") {
        openURL(url) { accepted in
            print(accepted ? "Success Reset" : "Failure")
        }
    }
    if action.id == "faq", let url = URL(string: "https://appleid.apple.com/faq") {
        openURL(url) { accepted in
            print(accepted ? "Success FAQ" : "Failure")
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;언제 보여줄지 제어하기 — Rule, Parameter, Event&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style="border-left: 4px solid #3a8c28; background: #f2f8ee; padding: 8px 16px; margin: 16px 0;" data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁 구조체에 아무 rules도 정의하지 않으면, 그 팁은 닫히거나 display frequency 한계를 넘기 전까지 계속 나타나요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;앱 상태 기반 — Parameter&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;추적하고 싶은 앱 상태를 변수로 정의하고, &lt;code&gt;Parameter&lt;/code&gt; 프로퍼티 래퍼로 감싼 뒤, &lt;code&gt;rules&lt;/code&gt; 프로퍼티에서 &lt;code&gt;#Rule&lt;/code&gt; 매크로로 조건을 정의해요.&lt;/span&gt;&lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct ParameterRuleTip: Tip {
    // Define the app state you want to track.
    @Parameter
    static var isLoggedIn: Bool = false

    var rules: [Rule] {
        // Define a rule based on the app state.
        #Rule(Self.$isLoggedIn) {
            // Set the conditions for when the tip displays.
            $0 == true
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;이렇게 정의해두면, 앱 상태가 바뀔 때(예: 버튼 탭으로 isLoggedIn을 토글) 팁이 그에 맞춰 나타나거나 사라져요.&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre class="stylus" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;Button("Tap") {
    ParameterRuleTip.isLoggedIn.toggle()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;사용자 행동 기반 — Event&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;사용자 행동을 추적하고 싶다면, &lt;code&gt;id&lt;/code&gt;를 가진 &lt;code&gt;Event&lt;/code&gt; 상수를 정의하고 그 이벤트를 참조하는 &lt;code&gt;#Rule&lt;/code&gt;을 정의해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;아래 예제는 이벤트가 3번 이상 발생하면 팁이 나타납니다.&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct EventRuleTip: Tip {
    // Define the user interaction you want to track.
    static let didTriggerControlEvent = Event(id: "didTriggerControlEvent")

    var rules: [Rule] {
        #Rule(Self.didTriggerControlEvent) {
            $0.donations.count &amp;gt;= 3
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;TipKit은 이벤트가 발생한 횟수를 자동으로 디스크에 저장하고, 이 상태는 앱을 재시작해도 유지돼요.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;여러 조건 조합하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Parameter와 Event 기반 규칙을 여러 개 정의하면, &lt;code&gt;rules&lt;/code&gt; 프로퍼티 안에서 이 규칙들은 AND로 결합돼요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;아래 &lt;code&gt;ComboTip&lt;/code&gt;은 로그인 상태(parameter)와 화면 진입 3회(event)를 모두 만족해야 나타납니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct ComboTip: Tip {
    @Parameter
    static var isLoggedIn: Bool = false

    static let enteredView = Event(id: "enteredView")

    var rules: [Rule] {
        // Note: These rules AND together.
        #Rule(Self.$isLoggedIn) {
            $0 == true
        }
        #Rule(Self.enteredView) {
            $0.donations.count &amp;gt;= 3
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;표시 빈도 제어하기 — Option&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Tip&lt;/code&gt;의 &lt;code&gt;options&lt;/code&gt; 프로퍼티에 &lt;code&gt;Option&lt;/code&gt;을 설정해서 표시 빈도를 제어할 수 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;MaxDisplayCount&lt;/code&gt;: 시스템이 자동으로 팁을 무효화하기 전까지 보여줄 최대 횟수를 지정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;MaxDisplayDuration&lt;/code&gt;: 팁이 무효화되기 전까지 표시되는 최대 시간을 지정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;
&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;IgnoresDisplayFrequency&lt;/code&gt;: 사전에 설정된 display frequency 간격을 이 팁이 따를지 여부를 제어&lt;br&gt;&lt;/span&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct OptionTip: Tip {
    var options: [Option] {
        // Show this tip once.
        MaxDisplayCount(1)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;여러 팁을 순서대로 — TipGroup&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;TipGroup&lt;/code&gt;은 여러 팁을 특정 순서로 보여주거나, 표시 가능한 첫 번째 팁만 보여줄 수 있는 클래스예요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;Priority에는 두 가지 케이스가 있어요.&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;firstAvailable&lt;/code&gt;(기본값): 표시 가능한 첫 번째 팁을 보여줌&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;
&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;ordered&lt;/code&gt;: 이전 팁들이 모두 &lt;code&gt;invalidated&lt;/code&gt; 상태가 됐을 때, 표시 가능한 팁을 보여줌&lt;/span&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="less" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;init(_ priority: TipGroup.Priority = .firstAvailable, @Tips.GroupBuilder _ builder: () -&amp;gt; [any Tip])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;currentTip&lt;/code&gt; 프로퍼티로 현재 표시 가능한 팁을 가져올 수 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁 모양 커스터마이징 — TipViewStyle&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;기본적으로 &lt;code&gt;TipView&lt;/code&gt;는 &lt;code&gt;MiniTipViewStyle&lt;/code&gt;(&lt;code&gt;TipViewStyle.miniTip&lt;/code&gt;)로 그려져요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;뷰 계층 전체에 커스텀 스타일을 적용하려면 &lt;code&gt;TipViewStyle&lt;/code&gt; 프로토콜을 채택한 타입을 만들고, &lt;code&gt;.tipViewStyle(_:)&lt;/code&gt; 모디파이어로 적용하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;TipViewStyle&lt;/code&gt;의 유일한 요구사항은 &lt;code&gt;makeBody(configuration:)&lt;/code&gt;예요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;여기서 받는 &lt;code&gt;TipViewStyleConfiguration&lt;/code&gt;에는 &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;actions&lt;/code&gt; 프로퍼티가 있어서, 이를 조합해 원하는 모습으로 그릴 수 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct HeadlineTipViewStyle: TipViewStyle {
    func makeBody(configuration: TipViewStyleConfiguration) -&amp;gt; some View {
        VStack(alignment: .leading) {
            configuration.image?
            configuration.title?.font(.headline)
            configuration.message?.font(.subheadline)

            ForEach(configuration.actions) { action in
                Button(action.title) { action.handler() }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="isbl" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;.tipViewStyle(HeadlineTipViewStyle())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;테스트하기 위한 API&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁이 더 이상 표시 자격이 없어지면 앱에서 사라지는데, 이건 개발 중 팁의 배치와 모양을 테스트하기 어렵게 만들어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;TipKit은 이를 위한 테스트 전용 API를 제공합니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;// Show all defined tips in the app.
Tips.showAllTipsForTesting()

// Show some tips, but not all.
Tips.showTipsForTesting([tip1, tip2, tip3])

// Hide all tips defined in the app.
Tips.hideAllTipsForTesting()

// Purge all TipKit-related data.
try Tips.resetDatastore()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Conclusion&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;TipKit은 &lt;code&gt;Tip&lt;/code&gt; 프로토콜로 내용을 정의하고, &lt;code&gt;TipView&lt;/code&gt;/&lt;code&gt;popoverTip&lt;/code&gt;으로 보여주는 단순한 구조 위에, Parameter·Event 기반 &lt;code&gt;Rule&lt;/code&gt;과 &lt;code&gt;Option&lt;/code&gt;으로 "언제, 얼마나 자주" 보여줄지를 세밀하게 제어할 수 있는 프레임워크예요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;직접 상태를 들고 조건문으로 온보딩 UI를 관리하던 걸, 선언적인 규칙으로 옮길 수 있게 됐다는 점이 핵심인 것 같습니다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt; &lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;References&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;figure id="og_1790988921872" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="TipKit | Apple Developer Documentation" data-og-description="Display tips that help people discover features in your app." data-og-host="developer.apple.com" data-og-source-url="https://developer.apple.com/documentation/tipkit" data-og-url="https://developer.apple.com/documentation/tipkit" data-og-image="https://scrap.kakaocdn.net/dn/d5v0d0/dJMb84qvvdz/yCk5ig0YHe3ApsRN3IjYok/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/2R7hF/dJMb8Rkpo3e/nzlLjupev03xFKV5STaGs1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512"&gt;&lt;a href="https://developer.apple.com/documentation/tipkit" target="_blank" rel="noopener" data-source-url="https://developer.apple.com/documentation/tipkit"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/d5v0d0/dJMb84qvvdz/yCk5ig0YHe3ApsRN3IjYok/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/2R7hF/dJMb8Rkpo3e/nzlLjupev03xFKV5STaGs1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');"&gt; &lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;TipKit | Apple Developer Documentation&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;Display tips that help people discover features in your app.&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id="og_1790988942323" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="Highlighting app features with TipKit | Apple Developer Documentation" data-og-description="Bring attention to new features in your app by using tips." data-og-host="developer.apple.com" data-og-source-url="https://developer.apple.com/documentation/tipkit/highlightingappfeatureswithtipkit" data-og-url="https://developer.apple.com/documentation/tipkit/highlightingappfeatureswithtipkit" data-og-image="https://scrap.kakaocdn.net/dn/exqkx/dJMb9dHK02X/eGw7mKF8rKjjkFhRFKhV0K/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bPfcjs/dJMb9iI3q1h/NPCaXiloxm8cpfi0gqLXCk/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512"&gt;&lt;a href="https://developer.apple.com/documentation/tipkit/highlightingappfeatureswithtipkit" target="_blank" rel="noopener" data-source-url="https://developer.apple.com/documentation/tipkit/highlightingappfeatureswithtipkit"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/exqkx/dJMb9dHK02X/eGw7mKF8rKjjkFhRFKhV0K/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bPfcjs/dJMb9iI3q1h/NPCaXiloxm8cpfi0gqLXCk/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');"&gt; &lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;Highlighting app features with TipKit | Apple Developer Documentation&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;Bring attention to new features in your app by using tips.&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id="og_1790988935087" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="Make features discoverable with TipKit - WWDC23 - Videos - Apple Developer" data-og-description="Teach people how to use your app with TipKit! Learn how you can create effective educational moments through tips. We'll share how you..." data-og-host="developer.apple.com" data-og-source-url="https://developer.apple.com/videos/play/wwdc2023/10229/" data-og-url="https://developer.apple.com/videos/play/wwdc2023/10229/" data-og-image="https://scrap.kakaocdn.net/dn/fkslz/dJMb8U9gORF/tKDlkOG1KeKc0b7h88Ozdk/img.jpg?width=500&amp;amp;height=282&amp;amp;face=66_80_146_168,https://scrap.kakaocdn.net/dn/bTGkrc/dJMb83kQeJH/DmGrDrLpzCIlWoYtckATEK/img.jpg?width=1800&amp;amp;height=1012&amp;amp;face=216_298_947_705"&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2023/10229/" target="_blank" rel="noopener" data-source-url="https://developer.apple.com/videos/play/wwdc2023/10229/"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/fkslz/dJMb8U9gORF/tKDlkOG1KeKc0b7h88Ozdk/img.jpg?width=500&amp;amp;height=282&amp;amp;face=66_80_146_168,https://scrap.kakaocdn.net/dn/bTGkrc/dJMb83kQeJH/DmGrDrLpzCIlWoYtckATEK/img.jpg?width=1800&amp;amp;height=1012&amp;amp;face=216_298_947_705');"&gt; &lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;Make features discoverable with TipKit - WWDC23 - Videos - Apple Developer&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;Teach people how to use your app with TipKit! Learn how you can create effective educational moments through tips. We'll share how you...&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id="og_1790988937090" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="Customize feature discovery with TipKit - WWDC24 - Videos - Apple Developer" data-og-description="Focused on feature discovery, the TipKit framework makes it easy to display tips in your app. Now you can group tips so features are..." data-og-host="developer.apple.com" data-og-source-url="https://developer.apple.com/videos/play/wwdc2024/10070/" data-og-url="https://developer.apple.com/videos/play/wwdc2024/10070/" data-og-image="https://scrap.kakaocdn.net/dn/cmQjch/dJMb8QMzxCT/k66TKdrVlfPPNih3h9w5sK/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282,https://scrap.kakaocdn.net/dn/4mBn4/dJMb9lltM40/IckfkO2TujCavZ63wDP9gk/img.jpg?width=1800&amp;amp;height=1012&amp;amp;face=0_0_1800_1012"&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2024/10070/" target="_blank" rel="noopener" data-source-url="https://developer.apple.com/videos/play/wwdc2024/10070/"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/cmQjch/dJMb8QMzxCT/k66TKdrVlfPPNih3h9w5sK/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282,https://scrap.kakaocdn.net/dn/4mBn4/dJMb9lltM40/IckfkO2TujCavZ63wDP9gk/img.jpg?width=1800&amp;amp;height=1012&amp;amp;face=0_0_1800_1012');"&gt; &lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;Customize feature discovery with TipKit - WWDC24 - Videos - Apple Developer&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;Focused on feature discovery, the TipKit framework makes it easy to display tips in your app. Now you can group tips so features are...&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://green1229.tistory.com/639</id>
    <link href="https://green1229.tistory.com/639"/>
    <summary type="html">&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;안녕하세요. &lt;span style="color: #409d00;"&gt;&lt;b&gt;그린&lt;/b&gt;&lt;/span&gt;입니다  &lt;/span&gt;&lt;br /&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;이번 포스팅에서는 &lt;span style="background-color: #9feec3;"&gt;&lt;b&gt;SwiftUI의 TipKit 프레임워크&lt;/b&gt;&lt;/span&gt;에 대해 애플 공식 문서를 기준으로 정리해보겠습니다  &lt;/span&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-filename="12312.001.jpeg" data-origin-width="400" data-origin-height="400"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/GC2cO/dJMcafByQNM/GCQssw0rzLh8TIGO5RYjJ0/img.jpg" data-phocus="https://blog.kakaocdn.net/dn/GC2cO/dJMcafByQNM/GCQssw0rzLh8TIGO5RYjJ0/img.jpg"&gt;&lt;img src="https://blog.kakaocdn.net/dn/GC2cO/dJMcafByQNM/GCQssw0rzLh8TIGO5RYjJ0/img.jpg" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGC2cO%2FdJMcafByQNM%2FGCQssw0rzLh8TIGO5RYjJ0%2Fimg.jpg" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="400" height="400" data-filename="12312.001.jpeg" data-origin-width="400" data-origin-height="400"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Intro&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;div style="background: #f2f8ee; border-radius: 10px; padding: 16px 20px; margin: 16px 0;"&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;주제:&lt;/b&gt; TipKit&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;프레임워크:&lt;/b&gt; TipKit&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;지원 버전:&lt;/b&gt; iOS/iPadOS 17 &amp;middot; macOS 14 &amp;middot; tvOS 16 &amp;middot; watchOS 10 &amp;middot; visionOS 1 이상&lt;/span&gt;&lt;/p&gt;
&lt;p style="margin: 4px 0;" data-ke-size="size16"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;b&gt;공식 문서:&lt;/b&gt; &lt;a style="color: #000000;" href="https://developer.apple.com/documentation/tipkit" target="_blank" rel="noopener"&gt;developer.apple.com/documentation/tipkit&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;TipKit이란&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;공식 문서는 TipKit을 이렇게 소개해요.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style="border-left: 4px solid #3a8c28; background: #f2f8ee; padding: 8px 16px; margin: 16px 0;" data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Use TipKit to show contextual tips that highlight new, interesting, or unused features people haven't discovered on their own yet.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;즉, &lt;b&gt;사람들이 아직 스스로 발견하지 못한 기능을 가볍게 알려주는 용도&lt;/b&gt;예요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Tip&lt;/code&gt; 프로토콜로 팁의 내용과, 그 팁이 언제 나타나야 하는지의 조건을 정의하고, &lt;code&gt;TipView&lt;/code&gt;로 그 기능에 시선을 끌게 만들어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;공식 문서는 사용 가이드도 명확히 제시해요.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;사람들을 부담주지 않도록 팁은 아껴서 써야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;스스로 발견하기 어려운, 눈에 잘 안 띄는 기능에만 써야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;앱을 쓸 때마다 매번 팁을 띄우지 말 것 &amp;mdash; 불필요하게 나타나면 방해가 됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁으로 앱 사용법을 처음부터 끝까지 안내하거나, 광고/프로모션 목적으로 쓰면 안 됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;디자인 가이드는 &lt;a style="color: #000000;" href="https://developer.apple.com/design/human-interface-guidelines/offering-help" target="_blank" rel="noopener"&gt;Human Interface Guidelines의 Offering Help&lt;/a&gt; 문서를 참고하라고 안내하고 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Tip 프로토콜 &amp;mdash; 팁의 내용 정의하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;공식 문서의 첫 예제 코드예요.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;import SwiftUI
import TipKit

// Define your tip's content.
struct FavoriteLandmarkTip: Tip {
    var title: Text {
        Text("Save as a Favorite")
    }

    var message: Text? {
        Text("Your favorite landmarks always appear at the top of the list.")
    }

    var image: Image? {
        Image(systemName: "star")
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Tip&lt;/code&gt;은 &lt;code&gt;Identifiable&lt;/code&gt;, &lt;code&gt;Sendable&lt;/code&gt;을 채택하는 프로토콜이에요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁이 유효하려면 &lt;code&gt;title&lt;/code&gt;은 반드시 설정해야 하고, &lt;code&gt;message&lt;/code&gt;와 &lt;code&gt;image&lt;/code&gt;는 선택이에요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;rules&lt;/code&gt;와 &lt;code&gt;options&lt;/code&gt; 프로퍼티에 &lt;code&gt;Rule&lt;/code&gt;과 &lt;code&gt;Option&lt;/code&gt; 인스턴스를 전달해서 팁이 언제 나타날지 제어할 수 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;앱 시작 시 팁 로드하기 &amp;mdash; Tips.configure(_:)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁을 화면에 띄우기 전에, &lt;b&gt;먼저 앱에 로드해야 해요. &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;공식 문서는 앱 세션마다 한 번, 예를 들어 &lt;code&gt;App&lt;/code&gt;의 &lt;code&gt;init()&lt;/code&gt;에서 &lt;code&gt;configure(_:)&lt;/code&gt;를 호출하는 걸 베스트 프랙티스로 안내해요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;@main
struct TipKitExamples: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }

    init() {
        do {
            // Configure and load all tips in the app.
            try Tips.configure()
        }
        catch {
            print("Error initializing tips: \(error)")
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;configure(_:)&lt;/code&gt;에는 &lt;code&gt;ConfigurationOption&lt;/code&gt; 배열을 전달해서 세부 설정도 가능해요.&lt;/span&gt;&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;displayFrequency(_:)&lt;/code&gt;: 팁이 뜬 뒤 다음 팁이 뜨기까지의 최소 간격을 설정. &lt;code&gt;.immediate&lt;/code&gt;(기본값), &lt;code&gt;.hourly&lt;/code&gt;, &lt;code&gt;.daily&lt;/code&gt;, &lt;code&gt;.weekly&lt;/code&gt;, &lt;code&gt;.monthly&lt;/code&gt; 중 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;datastoreLocation(_:)&lt;/code&gt;: 팁 상태를 저장할 위치를 커스터마이즈&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;cloudKitContainer(_:)&lt;/code&gt;: 팁 동기화에 쓸 CloudKit 컨테이너를 설정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;.monthly&lt;/code&gt;를 예로 들면, 공식 문서는 이렇게 설명해요.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;When you set this, the system shows no more than one tip per month.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁 띄우기 &amp;mdash; Inline과 Popover&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁을 보여주는 방법은 두 가지예요. &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;공식 문서는 가능하면 &lt;b&gt;인라인(Inline)&lt;/b&gt; 방식을 먼저 쓰라고 권장해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;사람들이 상호작용하고 싶어할 UI 요소를 가리지 않기 때문이에요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;인라인 팁 &amp;mdash; TipView&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct InlineView: View {
    // Create an instance of your tip content.
    let inlineTip = InlineTip()

    var body: some View {
        VStack(spacing: 20) {
            Text("A TipView embeds itself directly in the view.")

            // Place your tip near the feature you want to highlight.
            TipView(inlineTip, arrowEdge: .bottom)
            Button {
                // Invalidate the tip when someone uses the feature.
                inlineTip.invalidate(reason: .actionPerformed)
            } label: {
                Label("Favorite", systemImage: "star")
            }
            Spacer()
        }
        .padding()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;인라인 팁은 그 뷰 안에 직접 끼어들어 자리를 차지해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;그래서 팁이 나타나거나 사라질 때 주변 레이아웃이 그에 맞춰 바뀝니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팝오버 팁 &amp;mdash; popoverTip(_:arrowEdge:action:)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct PopoverView: View {
    let highlightTip = HighlightTip()
    
    var body: some View {
        Image(systemName: "wand.and.stars")
            .imageScale(.large)
            // Place the tip on the feature to highlight.
            .popoverTip(highlightTip)
            .onTapGesture {
                tip.invalidate(reason: .actionPerformed)
            }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팝오버 방식은 화면 위에 팁을 띄워서 아래 UI를 가려요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;레이아웃을 바꾸고 싶지 않거나, 팁이 가리는 컨트롤과 상호작용할 필요가 없을 때 적합해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팝오버는 이미 기능을 가리키고 있으니, 이미지는 생략하는 걸 고려해보라고도 안내합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁 무효화하기 &amp;mdash; invalidate(reason:)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;사람이 그 기능을 실제로 쓰고 나면, 팁을 프로그래밍적으로 무효화할 수 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;InvalidationReason&lt;/code&gt;에는 다음 네 가지 케이스가 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;actionPerformed&lt;/code&gt;: 팁이 설명하는 행동을 사용자가 실제로 수행함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;displayCountExceeded&lt;/code&gt;: 팁이 최대 표시 횟수를 초과함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;displayDurationExceeded&lt;/code&gt;: 팁이 최대 표시 기간을 초과함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;tipClosed&lt;/code&gt;: 사용자가 팁이 떠 있는 동안 명시적으로 닫기 버튼을 눌러 닫음&lt;br /&gt;&lt;/span&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;버튼 추가하기 &amp;mdash; actions&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁에 추가 정보나 옵션으로 안내하는 버튼을 넣을 수도 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt; &lt;code&gt;Tip&lt;/code&gt;의 &lt;code&gt;actions&lt;/code&gt; 프로퍼티에 &lt;code&gt;Action&lt;/code&gt; 인스턴스들을 정의하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class="less" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;struct PasswordResetTip: Tip {
    var title: Text {
        Text("Need Help?")
    }

    var message: Text? {
        Text("Do you need help logging in to your account?")
    }

    var image: Image? {
        Image(systemName: "lock.shield")
    }

    var actions: [Action] {
        Action(id: "reset-password", title: "Reset Password")
        Action(id: "faq", title: "View our FAQ")
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;TipView&lt;/code&gt;에 전달한 클로저에서 &lt;code&gt;action.id&lt;/code&gt;를 확인해서 각 버튼을 눌렀을 때의 동작을 분기할 수 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class="reasonml" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;TipView(passwordResetTip, arrowEdge: .bottom) { action in
    if action.id == "reset-password", let url = URL(string: "https://iforgot.apple.com") {
        openURL(url) { accepted in
            print(accepted ? "Success Reset" : "Failure")
        }
    }
    if action.id == "faq", let url = URL(string: "https://appleid.apple.com/faq") {
        openURL(url) { accepted in
            print(accepted ? "Success FAQ" : "Failure")
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;언제 보여줄지 제어하기 &amp;mdash; Rule, Parameter, Event&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style="border-left: 4px solid #3a8c28; background: #f2f8ee; padding: 8px 16px; margin: 16px 0;" data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁 구조체에 아무 rules도 정의하지 않으면, 그 팁은 닫히거나 display frequency 한계를 넘기 전까지 계속 나타나요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;앱 상태 기반 &amp;mdash; Parameter&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;추적하고 싶은 앱 상태를 변수로 정의하고, &lt;code&gt;Parameter&lt;/code&gt; 프로퍼티 래퍼로 감싼 뒤, &lt;code&gt;rules&lt;/code&gt; 프로퍼티에서 &lt;code&gt;#Rule&lt;/code&gt; 매크로로 조건을 정의해요.&lt;/span&gt;&lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct ParameterRuleTip: Tip {
    // Define the app state you want to track.
    @Parameter
    static var isLoggedIn: Bool = false

    var rules: [Rule] {
        // Define a rule based on the app state.
        #Rule(Self.$isLoggedIn) {
            // Set the conditions for when the tip displays.
            $0 == true
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;이렇게 정의해두면, 앱 상태가 바뀔 때(예: 버튼 탭으로 isLoggedIn을 토글) 팁이 그에 맞춰 나타나거나 사라져요.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre class="stylus" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;Button("Tap") {
    ParameterRuleTip.isLoggedIn.toggle()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;사용자 행동 기반 &amp;mdash; Event&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;사용자 행동을 추적하고 싶다면, &lt;code&gt;id&lt;/code&gt;를 가진 &lt;code&gt;Event&lt;/code&gt; 상수를 정의하고 그 이벤트를 참조하는 &lt;code&gt;#Rule&lt;/code&gt;을 정의해요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;아래 예제는 이벤트가 3번 이상 발생하면 팁이 나타납니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct EventRuleTip: Tip {
    // Define the user interaction you want to track.
    static let didTriggerControlEvent = Event(id: "didTriggerControlEvent")

    var rules: [Rule] {
        #Rule(Self.didTriggerControlEvent) {
            $0.donations.count &amp;gt;= 3
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;TipKit은 이벤트가 발생한 횟수를 자동으로 디스크에 저장하고, 이 상태는 앱을 재시작해도 유지돼요.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;여러 조건 조합하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Parameter와 Event 기반 규칙을 여러 개 정의하면, &lt;code&gt;rules&lt;/code&gt; 프로퍼티 안에서 이 규칙들은 AND로 결합돼요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;아래 &lt;code&gt;ComboTip&lt;/code&gt;은 로그인 상태(parameter)와 화면 진입 3회(event)를 모두 만족해야 나타납니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct ComboTip: Tip {
    @Parameter
    static var isLoggedIn: Bool = false

    static let enteredView = Event(id: "enteredView")

    var rules: [Rule] {
        // Note: These rules AND together.
        #Rule(Self.$isLoggedIn) {
            $0 == true
        }
        #Rule(Self.enteredView) {
            $0.donations.count &amp;gt;= 3
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;표시 빈도 제어하기 &amp;mdash; Option&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;Tip&lt;/code&gt;의 &lt;code&gt;options&lt;/code&gt; 프로퍼티에 &lt;code&gt;Option&lt;/code&gt;을 설정해서 표시 빈도를 제어할 수 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;MaxDisplayCount&lt;/code&gt;: 시스템이 자동으로 팁을 무효화하기 전까지 보여줄 최대 횟수를 지정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;MaxDisplayDuration&lt;/code&gt;: 팁이 무효화되기 전까지 표시되는 최대 시간을 지정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;IgnoresDisplayFrequency&lt;/code&gt;: 사전에 설정된 display frequency 간격을 이 팁이 따를지 여부를 제어&lt;br /&gt;&lt;/span&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct OptionTip: Tip {
    var options: [Option] {
        // Show this tip once.
        MaxDisplayCount(1)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;여러 팁을 순서대로 &amp;mdash; TipGroup&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;TipGroup&lt;/code&gt;은 여러 팁을 특정 순서로 보여주거나, 표시 가능한 첫 번째 팁만 보여줄 수 있는 클래스예요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;Priority에는 두 가지 케이스가 있어요.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;ul style="list-style-type: disc;" data-ke-list-type="disc"&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;firstAvailable&lt;/code&gt;(기본값): 표시 가능한 첫 번째 팁을 보여줌&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;ordered&lt;/code&gt;: 이전 팁들이 모두 &lt;code&gt;invalidated&lt;/code&gt; 상태가 됐을 때, 표시 가능한 팁을 보여줌&lt;/span&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="less" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;init(_ priority: TipGroup.Priority = .firstAvailable, @Tips.GroupBuilder _ builder: () -&amp;gt; [any Tip])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;currentTip&lt;/code&gt; 프로퍼티로 현재 표시 가능한 팁을 가져올 수 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁 모양 커스터마이징 &amp;mdash; TipViewStyle&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;기본적으로 &lt;code&gt;TipView&lt;/code&gt;는 &lt;code&gt;MiniTipViewStyle&lt;/code&gt;(&lt;code&gt;TipViewStyle.miniTip&lt;/code&gt;)로 그려져요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;뷰 계층 전체에 커스텀 스타일을 적용하려면 &lt;code&gt;TipViewStyle&lt;/code&gt; 프로토콜을 채택한 타입을 만들고, &lt;code&gt;.tipViewStyle(_:)&lt;/code&gt; 모디파이어로 적용하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;&lt;code&gt;TipViewStyle&lt;/code&gt;의 유일한 요구사항은 &lt;code&gt;makeBody(configuration:)&lt;/code&gt;예요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;여기서 받는 &lt;code&gt;TipViewStyleConfiguration&lt;/code&gt;에는 &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;actions&lt;/code&gt; 프로퍼티가 있어서, 이를 조합해 원하는 모습으로 그릴 수 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;struct HeadlineTipViewStyle: TipViewStyle {
    func makeBody(configuration: TipViewStyleConfiguration) -&amp;gt; some View {
        VStack(alignment: .leading) {
            configuration.image?
            configuration.title?.font(.headline)
            configuration.message?.font(.subheadline)

            ForEach(configuration.actions) { action in
                Button(action.title) { action.handler() }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="isbl" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;"&gt;&lt;code&gt;.tipViewStyle(HeadlineTipViewStyle())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;테스트하기 위한 API&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;팁이 더 이상 표시 자격이 없어지면 앱에서 사라지는데, 이건 개발 중 팁의 배치와 모양을 테스트하기 어렵게 만들어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;TipKit은 이를 위한 테스트 전용 API를 제공합니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class="swift" style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow: auto;" data-ke-language="swift"&gt;&lt;code&gt;// Show all defined tips in the app.
Tips.showAllTipsForTesting()

// Show some tips, but not all.
Tips.showTipsForTesting([tip1, tip2, tip3])

// Hide all tips defined in the app.
Tips.hideAllTipsForTesting()

// Purge all TipKit-related data.
try Tips.resetDatastore()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;Conclusion&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;TipKit은 &lt;code&gt;Tip&lt;/code&gt; 프로토콜로 내용을 정의하고, &lt;code&gt;TipView&lt;/code&gt;/&lt;code&gt;popoverTip&lt;/code&gt;으로 보여주는 단순한 구조 위에, Parameter&amp;middot;Event 기반 &lt;code&gt;Rule&lt;/code&gt;과 &lt;code&gt;Option&lt;/code&gt;으로 "언제, 얼마나 자주" 보여줄지를 세밀하게 제어할 수 있는 프레임워크예요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;직접 상태를 들고 조건문으로 온보딩 UI를 관리하던 걸, 선언적인 규칙으로 옮길 수 있게 됐다는 점이 핵심인 것 같습니다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size18"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style="border-left: 4px solid #3a8c28; padding-left: 12px; margin-top: 32px;" data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="font-family: 'Nanum Gothic'; color: #000000;"&gt;References&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;figure id="og_1790988921872" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="TipKit | Apple Developer Documentation" data-og-description="Display tips that help people discover features in your app." data-og-host="developer.apple.com" data-og-source-url="https://developer.apple.com/documentation/tipkit" data-og-url="https://developer.apple.com/documentation/tipkit" data-og-image="https://scrap.kakaocdn.net/dn/d5v0d0/dJMb84qvvdz/yCk5ig0YHe3ApsRN3IjYok/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/2R7hF/dJMb8Rkpo3e/nzlLjupev03xFKV5STaGs1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512"&gt;&lt;a href="https://developer.apple.com/documentation/tipkit" target="_blank" rel="noopener" data-source-url="https://developer.apple.com/documentation/tipkit"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/d5v0d0/dJMb84qvvdz/yCk5ig0YHe3ApsRN3IjYok/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/2R7hF/dJMb8Rkpo3e/nzlLjupev03xFKV5STaGs1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');"&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;TipKit | Apple Developer Documentation&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;Display tips that help people discover features in your app.&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id="og_1790988942323" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="Highlighting app features with TipKit | Apple Developer Documentation" data-og-description="Bring attention to new features in your app by using tips." data-og-host="developer.apple.com" data-og-source-url="https://developer.apple.com/documentation/tipkit/highlightingappfeatureswithtipkit" data-og-url="https://developer.apple.com/documentation/tipkit/highlightingappfeatureswithtipkit" data-og-image="https://scrap.kakaocdn.net/dn/exqkx/dJMb9dHK02X/eGw7mKF8rKjjkFhRFKhV0K/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bPfcjs/dJMb9iI3q1h/NPCaXiloxm8cpfi0gqLXCk/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512"&gt;&lt;a href="https://developer.apple.com/documentation/tipkit/highlightingappfeatureswithtipkit" target="_blank" rel="noopener" data-source-url="https://developer.apple.com/documentation/tipkit/highlightingappfeatureswithtipkit"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/exqkx/dJMb9dHK02X/eGw7mKF8rKjjkFhRFKhV0K/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bPfcjs/dJMb9iI3q1h/NPCaXiloxm8cpfi0gqLXCk/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');"&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;Highlighting app features with TipKit | Apple Developer Documentation&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;Bring attention to new features in your app by using tips.&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id="og_1790988935087" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="Make features discoverable with TipKit - WWDC23 - Videos - Apple Developer" data-og-description="Teach people how to use your app with TipKit! Learn how you can create effective educational moments through tips. We'll share how you..." data-og-host="developer.apple.com" data-og-source-url="https://developer.apple.com/videos/play/wwdc2023/10229/" data-og-url="https://developer.apple.com/videos/play/wwdc2023/10229/" data-og-image="https://scrap.kakaocdn.net/dn/fkslz/dJMb8U9gORF/tKDlkOG1KeKc0b7h88Ozdk/img.jpg?width=500&amp;amp;height=282&amp;amp;face=66_80_146_168,https://scrap.kakaocdn.net/dn/bTGkrc/dJMb83kQeJH/DmGrDrLpzCIlWoYtckATEK/img.jpg?width=1800&amp;amp;height=1012&amp;amp;face=216_298_947_705"&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2023/10229/" target="_blank" rel="noopener" data-source-url="https://developer.apple.com/videos/play/wwdc2023/10229/"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/fkslz/dJMb8U9gORF/tKDlkOG1KeKc0b7h88Ozdk/img.jpg?width=500&amp;amp;height=282&amp;amp;face=66_80_146_168,https://scrap.kakaocdn.net/dn/bTGkrc/dJMb83kQeJH/DmGrDrLpzCIlWoYtckATEK/img.jpg?width=1800&amp;amp;height=1012&amp;amp;face=216_298_947_705');"&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;Make features discoverable with TipKit - WWDC23 - Videos - Apple Developer&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;Teach people how to use your app with TipKit! Learn how you can create effective educational moments through tips. We'll share how you...&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id="og_1790988937090" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="Customize feature discovery with TipKit - WWDC24 - Videos - Apple Developer" data-og-description="Focused on feature discovery, the TipKit framework makes it easy to display tips in your app. Now you can group tips so features are..." data-og-host="developer.apple.com" data-og-source-url="https://developer.apple.com/videos/play/wwdc2024/10070/" data-og-url="https://developer.apple.com/videos/play/wwdc2024/10070/" data-og-image="https://scrap.kakaocdn.net/dn/cmQjch/dJMb8QMzxCT/k66TKdrVlfPPNih3h9w5sK/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282,https://scrap.kakaocdn.net/dn/4mBn4/dJMb9lltM40/IckfkO2TujCavZ63wDP9gk/img.jpg?width=1800&amp;amp;height=1012&amp;amp;face=0_0_1800_1012"&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2024/10070/" target="_blank" rel="noopener" data-source-url="https://developer.apple.com/videos/play/wwdc2024/10070/"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/cmQjch/dJMb8QMzxCT/k66TKdrVlfPPNih3h9w5sK/img.jpg?width=500&amp;amp;height=282&amp;amp;face=0_0_500_282,https://scrap.kakaocdn.net/dn/4mBn4/dJMb9lltM40/IckfkO2TujCavZ63wDP9gk/img.jpg?width=1800&amp;amp;height=1012&amp;amp;face=0_0_1800_1012');"&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;Customize feature discovery with TipKit - WWDC24 - Videos - Apple Developer&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;Focused on feature discovery, the TipKit framework makes it easy to display tips in your app. Now you can group tips so features are...&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</summary>
    <title>SwiftUI - TipKit</title>
    <updated>2026-10-03T09:56:55+09:00</updated>
    <dc:date>2026-10-03T09:56:55+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>코드리더</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1122" data-origin-height="1402"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/b2E6tl/dJMcab0lK1l/2v15CC0QgOwDtKiIKMv5W1/img.png" data-phocus="https://blog.kakaocdn.net/dn/b2E6tl/dJMcab0lK1l/2v15CC0QgOwDtKiIKMv5W1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/b2E6tl/dJMcab0lK1l/2v15CC0QgOwDtKiIKMv5W1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2E6tl%2FdJMcab0lK1l%2F2v15CC0QgOwDtKiIKMv5W1%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1122" height="1402" data-origin-width="1122" data-origin-height="1402"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt; 이번에 소개할 분은 Jake Song으로 알려져 있는 개발자, 송재경님입니다. 대부분 리니지의 아버지로 송재경님을 기억하지만, 저는 hanterm 개발자로 기억합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt;hanterm은 X윈도(POSIX 시스템의 GUI 환경)에서 한국어 입출력을 지원하는 터미널 에뮬레이터입니다.  KAIST 재학 시절(1991년) 송재경님은 리눅스/유닉스용 VT100/200 에뮬레이터를 개발합니다. 당시에는 UTF-8이 표준으로 정착되기 전이었습니다. 한글 표준 전산화 작업은 진행되었지만, 리눅스/유닉스 환경에서 한글을 사용하기는 쉽지 않았습니다. 오픈소스이긴 했지만, 다국어를 지원하는 프로그램, 특히 한글을 지원하는 프로그램은 그 나라의 몫이었습니다.  일반 사용자들에게 리눅스는 만만치 않았는데, 영어만 사용해야 하는 것은 또하나의 큰 장벽이었죠. 이 문제를 해결해 준 것이 한텀입니다. 물론 한텀을 리눅스에 설치하는 것도 쉽지 않았지만, 이후 한글 기반의 리눅스 배포판들이 나오기 시작하고, 여기에 한텀이 기본 탑재되면서 리눅스에서 한글의 사용이 쉬워졌습니다. 물론 그 뒤에는 UTF-8이 기본 지원되는 최신 터미널 환경이 보편화되면서 한텀은 기본 터미널 프로그램들에게 자리를 내주고, 우리의 기억속에 추억으로 남게 됩니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt;송재경님은 우리나라 게임계의 살아있는 역사라고 해도 과언이 아닌데요. KAIST 박사 과정을 중퇴한 이후 건국대 학생이던 김성배님과 함께 개발한 &amp;lt;쥬라기 공원&amp;gt;은 우리나라 최초의 상용 텍스트 머드게임이 됩니다.(예전에 소개했던 &lt;a style="color: #000000;" href="https://neozest2.tistory.com/entry/JangInKyeongMarieTelecom" target="_blank" rel="noopener"&gt;단군의 땅&lt;/a&gt;보다 일주일 먼저 천리안을 통해 서비스됩니다.) 그 뒤에는 대학 시절부터 알고 지내던 김정주 님과 함께 넥슨을 창업하고 바람의 나라를 개발합니다. 하지만 1995년말 김정주님과의 불화로 지분을 모두 반납하고(!!!) 넥슨을 퇴사합니다. 이후 아이네트에 입사하여 리니지를 개발하는데, 아이네트가 IMF로 경영상 어려움을 겪던 시기에 엔씨소프트로 이직, 리니지를 계속 만들어 갑니다. 하지만 인생지사 새옹지마, IMF는 경제적 위기였지만, 인터넷과 PC방이 큰 인기를 끌며 보급됩니다. 그 붐을 타고 리니지(와 스타크래프트)는 큰 성공을 거두게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt;2002년 말 송재경님은 김택진님과의 불화로 다시 엔씨소프트를 퇴사합니다. 이때 엔씨소프트의 지분을 모두 처분했는데, 대략 45억 수준이었다고 합니다. 어마어마한 돈이지만, 송재경님의 역할에 비해서는 적은 엑싯이었다라는 평이 많더군요. 이후 자신의 회사인 XL 게임즈를 세우고, 아키에이지를 개발했습니다. 아쉽지만 아키에이지는 상업적 성공을 거두지는 못했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt; &lt;span style="color: #000000; text-align: start;"&gt;송재경님의 이야기가&lt;/span&gt;  최근 다시 언론에 많이 소개되고 있습니다. 홀로 리니지와 비슷한 게임을 바이브코딩 방식으로 개발하고 있기 때문이죠. 이 게임은 사람과 AI를 모두 전체 게임월드의 플레이어로 바라보는 독특한 MMO 게임입니다. 보통 NPC는 정해진 역할만 담당하고, 사람이  제어하는 캐릭터와는 명확히 분리되죠.  아래 링크를 통해 한번 브라우저에서 체험해 보시길..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt;송재경님은 &lt;a style="color: #000000;" href="https://neozest2.tistory.com/entry/KilnamChon"&gt;전길남 박사님&lt;/a&gt;의 제자이기도 한데요. 석사학위 논문은 "&lt;span style="text-align: left;"&gt;첨가 메카니즘이 있는 규칙에 근거한 사용자 인터페이스 설계 시스템(Rule-based presentation design with augmentation)" 입니다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="639" data-origin-height="529"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bOsAYN/dJMcagmSa8d/rkIiNUnufjM260qjyo9Xo0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bOsAYN/dJMcagmSa8d/rkIiNUnufjM260qjyo9Xo0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bOsAYN/dJMcagmSa8d/rkIiNUnufjM260qjyo9Xo0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOsAYN%2FdJMcagmSa8d%2FrkIiNUnufjM260qjyo9Xo0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="639" height="529" data-origin-width="639" data-origin-height="529"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1872" data-origin-height="901"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/luqxh/dJMcafuK9h1/hkfBhLsuZoO9cqaqfidGQ0/img.png" data-phocus="https://blog.kakaocdn.net/dn/luqxh/dJMcafuK9h1/hkfBhLsuZoO9cqaqfidGQ0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/luqxh/dJMcafuK9h1/hkfBhLsuZoO9cqaqfidGQ0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fluqxh%2FdJMcafuK9h1%2FhkfBhLsuZoO9cqaqfidGQ0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1872" height="901" data-origin-width="1872" data-origin-height="901"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;figure id="og_1791028366791" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="OpenMMO" data-og-description="" data-og-host="openmmo.to.nexus" data-og-source-url="https://openmmo.to.nexus/" data-og-url="https://openmmo.to.nexus/" data-og-image=""&gt;&lt;a href="https://openmmo.to.nexus/" target="_blank" rel="noopener" data-source-url="https://openmmo.to.nexus/"&gt;
&lt;div class="og-image" style="background-image: url();"&gt; &lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;OpenMMO&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;openmmo.to.nexus&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt;"게임은 무조건 재미있어야 한다. 재미없는 게임은 아무리 잘 만들어도 의미가 없다."&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt;"나는 경영 체질이 아니다. 관리 업무보다는 차라리 코딩이 훨씬 편하고 재미있다."&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt;"리니지의 성공은 7할이 운이다. IMF 시절, PC방이라는 시대적 흐름과 잘 맞아떨어졌다."&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt;"(아키에이지는) 내가 개발자로서 정말 만들고 싶었던, 자유도가 극도로 높은 게임에 대한 도전이었다."&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt;"나는 천재가 아니다. 그냥 게임 만드는 것을 좋아하고, 오래 했을 뿐이다."&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt; “주인의식은 (오너인) 제가 가질게요” &lt;span style="letter-spacing: 0px;"&gt;- 신입사원이 &lt;/span&gt;&lt;span style="letter-spacing: 0px;"&gt;“주인 의식을 가지고 열심히 일하겠습니다”라고 인사를 하자 송재경님의 답변&lt;/span&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://neozest2.tistory.com/entry/JakeSongHanTermLineageXLGames</id>
    <link href="https://neozest2.tistory.com/entry/JakeSongHanTermLineageXLGames"/>
    <summary type="html">&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1122" data-origin-height="1402"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/b2E6tl/dJMcab0lK1l/2v15CC0QgOwDtKiIKMv5W1/img.png" data-phocus="https://blog.kakaocdn.net/dn/b2E6tl/dJMcab0lK1l/2v15CC0QgOwDtKiIKMv5W1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/b2E6tl/dJMcab0lK1l/2v15CC0QgOwDtKiIKMv5W1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2E6tl%2FdJMcab0lK1l%2F2v15CC0QgOwDtKiIKMv5W1%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1122" height="1402" data-origin-width="1122" data-origin-height="1402"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt; 이번에 소개할 분은 Jake Song으로 알려져 있는 개발자, 송재경님입니다. 대부분 리니지의 아버지로 송재경님을 기억하지만, 저는 hanterm 개발자로 기억합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt;hanterm은 X윈도(POSIX 시스템의 GUI 환경)에서 한국어 입출력을 지원하는 터미널 에뮬레이터입니다.&amp;nbsp; KAIST 재학 시절(1991년) 송재경님은 리눅스/유닉스용 VT100/200 에뮬레이터를 개발합니다. 당시에는 UTF-8이 표준으로 정착되기 전이었습니다. 한글 표준 전산화 작업은 진행되었지만, 리눅스/유닉스 환경에서 한글을 사용하기는 쉽지 않았습니다. 오픈소스이긴 했지만, 다국어를 지원하는 프로그램, 특히 한글을 지원하는 프로그램은 그 나라의 몫이었습니다.&amp;nbsp; 일반 사용자들에게 리눅스는 만만치 않았는데, 영어만 사용해야 하는 것은 또하나의 큰 장벽이었죠. 이 문제를 해결해 준 것이 한텀입니다. 물론 한텀을 리눅스에 설치하는 것도 쉽지 않았지만, 이후 한글 기반의 리눅스 배포판들이 나오기 시작하고, 여기에 한텀이 기본 탑재되면서 리눅스에서 한글의 사용이 쉬워졌습니다. 물론 그 뒤에는 UTF-8이 기본 지원되는 최신 터미널 환경이 보편화되면서 한텀은 기본 터미널 프로그램들에게 자리를 내주고, 우리의 기억속에 추억으로 남게 됩니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt;송재경님은 우리나라 게임계의 살아있는 역사라고 해도 과언이 아닌데요. KAIST 박사 과정을 중퇴한 이후 건국대 학생이던 김성배님과 함께 개발한 &amp;lt;쥬라기 공원&amp;gt;은 우리나라 최초의 상용 텍스트 머드게임이 됩니다.(예전에 소개했던 &lt;a style="color: #000000;" href="https://neozest2.tistory.com/entry/JangInKyeongMarieTelecom" target="_blank" rel="noopener"&gt;단군의 땅&lt;/a&gt;보다 일주일 먼저 천리안을 통해 서비스됩니다.) 그 뒤에는 대학 시절부터 알고 지내던 김정주 님과 함께 넥슨을 창업하고 바람의 나라를 개발합니다. 하지만 1995년말 김정주님과의 불화로 지분을 모두 반납하고(!!!) 넥슨을 퇴사합니다. 이후 아이네트에 입사하여 리니지를 개발하는데, 아이네트가 IMF로 경영상 어려움을 겪던 시기에 엔씨소프트로 이직, 리니지를 계속 만들어 갑니다. 하지만 인생지사 새옹지마, IMF는 경제적 위기였지만, 인터넷과 PC방이 큰 인기를 끌며 보급됩니다. 그 붐을 타고 리니지(와 스타크래프트)는 큰 성공을 거두게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt;2002년 말 송재경님은 김택진님과의 불화로 다시 엔씨소프트를 퇴사합니다. 이때 엔씨소프트의 지분을 모두 처분했는데, 대략 45억 수준이었다고 합니다. 어마어마한 돈이지만, 송재경님의 역할에 비해서는 적은 엑싯이었다라는 평이 많더군요. 이후 자신의 회사인 XL 게임즈를 세우고, 아키에이지를 개발했습니다. 아쉽지만 아키에이지는 상업적 성공을 거두지는 못했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt; &lt;span style="color: #000000; text-align: start;"&gt;송재경님의 이야기가&lt;/span&gt;&amp;nbsp; 최근 다시 언론에 많이 소개되고 있습니다. 홀로 리니지와 비슷한 게임을 바이브코딩 방식으로 개발하고 있기 때문이죠. 이 게임은 사람과 AI를 모두 전체 게임월드의 플레이어로 바라보는 독특한 MMO 게임입니다. 보통 NPC는 정해진 역할만 담당하고, 사람이&amp;nbsp; 제어하는 캐릭터와는 명확히 분리되죠.&amp;nbsp; 아래 링크를 통해 한번 브라우저에서 체험해 보시길..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&lt;span style="color: #000000;"&gt;송재경님은 &lt;a style="color: #000000;" href="https://neozest2.tistory.com/entry/KilnamChon"&gt;전길남 박사님&lt;/a&gt;의 제자이기도 한데요. 석사학위 논문은 "&lt;span style="text-align: left;"&gt;첨가 메카니즘이 있는 규칙에 근거한 사용자 인터페이스 설계 시스템(Rule-based presentation design with augmentation)" 입니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="639" data-origin-height="529"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bOsAYN/dJMcagmSa8d/rkIiNUnufjM260qjyo9Xo0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bOsAYN/dJMcagmSa8d/rkIiNUnufjM260qjyo9Xo0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bOsAYN/dJMcagmSa8d/rkIiNUnufjM260qjyo9Xo0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOsAYN%2FdJMcagmSa8d%2FrkIiNUnufjM260qjyo9Xo0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="639" height="529" data-origin-width="639" data-origin-height="529"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1872" data-origin-height="901"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/luqxh/dJMcafuK9h1/hkfBhLsuZoO9cqaqfidGQ0/img.png" data-phocus="https://blog.kakaocdn.net/dn/luqxh/dJMcafuK9h1/hkfBhLsuZoO9cqaqfidGQ0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/luqxh/dJMcafuK9h1/hkfBhLsuZoO9cqaqfidGQ0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fluqxh%2FdJMcafuK9h1%2FhkfBhLsuZoO9cqaqfidGQ0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1872" height="901" data-origin-width="1872" data-origin-height="901"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id="og_1791028366791" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="OpenMMO" data-og-description="" data-og-host="openmmo.to.nexus" data-og-source-url="https://openmmo.to.nexus/" data-og-url="https://openmmo.to.nexus/" data-og-image=""&gt;&lt;a href="https://openmmo.to.nexus/" target="_blank" rel="noopener" data-source-url="https://openmmo.to.nexus/"&gt;
&lt;div class="og-image" style="background-image: url();"&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;OpenMMO&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;openmmo.to.nexus&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt;"게임은 무조건 재미있어야 한다. 재미없는 게임은 아무리 잘 만들어도 의미가 없다."&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt;"나는 경영 체질이 아니다. 관리 업무보다는 차라리 코딩이 훨씬 편하고 재미있다."&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt;"리니지의 성공은 7할이 운이다. IMF 시절, PC방이라는 시대적 흐름과 잘 맞아떨어졌다."&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt;"(아키에이지는) 내가 개발자로서 정말 만들고 싶었던, 자유도가 극도로 높은 게임에 대한 도전이었다."&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt;"나는 천재가 아니다. 그냥 게임 만드는 것을 좋아하고, 오래 했을 뿐이다."&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style="style3"&gt;&lt;span style="color: #000000;"&gt; &amp;ldquo;주인의식은 (오너인) 제가 가질게요&amp;rdquo; &lt;span style="letter-spacing: 0px;"&gt;- 신입사원이 &lt;/span&gt;&lt;span style="letter-spacing: 0px;"&gt;&amp;ldquo;주인 의식을 가지고 열심히 일하겠습니다&amp;rdquo;라고 인사를 하자 송재경님의 답변&lt;/span&gt;&lt;/span&gt;&lt;/blockquote&gt;</summary>
    <title>유닉스 환경에서 한글 사용의 자유를, HanTerm 개발자 송재경..</title>
    <updated>2026-10-08T00:00:56+09:00</updated>
    <dc:date>2026-10-08T00:00:56+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>코드리더</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1024" data-origin-height="490"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/WyT3n/dJMcaaKGjzt/kBW0wd7kKde20V6z3wOKuK/img.png" data-phocus="https://blog.kakaocdn.net/dn/WyT3n/dJMcaaKGjzt/kBW0wd7kKde20V6z3wOKuK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/WyT3n/dJMcaaKGjzt/kBW0wd7kKde20V6z3wOKuK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWyT3n%2FdJMcaaKGjzt%2FkBW0wd7kKde20V6z3wOKuK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1024" height="490" data-origin-width="1024" data-origin-height="490"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt;1990년대 초, 대한민국 문서 작성 소프트웨어 시장은 한글과컴퓨터사의 '아래아한글'이 독점하고 있었습니다. 그 거대한 장벽 앞에 당당히 도전장을 내민 대학생들이 있었습니다.1992년 '21세기'라는 독자적인 워드프로세서를 개발하며 혜성처럼 등장한 이들은 비록 시장 점유율에서 한글을 넘어서지는 못했지만, 이 도전을 통해 '이스트소프트'가 탄생하게 됩니다. 한양대생들로 이루어진 개발팀의 리더였던 사람은 이스트소프트를 이끌고 있는 김장중 님입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="764" data-origin-height="1020"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/GdCld/dJMcafuIKri/B7Qk3ROieNTMivXke4P0Ok/img.png" data-phocus="https://blog.kakaocdn.net/dn/GdCld/dJMcafuIKri/B7Qk3ROieNTMivXke4P0Ok/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/GdCld/dJMcafuIKri/B7Qk3ROieNTMivXke4P0Ok/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGdCld%2FdJMcafuIKri%2FB7Qk3ROieNTMivXke4P0Ok%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="764" height="1020" data-origin-width="764" data-origin-height="1020"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt;92년에 발표된 21세기 워드프로세스는 비록 도스 환경에서 동작하는 프로그램이었지만, 아래아한글보다 먼저 벡터 글꼴을 지원하였습니다. 그 전 워드들은 비트맵 글꼴을 지원하였기 때문에, 가변 글꼴 크기, 즉 글자 크기를 키우면 계단이 나타났죠. &lt;span style="letter-spacing: 0px;"&gt;21세기 워드는 유려한 벡터 방식으로 글꼴이 렌더링되었습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="464" data-origin-height="158"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/uW8kK/dJMcafVTU4w/fGUqwaRteyGOkAUCtskumK/img.png" data-phocus="https://blog.kakaocdn.net/dn/uW8kK/dJMcafVTU4w/fGUqwaRteyGOkAUCtskumK/img.png" data-alt="비트맵 글꼴은 이런 느낌입니다. 커지면 글자를 이루는 점 하나의 크기도 커졌습니다."&gt;&lt;img src="https://blog.kakaocdn.net/dn/uW8kK/dJMcafVTU4w/fGUqwaRteyGOkAUCtskumK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuW8kK%2FdJMcafVTU4w%2FfGUqwaRteyGOkAUCtskumK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="464" height="158" data-origin-width="464" data-origin-height="158"&gt;&lt;/span&gt;&lt;figcaption&gt;비트맵 글꼴은 이런 느낌입니다. 커지면 글자를 이루는 점 하나의 크기도 커졌습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt;&lt;span style="letter-spacing: 0px;"&gt;기능은 뛰어났지만 기존 워드 시장을 제패한 아래아한글의 벽은 견고했고, 시장의 반응도 높았지만 상업적 성공을 이끌지 못하고, 결국 팀은 소스코드와 개발 기법을 담은 서적을 발표하고 윈도우 버전의 개발도 중단됩니다. 사실 한글과컴퓨터사의 저력도 만만치 않았습니다. 도스 기반의 21세기 워드의 품질은 조금씩 나아지고 있었는데, 92년 7월 27일 아래아한글 2.0이 출시됩니다. 아래아한글 2.0에서는 21세기 워드와 달리 그림도 추가할 수 있었고, 벡터 글꼴도 지원되었습니다. 1993년 9월 이스트소프트팀은 법인 이스트소프트를 설립한 후 21세기 워드는 27,500원의 가격에 출시합니다. 당시 아래아한글 전문가용이 20만원을 훌쩍 넘는 가격이었습니다. 21세기는 상대적으로 매우 저렴했지만, 홍보부족 및 아래아 한글에 밀려 실패하고 맙니다. 또한 당시 김장중님은 군 복무중이어서, 결국 다른 직원들은 모두 회사를 그만 둡니다.&lt;/span&gt;&lt;span style="letter-spacing: 0px;"&gt; &lt;/span&gt;&lt;span style="color: #333333; text-align: start;"&gt;21세기 워드의 개발진 중 한 명인 전준희님은 학교로 돌아가서 &lt;/span&gt;&lt;span style="letter-spacing: 0px;"&gt;2006년 구글 본사의 소프트웨어 엔지니어가 됩니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="643" data-origin-height="357"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/cIu4FQ/dJMcacEWipw/Hv4sUlAw9fhjwrOpZZobpK/img.png" data-phocus="https://blog.kakaocdn.net/dn/cIu4FQ/dJMcacEWipw/Hv4sUlAw9fhjwrOpZZobpK/img.png" data-alt="초기 개발자였던 김장중, 전준희, 최봉우, 박우진의 성씨로 이뤄진 각인이 보인다."&gt;&lt;img src="https://blog.kakaocdn.net/dn/cIu4FQ/dJMcacEWipw/Hv4sUlAw9fhjwrOpZZobpK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIu4FQ%2FdJMcacEWipw%2FHv4sUlAw9fhjwrOpZZobpK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="643" height="357" data-origin-width="643" data-origin-height="357"&gt;&lt;/span&gt;&lt;figcaption&gt;초기 개발자였던 김장중, 전준희, 최봉우, 박우진의 성씨로 이뤄진 각인이 보인다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt;하지만 김장중님은 홀로 남아서 이스트소프트를 꾸려 나갑니다. 군복무 이후에도 다양한 소프트웨어를 개발했습니다.  압축 프로그램인 알집, 보안 프로그램인 알약 등 알 시리즈를 만들었고, 인터넷 포털인 줌 인터넷, 카발 온라인 게임 등을 만들었습니다. 상업적 성공을 바탕으로 코스닥 상장 소프트웨어 업체를 일궈낸 김장중님. 21세기워드의 실패때문인지 그 이후부터는 기능보다는 마케팅에 더 신경을 많이 쓴다는 세간의 평가가 있었지만, 저는 소프트웨어 기업을 일궈낸 대표로써 충분히 그런 부분도 신경써야 한다고 생각합니다.&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="747" data-origin-height="314"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bEZevH/dJMcaid5q68/GlP2un4Knkx55JFUYrS5jK/img.png" data-phocus="https://blog.kakaocdn.net/dn/bEZevH/dJMcaid5q68/GlP2un4Knkx55JFUYrS5jK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bEZevH/dJMcaid5q68/GlP2un4Knkx55JFUYrS5jK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEZevH%2FdJMcaid5q68%2FGlP2un4Knkx55JFUYrS5jK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="747" height="314" data-origin-width="747" data-origin-height="314"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable="false" data-ke-type="horizontalRule" data-ke-style="style2"&gt;
&lt;blockquote data-path-to-node="10" data-ke-style="style3"&gt;컴퓨터를 학교나 학원에서 배운 적이 없어요. 컴퓨터를 잘하는 사람에게 어떤 것을 먼저 공부해야 하는지 물어 보고 책을 사서 혼자 공부했죠. 생각보다 어렵지 않더라고요. 지금 생각해 보면 너무나도 절실하게 배우고 싶어서 힘든 줄 모르고 공부한 것 같아요.&lt;/blockquote&gt;
&lt;blockquote data-path-to-node="10" data-ke-style="style3"&gt;“우리는 오랜 역사와 다양한 서비스 경험을 바탕으로 모두에게 게임의 즐거움을 제공하기 위해 노력해왔다. 이를 위해 탄탄한 개발력과 서비스 노하우를 기반으로 지속적으로 발전해 나가고 있다.”&lt;/blockquote&gt;
&lt;blockquote data-path-to-node="10" data-ke-style="style3"&gt;“페이스북이 아이디어가 좋아서 성공했다고 진단하는 이들이 있는데 저는 그런 견해에 동의하지 않는다. 페이스북은 시스템이 아주 탄탄하다. 기술적인 부분에서 상당히 뛰어나다는 것이다. 이런 것 없이 여러 사람들이 뛰어드니 나도 뛰어든다고 하면 성공할 확률은 상당히 낮다.”&lt;/blockquote&gt;
&lt;blockquote data-path-to-node="10" data-ke-style="style3"&gt;“93년 창업 당시에는 마이크로소프트의 성공 사례가 많이 거론됐다. 마이크로소프트의 DOS를 보니 저도 충분히 만들 수 있을 것 같았다. 소프트웨어에 대한 인지도도 높지 않았던 시기였지만 앞으로 소프트웨어의 시대가 올 것 같았다. 당시에 마이크로소프트는 작은 회사였다. 오히려 IBM과 애플간에 개방이냐 패쇄냐의 경쟁이 있었다.”&lt;/blockquote&gt;
&lt;blockquote data-path-to-node="10" data-ke-style="style3"&gt;“(인재상으로 의리를 강조하는 배경) 사업을 해보니 성공하려면 혼자 잘나서 되는 게 하나도 없다는 걸 알았다. 지금의 내가 있기까지 선배 혹은 회사의 시스템 지원이 있어야 했다. 어느 분야에서 밥값을 하려면 한 3년은 누군가 돌봐줘야 한다. 회사는 육성을 하는 것이다. 회사 혹은 선배들이 시간을 쏟아 뭔가를 지원해야 나라는 존재가 탄생한다는 걸 알아야 한다. 전 그것을 의리라고 본다. 신입 사원들이 들어와서 생활하는 것을 보면 얼마 있다가 도망갈 것 같은 사람이 보인다. 이는 태도에서도 묻어난다.” (2011/05/16, 블로터 인터뷰에서)&lt;/blockquote&gt;
&lt;p data-path-to-node="10" data-ke-size="size16"&gt; &lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=6V8RA9vhNnY" data-video-thumbnail="https://scrap.kakaocdn.net/dn/cZi2Ix/hyZQq4ZbZ7/VKZGzJS2fiYNvjHx1uibt1/img.jpg?width=480&amp;amp;height=360&amp;amp;face=150_100_219_175,https://scrap.kakaocdn.net/dn/baYqaI/hyZPSHSatZ/aVM9BrW8npHFE8eUCrFEP0/img.jpg?width=480&amp;amp;height=360&amp;amp;face=150_100_219_175,https://scrap.kakaocdn.net/dn/eb1MAa/hyZQDiWJr1/K1GEuVdCmeJ2gXhD7sYCTK/img.jpg?width=480&amp;amp;height=360&amp;amp;face=150_100_219_175" data-video-width="480" data-video-height="360" data-video-origin-width="480" data-video-origin-height="360" data-ke-mobilestyle="widthContent" data-video-title="[2017 기업가정신 롤모델] 이스트소프트 김장중 창업주" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/6V8RA9vhNnY" width="480" height="360" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-path-to-node="10" data-ke-size="size16"&gt; &lt;/p&gt;
&lt;figure id="og_1766838900033" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="21세기 워드프로세서 소스공개(S/W포함) | 김장중 - 교보문고" data-og-description="21세기 워드프로세서 소스공개(S/W포함) |" data-og-host="product.kyobobook.co.kr" data-og-source-url="https://product.kyobobook.co.kr/detail/S000000550884" data-og-url="https://product.kyobobook.co.kr/detail/S000000550884" data-og-image="https://scrap.kakaocdn.net/dn/uORw3/hyZQrv2MSz/HFvWCWjnl0tyMP7YdIWXMK/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/6HzRw/hyZQuTRIfF/qC8qQSqYEy0vO7QYIdBxXK/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/dPmZmu/hyZQrCODYI/0WpixlPBd5qcNAW3sHv0G1/img.jpg?width=1628&amp;amp;height=280&amp;amp;face=1226_71_1304_157"&gt;&lt;a href="https://product.kyobobook.co.kr/detail/S000000550884" target="_blank" rel="noopener" data-source-url="https://product.kyobobook.co.kr/detail/S000000550884"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/uORw3/hyZQrv2MSz/HFvWCWjnl0tyMP7YdIWXMK/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/6HzRw/hyZQuTRIfF/qC8qQSqYEy0vO7QYIdBxXK/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/dPmZmu/hyZQrCODYI/0WpixlPBd5qcNAW3sHv0G1/img.jpg?width=1628&amp;amp;height=280&amp;amp;face=1226_71_1304_157');"&gt; &lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;21세기 워드프로세서 소스공개(S/W포함) | 김장중 - 교보문고&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;21세기 워드프로세서 소스공개(S/W포함) |&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://neozest2.tistory.com/entry/JangJoongKimESTSoft</id>
    <link href="https://neozest2.tistory.com/entry/JangJoongKimESTSoft"/>
    <summary type="html">&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1024" data-origin-height="490"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/WyT3n/dJMcaaKGjzt/kBW0wd7kKde20V6z3wOKuK/img.png" data-phocus="https://blog.kakaocdn.net/dn/WyT3n/dJMcaaKGjzt/kBW0wd7kKde20V6z3wOKuK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/WyT3n/dJMcaaKGjzt/kBW0wd7kKde20V6z3wOKuK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWyT3n%2FdJMcaaKGjzt%2FkBW0wd7kKde20V6z3wOKuK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1024" height="490" data-origin-width="1024" data-origin-height="490"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt;1990년대 초, 대한민국 문서 작성 소프트웨어 시장은 한글과컴퓨터사의 '아래아한글'이 독점하고 있었습니다. 그 거대한 장벽 앞에 당당히 도전장을 내민 대학생들이 있었습니다.1992년 '21세기'라는 독자적인 워드프로세서를 개발하며 혜성처럼 등장한 이들은 비록 시장 점유율에서 한글을 넘어서지는 못했지만, 이 도전을 통해 '이스트소프트'가 탄생하게 됩니다. 한양대생들로 이루어진 개발팀의 리더였던 사람은 이스트소프트를 이끌고 있는 김장중 님입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="764" data-origin-height="1020"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/GdCld/dJMcafuIKri/B7Qk3ROieNTMivXke4P0Ok/img.png" data-phocus="https://blog.kakaocdn.net/dn/GdCld/dJMcafuIKri/B7Qk3ROieNTMivXke4P0Ok/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/GdCld/dJMcafuIKri/B7Qk3ROieNTMivXke4P0Ok/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGdCld%2FdJMcafuIKri%2FB7Qk3ROieNTMivXke4P0Ok%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="764" height="1020" data-origin-width="764" data-origin-height="1020"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt;92년에 발표된 21세기 워드프로세스는 비록 도스 환경에서 동작하는 프로그램이었지만, 아래아한글보다 먼저 벡터 글꼴을 지원하였습니다. 그 전 워드들은 비트맵 글꼴을 지원하였기 때문에, 가변 글꼴 크기, 즉 글자 크기를 키우면 계단이 나타났죠. &lt;span style="letter-spacing: 0px;"&gt;21세기 워드는 유려한 벡터 방식으로 글꼴이 렌더링되었습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="464" data-origin-height="158"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/uW8kK/dJMcafVTU4w/fGUqwaRteyGOkAUCtskumK/img.png" data-phocus="https://blog.kakaocdn.net/dn/uW8kK/dJMcafVTU4w/fGUqwaRteyGOkAUCtskumK/img.png" data-alt="비트맵 글꼴은 이런 느낌입니다. 커지면 글자를 이루는 점 하나의 크기도 커졌습니다."&gt;&lt;img src="https://blog.kakaocdn.net/dn/uW8kK/dJMcafVTU4w/fGUqwaRteyGOkAUCtskumK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuW8kK%2FdJMcafVTU4w%2FfGUqwaRteyGOkAUCtskumK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="464" height="158" data-origin-width="464" data-origin-height="158"/&gt;&lt;/span&gt;&lt;figcaption&gt;비트맵 글꼴은 이런 느낌입니다. 커지면 글자를 이루는 점 하나의 크기도 커졌습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt;&lt;span style="letter-spacing: 0px;"&gt;기능은 뛰어났지만 기존 워드 시장을 제패한 아래아한글의 벽은 견고했고, 시장의 반응도 높았지만 상업적 성공을 이끌지 못하고, 결국 팀은 소스코드와 개발 기법을 담은 서적을 발표하고 윈도우 버전의 개발도 중단됩니다. 사실 한글과컴퓨터사의 저력도 만만치 않았습니다. 도스 기반의 21세기 워드의 품질은 조금씩 나아지고 있었는데, 92년 7월 27일 아래아한글 2.0이 출시됩니다. 아래아한글 2.0에서는 21세기 워드와 달리 그림도 추가할 수 있었고, 벡터 글꼴도 지원되었습니다. 1993년 9월 이스트소프트팀은 법인 이스트소프트를 설립한 후 21세기 워드는 27,500원의 가격에 출시합니다. 당시 아래아한글 전문가용이 20만원을 훌쩍 넘는 가격이었습니다. 21세기는 상대적으로 매우 저렴했지만, 홍보부족 및 아래아 한글에 밀려 실패하고 맙니다. 또한 당시 김장중님은 군 복무중이어서, 결국 다른 직원들은 모두 회사를 그만 둡니다.&lt;/span&gt;&lt;span style="letter-spacing: 0px;"&gt;&amp;nbsp;&lt;/span&gt;&lt;span style="color: #333333; text-align: start;"&gt;21세기 워드의 개발진 중 한 명인 전준희님은 학교로 돌아가서&amp;nbsp;&lt;/span&gt;&lt;span style="letter-spacing: 0px;"&gt;2006년 구글 본사의 소프트웨어 엔지니어가 됩니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="643" data-origin-height="357"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/cIu4FQ/dJMcacEWipw/Hv4sUlAw9fhjwrOpZZobpK/img.png" data-phocus="https://blog.kakaocdn.net/dn/cIu4FQ/dJMcacEWipw/Hv4sUlAw9fhjwrOpZZobpK/img.png" data-alt="초기 개발자였던 김장중, 전준희, 최봉우, 박우진의 성씨로 이뤄진 각인이 보인다."&gt;&lt;img src="https://blog.kakaocdn.net/dn/cIu4FQ/dJMcacEWipw/Hv4sUlAw9fhjwrOpZZobpK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIu4FQ%2FdJMcacEWipw%2FHv4sUlAw9fhjwrOpZZobpK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="643" height="357" data-origin-width="643" data-origin-height="357"/&gt;&lt;/span&gt;&lt;figcaption&gt;초기 개발자였던 김장중, 전준희, 최봉우, 박우진의 성씨로 이뤄진 각인이 보인다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt;하지만 김장중님은 홀로 남아서 이스트소프트를 꾸려 나갑니다. 군복무 이후에도 다양한 소프트웨어를 개발했습니다.&amp;nbsp; 압축 프로그램인 알집, 보안 프로그램인 알약 등 알 시리즈를 만들었고, 인터넷 포털인 줌 인터넷, 카발 온라인 게임 등을 만들었습니다. 상업적 성공을 바탕으로 코스닥 상장 소프트웨어 업체를 일궈낸 김장중님. 21세기워드의 실패때문인지 그 이후부터는 기능보다는 마케팅에 더 신경을 많이 쓴다는 세간의 평가가 있었지만, 저는 소프트웨어 기업을 일궈낸 대표로써 충분히 그런 부분도 신경써야 한다고 생각합니다.&lt;/p&gt;
&lt;p data-path-to-node="6" data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="747" data-origin-height="314"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bEZevH/dJMcaid5q68/GlP2un4Knkx55JFUYrS5jK/img.png" data-phocus="https://blog.kakaocdn.net/dn/bEZevH/dJMcaid5q68/GlP2un4Knkx55JFUYrS5jK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bEZevH/dJMcaid5q68/GlP2un4Knkx55JFUYrS5jK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEZevH%2FdJMcaid5q68%2FGlP2un4Knkx55JFUYrS5jK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="747" height="314" data-origin-width="747" data-origin-height="314"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable="false" data-ke-type="horizontalRule" data-ke-style="style2" /&gt;
&lt;blockquote data-path-to-node="10" data-ke-style="style3"&gt;컴퓨터를 학교나 학원에서 배운 적이 없어요. 컴퓨터를 잘하는 사람에게 어떤 것을 먼저 공부해야 하는지 물어 보고 책을 사서 혼자 공부했죠. 생각보다 어렵지 않더라고요. 지금 생각해 보면 너무나도 절실하게 배우고 싶어서 힘든 줄 모르고 공부한 것 같아요.&lt;/blockquote&gt;
&lt;blockquote data-path-to-node="10" data-ke-style="style3"&gt;&amp;ldquo;우리는 오랜 역사와 다양한 서비스 경험을 바탕으로 모두에게 게임의 즐거움을 제공하기 위해 노력해왔다. 이를 위해 탄탄한 개발력과 서비스 노하우를 기반으로 지속적으로 발전해 나가고 있다.&amp;rdquo;&lt;/blockquote&gt;
&lt;blockquote data-path-to-node="10" data-ke-style="style3"&gt;&amp;ldquo;페이스북이 아이디어가 좋아서 성공했다고 진단하는 이들이 있는데 저는 그런 견해에 동의하지 않는다. 페이스북은 시스템이 아주 탄탄하다. 기술적인 부분에서 상당히 뛰어나다는 것이다. 이런 것 없이 여러 사람들이 뛰어드니 나도 뛰어든다고 하면 성공할 확률은 상당히 낮다.&amp;rdquo;&lt;/blockquote&gt;
&lt;blockquote data-path-to-node="10" data-ke-style="style3"&gt;&amp;ldquo;93년 창업 당시에는 마이크로소프트의 성공 사례가 많이 거론됐다. 마이크로소프트의 DOS를 보니 저도 충분히 만들 수 있을 것 같았다. 소프트웨어에 대한 인지도도 높지 않았던 시기였지만 앞으로 소프트웨어의 시대가 올 것 같았다. 당시에 마이크로소프트는 작은 회사였다. 오히려 IBM과 애플간에 개방이냐 패쇄냐의 경쟁이 있었다.&amp;rdquo;&lt;/blockquote&gt;
&lt;blockquote data-path-to-node="10" data-ke-style="style3"&gt;&amp;ldquo;(인재상으로 의리를 강조하는 배경) 사업을 해보니 성공하려면 혼자 잘나서 되는 게 하나도 없다는 걸 알았다. 지금의 내가 있기까지 선배 혹은 회사의 시스템 지원이 있어야 했다. 어느 분야에서 밥값을 하려면 한 3년은 누군가 돌봐줘야 한다. 회사는 육성을 하는 것이다. 회사 혹은 선배들이 시간을 쏟아 뭔가를 지원해야 나라는 존재가 탄생한다는 걸 알아야 한다. 전 그것을 의리라고 본다. 신입 사원들이 들어와서 생활하는 것을 보면 얼마 있다가 도망갈 것 같은 사람이 보인다. 이는 태도에서도 묻어난다.&amp;rdquo; (2011/05/16, 블로터 인터뷰에서)&lt;/blockquote&gt;
&lt;p data-path-to-node="10" data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type="video" data-ke-style="alignCenter" data-video-host="youtube" data-video-url="https://www.youtube.com/watch?v=6V8RA9vhNnY" data-video-thumbnail="https://scrap.kakaocdn.net/dn/cZi2Ix/hyZQq4ZbZ7/VKZGzJS2fiYNvjHx1uibt1/img.jpg?width=480&amp;amp;height=360&amp;amp;face=150_100_219_175,https://scrap.kakaocdn.net/dn/baYqaI/hyZPSHSatZ/aVM9BrW8npHFE8eUCrFEP0/img.jpg?width=480&amp;amp;height=360&amp;amp;face=150_100_219_175,https://scrap.kakaocdn.net/dn/eb1MAa/hyZQDiWJr1/K1GEuVdCmeJ2gXhD7sYCTK/img.jpg?width=480&amp;amp;height=360&amp;amp;face=150_100_219_175" data-video-width="480" data-video-height="360" data-video-origin-width="480" data-video-origin-height="360" data-ke-mobilestyle="widthContent" data-video-title="[2017 기업가정신 롤모델] 이스트소프트 김장중 창업주" data-original-url=""&gt;&lt;iframe src="https://www.youtube.com/embed/6V8RA9vhNnY" width="480" height="360" frameborder="" allowfullscreen="true"&gt;&lt;/iframe&gt;
&lt;figcaption style="display: none;"&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node="10" data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id="og_1766838900033" contenteditable="false" data-ke-type="opengraph" data-ke-align="alignCenter" data-og-type="website" data-og-title="21세기 워드프로세서 소스공개(S/W포함) | 김장중 - 교보문고" data-og-description="21세기 워드프로세서 소스공개(S/W포함) |" data-og-host="product.kyobobook.co.kr" data-og-source-url="https://product.kyobobook.co.kr/detail/S000000550884" data-og-url="https://product.kyobobook.co.kr/detail/S000000550884" data-og-image="https://scrap.kakaocdn.net/dn/uORw3/hyZQrv2MSz/HFvWCWjnl0tyMP7YdIWXMK/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/6HzRw/hyZQuTRIfF/qC8qQSqYEy0vO7QYIdBxXK/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/dPmZmu/hyZQrCODYI/0WpixlPBd5qcNAW3sHv0G1/img.jpg?width=1628&amp;amp;height=280&amp;amp;face=1226_71_1304_157"&gt;&lt;a href="https://product.kyobobook.co.kr/detail/S000000550884" target="_blank" rel="noopener" data-source-url="https://product.kyobobook.co.kr/detail/S000000550884"&gt;
&lt;div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/uORw3/hyZQrv2MSz/HFvWCWjnl0tyMP7YdIWXMK/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/6HzRw/hyZQuTRIfF/qC8qQSqYEy0vO7QYIdBxXK/img.png?width=458&amp;amp;height=664&amp;amp;face=0_0_458_664,https://scrap.kakaocdn.net/dn/dPmZmu/hyZQrCODYI/0WpixlPBd5qcNAW3sHv0G1/img.jpg?width=1628&amp;amp;height=280&amp;amp;face=1226_71_1304_157');"&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class="og-text"&gt;
&lt;p class="og-title" data-ke-size="size16"&gt;21세기 워드프로세서 소스공개(S/W포함) | 김장중 - 교보문고&lt;/p&gt;
&lt;p class="og-desc" data-ke-size="size16"&gt;21세기 워드프로세서 소스공개(S/W포함) |&lt;/p&gt;
&lt;p class="og-host" data-ke-size="size16"&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;</summary>
    <title>워드부터 알약까지: 한 개발자의 고집이 만든 국민 소프트웨어</title>
    <updated>2026-10-04T20:55:37+09:00</updated>
    <dc:date>2026-10-04T20:55:37+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>망나니개발자</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="color: #f15f5f;"&gt;1.&lt;span&gt;&lt;span&gt; String.format의 성능 문제와 개선 사항들 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable="false" data-ke-type="horizontalRule" data-ke-style="style5"&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ String.format 이란? ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;Java 기본 라이브러리에서 제공하는 String 클래스에는 format이라는 메서드가 있다. String.format()은 지정한 포맷 문자열과 인자를 통해 서식을 적용한 문자열을 반환한다. 예를 들어 다음과 같은 코드가 있다고 하자.&lt;/p&gt;
&lt;pre class="arduino"&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) {
        String formatted = String.format("Hello and welcome %d!", 2026);
        System.out.println(formatted);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;위의 코드를 실행하면 다음과 같은 결과가 출력된다.&lt;/p&gt;
&lt;pre class="angelscript"&gt;&lt;code&gt;Hello and welcome 2026!
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;포맷 문자열의 %d 자리에 두 번째 인자인 2026이 들어갔다. 이처럼 % 로 시작하는 부분을 서식 지정자라고 부르고, String.format() 은 서식 지정자를 차례대로 인자 값으로 바꾼 문자열을 만든다. 서식 지정자가 여러 개라면, 인자도 그 순서대로 넘기면 된다.&lt;/p&gt;
&lt;pre class="arduino"&gt;&lt;code&gt;String formatted = String.format("이름: %s, 나이: %d, 키: %.1f", "홍길동", 30, 175.55);
// 이름: 홍길동, 나이: 30, 키: 175.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ JDK 16 이하에서 String.format의 내부 구현 ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;JDK 16 이하에서는 String.format()이 다음과 같은 순서로 동작한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;먼저 &lt;a href="https://github.com/openjdk/jdk11u/blob/jdk-11.0.28-ga/src/java.base/share/classes/java/lang/String.java#L2896-L2898"&gt;Formatter 객체&lt;/a&gt;를 새롭게 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="926" data-origin-height="138"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/cqwaIi/dJMcahsJl8T/lZbHxw0Mr61Ayp2lb2SSaK/img.png" data-phocus="https://blog.kakaocdn.net/dn/cqwaIi/dJMcahsJl8T/lZbHxw0Mr61Ayp2lb2SSaK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/cqwaIi/dJMcahsJl8T/lZbHxw0Mr61Ayp2lb2SSaK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqwaIi%2FdJMcahsJl8T%2FlZbHxw0Mr61Ayp2lb2SSaK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="926" height="138" data-origin-width="926" data-origin-height="138"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그리고 Formatter는 결과를 담을 &lt;a href="https://github.com/openjdk/jdk11u/blob/jdk-11.0.28-ga/src/java.base/share/classes/java/util/Formatter.java#L1977-L1979"&gt;StringBuilder&lt;/a&gt;를 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1078" data-origin-height="140"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/oG1qp/dJMcag8dKrv/E7XmwwvBkcHYellAMpI8Uk/img.png" data-phocus="https://blog.kakaocdn.net/dn/oG1qp/dJMcag8dKrv/E7XmwwvBkcHYellAMpI8Uk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/oG1qp/dJMcag8dKrv/E7XmwwvBkcHYellAMpI8Uk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoG1qp%2FdJMcag8dKrv%2FE7XmwwvBkcHYellAMpI8Uk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1078" height="140" data-origin-width="1078" data-origin-height="140"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;또한 this를 타고 활용되는 내부 생성자에서는 숫자 출력에 쓸 0 문자를 &lt;a href="https://github.com/openjdk/jdk11u/blob/jdk-11.0.28-ga/src/java.base/share/classes/java/util/Formatter.java#L2435-L2442"&gt;getZero(l)&lt;/a&gt;로 미리 구해두기도 한다. 0 문자를 따로 구하는 이유는 0 하나만 알면 나머지 숫자 9개를 계산으로 만들 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그리고 내부에서는 기본 Locale이 Locale.US가 아니면 호출마다 DecimalFormatSymbols를 새로 만들기도 한다. 해당 객체는 숫자를 그 나라 방식의 숫자 문자로 출력하기 위해 사용된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="686" data-origin-height="272"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bK6mnV/dJMcahlX1Av/ENPKW1YgoJeQ28lk73GpXK/img.png" data-phocus="https://blog.kakaocdn.net/dn/bK6mnV/dJMcahlX1Av/ENPKW1YgoJeQ28lk73GpXK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bK6mnV/dJMcahlX1Av/ENPKW1YgoJeQ28lk73GpXK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK6mnV%2FdJMcahlX1Av%2FENPKW1YgoJeQ28lk73GpXK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="686" height="272" data-origin-width="686" data-origin-height="272"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;필요한 객체들이 생성되면, 포맷 문자열을 정규식으로 쪼갠다. Formatter.parse는 다음 정규식으로 서식 지정자를 찾는데, 이 정규식은 static 필드에 미리 컴파일해 둔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1086" data-origin-height="244"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/dtZfV6/dJMcackurfg/4x17NMkaICzG2WioH2sV5K/img.png" data-phocus="https://blog.kakaocdn.net/dn/dtZfV6/dJMcackurfg/4x17NMkaICzG2WioH2sV5K/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/dtZfV6/dJMcackurfg/4x17NMkaICzG2WioH2sV5K/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtZfV6%2FdJMcackurfg%2F4x17NMkaICzG2WioH2sV5K%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1086" height="244" data-origin-width="1086" data-origin-height="244"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;Pattern은 재사용하지만 fsPattern.matcher(s)로 만드는 Matcher는 호출마다 생성하며, 그 안에 그룹 위치를 담을 int[]도 함께 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1222" data-origin-height="576"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/nKjdR/dJMcackurfp/KVkLF1MNmT0wqDIi6Id951/img.png" data-phocus="https://blog.kakaocdn.net/dn/nKjdR/dJMcackurfp/KVkLF1MNmT0wqDIi6Id951/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/nKjdR/dJMcackurfp/KVkLF1MNmT0wqDIi6Id951/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnKjdR%2FdJMcackurfp%2FKVkLF1MNmT0wqDIi6Id951%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1222" height="576" data-origin-width="1222" data-origin-height="576"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그 뒤 m.find(i)로 서식 지정자를 하나씩 찾으면서 포맷 문자열을 두 종류의 조각으로 나눈다. 예를 들어 "name: %s, age: %d"는 "name: ", %s, ", age: ", %d 조각 4개가 담긴 ArrayList가 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1200" data-origin-height="580"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bpe0T6/dJMcaaNSTPS/OXgVt12VNYJavifNqrWM20/img.png" data-phocus="https://blog.kakaocdn.net/dn/bpe0T6/dJMcaaNSTPS/OXgVt12VNYJavifNqrWM20/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bpe0T6/dJMcaaNSTPS/OXgVt12VNYJavifNqrWM20/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpe0T6%2FdJMcaaNSTPS%2FOXgVt12VNYJavifNqrWM20%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1200" height="580" data-origin-width="1200" data-origin-height="580"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;만약 서식 지정자가 2개인 포맷 문자열이라면 정규식 탐색이 세 번 일어난다. 서식 지정자를 찾는 두 번과, 더 없다는 걸 확인하는 마지막 한 번이다. 참고로 이 정규식 매칭이 String.format 성능을 악화시키는 주범이다. 실제로 JDK 11 에서 String.format 을 반복 호출하며 CPU 를 프로파일링해 보면, 전체 시간의 절반가량(약 46~51%)이 정규식 매칭에 쓰인다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ JDK 17부터 개선된 String.format의 내부 구현 ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;String.format()의 성능에 대한 문제 제기가 여러 차례 지속되었고, JDK 17에서는 이를 &lt;a href="https://github.com/openjdk/jdk17u/blob/jdk-17.0.16-ga/src/java.base/share/classes/java/util/Formatter.java#L2714-L2755"&gt;개선&lt;/a&gt;하였다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;먼저 포맷 문자열을 정규식으로 쪼개던 부분에 정규식을 건너뛰는 빠른 경로를 추가했다. % 바로 뒤가 변환 문자 하나인 경우(%s, %d처럼 플래그, 너비, 정밀도가 없는 경우)에는 문자를 하나씩 읽어서 바로 조각을 만든다. 하지만 %.2f, %05d, %2$s처럼 옵션이 붙으면 여전히 Matcher를 만들어 정규식으로 해석한다. 그리고 최종적으로 JDK 23부터 정규식을 완전히 없앴다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1200" data-origin-height="580"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/boLzdU/dJMcablI2gk/kSG4KTSTI2K7xqNbd80Bpk/img.png" data-phocus="https://blog.kakaocdn.net/dn/boLzdU/dJMcablI2gk/kSG4KTSTI2K7xqNbd80Bpk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/boLzdU/dJMcablI2gk/kSG4KTSTI2K7xqNbd80Bpk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboLzdU%2FdJMcablI2gk%2FkSG4KTSTI2K7xqNbd80Bpk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1200" height="580" data-origin-width="1200" data-origin-height="580"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;또한 Formatter 생성자에서 하던 DecimalFormatSymbols 조회도 빠졌다. 지금은 숫자를 출력할 때 즉, 필요 시에만 조회한다. 다만 String.format 은 호출마다 Formatter 를 새로 만들기 때문에, 포맷 문자열에 숫자 서식이 있으면 여전히 호출마다 한 번은 조회한다. 그리고 리스트를 만들어 처리하는 구조는 그대로라, 이를 위한 객체 할당은 필요하다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ 가급적이면 덧셈 연산을 사용하기 ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;String.format 은 JDK 버전이 올라가면서 빨라졌지만, 호출할 때마다 포맷 문자열을 해석하고 조각 객체를 만드는 구조는 그대로다. 반면 + 를 사용한 문자열 덧셈은 JDK 9부터 이런 비용이 거의 없도록 바뀌었다. 예를 들어 다음과 같은 코드가 있다고 하자.&lt;/p&gt;
&lt;pre class="arduino"&gt;&lt;code&gt;static String concat(String name, int age) {
    return "name: " + name + ", age: " + age;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;JDK 8까지는 컴파일러가 이 코드를 다음과 같이 StringBuilder 를 사용하는 코드로 바꿔서 컴파일했다.&lt;/p&gt;
&lt;pre class="less"&gt;&lt;code&gt;new StringBuilder()
    .append("name: ")
    .append(name)
    .append(", age: ")
    .append(age)
    .toString();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 방식은 문자열을 이어 붙이는 방법이 컴파일하는 순간 .class 파일에 고정된다. 그래서 이후에 JDK가 더 빠른 방법을 만들더라도, 다시 컴파일하기 전까지는 기존 방식을 그대로 사용한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;JDK 9부터는 컴파일러가 StringBuilder 코드를 만들지 않는다. 대신 "String 하나와 int 하나를 받아 문자열을 만들어 달라" 는 요청만 바이트코드에 남기고, 실제로 이어 붙이는 방법은 실행 중인 JDK 의 StringConcatFactory 가 정하게 한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 요청에는 invokedynamic 이라는 바이트코드 명령을 사용한다. 일반적인 메서드 호출은 어떤 메서드를 부를지가 컴파일 시점에 정해지지만, invokedynamic 은 처음 실행될 때 실행할 코드를 정한다. 처음 실행될 때 JVM 이 StringConcatFactory.makeConcatWithConstants 를 한 번 호출해서 이 자리에서 실행할 코드를 받아 오고, 이후에는 받아 온 코드를 계속 사용한다.&lt;/p&gt;
&lt;pre class="awk"&gt;&lt;code&gt;invokedynamic makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
// 레시피: "name: \u0001, age: \u0001"
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이때 컴파일러는 결과 문자열의 틀을 레시피라는 문자열로 함께 넘긴다. 레시피 "name: \u0001, age: \u0001" 에서 name: 과 , age: 는 그대로 복사할 상수이고, \u0001 은 인자가 들어갈 빈칸이다. 그리고 바이트코드의 (Ljava/lang/String;I) 는 첫 번째 빈칸에 String 이, 두 번째 빈칸에 int 가 들어온다는 뜻이다. int 가 들어온다는 것을 미리 알기 때문에, Integer 로 박싱하지 않고 숫자를 바로 글자로 바꿀 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1200" data-origin-height="680"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/dvNdMz/dJMcadqbAYc/mbmYw8eDllVO5CaMackeH0/img.png" data-phocus="https://blog.kakaocdn.net/dn/dvNdMz/dJMcadqbAYc/mbmYw8eDllVO5CaMackeH0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/dvNdMz/dJMcadqbAYc/mbmYw8eDllVO5CaMackeH0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvNdMz%2FdJMcadqbAYc%2FmbmYw8eDllVO5CaMackeH0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1200" height="680" data-origin-width="1200" data-origin-height="680"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;StringConcatFactory 는 처음 실행될 때 이 틀과 타입을 보고, 이 모양의 문자열만 만드는 전용 코드를 한 번 만들어 둔다. 이후 name 이 "user", age 가 30 으로 호출되면 다음 순서로 문자열을 만든다.&lt;/p&gt;
&lt;ol style="list-style-type: decimal;" data-ke-list-type="decimal"&gt;
&lt;li&gt;상수 6바이트, "user" 4바이트, 상수 7바이트, 30 2바이트를 더해 결과 최종 길이 19바이트를 계산함&lt;/li&gt;
&lt;li&gt;19바이트짜리 byte[] 를 하나 할당함&lt;/li&gt;
&lt;li&gt;상수와 인자 값을 순서대로 복사해 채움&lt;/li&gt;
&lt;li&gt;채운 byte[] 로 "name: user, age: 30" 문자열을 만듦&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;결국 String.format 의 "name: %s, age: %d" 와 문자열 덧셈의 레시피 "name: \u0001, age: \u0001" 는 모두 결과 문자열의 모양을 적은 것이다. 차이는 해석하는 횟수다. String.format 은 호출할 때마다 포맷 문자열을 해석하지만, 문자열 덧셈은 처음 실행될 때 레시피를 한 번만 해석한다. 그래서 문자열 덧셈은 호출마다 조각 객체를 만들지 않는다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ 덧셈 연산의 성능 측정하기 ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;이 차이가 실제로 얼마나 되는지 확인하기 위해 같은 결과를 만드는 String.format("name: %s, age: %d", name, age) 와 문자열 덧셈을 비교해 보았다. Apple M4 Max, Corretto 11.0.28과 21.0.8, G1 GC 힙 2GB 환경에서 5초 워밍업 후 10초 동안 측정했다.&lt;/p&gt;
&lt;table style="border-collapse: collapse; width: 100%;" border="1" data-ke-align="alignLeft" data-ke-style="style4"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style="color: #333333; text-align: start;"&gt;JDK&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style="color: #333333; text-align: start;"&gt;방식&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;1건당 시간 (1개 스레드)&lt;/td&gt;
&lt;td&gt;1건당 할당&lt;/td&gt;
&lt;td&gt;초당 처리량 (12개 스레드)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;String.format&lt;/td&gt;
&lt;td&gt;약 331ns&lt;/td&gt;
&lt;td&gt;약 890B&lt;/td&gt;
&lt;td&gt;약 2,620만 건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;문자열 덧셈&lt;/td&gt;
&lt;td&gt;약 13ns&lt;/td&gt;
&lt;td&gt;약 48B&lt;/td&gt;
&lt;td&gt;약 6억 950만 건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;String.format&lt;/td&gt;
&lt;td&gt;약 87ns&lt;/td&gt;
&lt;td&gt;약 500B&lt;/td&gt;
&lt;td&gt;약 7,170만 건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;문자열 덧셈&lt;/td&gt;
&lt;td&gt;약 13ns&lt;/td&gt;
&lt;td&gt;약 48B&lt;/td&gt;
&lt;td&gt;약 5억 640만 건&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;JDK 21의 String.format 은 JDK 11보다 약 3.8배 빨라졌지만, 12개 스레드 기준으로 여전히 문자열 덧셈보다 처리량이 약 7배 적다. 할당량도 1건당 약 10배 많아서, 호출이 많은 서버에서는 Young GC를 더 자주 일으킨다. 문자열 덧셈 쪽을 프로파일링해 보면 시간의 절반 이상(약 50~55%)이 int 를 숫자 문자로 바꾸는 Integer.getChars 에 쓰인다. 결과 문자열을 만드는 데 꼭 필요한 일 말고는 거의 하지 않는다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt;따라서 반복 호출이 많은 경로에서는 String.format 대신 문자열 덧셈을 사용하는 것이 좋다. 로그 메시지는 String.format 이나 덧셈으로 직접 만들지 말고 SLF4J 의 자리표시자(log.info("name={}, age={}", name, age))를 사용하면, 로그 레벨이 꺼져 있을 때 문자열을 아예 만들지 않는다. 참고로 Kotlin 의 문자열 템플릿("name: $name, age: $age")도 문자열 덧셈으로 컴파일되므로 String.format 의 비용이 없다. 반대로 %.2f 같은 소수점 자릿수 지정이나 Locale 별 숫자 표기가 필요하고, 호출 빈도가 낮다면 String.format 을 써도 성능 문제는 거의 없다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;p data-ke-size="size16"&gt; &lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://mangkyu.tistory.com/478</id>
    <link href="https://mangkyu.tistory.com/478"/>
    <summary type="html">&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size="size26"&gt;&lt;b&gt;&lt;span style="color: #f15f5f;"&gt;1.&lt;span&gt;&lt;span&gt; String.format의 성능 문제와 개선 사항들 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable="false" data-ke-type="horizontalRule" data-ke-style="style5" /&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ String.format 이란? ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;Java 기본 라이브러리에서 제공하는 String 클래스에는 format이라는 메서드가 있다. String.format()은 지정한 포맷 문자열과 인자를 통해 서식을 적용한 문자열을 반환한다. 예를 들어 다음과 같은 코드가 있다고 하자.&lt;/p&gt;
&lt;pre class="arduino"&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) {
        String formatted = String.format("Hello and welcome %d!", 2026);
        System.out.println(formatted);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;위의 코드를 실행하면 다음과 같은 결과가 출력된다.&lt;/p&gt;
&lt;pre class="angelscript"&gt;&lt;code&gt;Hello and welcome 2026!
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;포맷 문자열의&amp;nbsp;%d&amp;nbsp;자리에 두 번째 인자인 2026이 들어갔다. 이처럼&amp;nbsp;%&amp;nbsp;로 시작하는 부분을 서식 지정자라고 부르고, String.format() 은 서식 지정자를 차례대로 인자 값으로 바꾼 문자열을 만든다. 서식 지정자가 여러 개라면, 인자도 그 순서대로 넘기면 된다.&lt;/p&gt;
&lt;pre class="arduino"&gt;&lt;code&gt;String formatted = String.format("이름: %s, 나이: %d, 키: %.1f", "홍길동", 30, 175.55);
// 이름: 홍길동, 나이: 30, 키: 175.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ JDK 16 이하에서 String.format의 내부 구현 ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;JDK 16 이하에서는 String.format()이 다음과 같은 순서로 동작한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;먼저 &lt;a href="https://github.com/openjdk/jdk11u/blob/jdk-11.0.28-ga/src/java.base/share/classes/java/lang/String.java#L2896-L2898"&gt;Formatter 객체&lt;/a&gt;를 새롭게 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="926" data-origin-height="138"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/cqwaIi/dJMcahsJl8T/lZbHxw0Mr61Ayp2lb2SSaK/img.png" data-phocus="https://blog.kakaocdn.net/dn/cqwaIi/dJMcahsJl8T/lZbHxw0Mr61Ayp2lb2SSaK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/cqwaIi/dJMcahsJl8T/lZbHxw0Mr61Ayp2lb2SSaK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqwaIi%2FdJMcahsJl8T%2FlZbHxw0Mr61Ayp2lb2SSaK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="926" height="138" data-origin-width="926" data-origin-height="138"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그리고 Formatter는 결과를 담을 &lt;a href="https://github.com/openjdk/jdk11u/blob/jdk-11.0.28-ga/src/java.base/share/classes/java/util/Formatter.java#L1977-L1979"&gt;StringBuilder&lt;/a&gt;를 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1078" data-origin-height="140"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/oG1qp/dJMcag8dKrv/E7XmwwvBkcHYellAMpI8Uk/img.png" data-phocus="https://blog.kakaocdn.net/dn/oG1qp/dJMcag8dKrv/E7XmwwvBkcHYellAMpI8Uk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/oG1qp/dJMcag8dKrv/E7XmwwvBkcHYellAMpI8Uk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoG1qp%2FdJMcag8dKrv%2FE7XmwwvBkcHYellAMpI8Uk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1078" height="140" data-origin-width="1078" data-origin-height="140"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;또한 this를 타고 활용되는 내부 생성자에서는 숫자 출력에 쓸 0 문자를 &lt;a href="https://github.com/openjdk/jdk11u/blob/jdk-11.0.28-ga/src/java.base/share/classes/java/util/Formatter.java#L2435-L2442"&gt;getZero(l)&lt;/a&gt;로 미리 구해두기도 한다. 0 문자를 따로 구하는 이유는 0 하나만 알면 나머지 숫자 9개를 계산으로 만들 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그리고 내부에서는 기본 Locale이 Locale.US가 아니면 호출마다 DecimalFormatSymbols를 새로 만들기도 한다. 해당 객체는 숫자를 그 나라 방식의 숫자 문자로 출력하기 위해 사용된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="686" data-origin-height="272"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bK6mnV/dJMcahlX1Av/ENPKW1YgoJeQ28lk73GpXK/img.png" data-phocus="https://blog.kakaocdn.net/dn/bK6mnV/dJMcahlX1Av/ENPKW1YgoJeQ28lk73GpXK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bK6mnV/dJMcahlX1Av/ENPKW1YgoJeQ28lk73GpXK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK6mnV%2FdJMcahlX1Av%2FENPKW1YgoJeQ28lk73GpXK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="686" height="272" data-origin-width="686" data-origin-height="272"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;필요한 객체들이 생성되면, 포맷 문자열을 정규식으로 쪼갠다. Formatter.parse는 다음 정규식으로 서식 지정자를 찾는데, 이 정규식은 static 필드에 미리 컴파일해 둔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1086" data-origin-height="244"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/dtZfV6/dJMcackurfg/4x17NMkaICzG2WioH2sV5K/img.png" data-phocus="https://blog.kakaocdn.net/dn/dtZfV6/dJMcackurfg/4x17NMkaICzG2WioH2sV5K/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/dtZfV6/dJMcackurfg/4x17NMkaICzG2WioH2sV5K/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtZfV6%2FdJMcackurfg%2F4x17NMkaICzG2WioH2sV5K%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1086" height="244" data-origin-width="1086" data-origin-height="244"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;Pattern은 재사용하지만 fsPattern.matcher(s)로 만드는 Matcher는 호출마다 생성하며, 그 안에 그룹 위치를 담을 int[]도 함께 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1222" data-origin-height="576"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/nKjdR/dJMcackurfp/KVkLF1MNmT0wqDIi6Id951/img.png" data-phocus="https://blog.kakaocdn.net/dn/nKjdR/dJMcackurfp/KVkLF1MNmT0wqDIi6Id951/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/nKjdR/dJMcackurfp/KVkLF1MNmT0wqDIi6Id951/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnKjdR%2FdJMcackurfp%2FKVkLF1MNmT0wqDIi6Id951%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1222" height="576" data-origin-width="1222" data-origin-height="576"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;그 뒤 m.find(i)로 서식 지정자를 하나씩 찾으면서 포맷 문자열을 두 종류의 조각으로 나눈다. 예를 들어 "name: %s, age: %d"는 "name: ", %s, ", age: ", %d 조각 4개가 담긴 ArrayList가 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1200" data-origin-height="580"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bpe0T6/dJMcaaNSTPS/OXgVt12VNYJavifNqrWM20/img.png" data-phocus="https://blog.kakaocdn.net/dn/bpe0T6/dJMcaaNSTPS/OXgVt12VNYJavifNqrWM20/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bpe0T6/dJMcaaNSTPS/OXgVt12VNYJavifNqrWM20/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpe0T6%2FdJMcaaNSTPS%2FOXgVt12VNYJavifNqrWM20%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1200" height="580" data-origin-width="1200" data-origin-height="580"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;만약 서식 지정자가 2개인 포맷 문자열이라면 정규식 탐색이 세 번 일어난다. 서식 지정자를 찾는 두 번과, 더 없다는 걸 확인하는 마지막 한 번이다. 참고로 이 정규식 매칭이 String.format 성능을 악화시키는 주범이다. 실제로 JDK 11 에서 String.format 을 반복 호출하며 CPU 를 프로파일링해 보면, 전체 시간의 절반가량(약 46~51%)이 정규식 매칭에 쓰인다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ JDK 17부터 개선된 String.format의 내부 구현 ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;String.format()의 성능에 대한 문제 제기가 여러 차례 지속되었고, JDK 17에서는 이를 &lt;a href="https://github.com/openjdk/jdk17u/blob/jdk-17.0.16-ga/src/java.base/share/classes/java/util/Formatter.java#L2714-L2755"&gt;개선&lt;/a&gt;하였다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;먼저 포맷 문자열을 정규식으로 쪼개던 부분에 정규식을 건너뛰는 빠른 경로를 추가했다. % 바로 뒤가 변환 문자 하나인 경우(%s, %d처럼 플래그, 너비, 정밀도가 없는 경우)에는 문자를 하나씩 읽어서 바로 조각을 만든다. 하지만 %.2f, %05d, %2$s처럼 옵션이 붙으면 여전히 Matcher를 만들어 정규식으로 해석한다. 그리고 최종적으로 JDK 23부터 정규식을 완전히 없앴다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1200" data-origin-height="580"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/boLzdU/dJMcablI2gk/kSG4KTSTI2K7xqNbd80Bpk/img.png" data-phocus="https://blog.kakaocdn.net/dn/boLzdU/dJMcablI2gk/kSG4KTSTI2K7xqNbd80Bpk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/boLzdU/dJMcablI2gk/kSG4KTSTI2K7xqNbd80Bpk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboLzdU%2FdJMcablI2gk%2FkSG4KTSTI2K7xqNbd80Bpk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1200" height="580" data-origin-width="1200" data-origin-height="580"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;또한 Formatter 생성자에서 하던 DecimalFormatSymbols 조회도 빠졌다. 지금은 숫자를 출력할 때 즉, 필요 시에만 조회한다. 다만 String.format 은 호출마다 Formatter 를 새로 만들기 때문에, 포맷 문자열에 숫자 서식이 있으면 여전히 호출마다 한 번은 조회한다. 그리고 리스트를 만들어 처리하는 구조는 그대로라, 이를 위한 객체 할당은 필요하다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ 가급적이면 덧셈 연산을 사용하기 ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;String.format 은 JDK 버전이 올라가면서 빨라졌지만, 호출할 때마다 포맷 문자열을 해석하고 조각 객체를 만드는 구조는 그대로다. 반면 + 를 사용한 문자열 덧셈은 JDK 9부터 이런 비용이 거의 없도록 바뀌었다. 예를 들어 다음과 같은 코드가 있다고 하자.&lt;/p&gt;
&lt;pre class="arduino"&gt;&lt;code&gt;static String concat(String name, int age) {
    return "name: " + name + ", age: " + age;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;JDK 8까지는 컴파일러가 이 코드를 다음과 같이 StringBuilder 를 사용하는 코드로 바꿔서 컴파일했다.&lt;/p&gt;
&lt;pre class="less"&gt;&lt;code&gt;new StringBuilder()
    .append("name: ")
    .append(name)
    .append(", age: ")
    .append(age)
    .toString();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 방식은 문자열을 이어 붙이는 방법이 컴파일하는 순간 .class 파일에 고정된다. 그래서 이후에 JDK가 더 빠른 방법을 만들더라도, 다시 컴파일하기 전까지는 기존 방식을 그대로 사용한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;JDK 9부터는 컴파일러가 StringBuilder 코드를 만들지 않는다. 대신 "String 하나와 int 하나를 받아 문자열을 만들어 달라" 는 요청만 바이트코드에 남기고, 실제로 이어 붙이는 방법은 실행 중인 JDK 의 StringConcatFactory 가 정하게 한다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이 요청에는 invokedynamic 이라는 바이트코드 명령을 사용한다. 일반적인 메서드 호출은 어떤 메서드를 부를지가 컴파일 시점에 정해지지만, invokedynamic 은 처음 실행될 때 실행할 코드를 정한다. 처음 실행될 때 JVM 이 StringConcatFactory.makeConcatWithConstants 를 한 번 호출해서 이 자리에서 실행할 코드를 받아 오고, 이후에는 받아 온 코드를 계속 사용한다.&lt;/p&gt;
&lt;pre class="awk"&gt;&lt;code&gt;invokedynamic makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
// 레시피: "name: \u0001, age: \u0001"
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;이때 컴파일러는 결과 문자열의 틀을 레시피라는 문자열로 함께 넘긴다. 레시피 "name: \u0001, age: \u0001" 에서 name: 과 , age: 는 그대로 복사할 상수이고, \u0001 은 인자가 들어갈 빈칸이다. 그리고 바이트코드의 (Ljava/lang/String;I) 는 첫 번째 빈칸에 String 이, 두 번째 빈칸에 int 가 들어온다는 뜻이다. int 가 들어온다는 것을 미리 알기 때문에, Integer 로 박싱하지 않고 숫자를 바로 글자로 바꿀 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1200" data-origin-height="680"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/dvNdMz/dJMcadqbAYc/mbmYw8eDllVO5CaMackeH0/img.png" data-phocus="https://blog.kakaocdn.net/dn/dvNdMz/dJMcadqbAYc/mbmYw8eDllVO5CaMackeH0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/dvNdMz/dJMcadqbAYc/mbmYw8eDllVO5CaMackeH0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvNdMz%2FdJMcadqbAYc%2FmbmYw8eDllVO5CaMackeH0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1200" height="680" data-origin-width="1200" data-origin-height="680"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;StringConcatFactory 는 처음 실행될 때 이 틀과 타입을 보고, 이 모양의 문자열만 만드는 전용 코드를 한 번 만들어 둔다. 이후 name 이 "user", age 가 30 으로 호출되면 다음 순서로 문자열을 만든다.&lt;/p&gt;
&lt;ol style="list-style-type: decimal;" data-ke-list-type="decimal"&gt;
&lt;li&gt;상수 6바이트, "user" 4바이트, 상수 7바이트, 30 2바이트를 더해 결과 최종 길이 19바이트를 계산함&lt;/li&gt;
&lt;li&gt;19바이트짜리 byte[] 를 하나 할당함&lt;/li&gt;
&lt;li&gt;상수와 인자 값을 순서대로 복사해 채움&lt;/li&gt;
&lt;li&gt;채운 byte[] 로 "name: user, age: 30" 문자열을 만듦&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;결국 String.format 의 "name: %s, age: %d" 와 문자열 덧셈의 레시피 "name: \u0001, age: \u0001" 는 모두 결과 문자열의 모양을 적은 것이다. 차이는 해석하는 횟수다. String.format 은 호출할 때마다 포맷 문자열을 해석하지만, 문자열 덧셈은 처음 실행될 때 레시피를 한 번만 해석한다. 그래서 문자열 덧셈은 호출마다 조각 객체를 만들지 않는다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size="size23"&gt;&lt;b&gt;[ 덧셈 연산의 성능 측정하기 ]&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size="size16"&gt;이 차이가 실제로 얼마나 되는지 확인하기 위해 같은 결과를 만드는 String.format("name: %s, age: %d", name, age) 와 문자열 덧셈을 비교해 보았다. Apple M4 Max, Corretto 11.0.28과 21.0.8, G1 GC 힙 2GB 환경에서 5초 워밍업 후 10초 동안 측정했다.&lt;/p&gt;
&lt;table style="border-collapse: collapse; width: 100%;" border="1" data-ke-align="alignLeft" data-ke-style="style4"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style="color: #333333; text-align: start;"&gt;JDK&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style="color: #333333; text-align: start;"&gt;방식&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;1건당 시간 (1개 스레드)&lt;/td&gt;
&lt;td&gt;1건당 할당&lt;/td&gt;
&lt;td&gt;초당 처리량 (12개 스레드)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;String.format&lt;/td&gt;
&lt;td&gt;약 331ns&lt;/td&gt;
&lt;td&gt;약 890B&lt;/td&gt;
&lt;td&gt;약 2,620만 건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;문자열 덧셈&lt;/td&gt;
&lt;td&gt;약 13ns&lt;/td&gt;
&lt;td&gt;약 48B&lt;/td&gt;
&lt;td&gt;약 6억 950만 건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;String.format&lt;/td&gt;
&lt;td&gt;약 87ns&lt;/td&gt;
&lt;td&gt;약 500B&lt;/td&gt;
&lt;td&gt;약 7,170만 건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;문자열 덧셈&lt;/td&gt;
&lt;td&gt;약 13ns&lt;/td&gt;
&lt;td&gt;약 48B&lt;/td&gt;
&lt;td&gt;약 5억 640만 건&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;JDK 21의 String.format 은 JDK 11보다 약 3.8배 빨라졌지만, 12개 스레드 기준으로 여전히 문자열 덧셈보다 처리량이 약 7배 적다. 할당량도 1건당 약 10배 많아서, 호출이 많은 서버에서는 Young GC를 더 자주 일으킨다. 문자열 덧셈 쪽을 프로파일링해 보면 시간의 절반 이상(약 50~55%)이 int 를 숫자 문자로 바꾸는 Integer.getChars 에 쓰인다. 결과 문자열을 만드는 데 꼭 필요한 일 말고는 거의 하지 않는다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;따라서 반복 호출이 많은 경로에서는 String.format 대신 문자열 덧셈을 사용하는 것이 좋다. 로그 메시지는 String.format 이나 덧셈으로 직접 만들지 말고 SLF4J 의 자리표시자(log.info("name={}, age={}", name, age))를 사용하면, 로그 레벨이 꺼져 있을 때 문자열을 아예 만들지 않는다. 참고로 Kotlin 의 문자열 템플릿("name: $name, age: $age")도 문자열 덧셈으로 컴파일되므로 String.format 의 비용이 없다. 반대로 %.2f 같은 소수점 자릿수 지정이나 Locale 별 숫자 표기가 필요하고, 호출 빈도가 낮다면 String.format 을 써도 성능 문제는 거의 없다.&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size="size16"&gt;&amp;nbsp;&lt;/p&gt;</summary>
    <title>[Java] String.format의 성능 문제와 개선 사항들</title>
    <updated>2026-10-06T10:00:57+09:00</updated>
    <dc:date>2026-10-06T10:00:57+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>문동욱</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;이번 포스팅에서는 클로드나 GPT 같은 AI에게 날씨를 계산해달라고 하면 어떤 일이 벌어지는지, 그리고 데이터가 부족한 물리 문제를 풀기 위해 등장한 PINO(Physics-Informed Neural Operator)라는 방식에 대한 이야기를 해보려고 한다.&lt;/p&gt;
&lt;p&gt;요즘 주변을 보면 클로드나 GPT 같은 LLM을 거의 만능 도구처럼 생각하는 분들이 꽤 많은 것 같다. 뭐 AI를 활용해서 코드도 짜고, 논문도 요약하고, 계약서도 검토하는 세상이니 그렇게 느끼는 것도 무리는 아니다. 필자 역시 하루 중 상당 시간을 이런 AI와 함께 일하고 있고, 확실히 예전에 비해 AI의 성능이 발전하고 있음을 느낀다.&lt;/p&gt;
&lt;p&gt;그런데 여기에는 함정이 하나 있다. 만약 우리가 LLM에게 “내일 서울의 기온 변화를 계산해줘”라고 부탁하면, LLM은 실제로 대기의 온도와 기압이 어떻게 변할지 방정식을 푸는 대신, 계산 결과처럼 보이는 그럴듯한 문장을 만들어낸다.&lt;/p&gt;
&lt;p&gt;왜냐하면 LLM은 엄청난 양의 글을 읽으며 언어를 배운 모델이고, 지구의 대기가 움직이는 모습을 보며 배운 모델이 아니기 때문이다.&lt;/p&gt;
&lt;p&gt;사실 클로드와 같은 트랜스포머 구조로 만든 모델도 날씨 예측 분야에서 이미 기존 수치 예보와 겨룰 만한 성적을 내고 있기는 하다. 다만 이런 모델들은 수십 년 치 지구 대기 기록을 학습한 녀석들이고, 날씨처럼 학습 데이터가 넉넉한 분야는 사실 예외에 가깝다.&lt;/p&gt;
&lt;p&gt;자동차 주변의 공기 흐름이나 핵융합로 안의 플라스마처럼 대부분의 물리 문제는 학습시킬 데이터가 턱없이 부족하다. 대신 이런 문제들에는 한 가지 핵심적인 포인트가 있는데, 바로 그 현상을 지배하는 방정식을 우리가 이미 알고 있다는 것이다.&lt;/p&gt;
&lt;p&gt;그래서 이번 포스팅에서는 LLM이 날씨를 계산하지 못하는 이유에서 출발해서, 정답 데이터 대신 물리 방정식으로 스스로를 채점하며 배우는 PINO라는 방식을 소개하고 이 친구가 어떻게 동작하는지 한번 따라가 보려고 한다.&lt;/p&gt;
&lt;!-- more --&gt;
&lt;h2 id="트랜스포머는-무엇을-잘하고-무엇을-못할까" style="position:relative;"&gt;트랜스포머는 무엇을 잘하고 무엇을 못할까&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%ED%8A%B8%EB%9E%9C%EC%8A%A4%ED%8F%AC%EB%A8%B8%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%9E%98%ED%95%98%EA%B3%A0-%EB%AC%B4%EC%97%87%EC%9D%84-%EB%AA%BB%ED%95%A0%EA%B9%8C" aria-label="트랜스포머는 무엇을 잘하고 무엇을 못할까 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;PINO 이야기를 하려면 먼저 클로드와 GPT가 공통으로 쓰는 트랜스포머라는 신경망 구조가 무엇인지, 그리고 클로드가 날씨를 계산하지 못하는 진짜 이유가 무엇인지부터 이야기해야 한다.&lt;/p&gt;
&lt;h3 id="클로드와-gpt의-공통점" style="position:relative;"&gt;클로드와 GPT의 공통점&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%ED%81%B4%EB%A1%9C%EB%93%9C%EC%99%80-gpt%EC%9D%98-%EA%B3%B5%ED%86%B5%EC%A0%90" aria-label="클로드와 gpt의 공통점 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;우리가 자주 사용하는 클로드, GPT, 제미나이 같은 AI를 묶어서 보통 LLM(Large Language Model)이라고 부른다. 말 그대로 엄청난 양의 글을 읽고 언어를 다루는 법을 배운 모델이다.&lt;/p&gt;
&lt;p&gt;이 모델들은 회사도 다르고 성능도 제각각이지만, 사실 요즘 LLM 대부분은 트랜스포머라는 같은 설계도를 바탕으로 만들어져 있다. 2017년 구글 연구진이 발표한 구조인데, GPT(Generative Pre-trained Transformer)라는 이름에도 이미 흔적이 남아 있다.&lt;/p&gt;
&lt;p&gt;개발자에게 익숙한 비유를 들어보자면 트랜스포머는 프레임워크에 가깝고 클로드나 GPT는 그 프레임워크로 만든 앱에 가깝다. 같은 React로 만들었어도 토스와 넷플릭스가 전혀 다른 서비스인 것처럼, 같은 트랜스포머를 바탕으로 했어도 각 회사의 모델은 학습시킨 데이터와 세부 설계가 다르다는 것이다. 물론 각 회사가 트랜스포머를 저마다 변형해서 쓰기 때문에 프레임워크처럼 딱 떨어지는 관계는 아니지만, 뼈대를 공유한다는 점에서는 비슷하다.&lt;/p&gt;
&lt;p&gt;그러니 클로드가 날씨를 계산하지 못하는 이유를 따질 때는 대화형 AI에게 물리 계산을 직접 묻는 경우와, 트랜스포머라는 설계도 자체를 물리 데이터로 학습시키는 경우를 구분해서 봐야 한다. 이 구분은 뒤에서 다시 정리하기로 하고 먼저 트랜스포머가 어떤 방식으로 동작하는지부터 보자.&lt;/p&gt;
&lt;h3 id="어텐션-모든-단어를-서로-견주어-보는-연산" style="position:relative;"&gt;어텐션, 모든 단어를 서로 견주어 보는 연산&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%EC%96%B4%ED%85%90%EC%85%98-%EB%AA%A8%EB%93%A0-%EB%8B%A8%EC%96%B4%EB%A5%BC-%EC%84%9C%EB%A1%9C-%EA%B2%AC%EC%A3%BC%EC%96%B4-%EB%B3%B4%EB%8A%94-%EC%97%B0%EC%82%B0" aria-label="어텐션 모든 단어를 서로 견주어 보는 연산 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;LLM이 하도 많은 일들을 수행해 주다 보니 만능처럼 느껴지지만, 결국 이 친구가 답을 만드는 방식을 한 문장으로 요약하면 지금까지의 글을 보고 다음에 올 단어 조각을 예측하는 것이다. 그리고 이 단어 조각들을 “토큰”이라고 부른다.&lt;/p&gt;
&lt;p&gt;예를 들어 “안녕하세요”라는 문장은 모델 내부에서 “안녕”, “하세요” 같은 몇 개의 조각으로 쪼개지고, 각 조각은 숫자 배열로 바뀐다. 모델은 이 배열들의 나열을 보고 다음 조각이 무엇일지 확률적으로 고른다.&lt;/p&gt;
&lt;p&gt;우리가 클로드와 대화할 때 답변이 조금씩 흘러나오는 것도 이 방식과 관련이 있다. 모델은 답 전체를 미리 만들어두는 게 아니라 토큰을 하나 고르고, 그걸 이어 붙인 글을 다시 보고 다음 토큰을 고르는 식으로 답을 쌓아 올린다. 그러니 답이 다 만들어질 때까지 기다리게 하는 대신, 만들어지는 대로 바로 보여주는 편이 사용자 입장에서 훨씬 덜 답답한 것이다.&lt;/p&gt;
&lt;p&gt;그런데 다음 조각을 잘 고르려면 먼저 지금까지의 글을 제대로 이해해야 한다. 예를 들어 “어제 사과를 샀는데 그것이 너무”라는 글 다음에 올 말을 고른다고 해보자. “달았다”나 “비쌌다” 같은 말을 떠올리려면, “그것”이 사과를 가리킨다는 걸 알아야 한다. 문제는 “그것”이라는 토큰 하나만 봐서는 아무 정보가 없다는 것이다. 단어의 의미는 결국 주변 단어와의 관계 속에서 정해진다.&lt;/p&gt;
&lt;p&gt;그래서 트랜스포머는 다음 토큰을 고르기 전에, 각 토큰이 글 속의 다른 토큰들을 둘러보고 필요한 정보를 가져와 자기 숫자 배열을 고쳐 쓰게 한다. “그것”은 “사과”에게서 정보를 가져와 “사과를 가리키는 그것”이라는 의미를 품은 배열로 바뀌는 식이다. 이렇게 토큰끼리 정보를 주고받는 과정이 바로 트랜스포머 설계도의 핵심인 어텐션이다. 이 과정을 여러 층에 걸쳐 반복하고 나면, 마지막 토큰의 배열에는 앞 글 전체의 문맥이 녹아 있게 되고, 모델은 이 배열을 보고 다음 토큰을 고른다.&lt;/p&gt;
&lt;p&gt;어텐션을 수식으로 쓰면 다음과 같다.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mtext&gt;Attention&lt;/mtext&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;Q&lt;/mi&gt;&lt;mo separator="true"&gt;,&lt;/mo&gt;&lt;mi&gt;K&lt;/mi&gt;&lt;mo separator="true"&gt;,&lt;/mo&gt;&lt;mi&gt;V&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mtext&gt;softmax&lt;/mtext&gt;&lt;mrow&gt;&lt;mo fence="true"&gt;(&lt;/mo&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;Q&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;K&lt;/mi&gt;&lt;mi&gt;T&lt;/mi&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;msqrt&gt;&lt;mi&gt;d&lt;/mi&gt;&lt;/msqrt&gt;&lt;/mfrac&gt;&lt;mo fence="true"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;mi&gt;V&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d}}\right)V&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Attention&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;Q&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.07153em;"&gt;K&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.22222em;"&gt;V&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:2.4684em;vertical-align:-0.95em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;softmax&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.5183em;"&gt;&lt;span style="top:-2.1778em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.9322em;"&gt;&lt;span class="svg-align" style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord" style="padding-left:0.833em;"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-2.8922em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="hide-tail" style="min-width:0.853em;height:1.08em;"&gt;&lt;svg xmlns="http://www.w3.org/2000/svg" width="400em" height="1.08em" viewbox="0 0 400000 1080" preserveaspectratio="xMinYMin slice"&gt;&lt;path d="M95,702
c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14
c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54
c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10
s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429
c69,-144,104.5,-217.7,106.5,-221
l0 -0
c5.3,-9.3,12,-14,20,-14
H400000v40H845.2724
s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7
c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z
M834 80h400000v40h-400000z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.1078em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;Q&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.07153em;"&gt;K&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8413em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.13889em;"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.93em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.22222em;"&gt;V&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;기호가 잔뜩 붙어 있지만 하는 일은 단순하다. 예컨대 회의실에 글 속의 토큰들이 한 사람씩 앉아 있다고 생각해보자. 각자 “내 의미를 정하려면 이런 정보가 필요해”라는 쿼리(Query) &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;Q&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;Q&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8778em;vertical-align:-0.1944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;Q&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;와, “나는 이런 정보를 갖고 있어”라는 키(Key) &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;K&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;K&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.07153em;"&gt;K&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;와, 실제로 건네줄 수 있는 밸류(Value) &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;V&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;V&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.22222em;"&gt;V&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;를 하나씩 들고 있다. 데이터베이스에서 쿼리로 키를 찾아 밸류를 꺼내는 걸 떠올리면 이름이 바로 와닿을 것이다. 앞의 예시로 치면 “그것”의 쿼리에는 “내가 가리키는 대상이 뭐지?”가 담겨 있고, “사과”의 키에는 “나는 먹을 수 있는 물건이야”가 담겨 있는 셈이다.&lt;/p&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;Q&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;K&lt;/mi&gt;&lt;mi&gt;T&lt;/mi&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;QK^T&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.0358em;vertical-align:-0.1944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;Q&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.07153em;"&gt;K&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8413em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.13889em;"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 모든 사람이 서로의 키를 보면서 “저 사람이 내 질문에 얼마나 도움이 될까”를 점수로 매기는 과정이다. “그것”의 쿼리와 “사과”의 키는 잘 맞으니 높은 점수가 나온다. softmax는 그 점수를 합이 1이 되는 비율로 바꿔주고, 마지막으로 그 비율만큼 각자의 밸류를 가져와 섞는다. 그러면 “그것”은 “사과”의 밸류를 가장 많이 섞어서 자기 배열을 고쳐 쓰게 된다. 앞에서 말한 “다른 토큰에게서 정보를 가져온다”는 게 바로 이 과정이다. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msqrt&gt;&lt;mi&gt;d&lt;/mi&gt;&lt;/msqrt&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sqrt{d}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.04em;vertical-align:-0.1078em;"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.9322em;"&gt;&lt;span class="svg-align" style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord" style="padding-left:0.833em;"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-2.8922em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="hide-tail" style="min-width:0.853em;height:1.08em;"&gt;&lt;svg xmlns="http://www.w3.org/2000/svg" width="400em" height="1.08em" viewbox="0 0 400000 1080" preserveaspectratio="xMinYMin slice"&gt;&lt;path d="M95,702
c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14
c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54
c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10
s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429
c69,-144,104.5,-217.7,106.5,-221
l0 -0
c5.3,-9.3,12,-14,20,-14
H400000v40H845.2724
s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7
c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z
M834 80h400000v40h-400000z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.1078em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;로 나누는 건 점수의 크기를 적당히 맞춰주는 장치 정도로 이해하면 된다.&lt;/p&gt;
&lt;p&gt;여기서 계산량을 한번 따져보자. 회의실에 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;N&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;명이 있으면 모든 사람이 모든 사람의 키를 한 번씩 봐야 하니, 점수 계산이 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;mo&gt;×&lt;/mo&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;N \times N&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7667em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;번 일어난다. &lt;small&gt;(정확히는 클로드나 GPT 같은 모델에서 각 토큰은 자기보다 앞에 있는 토큰의 키만 본다. 아직 쓰이지 않은 뒷부분을 미리 볼 수는 없기 때문인데, 그래도 계산량이 토큰 수의 제곱에 비례한다는 점은 같다)&lt;/small&gt; 문장 하나가 1,000개의 토큰으로 이루어져 있다면 점수는 100만 개인데, 요즘 GPU에게 이 정도는 별 일이 아니다.&lt;/p&gt;
&lt;p&gt;그리고 이 구조는 언어와 궁합이 아주 좋다. “그것”이 가리키는 대상이 몇 문장 앞에 있더라도, 어텐션은 거리와 상관없이 모든 단어를 한 번에 둘러볼 수 있기 때문이다. 게다가 누가 누구를 참고할지를 문장마다 새로 계산하기 때문에, “그것”이 어떤 문장에서는 사과를, 다른 문장에서는 자동차를 가리키는 식의 유연함도 자연스럽게 다룬다. 트랜스포머가 언어 모델의 표준이 된 데에는 이런 이유가 있다.&lt;/p&gt;
&lt;h3 id="클로드에게-날씨를-물어보면" style="position:relative;"&gt;클로드에게 날씨를 물어보면&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%ED%81%B4%EB%A1%9C%EB%93%9C%EC%97%90%EA%B2%8C-%EB%82%A0%EC%94%A8%EB%A5%BC-%EB%AC%BC%EC%96%B4%EB%B3%B4%EB%A9%B4" aria-label="클로드에게 날씨를 물어보면 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;그렇다면 클로드에게 “이 기압 배치에서 내일 서울의 기온이 어떻게 변할지 계산해줘”라고 물으면 어떤 일이 벌어질까? 앞에서 본 것처럼 클로드가 하는 일은 결국 다음에 올 토큰을 고르는 것이다. 그러니 클로드는 대기의 온도와 기압을 실제로 계산하는 게 아니라, 계산 결과처럼 보이는 문장을 생성한다.&lt;/p&gt;
&lt;p&gt;클로드가 학습한 건 사람이 쓴 엄청난 양의 글이다. 그 안에는 날씨에 관한 글도 잔뜩 들어 있겠지만, 지구의 대기가 시간에 따라 실제로 어떻게 움직였는지를 기록한 숫자 데이터와는 거리가 멀다. 날씨 기사를 수만 편 읽었다고 해서 내일 날씨를 계산할 수 있게 되는 건 아닌 것과 같다.&lt;/p&gt;
&lt;p&gt;물론 클로드에게 날씨 시뮬레이션 코드를 짜달라고 하면 꽤 그럴듯한 코드를 짜준다. 그러니 정확히 말하자면 클로드는 날씨를 계산하지 “못한다”기보다는, 직접 물었을 때 계산을 “하지 않는다”에 더 가깝다.&lt;/p&gt;
&lt;p&gt;여기에 문제가 하나 더 있는데, 바로 패턴을 보고 배운 모델은 물리 법칙을 지킨다는 보장이 없다는 것이다. 언어 모델은 학습 데이터에서 본 패턴을 바탕으로 그럴듯한 답을 만든다. 언어에서는 이 정도로 그럴듯하면 대부분 충분하고, 틀리면 다시 물어보거나 사람이 확인하면 된다.&lt;/p&gt;
&lt;p&gt;하지만 물리 세계에서는 보존 법칙이라는 엄격한 규칙이 존재하기 때문에 이런 식으로 계산하면 안 된다. 어떤 영역 안의 물이나 에너지는 늘어날 수도 있고 줄어들 수도 있지만, 그 변화량은 경계로 들어오고 나간 양과 열원이나 비 같은 원천을 모두 더한 값과 정확히 맞아떨어져야 한다.&lt;/p&gt;
&lt;p&gt;그런데 패턴만 보고 배운 모델은 이러한 규칙을 모르기 때문에 들어온 적도 나간 적도 없는 물을 조금씩 없애거나, 출처 없는 에너지를 조금씩 만들어낼 수 있다. 더 곤란한 점은 이런 작은 오차가 시간이 지날수록 쌓인다는 것이다. 1시간 뒤 예측을 다시 입력으로 넣어서 2시간 뒤를 예측하고, 그걸 또 넣어서 3시간 뒤를 예측하는 식으로 굴리다 보면, 처음에는 눈에 띄지 않던 오차가 며칠 뒤에는 전혀 엉뚱한 날씨를 만들어낸다. 이건 마치 복사기로 복사본을 다시 복사하는 걸 반복하면 점점 글자가 뭉개지는 것과 비슷하다.&lt;/p&gt;
&lt;p&gt;이 문제는 클로드만의 문제가 아니라 데이터를 보고 패턴을 배우는 모델이라면 모두 안고 있는 문제이고, 뒤에서 PINO가 정면으로 다루는 문제이기도 하다.&lt;/p&gt;
&lt;h3 id="날씨를-잘-맞히는-트랜스포머도-있다" style="position:relative;"&gt;날씨를 잘 맞히는 트랜스포머도 있다&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%EB%82%A0%EC%94%A8%EB%A5%BC-%EC%9E%98-%EB%A7%9E%ED%9E%88%EB%8A%94-%ED%8A%B8%EB%9E%9C%EC%8A%A4%ED%8F%AC%EB%A8%B8%EB%8F%84-%EC%9E%88%EB%8B%A4" aria-label="날씨를 잘 맞히는 트랜스포머도 있다 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;그렇다면 트랜스포머로는 애초에 날씨를 예측할 수 없는 걸까? 그렇지 않다. 오히려 날씨는 트랜스포머가 크게 성공한 분야다.&lt;/p&gt;
&lt;p&gt;2023년 화웨이 연구진이 Nature에 발표한 Pangu-Weather는 트랜스포머 구조를 바탕으로 만든 날씨 모델인데, 5일 뒤 예보를 포함한 주요 지표에서 유럽중기예보센터(ECMWF)의 수치 예보보다 더 정확한 결과를 보고했다. 수치 예보를 운영하던 ECMWF조차 2025년 2월부터 트랜스포머 기반의 AI 예보 모델인 AIFS를 실제 운영에 투입했다.&lt;/p&gt;
&lt;p&gt;물론 언어 모델을 그대로 가져다 쓴 건 아니고, 날씨 데이터에 맞게 꽤 많이 손을 봤다. 날씨를 예측한다는 건 대기의 온도, 기압, 바람 같은 값이 시간에 따라 어떻게 변할지 계산하는 일인데, 이 값들은 단어 토큰처럼 뚝뚝 끊어져 있지 않다. 예를 들어 서울과 판교 사이의 기온은 어느 지점에서 갑자기 바뀌지 않고, 연속적인 값으로 자연스럽게 이어진다. 서울에서 판교로 이동했다고 해서 갑자기 기온이 20도였다가 쨘하고 19도로 변하는 게 아니란 말이다.&lt;/p&gt;
&lt;p&gt;문제는 컴퓨터가 디지털 방식이라 이런 연속적인 값을 그대로 다룰 수 없다는 데 있다. 값이 연속적이라는 의미는 마치 0과 1 사이에 무한하게 많은 값이 존재하는 것을 표현하는 것과 동일한데, 컴퓨터가 이런 무한한 연산을 할 수는 없기 때문이다.&lt;/p&gt;
&lt;p&gt;그래서 컴퓨터로 날씨를 계산할 때는 공간을 격자로 잘게 나눠서 칸마다 값을 저장한다. 실제로 기상 분야에서 널리 쓰이는 데이터 중 하나인 ERA5는 지구를 가로 1,440칸, 세로 721칸으로 나누는데, 이것만 해도 한 시점에 약 104만 개의 칸이 나온다.&lt;/p&gt;
&lt;p&gt;만약 이 칸 하나하나를 토큰으로 보고 어텐션을 걸면, 104만 명이 모인 회의실에서 모든 사람이 모든 사람의 키를 확인하는 꼴이 되고, 한 시점에 대한 점수 계산만 약 1조 번이 되어버린다.&lt;/p&gt;
&lt;p&gt;게다가 이건 한 고도의 한 변수만 본 경우라서, 고도와 변수를 다 넣으면 GPU가 몇 대 있어도 모자란 슬픈 상황이 펼쳐진다. 그래서 트랜스포머를 날씨에 쓰는 모델들은 몇 칸씩 묶은 덩어리를 토큰 하나로 쓰거나, 가까운 칸끼리만 어텐션을 거는 식으로 비용을 줄인다.&lt;/p&gt;
&lt;p&gt;비용 말고 해상도도 골칫거리다. 지구를 나눈 칸 수인 해상도가 바뀌면 모델이 배운 규칙이 어긋날 수 있기 때문이다.&lt;/p&gt;
&lt;p&gt;격자 데이터를 다루는 트랜스포머는 보통 토큰마다 “몇 번째 자리”라는 칸 번호를 붙이고, 그 번호를 바탕으로 “바로 옆 자리는 이만큼 참고하고, 세 칸 떨어진 자리는 이만큼 참고하라” 같은 관계를 학습한다.&lt;/p&gt;
&lt;p&gt;그런데 잘 생각해보면 64x64 격자에서 학습한 바로 옆 칸은 실제 거리로 몇 km였는데, 256x256 격자에서는 같은 바로 옆 칸이 그 4분의 1 거리밖에 안 된다. 즉, 같은 날씨를 보여주려고 해도 칸을 나누는 방식이 달라지면 학습한 규칙이 어긋나기 때문에, 따로 보정을 해주거나 모델을 새로 학습시켜야 한다. 이건 어텐션 자체의 문제라기보다는 칸 번호로 위치를 표현하는 방식의 문제라서 이를 보완하는 기법들도 있지만, 어쨌든 격자 데이터를 다루는 모델이라면 신경 써야 하는 부분이다.&lt;/p&gt;
&lt;p&gt;하지만 이런 모델들이 날씨를 잘 맞히는 진짜 비결은 따로 있다. 바로 데이터다. 앞에서 말한 ERA5는 ECMWF가 과거의 관측 기록을 모아 지구 대기의 상태를 시간 단위로 복원해둔 데이터인데, 지금은 1940년부터 현재까지를 담고 있다. 트랜스포머 기반인 Pangu-Weather도, 그래프 신경망이라는 다른 구조를 쓰는 구글 딥마인드의 GraphCast도 이 중 1979년부터 2017년까지 39년 치를 학습했다. 지구의 대기가 39년 동안 시간마다 어떻게 움직였는지를 통째로 보고 배운 셈이다.&lt;/p&gt;
&lt;h3 id="데이터가-없는-문제는-어떻게-할까" style="position:relative;"&gt;데이터가 없는 문제는 어떻게 할까&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%97%86%EB%8A%94-%EB%AC%B8%EC%A0%9C%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%A0%EA%B9%8C" aria-label="데이터가 없는 문제는 어떻게 할까 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;문제는 날씨처럼 수십 년 치 기록이 차곡차곡 쌓여 있는 분야가 오히려 예외라는 것이다.&lt;/p&gt;
&lt;p&gt;이제 막 설계한 자동차 주변의 공기 흐름은 그 차가 세상에 나오기도 전이니 관측 기록이 있을 리 없다. 핵융합로 안의 플라스마는 측정 자체가 어렵고, 몸속에 꽂아둔 가느다란 관 안에서 세균이 어떻게 움직이는지도 마찬가지다. 결국 학습 데이터가 필요하면 시뮬레이터를 수천 번 돌려서 직접 만들어야 하는데, 이게 얼마나 곤란한 일인지는 뒤에서 다시 이야기하겠다.&lt;/p&gt;
&lt;p&gt;대신 이런 문제들에는 데이터 대신 기댈 구석이 하나 있다. 바로 그 현상을 지배하는 방정식이다. 공기가 어떻게 흐르는지, 열이 어떻게 퍼지는지는 이미 200년 가까이 방정식으로 정리되어 왔다. 그렇다면 정답 데이터를 잔뜩 보여주는 대신, 그 방정식을 가르치면 되지 않을까? 이 질문에 대한 답이 바로 이 글의 주인공인 PINO다.&lt;/p&gt;
&lt;h2 id="pino는-어떤-문제를-풀까" style="position:relative;"&gt;PINO는 어떤 문제를 풀까&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#pino%EB%8A%94-%EC%96%B4%EB%96%A4-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%ED%92%80%EA%B9%8C" aria-label="pino는 어떤 문제를 풀까 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;그렇다면 데이터는 부족하지만 방정식은 알고 있는 문제에 맞는 도구는 어떤 모습일까? 우선 PINO의 원리를 뜯어보기 전에 PINO가 결과적으로 무엇을 해주는지부터 보자.&lt;/p&gt;
&lt;p&gt;예를 들어 자동차 차체 주변의 공기 흐름을 알고 싶을 때는 물리 법칙을 한 단계씩 정직하게 계산하는 프로그램인 물리 시뮬레이터를 돌리게 된다. 이 프로그램은 정확하지만 느리기 때문에 설계안 하나를 평가하는 데 몇 시간에서 며칠씩 걸리기도 한다. 이렇게 차체 모양을 조금 바꿔볼 때마다 며칠씩 기다려야 한다면 계산을 시도해볼 수 있는 설계안은 꽤나 제한적일 수밖에 없다.&lt;/p&gt;
&lt;p&gt;PINO는 이런 시뮬레이터를 흉내 내는 신경망이다. 한 번 학습시켜두면 새로운 조건을 넣었을 때 시뮬레이터보다 수백 배에서 많게는 백만 배 가까이 빠르게 결과를 근사해서 내놓는다. 여기까지만 들으면 “그냥 시뮬레이션 결과로 학습시킨 신경망 아닌가”라는 생각이 들 수 있는데, PINO에는 몇 가지 특이한 성질이 더 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;첫째, 학습 데이터가 적게 필요하다. 극단적으로는 정답 데이터가 하나도 없어도 물리 법칙만 알려주면 학습이 된다.&lt;/p&gt;
&lt;p&gt;둘째, 모델이 내놓는 답이 물리 법칙을 얼마나 지키는지를 학습 과정에서 직접 채점한다. 그래서 그럴듯하기만 한 답보다는 물리적으로 말이 되는 답에 가까워진다.&lt;/p&gt;
&lt;p&gt;셋째, 해상도와 상관없이 동작한다. 거친 격자로 학습시킨 모델에게 훨씬 촘촘한 격자를 줘도 그대로 예측을 해낸다. 앞에서 본 칸 번호에 묶이는 문제가 없다는 것이다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;실제 사례를 하나 들어보면 감이 더 잘 온다. 칼텍 연구진은 병원에서 쓰는 카테터라는 가느다란 관을 다시 설계하는 데 PINO의 뼈대가 되는 Neural Operator 기술을 썼다.&lt;/p&gt;
&lt;p&gt;카테터를 몸에 오래 꽂아두면 세균이 관을 타고 거슬러 올라와 감염을 일으키는 문제가 있다. 연구진은 세균이 헤엄치는 원리에서 착안해서 관 안쪽 벽에 상어 지느러미처럼 생긴 삼각형 돌기를 넣으면 세균이 거슬러 오르기 어려워진다는 아이디어를 얻었다.&lt;/p&gt;
&lt;p&gt;문제는 돌기의 크기, 높이, 간격을 어떻게 잡아야 가장 효과적인지 알아내려면 수많은 모양에 대해 시뮬레이션을 돌려야 한다는 것이다. 연구진은 돌기 모양을 넣으면 관 속에 세균이 어떻게 퍼질지를 예측하는 신경망을 만들어 돌기 모양을 다듬었고, 3D 프린터로 만든 시제품으로 실험해보니 거슬러 오른 세균이 매끈한 관보다 10배에서 100배 이상 적었다고 보고했다.&lt;/p&gt;
&lt;p&gt;어떻게 이런 일이 가능할까? 이 질문에 답하려면 PINO라는 이름을 몇 조각으로 나눠서 봐야 한다. 이 글에서는 다음 네 개의 질문을 차례로 따라가 보려고 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li&gt;물리 시뮬레이션은 애초에 왜 느릴까?&lt;/li&gt;
&lt;li&gt;해상도와 상관없이 동작하는 신경망은 어떻게 만들 수 있을까?&lt;/li&gt;
&lt;li&gt;거기에 푸리에 변환은 왜 필요할까?&lt;/li&gt;
&lt;li&gt;정답 데이터 없이 어떻게 채점을 할 수 있을까?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;이 섹션에서는 먼저 앞의 두 질문을 다루고, 푸리에 변환과 채점 방법은 이어지는 섹션에서 하나씩 풀어보려고 한다.&lt;/p&gt;
&lt;h3 id="물리-시뮬레이션은-왜-느릴까" style="position:relative;"&gt;물리 시뮬레이션은 왜 느릴까&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%EB%AC%BC%EB%A6%AC-%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%EC%85%98%EC%9D%80-%EC%99%9C-%EB%8A%90%EB%A6%B4%EA%B9%8C" aria-label="물리 시뮬레이션은 왜 느릴까 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;게임 개발을 해본 사람이라면 물리 엔진의 &lt;code class="language-text"&gt;update&lt;/code&gt; 함수가 익숙할 것이다. 매 프레임마다 “지금 상태”를 받아서 “아주 짧은 시간이 지난 뒤의 상태”를 계산하고, 이걸 1초에 60번씩 반복한다. 공이 떨어지는 것도, 캐릭터가 점프하는 것도 전부 이 작은 갱신의 반복이다.&lt;/p&gt;
&lt;p&gt;물리 시뮬레이션도 본질적으로 똑같다. 다만 공 하나가 아니라 공간의 모든 칸에 대해 &lt;code class="language-text"&gt;update&lt;/code&gt;를 돌린다는 점이 다르다. 그리고 물리학자들은 이 &lt;code class="language-text"&gt;update&lt;/code&gt; 규칙을 수식으로 적어두는데, 이런 식을 편미분방정식이라고 부른다. 이름은 어려워 보이지만 “각 칸의 값이 한 프레임 동안 어떻게 변하는지 적어놓은 규칙”이라고 이해하면 충분하다.&lt;/p&gt;
&lt;p&gt;가장 단순한 예시가 열이 퍼지는 규칙이다. 쇠막대 한가운데를 라이터로 달궜다가 떼면, 뜨거운 부분은 점점 식고 주변은 데워지면서 결국 막대 전체의 온도가 고르게 된다. 이걸 수식으로 적으면 이렇다.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\frac{\partial u}{\partial t} = \alpha \frac{\partial^2 u}{\partial x^2}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:2.0574em;vertical-align:-0.686em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.3714em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.686em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:2.1771em;vertical-align:-0.686em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.4911em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.7401em;"&gt;&lt;span style="top:-2.989em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8141em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.686em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;위 수식에서 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;u&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 온도, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;t&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6151em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 시간, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;x&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 막대 위의 위치다. 즉, 왼쪽은 “이 칸의 온도가 지금 얼마나 빨리 변하고 있는가”라는 뜻이고, 오른쪽의 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\frac{\partial^2 u}{\partial x^2}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.3629em;vertical-align:-0.345em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.0179em;"&gt;&lt;span style="top:-2.655em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.7463em;"&gt;&lt;span style="top:-2.786em;margin-right:0.0714em;"&gt;&lt;span class="pstrut" style="height:2.5em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.394em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8913em;"&gt;&lt;span style="top:-2.931em;margin-right:0.0714em;"&gt;&lt;span class="pstrut" style="height:2.5em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.345em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 “양 옆 칸이 이 칸보다 평균적으로 얼마나 더 뜨거운가”라는 뜻이다. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\alpha&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 구리처럼 열을 잘 전하는 재료는 크고, 나무처럼 잘 못 전하는 재료는 작은 재료별 상수다.&lt;/p&gt;
&lt;p&gt;결국 이 식을 말로 풀어보면 “양 옆이 더 뜨거운 칸은 데워지고, 양 옆이 더 차가운 칸은 식는다”는 너무나 당연한 이야기를 하고 있는 것이다.&lt;/p&gt;
&lt;p&gt;그리고 개발자에게 익숙한 표현으로 바꿔보자면 이건 이미지 블러 필터와도 비슷하다. 블러는 각 픽셀을 주변 픽셀의 평균에 가깝게 만드는 연산이기 때문에 블러 연산을 여러 번 반복하면 이미지가 점점 뭉개지면서 결국에는 평균 값에 수렴하게 되어 이미지를 알아볼 수 없게 된다. 사실 열이 퍼지는 과정도 이와 똑같은 연산이다.&lt;/p&gt;
&lt;p&gt;수식으로만 보면 이해가 어려우니 한번 코드로 옮겨보자. 배열을 다루는 자잘한 작업은 es-toolkit의 &lt;code class="language-text"&gt;range&lt;/code&gt;, &lt;code class="language-text"&gt;zipWith&lt;/code&gt; 같은 함수에 맡겼다.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;import&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; range&lt;span class="token punctuation"&gt;,&lt;/span&gt; zipWith &lt;span class="token punctuation"&gt;}&lt;/span&gt; &lt;span class="token keyword"&gt;from&lt;/span&gt; &lt;span class="token string"&gt;'es-toolkit'&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;Samples&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; neighborPull &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;center&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token keyword"&gt;const&lt;/span&gt; left &lt;span class="token operator"&gt;=&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;%&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;const&lt;/span&gt; right &lt;span class="token operator"&gt;=&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell &lt;span class="token operator"&gt;+&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;%&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;left &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; center &lt;span class="token operator"&gt;+&lt;/span&gt; right&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;cellSize &lt;span class="token operator"&gt;*&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; heatStep &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;zipWith&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token function"&gt;neighborPull&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;,&lt;/span&gt; pull&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; value &lt;span class="token operator"&gt;+&lt;/span&gt; alpha &lt;span class="token operator"&gt;*&lt;/span&gt; pull &lt;span class="token operator"&gt;*&lt;/span&gt; timeStep&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;simulateHeat&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;initial&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; steps&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;steps&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;reduce&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token function"&gt;heatStep&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; initial&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;Samples&lt;/code&gt;는 막대 위 칸마다 재서 적어둔 온도 값의 배열이다. &lt;code class="language-text"&gt;neighborPull&lt;/code&gt;은 양 옆 칸이 이 칸을 얼마나 끌어당기는지를 계산한다. &lt;code class="language-text"&gt;(left - 2 * center + right)&lt;/code&gt;는 &lt;code class="language-text"&gt;(left - center) + (right - center)&lt;/code&gt;를 줄여 쓴 것이라, 양 옆이 더 뜨거우면 양수, 더 차가우면 음수가 나온다.&lt;/p&gt;
&lt;p&gt;&lt;code class="language-text"&gt;heatStep&lt;/code&gt;은 끌어당기는 만큼 온도를 살짝 조정하는 한 프레임짜리 &lt;code class="language-text"&gt;update&lt;/code&gt;이고, &lt;code class="language-text"&gt;simulateHeat&lt;/code&gt;은 그걸 &lt;code class="language-text"&gt;steps&lt;/code&gt;번 반복한다. 여기서는 막대의 양 끝이 이어진 고리 모양이라고 가정했다.&lt;/p&gt;
&lt;p&gt;수식은 무시무시해 보였지만, 막상 코드로 옮기니 꼴랑 이웃한 값 세 개를 빼고 더하는 게 전부다. 그렇다면 계산이 이렇게 간단한데 왜 날씨 예측에는 슈퍼컴퓨터가 필요할까?&lt;/p&gt;
&lt;p&gt;첫 번째 이유는 &lt;code class="language-text"&gt;timeStep&lt;/code&gt;, 즉 한 프레임의 시간 간격을 마음대로 키울 수 없다는 데 있다. 게임에서도 프레임이 너무 띄엄띄엄 계산되면 빠른 총알이 벽을 뚫고 지나가는 버그가 생기는데, 물리 시뮬레이션도 비슷하다.&lt;/p&gt;
&lt;p&gt;만약 이 코드에서 &lt;code class="language-text"&gt;timeStep&lt;/code&gt; 값을 너무 크게 잡으면, 이웃한 칸들의 값이 위아래로 번갈아 튀기 시작하고 그 폭이 매 프레임 커지다가 결국 감당할 수 없을 만큼 커져서 발산해버린다. 안정적으로 돌리려면 대략 다음 조건을 지켜야 한다.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;Δ&lt;/mi&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;mo&gt;≤&lt;/mo&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;Δ&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\Delta t \le \frac{\Delta x^2}{2\alpha}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8193em;vertical-align:-0.136em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;≤&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:2.1771em;vertical-align:-0.686em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.4911em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8141em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.686em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;Δ&lt;/mi&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\Delta t&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 코드의 &lt;code class="language-text"&gt;timeStep&lt;/code&gt;, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;Δ&lt;/mi&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\Delta x&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 코드의 &lt;code class="language-text"&gt;cellSize&lt;/code&gt;다. 오른쪽에 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;Δ&lt;/mi&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\Delta x&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;가 제곱으로 들어 있다는 게 핵심인데, 칸을 절반 크기로 쪼개면 시간 간격은 4분의 1로 줄여야 한다는 뜻이다. 칸 수는 2배가 되고 프레임 수는 4배가 되니, 전체 계산량은 8배가 된다. 정밀하게 보려고 할수록 비용이 가파르게 올라가는 구조다.&lt;/p&gt;
&lt;p&gt;두 번째 이유는 실제 물리가 열 퍼짐보다 훨씬 복잡하다는 것이다. 공기 흐름은 3차원이고, 바람이 바람을 실어 나르면서 작은 소용돌이와 큰 소용돌이가 서로 영향을 주고받는다. 이런 걸 제대로 보려면 아주 작은 칸과 아주 짧은 시간 간격이 필요하고, 그래서 설계안 하나를 확인하는 데 며칠씩 기다려야 하는 눈물나는 상황이 생긴다.&lt;/p&gt;
&lt;p&gt;즉, 물리 시뮬레이션이 느린 이유는 규칙을 몰라서가 아니라 한 프레임짜리 규칙은 정확히 알고 있지만, 그 단순한 규칙을 아주 촘촘하게 아주 많이 반복해야 하기 때문이다. 그렇다면 이 반복을 전부 거치지 않고, 초기 상태를 넣으면 최종 상태가 바로 나오는 지름길을 신경망에게 배우게 할 수는 없을까? 바로 여기서 두 번째 질문으로 넘어간다.&lt;/p&gt;
&lt;h3 id="해상도와-상관없이-동작하는-신경망" style="position:relative;"&gt;해상도와 상관없이 동작하는 신경망&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%ED%95%B4%EC%83%81%EB%8F%84%EC%99%80-%EC%83%81%EA%B4%80%EC%97%86%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94-%EC%8B%A0%EA%B2%BD%EB%A7%9D" aria-label="해상도와 상관없이 동작하는 신경망 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;우리가 흔히 아는 신경망은 쉽게 말해 길이가 정해진 숫자 배열을 받아서 숫자 배열을 돌려주는 함수다. 이미지 분류 모델이라면 224x224 픽셀 배열을 받아서 “고양이 0.9, 강아지 0.08, 여우 0.02”처럼 클래스마다 확률이 하나씩 담긴 배열을 돌려준다. 입력의 크기가 모델 설계에 박혀 있다는 게 포인트다.&lt;/p&gt;
&lt;p&gt;Neural Operator가 하고 싶은 건 이것과 조금 다르다. TypeScript 타입으로 비교해보면 차이가 선명하게 보인다.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token comment"&gt;// 일반적인 신경망(Neural Network)&lt;/span&gt;

&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;NeuralNetwork&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token comment"&gt;// Neural Operator&lt;/span&gt;

&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;Field&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;position&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;NeuralOperator&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token operator"&gt;:&lt;/span&gt; Field&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Field&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;NeuralNetwork&lt;/code&gt;는 배열을 받는 반면, &lt;code class="language-text"&gt;NeuralOperator&lt;/code&gt;는 “위치를 넣으면 그 위치의 값을 알려주는 함수”인 &lt;code class="language-text"&gt;Field&lt;/code&gt;를 받아서 또 다른 &lt;code class="language-text"&gt;Field&lt;/code&gt;를 돌려준다. 열 퍼짐으로 치면 입력은 “처음 온도 분포”, 출력은 “10초 뒤 온도 분포”다. 앞의 &lt;code class="language-text"&gt;Samples&lt;/code&gt;가 막대 위 칸마다 온도를 재서 적어둔 배열이라면, &lt;code class="language-text"&gt;Field&lt;/code&gt;는 재기 전의 온도 분포 그 자체라고 보면 된다. 이렇게 연속적인 분포에서 일정한 간격으로 값을 읽어 배열로 만드는 걸 샘플링이라고 한다. &lt;small&gt;(샘플링은 예전에 &lt;a href="https://evan-moon.github.io/2019/07/10/javascript-audio-waveform/"&gt;컴퓨터는 어떻게 소리를 들을까?&lt;/a&gt; 포스팅에서 소리를 예로 자세히 다룬 적이 있다)&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;수학에서는 이렇게 함수를 받아서 함수를 돌려주는 것을 연산자, 영어로 오퍼레이터라고 부른다. 개발자들이 일반적으로 이야기하는 고차 함수의 수학 버전이라고 보면 된다. Neural Operator라는 이름은 결국 “고차 함수를 배우는 신경망”이라는 뜻이다. 그러니 입력 온도 분포를 64칸으로 샘플링해서 넣든 256칸으로 샘플링해서 넣든 같은 신경망을 쓸 수 있고, 결과를 몇 칸으로 샘플링해서 볼지도 사용하는 쪽이 정하면 된다.&lt;/p&gt;
&lt;p&gt;프론트엔드 개발자에게 익숙한 비유를 들자면 PNG와 SVG의 차이와 비슷하다. PNG는 이미지를 픽셀로 표현하기 때문에 확대하면 깨지지만, SVG는 “여기서 저기까지 곡선을 그어라”라는 규칙을 저장하기 때문에 해상도에 구애받지 않고 어떤 크기로 그려도 선명한 것이다. 결국 Neural Operator는 물리 현상을 PNG가 아니라 SVG처럼 다루고 싶은 것이라고 보면 된다.&lt;/p&gt;
&lt;p&gt;물론 실제 학습은 결국 격자 위의 데이터로 하기 때문에 이 비유가 완벽하게 들어맞지는 않지만 “특정 해상도가 아니라 그 밑에 깔린 규칙을 배운다”는 방향성은 꽤 비슷하다.&lt;/p&gt;
&lt;p&gt;이게 왜 자연스러운 목표일까? 앞에서 본 열 퍼짐 규칙을 떠올려보자. “양 옆이 더 뜨거우면 데워진다”는 규칙은 막대를 10칸으로 나누든 1,000칸으로 나누든 똑같다. 규칙 자체에는 해상도라는 개념이 없다. 그렇다면 그 규칙을 배운 모델도 해상도와 상관없이 동작하는 게 맞다.&lt;/p&gt;
&lt;p&gt;그런데 말은 쉽지, 신경망은 결국 숫자 배열을 곱하고 더하는 기계인데, 도대체 어떻게 해야 배열 크기와 상관없이 같은 규칙을 적용하게 만들 수 있을까? 바로 여기서 푸리에 변환이 등장한다.&lt;/p&gt;
&lt;h2 id="푸리에-변환으로-세상을-사인파의-합으로-바라보기" style="position:relative;"&gt;푸리에 변환으로 세상을 사인파의 합으로 바라보기&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%ED%91%B8%EB%A6%AC%EC%97%90-%EB%B3%80%ED%99%98%EC%9C%BC%EB%A1%9C-%EC%84%B8%EC%83%81%EC%9D%84-%EC%82%AC%EC%9D%B8%ED%8C%8C%EC%9D%98-%ED%95%A9%EC%9C%BC%EB%A1%9C-%EB%B0%94%EB%9D%BC%EB%B3%B4%EA%B8%B0" aria-label="푸리에 변환으로 세상을 사인파의 합으로 바라보기 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;세 번째 질문의 주인공인 푸리에 변환은 PINO의 뼈대가 되는 FNO(Fourier Neural Operator)의 F이기도 하다. 그만큼 이 글에서 가장 중요한 재료다. 여기서는 먼저 푸리에 변환이 무엇을 하는 친구인지 보고, 그 푸리에 변환으로 앞에서 본 열 퍼짐을 다시 바라봤을 때 어떤 일이 생기는지 따라가보자.&lt;/p&gt;
&lt;h3 id="파형에서-스펙트럼-구하기" style="position:relative;"&gt;파형에서 스펙트럼 구하기&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%ED%8C%8C%ED%98%95%EC%97%90%EC%84%9C-%EC%8A%A4%ED%8E%99%ED%8A%B8%EB%9F%BC-%EA%B5%AC%ED%95%98%EA%B8%B0" aria-label="파형에서 스펙트럼 구하기 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;한번 음악 앱의 이퀄라이저를 떠올려보자. 화면에는 저음부터 고음까지 막대그래프가 오르락내리락하고, 그 아래에 슬라이더가 줄지어 있다. 저음 슬라이더를 올리면 쿵쿵거리는 베이스가 강해지고, 고음 슬라이더를 내리면 소리가 먹먹해진다. 그런데 곰곰이 생각해보면 이상하다. 스피커로 나오는 소리는 결국 공기가 떨리는 구불구불한 파동 하나일 뿐인데, 어떻게 그 안에서 저음만 골라서 키울 수 있을까?&lt;/p&gt;
&lt;p&gt;여기서 푸리에 변환이 등장한다. 19세기 초 프랑스 수학자 조제프 푸리에(Joseph Fourier)는 아무리 복잡하게 생긴 파형이라도 결국 단순한 사인파들이 겹쳐진 모습으로 표현할 수 있다는 아이디어를 제시했다. 흥미롭게도 푸리에가 이 아이디어를 낸 이유가 바로 앞에서 본 열 퍼짐 규칙을 풀기 위해서였다.&lt;/p&gt;
&lt;center&gt;
  &lt;span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 640px; "&gt;
      &lt;a class="gatsby-resp-image-link" href="https://evan-moon.github.io/static/59b6365dd013753c1fa1b3bca1393b9b/5a190/fourier.png" style="display: block" target="_blank" rel="noopener"&gt;
    &lt;span class="gatsby-resp-image-background-image" style="padding-bottom: 70%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAIAAACgpqunAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB7klEQVR42o1TS5LTMBD1ao7ADVhyDg7AOWDLSaiCDSsWLNjNZqqgoIodn/AZppJMSIIdx1iS1ZKsv63YRkmoBIah4G2sT7/W69ftZPgDfddHDP+BpO9/Cd19g25MBp0P/yb/ZO348U1V1sG2Nmf1uHRl3YfuqKi/qijRRiup4sq7NoSNJpJMkaHKU63mlbxELTcx6T56s9nEmCOZMVZizDg3xnLQ1jSGG/oV8wwM1Z6oelLqJe2aYJ0rEQKAg9JEiJqnKyVERQGVGKhU0jnbmErBomIpBO3FEuNvBRX8O+V1XR/JHAC/eS+nc7MuhBAoX2NEGSglrTcNy6p8klWMUS619QUTWoqDtwlQyhGWH875ZCaXmZzNKVAA5kxLEGBCBESGZdYqa++/zl/M8bb4btvPxHmPS8TytUpz9u6jiu9XFaM0RmBEcMVs27SNezKmt54ubjw6HxU8XnV72XsPhZQEISmlpUBGnyDL43ldC6Ply0zcOctPHny5+3yBmVAcVvm6KApCSHLwvWkaEIKucvR2RFfreBJrv/dqdfJwfPvZ9KLkQx9UzO68i4Za673/bUjatuXRsy22hZ3N0M3Hn08v8dBvexu6iP6aCTtMZ+xE1BMLiVumrLR2V+Fw7bAnV/ZRT5qmIYRDuu7vP8kPSTUZqTEntNIAAAAASUVORK5CYII='); background-size: cover; display: block;"&gt;&lt;/span&gt;
  &lt;img class="gatsby-resp-image-image" alt="fourier" title="" src="https://evan-moon.github.io/static/59b6365dd013753c1fa1b3bca1393b9b/6af66/fourier.png" srcset="/static/59b6365dd013753c1fa1b3bca1393b9b/69538/fourier.png 160w,
/static/59b6365dd013753c1fa1b3bca1393b9b/72799/fourier.png 320w,
/static/59b6365dd013753c1fa1b3bca1393b9b/6af66/fourier.png 640w,
/static/59b6365dd013753c1fa1b3bca1393b9b/5a190/fourier.png 800w" sizes="(max-width: 640px) 100vw, 640px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" loading="lazy" decoding="async"&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;small&gt;세상 모든 파형은 단순한 사인파의 겹침으로 나타낼 수 있다는 것이 푸리에 변환의 핵심이다&lt;/small&gt;
&lt;/center&gt;
&lt;p&gt;그림의 왼쪽이 우리가 실제로 보는 구불구불한 파형이고, 가운데가 그 파형을 이루는 사인파들, 오른쪽이 각 사인파가 얼마나 섞여 있는지를 나타낸 막대그래프다. 푸리에 변환은 왼쪽에서 오른쪽을 뽑아내는 도구이고, 이퀄라이저 화면의 막대그래프가 바로 이 결과다. 이 막대그래프를 주파수 스펙트럼, 줄여서 스펙트럼이라고 부른다. 역변환은 반대로 스펙트럼을 보고 원래 파형을 정확히 되살려낸다.&lt;/p&gt;
&lt;p&gt;그림에서 보듯 사인파들끼리 다른 점은 얼마나 촘촘하게 출렁이느냐다. 이 글에서는 신호 전체를 한 구간으로 봤을 때 사인파가 그 구간 동안 출렁이는 횟수를 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;라고 부르겠다. 이 횟수가 곧 주파수이고, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;가 작은 사인파를 저주파, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;가 큰 사인파를 고주파라고 부른다. 음악으로 치면 저주파가 저음, 고주파가 고음이다.&lt;/p&gt;
&lt;p&gt;스펙트럼을 구하는 수식은 이렇게 생겼다.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;munderover&gt;&lt;mo&gt;∑&lt;/mo&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;/mrow&gt;&lt;/munderover&gt;&lt;msub&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/msub&gt;&lt;mi&gt;cos&lt;/mi&gt;&lt;mo&gt;⁡&lt;/mo&gt;&lt;mrow&gt;&lt;mo fence="true"&gt;(&lt;/mo&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;mi&gt;π&lt;/mi&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mfrac&gt;&lt;mo fence="true"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;a_k = \sum_{n=0}^{N-1} u_n \cos\left(2\pi \frac{kn}{N}\right)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.5806em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:3.0954em;vertical-align:-1.2671em;"&gt;&lt;/span&gt;&lt;span class="mop op-limits"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.8283em;"&gt;&lt;span style="top:-1.8829em;margin-left:0em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;span class="mrel mtight"&gt;=&lt;/span&gt;&lt;span class="mord mtight"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.05em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="mop op-symbol large-op"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-4.3em;margin-left:0em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;span class="mbin mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.2671em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.1514em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mop"&gt;cos&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;π&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.3714em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;kn&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.686em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;b&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;munderover&gt;&lt;mo&gt;∑&lt;/mo&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;/mrow&gt;&lt;/munderover&gt;&lt;msub&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/msub&gt;&lt;mi&gt;sin&lt;/mi&gt;&lt;mo&gt;⁡&lt;/mo&gt;&lt;mrow&gt;&lt;mo fence="true"&gt;(&lt;/mo&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;mi&gt;π&lt;/mi&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mfrac&gt;&lt;mo fence="true"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;b_k = -\sum_{n=0}^{N-1} u_n \sin\left(2\pi \frac{kn}{N}\right)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8444em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;b&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:3.0954em;vertical-align:-1.2671em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;−&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mop op-limits"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.8283em;"&gt;&lt;span style="top:-1.8829em;margin-left:0em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;span class="mrel mtight"&gt;=&lt;/span&gt;&lt;span class="mord mtight"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.05em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="mop op-symbol large-op"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-4.3em;margin-left:0em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;span class="mbin mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.2671em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.1514em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mop"&gt;sin&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;π&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.3714em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;kn&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.686em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;기호가 여러 개 나오니 하나씩 뜯어보자. 먼저 재료부터 보면, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/msub&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;u_n&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.5806em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.1514em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;은 원래 신호의 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;n&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;번째 칸의 값이고 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;N&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;은 전체 칸 수다.&lt;/p&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mo&gt;∑&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sum&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mop op-symbol small-op" style="position:relative;top:0em;"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;은 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;n&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;을 0부터 끝까지 바꿔가며 전부 더하라는 뜻이다. 코드로 치면 &lt;code class="language-text"&gt;reduce&lt;/code&gt;로 합계를 구하는 것과 같은데, 아래 코드에서는 이를 한 번에 해주는 es-toolkit의 &lt;code class="language-text"&gt;sumBy&lt;/code&gt;를 썼다. &lt;code class="language-text"&gt;sumBy(배열, 함수)&lt;/code&gt;는 배열의 원소마다 함수를 적용한 결과를 전부 더해준다.&lt;/p&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;cos&lt;/mi&gt;&lt;mo&gt;⁡&lt;/mo&gt;&lt;mrow&gt;&lt;mo fence="true"&gt;(&lt;/mo&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;mi&gt;π&lt;/mi&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mfrac&gt;&lt;mo fence="true"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\cos\left(2\pi \frac{kn}{N}\right)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.2301em;vertical-align:-0.35em;"&gt;&lt;/span&gt;&lt;span class="mop"&gt;cos&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size1"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;π&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8801em;"&gt;&lt;span style="top:-2.655em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.394em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;kn&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.345em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size1"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;은 전체 구간에서 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;번 출렁이는 기준 사인파의 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;n&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;번째 칸 값이다. 괄호 안이 복잡해 보이지만, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;N&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;칸을 지나는 동안 딱 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;번 출렁이도록 맞춰둔 장치 정도로 보면 된다.&lt;/p&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;a_k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.5806em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;와 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;b&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;b_k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8444em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;b&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;처럼 코사인과 사인 두 줄로 나눠 계산하는 건 사인파의 세기뿐 아니라 옆으로 밀린 정도까지 기록하기 위해서인데, 이 글에서는 세기만 신경 써도 충분하다.&lt;/p&gt;
&lt;p&gt;그러니까 이 수식이 하는 일은 “신호와 기준 사인파를 칸마다 곱해서 전부 더하기”다. 신호 안에 그 사인파가 많이 들어 있으면 둘의 봉우리와 골짜기가 잘 겹쳐서 합이 커지고, 없으면 양수와 음수가 상쇄되어 0에 가까워진다. 기준 사인파를 하나씩 대보면서 얼마나 잘 겹치는지 재보는 패턴 매칭이다.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;import&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; sumBy &lt;span class="token punctuation"&gt;}&lt;/span&gt; &lt;span class="token keyword"&gt;from&lt;/span&gt; &lt;span class="token string"&gt;'es-toolkit'&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;Component&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt; sine&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt; &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;waveAngle&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellCount&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;2&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;PI&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; index &lt;span class="token operator"&gt;*&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; toComponents &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Component&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; cellCount &lt;span class="token operator"&gt;=&lt;/span&gt; signal&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cellCount &lt;span class="token operator"&gt;/&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    cosine&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token function"&gt;sumBy&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; value &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;cos&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function"&gt;waveAngle&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    sine&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token operator"&gt;-&lt;/span&gt;&lt;span class="token function"&gt;sumBy&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; value &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;sin&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function"&gt;waveAngle&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; fromComponents &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;components&lt;span class="token operator"&gt;:&lt;/span&gt; Component&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; cellCount &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;components&lt;span class="token punctuation"&gt;.&lt;/span&gt;length &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;weight&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;index &lt;span class="token operator"&gt;===&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt; &lt;span class="token operator"&gt;||&lt;/span&gt; index &lt;span class="token operator"&gt;===&lt;/span&gt; components&lt;span class="token punctuation"&gt;.&lt;/span&gt;length &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt; &lt;span class="token operator"&gt;?&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt; &lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    &lt;span class="token punctuation"&gt;(&lt;/span&gt;cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="token function"&gt;sumBy&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;components&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; index&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token keyword"&gt;const&lt;/span&gt; angle &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;waveAngle&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
        &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token function"&gt;weight&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;cosine &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;cos&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;angle&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;-&lt;/span&gt; sine &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;sin&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;angle&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
      &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;toComponents&lt;/code&gt;가 스펙트럼을 구하는 함수이고, &lt;code class="language-text"&gt;fromComponents&lt;/code&gt;가 스펙트럼으로 다시 파형을 합치는 함수다. &lt;code class="language-text"&gt;Component&lt;/code&gt;의 &lt;code class="language-text"&gt;cosine&lt;/code&gt;과 &lt;code class="language-text"&gt;sine&lt;/code&gt;이 수식의 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;a_k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.5806em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;와 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;b&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;b_k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8444em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;b&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;에 해당하고, 수식의 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 &lt;code class="language-text"&gt;index&lt;/code&gt;, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;n&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;은 &lt;code class="language-text"&gt;cell&lt;/code&gt;, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;N&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;은 &lt;code class="language-text"&gt;cellCount&lt;/code&gt;로 옮겨졌다. 수식의 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mo&gt;∑&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sum&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mop op-symbol small-op" style="position:relative;top:0em;"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;은 &lt;code class="language-text"&gt;sumBy&lt;/code&gt;가 됐을 뿐이고, 스펙트럼의 칸마다 신호 전체를 한 번씩 훑는 이중 반복문이다.&lt;/p&gt;
&lt;p&gt;하나 눈여겨볼 점은 &lt;code class="language-text"&gt;toComponents&lt;/code&gt;가 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;를 칸 수의 절반까지만 센다는 것이다. 64칸짜리 신호 안에서 사인파가 출렁일 수 있는 횟수는 32번이 한계라서 그 이상은 볼 필요가 없기 때문이다. 그 대신 &lt;code class="language-text"&gt;fromComponents&lt;/code&gt;에서 다시 합칠 때는 맨 처음과 맨 마지막 사인파를 뺀 나머지를 두 배로 쳐준다. 실제 FNO 구현도 이렇게 절반짜리 스펙트럼을 쓴다.&lt;/p&gt;
&lt;p&gt;직접 실험을 해보자. 3번 출렁이는 저주파 사인파와 10번 출렁이는 고주파 사인파를 섞은 신호를 만들고 스펙트럼을 구해본다.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;const&lt;/span&gt; cellCount &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token number"&gt;64&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;const&lt;/span&gt; grid &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;2&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;PI&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;const&lt;/span&gt; signal &lt;span class="token operator"&gt;=&lt;/span&gt; grid&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;position&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;sin&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;3&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; position&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt; &lt;span class="token number"&gt;0.3&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;sin&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;10&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; position&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; strengths &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;toComponents&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;hypot&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cosine&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;Math.sin(3 * position)&lt;/code&gt;은 전체 구간에서 3번 출렁이는 사인파이고, &lt;code class="language-text"&gt;strengths&lt;/code&gt;에는 스펙트럼의 칸마다 그 사인파가 얼마나 센지가 담긴다.&lt;/p&gt;
&lt;p&gt;&lt;code class="language-text"&gt;strengths&lt;/code&gt;를 찍어보면 3번과 10번 칸에서만 값이 튀어 오르고 나머지는 거의 0이 되는 것을 볼 수 있는데, 눈으로 보기에는 구불구불한 선이었던 신호가 스펙트럼으로 보면 “3번 사인파 이만큼, 10번 사인파 조금”이라는 아주 단순한 정보로 바뀌는 것이다.&lt;/p&gt;
&lt;p&gt;참고로 이 &lt;code class="language-text"&gt;toComponents&lt;/code&gt;는 필자가 이해를 위해 가장 단순하게 짠 버전이라 칸 수가 늘어나면 겁나 느려진다. 실제로는 중간 계산을 재사용해서 같은 결과를 훨씬 빠르게 내는 FFT(Fast Fourier Transform)라는 알고리즘을 쓴다. 필자가 짠 버전은 칸이 100만 개라면 1조 번 가까이 계산하지만 FFT는 대략 2,000만 번 정도면 해결되기 때문에 차이가 크다.&lt;/p&gt;
&lt;p&gt;그런데 스펙트럼을 알면 뭐가 좋아지는 걸까? 여기서 이 글 전체에서 가장 중요한 관찰이 나온다.&lt;/p&gt;
&lt;h3 id="블러를-걸면-고주파부터-사라진다" style="position:relative;"&gt;블러를 걸면 고주파부터 사라진다&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%EB%B8%94%EB%9F%AC%EB%A5%BC-%EA%B1%B8%EB%A9%B4-%EA%B3%A0%EC%A3%BC%ED%8C%8C%EB%B6%80%ED%84%B0-%EC%82%AC%EB%9D%BC%EC%A7%84%EB%8B%A4" aria-label="블러를 걸면 고주파부터 사라진다 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;필자는 앞에서 열이 퍼지는 과정이 마치 블러 필터와 같다고 했다. 그렇다면 사진에 블러를 걸면 가장 먼저 사라지는 게 뭘까? 바로 머리카락 한 올, 글씨의 가장자리 같은 날카롭고 세밀한 부분이다. 반면 “왼쪽은 밝고 오른쪽은 어둡다” 같은 큰 윤곽은 블러를 꽤 여러 번 걸어도 남아 있다.&lt;/p&gt;
&lt;p&gt;이걸 사인파로 다시 설명하면 블러는 고주파를 빠르게 지우고 저주파는 천천히 지운다는 것으로 이해할 수 있다. 열이 퍼질 때도 정확히 같은 일이 일어난다. 실제로 열 퍼짐 규칙을 스펙트럼에 적용해서 풀면 각 사인파의 세기가 시간에 따라 이렇게 변한다.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mover accent="true"&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;msub&gt;&lt;mover accent="true"&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mtext&gt; &lt;/mtext&gt;&lt;msup&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mrow&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\hat{u}_k(t) = \hat{u}_k(0) \, e^{-\alpha k^2 t}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.6944em;"&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="accent-body" style="left:-0.2222em;"&gt;&lt;span class="mord"&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.2869em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.6944em;"&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="accent-body" style="left:-0.2222em;"&gt;&lt;span class="mord"&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.0369em;"&gt;&lt;span style="top:-3.113em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8913em;"&gt;&lt;span style="top:-2.931em;margin-right:0.0714em;"&gt;&lt;span class="pstrut" style="height:2.5em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mover accent="true"&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\hat{u}_k(0)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.6944em;"&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="accent-body" style="left:-0.2222em;"&gt;&lt;span class="mord"&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;은 처음 스펙트럼에 적힌 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;번 사인파의 세기이고, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mover accent="true"&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\hat{u}_k(t)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.6944em;"&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="accent-body" style="left:-0.2222em;"&gt;&lt;span class="mord"&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 시간 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;t&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6151em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;가 지난 뒤의 세기다. 그 사이에 곱해지는 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mrow&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;e^{-\alpha k^2 t}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.9869em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.9869em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8913em;"&gt;&lt;span style="top:-2.931em;margin-right:0.0714em;"&gt;&lt;span class="pstrut" style="height:2.5em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 시간 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;t&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6151em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;가 지난 뒤 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;번 사인파가 얼마나 남는지를 나타내는 비율이다. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mrow&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mtext&gt;무언가&lt;/mtext&gt;&lt;/mrow&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;e^{-\text{무언가}}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8413em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8413em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord hangul_fallback mtight"&gt;무언가&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 “무언가”가 0이면 1이고, 커질수록 0에 가까워지는 값이다. 그러니까 이 식은 “자주 출렁이는 고주파가 시간이 지날수록 더 빨리 사라진다”는 말이다.&lt;/p&gt;
&lt;p&gt;숫자로 보면 더 실감이 난다. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\alpha = 1&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0.1&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;t = 0.1&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6151em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;일 때 1번 출렁이는 저주파는 약 90%가 남는다. 5번 출렁이는 사인파는 약 8%만 남는다. 10번 출렁이는 고주파는 0.005%로 사실상 사라진다고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;그리고 이 식을 코드로 옮기면, 앞에서 &lt;code class="language-text"&gt;update&lt;/code&gt;를 수천 번 돌리며 노가다를 해야 했던 시뮬레이션이 스펙트럼 위에서는 곱셈 한 번으로 끝난다.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;const&lt;/span&gt; solveHeat &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;initial&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; time&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;fromComponents&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    &lt;span class="token function"&gt;toComponents&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;initial&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; index&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
      &lt;span class="token keyword"&gt;const&lt;/span&gt; remaining &lt;span class="token operator"&gt;=&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;exp&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token operator"&gt;-&lt;/span&gt;alpha &lt;span class="token operator"&gt;*&lt;/span&gt; index &lt;span class="token operator"&gt;*&lt;/span&gt; index &lt;span class="token operator"&gt;*&lt;/span&gt; time&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
      &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token operator"&gt;:&lt;/span&gt; cosine &lt;span class="token operator"&gt;*&lt;/span&gt; remaining&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine&lt;span class="token operator"&gt;:&lt;/span&gt; sine &lt;span class="token operator"&gt;*&lt;/span&gt; remaining &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;스펙트럼의 &lt;code class="language-text"&gt;index&lt;/code&gt;가 곧 수식의 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;라서, 스펙트럼을 구하고, 각 사인파의 세기에 남는 비율을 곱하고 다시 합치는 게 전부다.&lt;/p&gt;
&lt;p&gt;푸리에 변환이 원래 열 퍼짐 문제를 풀려고 만들어진 도구라는 게 이 대목에서 실감이 난다. 칸 위에서 보면 “이웃과 비교해서 조금씩 조정하기”를 끝없이 반복해야 하는 문제가, 스펙트럼 위에서 보면 “사인파마다 슬라이더 하나씩 조절하기”로 바뀐다. 같은 데이터라도 어떤 관점에서 보느냐에 따라 문제의 난이도가 이렇게 달라진다.&lt;/p&gt;
&lt;p&gt;그렇다면 날씨도 이렇게 풀면 되지 않을까? 아쉽게도 이런 깔끔한 지름길은 열 퍼짐처럼 단순한 규칙에서만 나온다. 열 퍼짐에서는 사인파들이 서로 간섭하지 않고 각자 자기 속도로 사라지기 때문에 사인파마다 슬라이더를 따로 조절하는 것만으로 정답이 나온다.&lt;/p&gt;
&lt;p&gt;하지만 공기 흐름 같은 복잡한 문제는 사정이 다르다. 바람이 바람을 실어 나르기 때문에 큰 소용돌이가 부서지면서 작은 소용돌이를 만들어내고, 작은 소용돌이들이 모여서 또 다시 큰 흐름에 영향을 준다. 스펙트럼으로 보면 저주파와 고주파가 서로 마구 뒤섞이는 것이다.&lt;/p&gt;
&lt;p&gt;이런 문제에서는 한 프레임짜리 규칙은 정확히 알아도, 그걸 수만 번 반복한 결과로 한 번에 건너뛰는 지름길은 수식으로 구할 수 없다. 그래서 그 지름길을 데이터에서 배우는 신경망이 필요해진다.&lt;/p&gt;
&lt;p&gt;그래도 열 퍼짐에서 얻은 관찰은 두 가지 중요한 힌트를 준다.&lt;/p&gt;
&lt;p&gt;첫 번째 힌트는 고주파는 버려도 되는 경우가 많다는 것이다. 어차피 금방 사라질 고주파까지 정성껏 계산할 필요 없이, 저주파 몇 개만 잘 다루면 대부분의 정보를 잡을 수 있다. JPEG 압축이 사진 용량을 줄이는 원리도 이와 비슷하다.&lt;/p&gt;
&lt;p&gt;JPEG는 사진을 주파수 성분으로 바꾼 뒤, 사람 눈에 잘 안 띄는 고주파 정보를 과감하게 버린다. 어차피 사람 눈으로 보면 그 나물에 그 밥이기 때문이다. 물론 소용돌이가 끝없이 잘게 쪼개지는 난류처럼 고주파가 중요한 문제에서는 이게 약점이 되어, 예측이 뭉개지는 원인이 되기도 한다.&lt;/p&gt;
&lt;p&gt;두 번째 힌트는 슬라이더가 칸이 아니라 사인파에 붙어 있다는 것이다. 신호를 64칸으로 샘플링하든 256칸으로 샘플링하든, 3번 출렁이는 사인파는 똑같이 3번 출렁이는 사인파다. 그러니 “3번 사인파는 40%만 남겨라”라는 규칙은 해상도와 무관하게 적용할 수 있다.&lt;/p&gt;
&lt;p&gt;이건 필자처럼 음향에 대한 작업을 해본 사람이라면 익숙한 이야기일 수 있다. 44.1kHz로 녹음한 파일이든 96kHz로 녹음한 파일이든, “100Hz 대역을 3dB 올려라”라는 이퀄라이저 설정은 똑같이 적용된다. 샘플레이트는 소리를 얼마나 촘촘하게 기록했느냐의 문제일 뿐, 100Hz라는 소리 자체의 성질은 바뀌지 않기 때문이다.&lt;/p&gt;
&lt;p&gt;바로 이 두 힌트가 FNO의 출발점이 된다.&lt;/p&gt;
&lt;h2 id="재료를-조립해서-신경망-만들기" style="position:relative;"&gt;재료를 조립해서 신경망 만들기&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%EC%9E%AC%EB%A3%8C%EB%A5%BC-%EC%A1%B0%EB%A6%BD%ED%95%B4%EC%84%9C-%EC%8B%A0%EA%B2%BD%EB%A7%9D-%EB%A7%8C%EB%93%A4%EA%B8%B0" aria-label="재료를 조립해서 신경망 만들기 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;재료는 모두 모였으니 이제 조립할 차례다. 조립은 두 단계로 이루어진다. 먼저 열 퍼짐에서 얻은 두 힌트를 그대로 신경망 구조로 옮긴 FNO를 만들고, 그 위에 네 번째 질문의 답인 검산을 얹으면 PINO가 된다.&lt;/p&gt;
&lt;h3 id="슬라이더를-스스로-맞추는-이퀄라이저-fno" style="position:relative;"&gt;슬라이더를 스스로 맞추는 이퀄라이저, FNO&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%8D%94%EB%A5%BC-%EC%8A%A4%EC%8A%A4%EB%A1%9C-%EB%A7%9E%EC%B6%94%EB%8A%94-%EC%9D%B4%ED%80%84%EB%9D%BC%EC%9D%B4%EC%A0%80-fno" aria-label="슬라이더를 스스로 맞추는 이퀄라이저 fno permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;2020년 칼텍의 종위 리(Zongyi Li)와 애니마 아난드쿠마르(Anima Anandkumar) 등이 발표한 FNO는 지금까지의 아이디어를 그대로 신경망으로 옮긴 구조다. 한 줄로 요약하면 FNO는 “슬라이더 위치를 스스로 학습하는 이퀄라이저”다.&lt;/p&gt;
&lt;p&gt;열 퍼짐 문제에서는 각 사인파의 슬라이더를 어디에 둬야 하는지 남는 비율 공식으로 정확히 알고 있었다. 공기 흐름이나 플라스마처럼 사인파끼리 섞이는 문제에서는 그 값을 공식으로 구할 수 없다. 그렇다면 처음 상태를 넣으면 나중 상태가 나오는 예제를 잔뜩 보여주면서 슬라이더 위치를 학습시키면 된다. 바로 이게 FNO의 핵심 아이디어다.&lt;/p&gt;
&lt;p&gt;한 레이어를 수식으로 쓰면 이렇다.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;out&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mi&gt;σ&lt;/mi&gt;&lt;mo fence="false" stretchy="true" minsize="1.8em" maxsize="1.8em"&gt;(&lt;/mo&gt;&lt;mi&gt;W&lt;/mi&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;in&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;msup&gt;&lt;mi mathvariant="script"&gt;F&lt;/mi&gt;&lt;mrow&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;/mrow&gt;&lt;/msup&gt;&lt;mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em"&gt;(&lt;/mo&gt;&lt;mi&gt;R&lt;/mi&gt;&lt;mo&gt;⋅&lt;/mo&gt;&lt;mi mathvariant="script"&gt;F&lt;/mi&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;in&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em"&gt;)&lt;/mo&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo fence="false" stretchy="true" minsize="1.8em" maxsize="1.8em"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;v_{\text{out}}(x) = \sigma\Big( W v_{\text{in}}(x) + \mathcal{F}^{-1}\big( R \cdot \mathcal{F}(v_{\text{in}}) \big)(x) \Big)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.2806em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;out&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.8em;vertical-align:-0.65em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;σ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size2"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.13889em;"&gt;W&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3175em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;in&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.2141em;vertical-align:-0.35em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathcal" style="margin-right:0.09931em;"&gt;F&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8641em;"&gt;&lt;span style="top:-3.113em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size1"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.00773em;"&gt;R&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;⋅&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.8em;vertical-align:-0.65em;"&gt;&lt;/span&gt;&lt;span class="mord mathcal" style="margin-right:0.09931em;"&gt;F&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3175em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;in&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size1"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size2"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;기호가 많아 보이지만 이미 다 아는 것들이다. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;in&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;v_{\text{in}}(x)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3175em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;in&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 레이어에 들어온 값 중 위치 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;x&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;의 값이고, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;out&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;v_{\text{out}}(x)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.2806em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;out&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 레이어에서 나가는 값이다. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi mathvariant="script"&gt;F&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\mathcal{F}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathcal" style="margin-right:0.09931em;"&gt;F&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 스펙트럼 구하기, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi mathvariant="script"&gt;F&lt;/mi&gt;&lt;mrow&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;/mrow&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\mathcal{F}^{-1}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8141em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathcal" style="margin-right:0.09931em;"&gt;F&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8141em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;은 스펙트럼으로 다시 합치기다. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;R&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;R&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.00773em;"&gt;R&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;은 학습할 슬라이더 값들이다. 그러니까 괄호 안의 오른쪽 항은 스펙트럼을 구하고, 슬라이더만큼 세기를 조절하고, 다시 합친다는 뜻이다.&lt;/p&gt;
&lt;p&gt;왼쪽 항 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;W&lt;/mi&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;in&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;W v_{\text{in}}(x)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.13889em;"&gt;W&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3175em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;in&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 각 칸에서 자기 자신의 값만 보고 살짝 변환하는 부분이고, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;σ&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sigma&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;σ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 활성화 함수라고 부르는 비선형 변환이다. 이 둘은 왜 필요할까?&lt;/p&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;W&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;W&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.13889em;"&gt;W&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 항은 스펙트럼을 거치지 않고 칸마다 원래 값을 바로 넘겨주는 길이다. 이퀄라이저는 고주파를 잘라내다 보니 세밀한 정보를 잃기 쉬운데, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;W&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;W&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.13889em;"&gt;W&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 항이 잃은 정보를 보완해준다고 이해하면 충분하다.&lt;/p&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;σ&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sigma&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;σ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;가 필요한 이유는 이퀄라이저만으로는 할 수 없는 일이 있기 때문이다. 이퀄라이저는 사인파마다 세기를 키우거나 줄일 뿐, 3번 사인파를 10번 사인파로 바꾸지는 못한다. 그래서 이퀄라이저를 열 번 연달아 걸어도 결국 슬라이더 값만 다른 이퀄라이저 하나를 건 것과 같고, 레이어를 아무리 쌓아도 소용이 없다.&lt;/p&gt;
&lt;p&gt;그런데 앞에서 본 공기 흐름처럼 큰 소용돌이가 작은 소용돌이를 만드는 문제를 풀려면, 사인파끼리 섞이면서 새로운 사인파가 생겨나는 연산이 필요하다. 그 역할을 하는 게 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;σ&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sigma&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;σ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;다. 여기서는 가장 단순한 형태인 “음수는 0으로 바꾸는 함수”를 쓴다.&lt;/p&gt;
&lt;p&gt;이 단순한 함수가 어떻게 사인파를 섞을까? 1번 사인파 하나에서 음수 부분을 0으로 바꾸면, 아랫부분이 평평하게 깎인 모양이 된다. 이건 더 이상 사인파 하나로 나타낼 수 없는 모양이라, 스펙트럼을 구해보면 원래 없던 2번, 4번 같은 사인파들이 새로 생겨나 있다. 이퀄라이저가 잘라낸 고주파를 다음 레이어에서 다시 만들어낼 수 있는 것도 이 덕분이다.&lt;/p&gt;
&lt;p&gt;대략 이해했다면 이제 수식을 코드로 옮겨보자. 먼저 이퀄라이저 부분이다.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;const&lt;/span&gt; applyEqualizer &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; sliders&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;fromComponents&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    &lt;span class="token function"&gt;toComponents&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; index&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
      &lt;span class="token keyword"&gt;const&lt;/span&gt; gain &lt;span class="token operator"&gt;=&lt;/span&gt; sliders&lt;span class="token punctuation"&gt;[&lt;/span&gt;index&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;??&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
      &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token operator"&gt;:&lt;/span&gt; cosine &lt;span class="token operator"&gt;*&lt;/span&gt; gain&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine&lt;span class="token operator"&gt;:&lt;/span&gt; sine &lt;span class="token operator"&gt;*&lt;/span&gt; gain &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 함수는 위에서 작성했던 &lt;code class="language-text"&gt;solveHeat&lt;/code&gt;와 거의 똑같이 생겼지만, 남는 비율 공식 대신 &lt;code class="language-text"&gt;sliders&lt;/code&gt; 배열에서 값을 꺼내 쓴다는 차이가 있다.&lt;/p&gt;
&lt;p&gt;&lt;code class="language-text"&gt;sliders&lt;/code&gt; 배열의 길이가 곧 남겨둘 사인파의 개수인데, 슬라이더를 12개만 두면 0번부터 11번까지의 저주파만 살아남고 그보다 높은 고주파는 &lt;code class="language-text"&gt;?? 0&lt;/code&gt;에 걸려서 전부 버려진다. 첫 번째 힌트였던 “고주파는 버려도 된다”가 이 한 줄에 들어 있다. 실제 FNO는 세기뿐 아니라 사인파를 옆으로 미는 정도까지 학습하지만, 원리를 이해하는 데는 세기만으로 충분하다.&lt;/p&gt;
&lt;p&gt;이제 이퀄라이저를 만들었으니 레이어 하나와, 레이어를 여러 층 쌓은 모델 전체를 만들 수 있다.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;FourierLayer&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
  sliders&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  scale&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  bias&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;relu&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;max&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; value&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; applyLayer &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; sliders&lt;span class="token punctuation"&gt;,&lt;/span&gt; scale&lt;span class="token punctuation"&gt;,&lt;/span&gt; bias &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; FourierLayer&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;zipWith&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token function"&gt;applyEqualizer&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token punctuation"&gt;,&lt;/span&gt; sliders&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;,&lt;/span&gt; equalized&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token function"&gt;relu&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;scale &lt;span class="token operator"&gt;*&lt;/span&gt; value &lt;span class="token operator"&gt;+&lt;/span&gt; bias &lt;span class="token operator"&gt;+&lt;/span&gt; equalized&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;fourierNeuralOperator&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;layers&lt;span class="token operator"&gt;:&lt;/span&gt; FourierLayer&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  layers&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;reduce&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;applyLayer&lt;span class="token punctuation"&gt;,&lt;/span&gt; input&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;수식의 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;W&lt;/mi&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;in&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;W v_{\text{in}}(x)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.13889em;"&gt;W&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3175em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;in&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;가 &lt;code class="language-text"&gt;scale * value + bias&lt;/code&gt;로, 이퀄라이저 부분이 &lt;code class="language-text"&gt;applyEqualizer&lt;/code&gt;로, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;σ&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sigma&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;σ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;가 &lt;code class="language-text"&gt;relu&lt;/code&gt;로 옮겨졌다.&lt;/p&gt;
&lt;p&gt;물론 실제 FNO는 칸마다 숫자 하나가 아니라 수십 개의 채널을 두는 등 더 복잡하지만, 핵심 뼈대는 이 정도로도 구현 가능하다. 논문의 수식은 꽤 무시무시해 보였는데, 코드로 옮기고 나니 “스펙트럼 구하고, 저주파 몇 개만 슬라이더로 조절하고, 되돌린다”는 세 단계 정도로 간단하게 표현된다.&lt;/p&gt;
&lt;p&gt;그렇다면 학습은 구체적으로 무엇을 하는 걸까? 이건 뭐 대부분의 AI 모델이 학습하는 방법과 유사한데, 결국은 모델의 예측이 정답과 얼마나 다른지를 하나의 숫자로 계산한 뒤, &lt;code class="language-text"&gt;sliders&lt;/code&gt;, &lt;code class="language-text"&gt;scale&lt;/code&gt;, &lt;code class="language-text"&gt;bias&lt;/code&gt; 값을 그 숫자가 줄어드는 방향으로 조금씩 고쳐나가는 것이다. 이 숫자를 로스(loss)라고 부른다.&lt;/p&gt;
&lt;p&gt;어느 방향으로 고쳐야 하는지는 값 하나를 아주 살짝 움직였을 때 로스가 늘어나는지 줄어드는지를 보면 알 수 있다. 신경망 라이브러리는 이 방향을 모든 값에 대해 한 번에 자동으로 계산해주는 기능을 갖고 있고, 이걸 자동 미분이라고 부른다. 이 기능은 뒤에서 카테터 사례를 이야기할 때 다시 등장한다.&lt;/p&gt;
&lt;p&gt;이 구조가 앞에서 이야기한 격자 데이터의 문제들을 어떻게 다루는지 보자.&lt;/p&gt;
&lt;p&gt;먼저 계산량이다. FFT 덕분에 스펙트럼을 구하고 되돌리는 게 빠르고, 슬라이더 조절은 남겨둔 사인파 12개에 대해서만 하면 된다. &lt;small&gt;(실제 FNO는 채널이 여러 개라서 사인파마다 작은 행렬 곱셈을 한 번씩 한다)&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;다음은 시야다. 가장 낮은 주파수를 가진 1번 사인파는 전체 구간에 걸쳐 크게 한 번 출렁인다. 이 사인파의 세기를 조절하는 건 전체 구간의 정보를 한 번에 섞는 것과 같다. 그래서 FNO는 레이어 하나만으로도 공간 전체를 한 번에 본다고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;이런 점에서는 모든 토큰을 한 번에 보는 어텐션과 닮았는데, 어텐션이 “누가 누구를 참고할지”를 입력마다 새로 계산하는 반면 FNO는 “어떤 사인파를 얼마나 남길지”라는 고정된 규칙을 배운다는 게 다른 점이다.&lt;/p&gt;
&lt;p&gt;마지막으로 가장 흥미로운 해상도 독립성이다. 슬라이더가 칸이 아니라 사인파에 붙어 있으니, 같은 슬라이더를 64칸짜리 입력에도 256칸짜리 입력에도 그대로 쓸 수 있다. 직접 확인해보자.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;const&lt;/span&gt; sample &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;field&lt;span class="token operator"&gt;:&lt;/span&gt; Field&lt;span class="token punctuation"&gt;,&lt;/span&gt; resolution&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;resolution&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token function"&gt;field&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;2&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;PI&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; resolution&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; initialTemperature&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token function-variable function"&gt;Field&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;position&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;exp&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token operator"&gt;-&lt;/span&gt;&lt;span class="token number"&gt;4&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;position &lt;span class="token operator"&gt;-&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;PI&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;**&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; sliders &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;12&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;waves&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;exp&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token operator"&gt;-&lt;/span&gt;&lt;span class="token number"&gt;0.05&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; waves &lt;span class="token operator"&gt;*&lt;/span&gt; waves&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; coarse &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;applyEqualizer&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function"&gt;sample&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;initialTemperature&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;64&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; sliders&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;const&lt;/span&gt; fine &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;applyEqualizer&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function"&gt;sample&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;initialTemperature&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;256&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; sliders&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;같은 온도 분포를 64칸과 256칸으로 각각 샘플링한 다음, 같은 슬라이더를 적용했다. 여기서는 학습을 거치는 대신, 앞의 남는 비율 공식에서 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0.05&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\alpha t = 0.05&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6151em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.05&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;인 경우의 값을 슬라이더에 직접 넣었다. FNO가 열 퍼짐 데이터로 학습한다면 이상적으로는 바로 이 값을 찾아내야 한다.&lt;/p&gt;
&lt;p&gt;&lt;code class="language-text"&gt;coarse[cell]&lt;/code&gt;과 &lt;code class="language-text"&gt;fine[4 * cell]&lt;/code&gt;을 비교해보면 소수점 아래 열 몇 자리까지 같은 값이 나온다. 64칸 기준으로 정한 슬라이더를 256칸짜리 입력에 그대로 써도 같은 결과를 낸다는 뜻이다. 물론 이건 이퀄라이저 부분만 떼어서 비교한 것이다. 실제 FNO는 레이어 사이에 &lt;code class="language-text"&gt;relu&lt;/code&gt; 같은 비선형 연산이 끼어 있어서 해상도가 바뀌면 결과가 조금씩 달라지고, 64칸 데이터에 애초에 없던 세부를 256칸에서 새로 알아내는 것도 아니다.&lt;/p&gt;
&lt;p&gt;“같은 온도 분포를 샘플링했으니 당연한 거 아닌가”라는 생각이 들 수 있으니, 대조군도 하나 만들어보자. 이번에는 앞에서 이야기한 것처럼 칸 번호를 기준으로 관계를 정해둔 규칙이다.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;const&lt;/span&gt; cellRule &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token keyword"&gt;const&lt;/span&gt; left &lt;span class="token operator"&gt;=&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;%&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;const&lt;/span&gt; right &lt;span class="token operator"&gt;=&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell &lt;span class="token operator"&gt;+&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;%&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token number"&gt;0.5&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; value &lt;span class="token operator"&gt;+&lt;/span&gt; &lt;span class="token number"&gt;0.25&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;left &lt;span class="token operator"&gt;+&lt;/span&gt; right&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;applyTimes&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function-variable function"&gt;rule&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; times&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; input&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;times&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;reduce&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token function"&gt;rule&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; input&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; coarseCell &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;applyTimes&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cellRule&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;10&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token function"&gt;sample&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;initialTemperature&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;64&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;const&lt;/span&gt; fineCell &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;applyTimes&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cellRule&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;10&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token function"&gt;sample&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;initialTemperature&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;256&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;cellRule&lt;/code&gt;은 “자기 값 절반, 바로 옆 칸 값 4분의 1씩 섞어라”라는, 칸 단위로 정해진 규칙이다. 64칸에서 이 규칙을 10번 적용하면 막대 가운데의 가장 높은 온도가 1에서 약 0.85까지 내려가는데, 256칸에서는 약 0.99로 거의 그대로다. 256칸에서의 “바로 옆 칸”은 실제로는 4분의 1 거리밖에 안 되니, 같은 규칙을 적용해도 열이 훨씬 조금 퍼진 것이다. 칸 번호를 기준으로 관계를 배운 모델이 해상도에 묶이는 이유가 바로 이것이다.&lt;/p&gt;
&lt;p&gt;FNO 논문에서는 이 성질을 제로샷 초해상도라고 불렀다. 거친 해상도로 학습한 모델을 한 번도 본 적 없는 촘촘한 해상도에서 그대로 돌려보는 실험을 보여줬고, 후속 PINO 논문은 이렇게 해상도를 올려도 정확도가 떨어지지 않았다고 보고했다.&lt;/p&gt;
&lt;h3 id="정답-없이-채점하는-방법-pino" style="position:relative;"&gt;정답 없이 채점하는 방법, PINO&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%EC%A0%95%EB%8B%B5-%EC%97%86%EC%9D%B4-%EC%B1%84%EC%A0%90%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-pino" aria-label="정답 없이 채점하는 방법 pino permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;FNO까지 오면 빠르고 해상도에 자유로운 모델은 만들 수 있다. 그런데 아직 문제가 하나 남아 있다. FNO도 결국 정답을 보고 배운다는 점이다.&lt;/p&gt;
&lt;p&gt;학습을 시키려면 “이런 초기 조건이면 결과는 이렇다”라는 예제가 천 개 단위로 필요한데, 결국 그 정답은 누가 만드냐? 앞에서 며칠씩 걸린다고 했던 그 느린 시뮬레이터가 만든다.&lt;/p&gt;
&lt;p&gt;아니 시뮬레이션이 느려서 신경망을 만들려는 건데, 그 신경망을 학습시키려면 느린 시뮬레이션을 천 번 넘게 돌려야 하는 이상한 상황이 펼쳐지는 것이다. 게다가 예제만 보고 흉내 낸 모델은 여전히 물리 법칙을 지킨다는 보장이 없다.&lt;/p&gt;
&lt;p&gt;그렇다면 정답 없이 채점할 수는 없을까?&lt;/p&gt;
&lt;p&gt;학창 시절 수학 시험을 떠올려보자. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;5&lt;/mn&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;mn&gt;6&lt;/mn&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;x^2 - 5x + 6 = 0&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8974em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8141em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;5&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;6&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;을 풀어서 &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;x = 2&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;라는 답을 얻었다면, 답지가 없어도 맞았는지 확인할 방법이 있다. 원래 식에 2를 넣어보면 된다. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mn&gt;4&lt;/mn&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;10&lt;/mn&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;mn&gt;6&lt;/mn&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;4 - 10 + 6 = 0&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;4&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;10&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;6&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;이니 맞은 답이다. 이게 검산이다.&lt;/p&gt;
&lt;p&gt;PINO의 아이디어가 바로 이것이다. 앞에서 우리는 공기 흐름의 지름길은 모르지만, 한 프레임짜리 규칙은 정확히 안다고 했다. 그렇다면 모델이 내놓은 예측을 그 규칙에 직접 넣어보고, 규칙을 얼마나 어겼는지를 로스에 더할 수 있다. 열 퍼짐이라면 모델이 예측한 온도 변화를 보고 “양 옆이 더 뜨거운 칸이 정말 그만큼 데워졌는가”를 칸마다 확인하는 식이다. 이 채점에는 정답 데이터가 필요 없고, 한 프레임짜리 규칙만 알면 된다.&lt;/p&gt;
&lt;p&gt;그런데 여기에는 함정이 하나 있는데, 바로 모든 칸에 0을 찍는 성의 없는 답도 어찌어찌 검산을 통과한다는 점이다. 온도가 전부 0이면 양 옆과의 차이도 0이고 변화도 0이니, 규칙을 완벽하게 지킨 셈이 된다.&lt;/p&gt;
&lt;p&gt;다만 이 함정은 시작 상태로 막을 수 있다. 규칙은 온도가 어떻게 변하는지만 말해줄 뿐, 어디서 출발하는지는 말해주지 않는다. 앞의 &lt;code class="language-text"&gt;simulateHeat&lt;/code&gt;에서도 &lt;code class="language-text"&gt;heatStep&lt;/code&gt;이라는 규칙만으로는 결과가 정해지지 않았고, &lt;code class="language-text"&gt;reduce&lt;/code&gt;에 넘긴 처음 값 &lt;code class="language-text"&gt;initial&lt;/code&gt;이 있어야 결과가 하나로 정해졌다.&lt;/p&gt;
&lt;p&gt;PINO도 마찬가지다. 비록 최종 결과는 모르더라도 처음 온도 분포는 우리가 직접 넣어준 것이니 알고 있기 때문이다. 그래서 “예측의 시작이 우리가 넣어준 처음 상태와 같은가”를 함께 채점하면, 규칙을 지키는 수많은 답 가운데 우리가 준 시작점에서 출발하는 답 하나로 좁혀진다.&lt;/p&gt;
&lt;p&gt;PINO의 로스를 말로 쓰면 이렇게 된다.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mtext&gt;로스&lt;/mtext&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mtext&gt;시작 상태와의 차이&lt;/mtext&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;mtext&gt;정답과의 차이&lt;/mtext&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;mi&gt;λ&lt;/mi&gt;&lt;mo&gt;×&lt;/mo&gt;&lt;mtext&gt;규칙 위반 정도&lt;/mtext&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\text{로스} = \text{시작 상태와의 차이} + \text{정답과의 차이} + \lambda \times \text{규칙 위반 정도}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord hangul_fallback"&gt;로스&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7667em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord hangul_fallback"&gt;시작&lt;/span&gt;&lt;span class="mord"&gt; &lt;/span&gt;&lt;span class="mord hangul_fallback"&gt;상태와의&lt;/span&gt;&lt;span class="mord"&gt; &lt;/span&gt;&lt;span class="mord hangul_fallback"&gt;차이&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7667em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord hangul_fallback"&gt;정답과의&lt;/span&gt;&lt;span class="mord"&gt; &lt;/span&gt;&lt;span class="mord hangul_fallback"&gt;차이&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7778em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;λ&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord hangul_fallback"&gt;규칙&lt;/span&gt;&lt;span class="mord"&gt; &lt;/span&gt;&lt;span class="mord hangul_fallback"&gt;위반&lt;/span&gt;&lt;span class="mord"&gt; &lt;/span&gt;&lt;span class="mord hangul_fallback"&gt;정도&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;정답과의 차이는 정답 데이터가 있을 때만 채점하는 항목이고, 규칙 위반 정도가 새로 추가된 검산 항목이다. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;λ&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\lambda&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;λ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;는 검산 항목을 얼마나 중요하게 볼지 정하는 비율이다. 실제 PINO 논문의 식도 이렇게 몇 가지 항목에 가중치를 붙여 더하는 구조다.&lt;/p&gt;
&lt;p&gt;사실 검산을 하려면 한 시점의 결과만으로는 부족하고, 시간이 흐르면서 온도가 어떻게 변해가는지까지 봐야 한다. 실제 PINO는 시간까지 연속적인 함수로 다루면서 온도가 변하는 속도를 미분으로 바로 구하는데, 이걸 코드로 다 옮기기에는 너무 번거로우니 여기서는 그냥 모델이 여러 시점의 온도 분포를 한꺼번에 예측해서 배열로 돌려준다고 치자. 이 배열은 궤적이라는 뜻으로 &lt;code class="language-text"&gt;Trajectory&lt;/code&gt;라고 부르겠다.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;import&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; meanBy&lt;span class="token punctuation"&gt;,&lt;/span&gt; windowed &lt;span class="token punctuation"&gt;}&lt;/span&gt; &lt;span class="token keyword"&gt;from&lt;/span&gt; &lt;span class="token string"&gt;'es-toolkit'&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;Trajectory&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;meanSquare&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;values&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token function"&gt;meanBy&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;values&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; value &lt;span class="token operator"&gt;*&lt;/span&gt; value&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;gap&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;predicted&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; expected&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;meanSquare&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function"&gt;zipWith&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;predicted&lt;span class="token punctuation"&gt;,&lt;/span&gt; expected&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;a&lt;span class="token punctuation"&gt;,&lt;/span&gt; b&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; a &lt;span class="token operator"&gt;-&lt;/span&gt; b&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;lawViolation&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;trajectory&lt;span class="token operator"&gt;:&lt;/span&gt; Trajectory&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;meanSquare&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    &lt;span class="token function"&gt;windowed&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;trajectory&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;flatMap&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;current&lt;span class="token punctuation"&gt;,&lt;/span&gt; next&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="token function"&gt;zipWith&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;current&lt;span class="token punctuation"&gt;,&lt;/span&gt; next&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token function"&gt;neighborPull&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;current&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;now&lt;span class="token punctuation"&gt;,&lt;/span&gt; later&lt;span class="token punctuation"&gt;,&lt;/span&gt; pull&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;later &lt;span class="token operator"&gt;-&lt;/span&gt; now&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; timeStep &lt;span class="token operator"&gt;-&lt;/span&gt; alpha &lt;span class="token operator"&gt;*&lt;/span&gt; pull&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;gap&lt;/code&gt;은 두 온도 분포가 얼마나 다른지 재는 함수다. 칸마다 차이를 제곱해서 평균을 내는데, 이건 너무 뜨겁게 나오든 너무 차갑게 나오든 똑같이 로스로 치기 위해서다.&lt;/p&gt;
&lt;p&gt;&lt;code class="language-text"&gt;lawViolation&lt;/code&gt;은 연속된 두 시점을 한 쌍씩 묶은 다음, 각 칸에 대해 “실제로 온도가 변한 속도”와 “규칙대로라면 변했어야 할 속도”의 차이를 계산한다. 시뮬레이션에 썼던 &lt;code class="language-text"&gt;neighborPull&lt;/code&gt;이 여기서 그대로 다시 쓰인다는 점이 포인트이다. 시뮬레이터는 이 규칙으로 다음 상태를 만들어냈고, PINO는 같은 규칙으로 모델의 답을 검사한다. 모델이 규칙을 완벽하게 지켰다면 이 값은 0이 된다.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;LossInput&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
  prediction&lt;span class="token operator"&gt;:&lt;/span&gt; Trajectory&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  initial&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  answer&lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  alpha&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  timeStep&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  cellSize&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  lambda&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;pinoLoss&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt; prediction&lt;span class="token punctuation"&gt;,&lt;/span&gt; initial&lt;span class="token punctuation"&gt;,&lt;/span&gt; answer&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;,&lt;/span&gt; lambda &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; LossInput&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; startGap &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;gap&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;prediction&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; initial&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; answerGap &lt;span class="token operator"&gt;=&lt;/span&gt; answer &lt;span class="token operator"&gt;?&lt;/span&gt; &lt;span class="token function"&gt;gap&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;prediction&lt;span class="token punctuation"&gt;[&lt;/span&gt;prediction&lt;span class="token punctuation"&gt;.&lt;/span&gt;length &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; answer&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;return&lt;/span&gt; startGap &lt;span class="token operator"&gt;+&lt;/span&gt; answerGap &lt;span class="token operator"&gt;+&lt;/span&gt; lambda &lt;span class="token operator"&gt;*&lt;/span&gt; &lt;span class="token function"&gt;lawViolation&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;prediction&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;initial&lt;/code&gt;은 필수이고 &lt;code class="language-text"&gt;answer&lt;/code&gt;는 선택적 인자라는 점에 주목하자. 시작 상태는 우리가 넣어준 것이니 항상 알고 있지만, 정답은 있을 수도 있고 없을 수도 있다. 정답이 있으면 정답과의 차이도 보고, 없으면 시작 상태와 검산만으로 채점한다. 이것이 바로 PINO가 가진 유연함이다.&lt;/p&gt;
&lt;p&gt;앞에서 소개한 “데이터가 적어도 된다”는 성질이 바로 이 채점 방식에서 나온다. 비싼 시뮬레이션 정답은 거친 해상도로 조금만 준비하고, 검산은 훨씬 촘촘한 해상도에서 한다. FNO가 해상도에 자유로우니 둘을 섞어 쓸 수 있는 것이다.&lt;/p&gt;
&lt;p&gt;극단적으로는 정답 없이 시작 상태와 검산만으로도 학습이 되고, 논문에서는 이 방식으로 소용돌이가 복잡하게 얽힌 흐름 문제까지 풀어냈다고 보고했다. 물론 이 방법도 만능은 아니라서 같은 흐름을 훨씬 긴 시간 동안 예측하는 문제에 대해 정답 데이터 없이는 PINO도 제대로 풀지 못했다고 한다.&lt;/p&gt;
&lt;p&gt;검산은 실전에서도 쓸모가 있다. 처음 보는 문제가 들어오면 정답은 몰라도 규칙은 알고 있으니, 예측을 한 뒤 그 문제 하나에 대해 “규칙을 더 잘 지키는 방향”으로 모델을 조금 더 다듬을 수 있다.&lt;/p&gt;
&lt;p&gt;사실 물리 법칙으로 채점한다는 아이디어 자체는 PINO가 처음은 아니다. 이 아이디어가 널리 알려진 계기는 2019년에 발표된 PINN(Physics-Informed Neural Network)이라는 방법인데, PINN은 문제 하나를 풀 때마다 신경망을 처음부터 새로 학습시켜야 했다.&lt;/p&gt;
&lt;p&gt;게다가 저주파와 고주파가 복잡하게 뒤섞인 문제, 예를 들어 큰 소용돌이와 작은 소용돌이가 얽힌 흐름에서는 학습이 잘 되지 않는 경우가 많았다. 신경망은 원래 저주파부터 쉽게 배우고 고주파는 잘 배우지 못하는 경향이 있기 때문이다. PINO는 이 검산 방식을 FNO 위에 얹어서, 문제 하나가 아니라 비슷한 문제들 전체를 푸는 규칙을 배우게 만들었다. 한 번 학습하면 새로운 조건이 들어와도 추론 한 번으로 답을 낸다.&lt;/p&gt;
&lt;p&gt;이제 처음의 질문, “어떻게 이런 일이 가능할까”에 대한 답을 모아볼 수 있다. 시뮬레이션이 느린 건 한 프레임짜리 단순한 규칙을 아주 촘촘하게 반복해야 하기 때문이었다. 스펙트럼 위에서 보면 그 반복이 슬라이더 조절로 바뀌고, 슬라이더는 칸이 아니라 사인파에 붙어 있어서 해상도와 상관이 없다. FNO는 공식으로 구할 수 없는 슬라이더 값을 데이터로 학습하는 구조이고, PINO는 거기에 한 프레임짜리 규칙으로 하는 검산을 붙여서, 정답이 부족해도 물리적으로 말이 되는 답을 내도록 만든 것이다.&lt;/p&gt;
&lt;h2 id="현실-속의-neural-operator" style="position:relative;"&gt;현실 속의 Neural Operator&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%ED%98%84%EC%8B%A4-%EC%86%8D%EC%9D%98-neural-operator" aria-label="현실 속의 neural operator permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;원리를 알고 나면 자연스럽게 두 가지가 궁금해진다. 실제로 어디에 쓰이고 있는지, 그리고 그 결과를 얼마나 믿을 수 있는지다.&lt;/p&gt;
&lt;p&gt;미리 짚어둘 점이 하나 있다. 여기서 소개하는 사례 상당수는 검산 없이 시뮬레이션 데이터로만 학습한 FNO 계열 모델이다. PINO는 이 FNO에 검산을 더한 확장이니, 아래 사례들은 PINO의 바탕이 되는 기술이 현실에서 어떻게 쓰이고 있는지를 보여주는 것으로 이해하면 된다.&lt;/p&gt;
&lt;h3 id="어디에-쓰이고-있을까" style="position:relative;"&gt;어디에 쓰이고 있을까&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%EC%96%B4%EB%94%94%EC%97%90-%EC%93%B0%EC%9D%B4%EA%B3%A0-%EC%9E%88%EC%9D%84%EA%B9%8C" aria-label="어디에 쓰이고 있을까 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;여기까지 이해하고 나면 이 기술이 어디에 쓰일지 대략 감이 온다. 공통점은 기존에 시뮬레이션을 몇 시간에서 며칠씩 돌려야 했던 일, 그리고 그걸 수천 번 반복해야 의미가 생기는 일이다.&lt;/p&gt;
&lt;p&gt;가장 대표적인 분야는 기상 예측이다. 엔비디아가 2022년에 발표한 FourCastNet은 전 지구 날씨를 예측하는 모델인데, 흥미롭게도 트랜스포머 뼈대에서 어텐션 자리를 FNO식 푸리에 연산으로 바꾼 구조를 쓴다. 논문에서는 일주일 치 예보를 2초도 안 걸려 만들어내며, 기존 수치 예보보다 몇 자릿수 빠르다고 보고했다.&lt;/p&gt;
&lt;p&gt;다만 이 속도는 이미 학습을 마친 모델로 예측만 하는 시간이라 학습에 든 비용은 빠져 있고, 정확도도 당시 ECMWF의 수치 예보보다는 조금 낮았다. 그리고 이 모델 역시 앞에서 이야기한 수십 년 치 대기 기록으로 학습했다는 점에서, 날씨가 데이터 부자 분야라는 사실을 다시 확인시켜준다.&lt;/p&gt;
&lt;p&gt;속도가 이렇게 빨라지면 초기 조건을 조금씩 바꾼 예측을 수백, 수천 개 돌려보면서 “태풍이 이 경로로 갈 확률이 70%“처럼 불확실성까지 계산할 수 있다. 기존 방식으로는 비용 때문에 수십 개 정도가 한계였다.&lt;/p&gt;
&lt;p&gt;자동차와 항공기 설계에도 쓰인다. 차체 모양에 따라 공기 저항이 어떻게 달라지는지 예측하는 문제는 전통적으로 느린 시뮬레이션의 영역이었다.&lt;/p&gt;
&lt;p&gt;칼텍과 엔비디아 연구진이 발표한 GINO(Geometry-Informed Neural Operator)라는 모델은 복잡한 3차원 차체 모양을 받아서 표면에 걸리는 압력을 예측하는데, 공기 저항 계산에서 GPU로 최적화한 기존 시뮬레이터보다 약 2만 6천 배 빠르다고 보고했다. 설계안 하나를 평가하는 시간이 이만큼 줄어들면, 사람이 직관으로 고른 몇 개의 설계안이 아니라 수천 개의 후보를 훑어보는 것이 현실적인 선택지가 된다.&lt;/p&gt;
&lt;p&gt;앞에서 소개한 카테터 사례는 조금 다른 방향인데, 예측을 빠르게 하는 데서 그치지 않고 아예 예측을 거꾸로 돌려서 썼기 때문이다.&lt;/p&gt;
&lt;p&gt;필자는 FNO 섹션에서 신경망 라이브러리는 값을 살짝 움직였을 때 로스가 어느 방향으로 변하는지 자동으로 계산할 수 있다고 했다. 같은 기능을 모델의 가중치가 아니라 입력에 적용하면, “돌기의 크기나 간격을 어느 쪽으로 바꿔야 세균이 덜 올라오는가”를 계산할 수 있고 연구진도 실제로 이 방법으로 돌기 모양을 다듬었다.&lt;/p&gt;
&lt;p&gt;결과를 예측하는 도구를 뒤집어서, 원하는 결과를 내는 설계를 찾는 도구로 쓴 것이다. 기존 시뮬레이터로도 adjoint 방법처럼 거꾸로 따라가는 기법이 있긴 하지만, 시뮬레이터마다 따로 구현해야 하고 계산도 무겁다.&lt;/p&gt;
&lt;p&gt;이 밖에도 땅속에 묻은 이산화탄소가 수십 년에 걸쳐 어떻게 퍼질지 예측하는 탄소 저장 연구, 핵융합로 안의 플라스마 변화를 기존 시뮬레이션보다 백만 배가량 빠르게 예측하는 연구, 지진파 전파 예측, 3D 프린팅 중 생기는 뒤틀림 예측, 공장 설비를 실시간 물리 모델로 복제해두는 디지털 트윈 등 다양한 분야로 연구가 넓어지고 있다.&lt;/p&gt;
&lt;h3 id="그래서-만능일까" style="position:relative;"&gt;그래서 만능일까&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%EA%B7%B8%EB%9E%98%EC%84%9C-%EB%A7%8C%EB%8A%A5%EC%9D%BC%EA%B9%8C" aria-label="그래서 만능일까 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;이쯤 되면 “그럼 PINO가 물리 시뮬레이션의 정답이구나”라는 생각이 들 수 있다. 클로드와 같은 LLM이 만능이 아닌 것처럼 PINO도 만능이라고 볼 수는 없다. 각각의 신경망 구조가 가지는 강력한 점과 한계가 공존하는 것이다.&lt;/p&gt;
&lt;p&gt;PINO의 첫 번째 한계는 물리 법칙을 반드시 지키는 건 아니라는 점이다. PINO의 검산 항목은 로스에 더해지는 값일 뿐 강제 규칙이 아니다. 물론 모델은 로스를 줄이는 방향으로 학습하겠지만, 100% 위반하지 않는다는 법은 없다.&lt;/p&gt;
&lt;p&gt;즉, PINO는 물리 법칙을 “대체로 잘 지키는” 모델이지 “반드시 지키는” 모델은 아니라는 의미이다. PINO를 소개하는 글 중에는 물리 문제를 오류 없이 푼다는 뉘앙스를 풍기는 것도 있는데, 원리를 따라가보면 그보다는 기존 방법보다 훨씬 빠르게, 꽤 적은 오류로 근사한다는 쪽이 더 정확한 표현이다.&lt;/p&gt;
&lt;p&gt;두 번째는 푸리에 변환 자체의 제약이다. 기본적인 푸리에 변환은 신호가 같은 모양으로 끝없이 반복된다고 가정하고, 칸도 반듯한 격자로 나뉘어 있어야 잘 동작한다. 하지만 실제 자동차나 혈관은 모양도 제멋대로이고 반복되지도 않는다. 그래서 이를 보완하기 위해 복잡한 모양을 반듯한 격자로 펴주는 방법이나, 가장자리를 더 정확하게 다루는 방법 같은 후속 연구가 계속 나오고 있다.&lt;/p&gt;
&lt;p&gt;세 번째는 배운 범위 밖의 상황에 약하다는 점이다. 느린 물 흐름만 보고 배운 모델에게 초음속 기류를 물어보면 제대로 답하기 어렵다. 이건 사실 모든 기계학습 모델이 가진 한계이고, 트랜스포머도 마찬가지다. 그래서 실무에서는 PINO 같은 모델로 수천 개의 후보를 빠르게 걸러내고, 최종 후보 몇 개만 기존 시뮬레이터로 꼼꼼하게 검증하는 방식이 더 현실적인 쓰임새다.&lt;/p&gt;
&lt;p&gt;그리고 트랜스포머와의 관계도 흑백으로 볼 문제는 아니다. 앞에서 FNO가 어텐션처럼 전체를 한 번에 본다고 했는데, 실제로 최근에는 어텐션을 Neural Operator의 틀 안에서 다시 설계한 연구도 활발하다.&lt;/p&gt;
&lt;p&gt;결국 핵심은 트랜스포머냐 아니냐가 아니라, 데이터가 얼마나 있는지, 그리고 문제에 대해 이미 알고 있는 규칙을 모델에 얼마나 담을 수 있는지에 있다고 생각한다. 데이터가 넘치면 데이터에서 배우면 되고, 데이터가 부족하면 아는 규칙을 가르치면 된다.&lt;/p&gt;
&lt;h2 id="마치며" style="position:relative;"&gt;마치며&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/#%EB%A7%88%EC%B9%98%EB%A9%B0" aria-label="마치며 permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;필자가 이 주제를 파보게 된 건 단순한 의문 때문이었다. 요즘은 다들 LLM을 만능처럼 이야기하는데 정말 만능일 리는 없지 않은가? 그렇다면 LLM이 못하는 건 뭐고, 그 한계는 어디서 오는 걸까?&lt;/p&gt;
&lt;p&gt;따라가 보니 답은 생각보다 단순했는데, 결국 모델은 본 만큼만 안다는 것이다. 클로드가 날씨를 계산하지 못하는 건 트랜스포머라는 구조가 모자라서가 아니라 대기를 본 적이 없기 때문이고, 같은 트랜스포머라도 수십 년 치 대기 기록을 본 녀석은 기존 수치 예보보다 날씨를 더 잘 맞히기도 한다. 즉, “AI가 이걸 할 수 있을까?”라는 질문은 사실 “AI가 이걸 본 적이 있을까?”라는 질문에 더 가깝다.&lt;/p&gt;
&lt;p&gt;그렇다면 본 적이 없는 문제는 어떻게 해야 할까? PINO가 내놓은 답은 그럴듯한 답을 그냥 믿지 않고, 이미 알고 있는 규칙으로 검산하는 것이었다. 정답 데이터는 없어도 방정식은 알고 있으니, 모델의 답이 방정식을 얼마나 어겼는지를 따져서 물리적으로 말이 되는 답 쪽으로 끌고 가는 것이다.&lt;/p&gt;
&lt;p&gt;필자는 사실 이건 개발자가 매일 LLM과 일하는 상황과도 크게 다르지 않다고 생각한다. LLM은 우리 서비스의 코드베이스나 도메인 규칙을 본 적이 없어서 그 부분에서는 그럴듯해 보이는 코드를 만들어낼 수밖에 없기 때문이다.&lt;/p&gt;
&lt;p&gt;그래서 중요한 것은 LLM의 답을 믿을지 말지 고민하는 것보다, PINO가 방정식으로 채점하듯 타입, 테스트, 도메인 규칙으로 그 답을 검산할 수 있는 구조를 만들어두는 것이다. 그리고 결국 데이터가 부족한 곳에서 AI를 제대로 굴러가게 만드는 건 그 문제의 규칙을 알고 있는 사람의 몫이라고 생각한다.&lt;/p&gt;
&lt;p&gt;이상으로 클로드는 왜 날씨를 계산하지 못할까 포스팅을 마친다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/</id>
    <link href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/"/>
    <summary type="html">이번 포스팅에서는 클로드나 GPT 같은 AI에게 날씨를 계산해달라고 하면 어떤 일이 벌어지는지, 그리고 데이터가 부족한 물리 문제를 풀기 위해 등장한 PINO(Physics-Informed Neural Operator)라는 방식에 대한 이야기를 해보려고 한다. 요즘 주변을 보면 클로드나 GPT 같은 LLM을 거의 만능 도구처럼 생각하는 분들이 꽤 많은 것 같다. 뭐 AI를 활용해서 코드도 짜고, 논문도 요약하고, 계약서도 검토하는 세상이니 그렇게 느끼는 것도 무리는 아니다. 필자 역시 하루 중 상당 시간을 이런 AI와 함께 일하고 있고, 확실히 예전에 비해 AI의 성능이 발전하고 있음을 느낀다. 그런데 여기에는 함정이 하나 있다. 만약 우리가 LLM에게 “내일 서울의 기온 변화를 계산해줘”라고 부탁하면, LLM은 실제로 대기의 온도와 기압이 어떻게 변할지 방정식을 푸는 대신, 계산 결과처럼 보이는 그럴듯한 문장을 만들어낸다. 왜냐하면 LLM은 엄청난 양의 글을 읽으며 언어를 배운 모델이고, 지구의 대기가 움직이는 모습을 보며 배운 모델이 아니기 때문이다. 사실 클로드와 같은 트랜스포머 구조로 만든 모델도 날씨 예측 분야에서 이미 기존 수치 예보와 겨룰 만한 성적을 내고 있기는 하다. 다만 이런 모델들은 수십 년 치 지구 대기 기록을 학습한 녀석들이고, 날씨처럼 학습 데이터가 넉넉한 분야는 사실 예외에 가깝다. 자동차 주변의 공기 흐름이나 핵융합로 안의 플라스마처럼 대부분의 물리 문제는 학습시킬 데이터가 턱없이 부족하다. 대신 이런 문제들에는 한 가지 핵심적인 포인트가 있는데, 바로 그 현상을 지배하는 방정식을 우리가 이미 알고 있다는 것이다. 그래서 이번 포스팅에서는 LLM이 날씨를 계산하지 못하는 이유에서 출발해서, 정답 데이터 대신 물리 방정식으로 스스로를 채점하며 배우는 PINO라는 방식을 소개하고 이 친구가 어떻게 동작하는지 한번 따라가 보려고 한다.</summary>
    <title>클로드는 왜 날씨를 계산하지 못할까</title>
    <updated>2026-10-04T05:31:22+09:00</updated>
    <dc:date>2026-10-04T05:31:22+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>문동욱</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;In this post, I want to talk about what actually happens when you ask an AI like Claude or GPT to compute the weather, and about PINO (Physics-Informed Neural Operator), an approach that emerged to tackle physics problems where data is scarce.&lt;/p&gt;
&lt;p&gt;Looking around these days, quite a few people seem to treat LLMs like Claude or GPT as an all-purpose tool. And in a world where AI writes code, summarizes papers, and reviews contracts, it’s not hard to see why. I spend a good chunk of my day working alongside these AIs too, and I can definitely feel how much better they’ve gotten compared to before.&lt;/p&gt;
&lt;p&gt;But there’s a catch. If we ask an LLM to “compute how the temperature in Seoul will change tomorrow,” it doesn’t actually solve the equations for how the atmosphere’s temperature and pressure will evolve. Instead, it produces a plausible-sounding sentence that looks like the result of a computation.&lt;/p&gt;
&lt;p&gt;That’s because an LLM is a model that learned language by reading an enormous amount of text, not a model that learned by watching the Earth’s atmosphere move.&lt;/p&gt;
&lt;p&gt;To be fair, models built on the same Transformer architecture as Claude are already posting results in weather forecasting that rival traditional numerical weather prediction. But those models were trained on decades of records of the Earth’s atmosphere, and a field like weather, where training data is plentiful, is actually closer to the exception.&lt;/p&gt;
&lt;p&gt;Most physics problems, like the airflow around a car or the plasma inside a fusion reactor, have nowhere near enough data to train on. What these problems do have is one crucial thing going for them: we already know the equations that govern the phenomenon.&lt;/p&gt;
&lt;p&gt;So in this post, I’ll start from why LLMs can’t compute the weather, then introduce PINO, an approach that learns by scoring itself against physics equations instead of answer data, and walk through how it actually works.&lt;/p&gt;
&lt;!-- more --&gt;
&lt;h2 id="what-transformers-are-good-at-and-what-they-arent" style="position:relative;"&gt;What Transformers Are Good At, and What They Aren’t&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#what-transformers-are-good-at-and-what-they-arent" aria-label="what transformers are good at and what they arent permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;To talk about PINO, we first need to cover what the Transformer is, the neural network architecture that Claude and GPT have in common, and the real reason Claude can’t compute the weather.&lt;/p&gt;
&lt;h3 id="what-claude-and-gpt-have-in-common" style="position:relative;"&gt;What Claude and GPT Have in Common&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#what-claude-and-gpt-have-in-common" aria-label="what claude and gpt have in common permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;The AIs we use all the time, like Claude, GPT, and Gemini, are usually grouped together as LLMs (Large Language Models). As the name says, they’re models that read an enormous amount of text and learned how to handle language.&lt;/p&gt;
&lt;p&gt;These models come from different companies and vary in capability, but in fact most of today’s LLMs are built on the same blueprint, called the Transformer. It’s an architecture published by Google researchers in 2017, and its trace is already visible in the name GPT (Generative Pre-trained Transformer).&lt;/p&gt;
&lt;p&gt;To borrow an analogy developers will find familiar, the Transformer is closer to a framework, and Claude or GPT is closer to an app built with that framework. Just as Toss (a Korean fintech app) and Netflix are completely different services even though both are built with React, each company’s model differs in the data it was trained on and its detailed design, even though they’re all based on the same Transformer. Of course, since each company modifies the Transformer in its own way, the relationship isn’t as clean-cut as a framework, but they’re similar in that they share the same skeleton.&lt;/p&gt;
&lt;p&gt;So when we look at why Claude can’t compute the weather, we need to separate two cases: asking a conversational AI to do a physics calculation directly, and training the Transformer blueprint itself on physics data. I’ll come back to this distinction later. First, let’s see how the Transformer works.&lt;/p&gt;
&lt;h3 id="attention-an-operation-that-compares-every-word-against-every-other" style="position:relative;"&gt;Attention, an Operation That Compares Every Word Against Every Other&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#attention-an-operation-that-compares-every-word-against-every-other" aria-label="attention an operation that compares every word against every other permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;LLMs do so many things for us that they feel almost omnipotent, but if you sum up how they produce answers in a single sentence, it comes down to looking at the text so far and predicting the next piece of a word. These word pieces are called “tokens.”&lt;/p&gt;
&lt;p&gt;For example, a word like “unbelievable” gets split inside the model into a few pieces like “un”, “believ”, and “able”, and each piece is turned into an array of numbers. The model looks at this sequence of arrays and probabilistically picks what the next piece will be.&lt;/p&gt;
&lt;p&gt;This is also related to why Claude’s answers trickle out bit by bit when we chat with it. The model doesn’t build the whole answer in advance. It picks one token, looks again at the text with that token appended, picks the next token, and builds up the answer that way. So instead of making the user wait until the answer is fully done, showing it as it’s being produced is far less frustrating from the user’s side.&lt;/p&gt;
&lt;p&gt;But to pick the next piece well, the model first has to properly understand the text so far. Say we’re choosing what comes after “I bought an apple yesterday, and it was so”. To come up with words like “sweet” or “expensive”, you need to know that “it” refers to the apple. The problem is that the token “it” by itself carries no information at all. In the end, a word’s meaning is determined by its relationships with the words around it.&lt;/p&gt;
&lt;p&gt;So before picking the next token, the Transformer lets each token look around at the other tokens in the text, pull in the information it needs, and rewrite its own array of numbers. “It” pulls information from “apple” and turns into an array that carries the meaning “the it that refers to the apple.” This process of tokens exchanging information with each other is attention, the core of the Transformer blueprint. After repeating this process across many layers, the context of the entire preceding text is baked into the last token’s array, and the model looks at that array to pick the next token.&lt;/p&gt;
&lt;p&gt;Written as a formula, attention looks like this.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mtext&gt;Attention&lt;/mtext&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;Q&lt;/mi&gt;&lt;mo separator="true"&gt;,&lt;/mo&gt;&lt;mi&gt;K&lt;/mi&gt;&lt;mo separator="true"&gt;,&lt;/mo&gt;&lt;mi&gt;V&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mtext&gt;softmax&lt;/mtext&gt;&lt;mrow&gt;&lt;mo fence="true"&gt;(&lt;/mo&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;Q&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;K&lt;/mi&gt;&lt;mi&gt;T&lt;/mi&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;msqrt&gt;&lt;mi&gt;d&lt;/mi&gt;&lt;/msqrt&gt;&lt;/mfrac&gt;&lt;mo fence="true"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;mi&gt;V&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d}}\right)V&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;Attention&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;Q&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.07153em;"&gt;K&lt;/span&gt;&lt;span class="mpunct"&gt;,&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.22222em;"&gt;V&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:2.4684em;vertical-align:-0.95em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;softmax&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.5183em;"&gt;&lt;span style="top:-2.1778em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.9322em;"&gt;&lt;span class="svg-align" style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord" style="padding-left:0.833em;"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-2.8922em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="hide-tail" style="min-width:0.853em;height:1.08em;"&gt;&lt;svg xmlns="http://www.w3.org/2000/svg" width="400em" height="1.08em" viewbox="0 0 400000 1080" preserveaspectratio="xMinYMin slice"&gt;&lt;path d="M95,702
c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14
c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54
c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10
s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429
c69,-144,104.5,-217.7,106.5,-221
l0 -0
c5.3,-9.3,12,-14,20,-14
H400000v40H845.2724
s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7
c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z
M834 80h400000v40h-400000z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.1078em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;Q&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.07153em;"&gt;K&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8413em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.13889em;"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.93em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.22222em;"&gt;V&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;There are a lot of symbols attached, but what it does is simple. Imagine the tokens in the text each sitting in a meeting room, one per seat. Each of them holds three things: a query &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;Q&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;Q&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8778em;vertical-align:-0.1944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;Q&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; that says “here’s the information I need to pin down my meaning,” a key &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;K&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;K&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.07153em;"&gt;K&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; that says “here’s the information I have,” and a value &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;V&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;V&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.22222em;"&gt;V&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; that it can actually hand over. If you think of looking up a key with a query in a database and pulling out the value, the names should click right away. In the earlier example, the query of “it” holds “what am I referring to?”, and the key of “apple” holds “I’m something you can eat.”&lt;/p&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;Q&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;K&lt;/mi&gt;&lt;mi&gt;T&lt;/mi&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;QK^T&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.0358em;vertical-align:-0.1944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;Q&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.07153em;"&gt;K&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8413em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.13889em;"&gt;T&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is the process where everyone looks at everyone else’s key and scores “how helpful would that person be for my question?” The query of “it” and the key of “apple” match well, so they get a high score. softmax turns those scores into proportions that sum to 1, and finally each token pulls in everyone’s values according to those proportions and mixes them. So “it” mixes in mostly the value of “apple” and rewrites its own array. This is exactly what I meant earlier by “pulling information from other tokens.” You can think of dividing by &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msqrt&gt;&lt;mi&gt;d&lt;/mi&gt;&lt;/msqrt&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sqrt{d}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.04em;vertical-align:-0.1078em;"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.9322em;"&gt;&lt;span class="svg-align" style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord" style="padding-left:0.833em;"&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-2.8922em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="hide-tail" style="min-width:0.853em;height:1.08em;"&gt;&lt;svg xmlns="http://www.w3.org/2000/svg" width="400em" height="1.08em" viewbox="0 0 400000 1080" preserveaspectratio="xMinYMin slice"&gt;&lt;path d="M95,702
c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14
c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54
c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10
s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429
c69,-144,104.5,-217.7,106.5,-221
l0 -0
c5.3,-9.3,12,-14,20,-14
H400000v40H845.2724
s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7
c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z
M834 80h400000v40h-400000z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.1078em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; as just a device for keeping the size of the scores in a reasonable range.&lt;/p&gt;
&lt;p&gt;Now let’s count the cost. If there are &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;N&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; people in the meeting room, everyone has to look at everyone’s key once, so the scoring happens &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;mo&gt;×&lt;/mo&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;N \times N&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7667em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; times. &lt;small&gt;(Strictly speaking, in models like Claude or GPT each token only looks at the keys of tokens before it. It can’t peek at the part that hasn’t been written yet, but the cost is still proportional to the square of the number of tokens)&lt;/small&gt; If a piece of text consists of 1,000 tokens, that’s a million scores, which is no big deal for today’s GPUs.&lt;/p&gt;
&lt;p&gt;And this structure is a really good fit for language. Even if the thing “it” refers to is several sentences back, attention can look at every word at once regardless of distance. On top of that, since who references whom is computed fresh for every sentence, it naturally handles the flexibility of “it” referring to an apple in one sentence and a car in another. That’s a big part of why the Transformer became the standard for language models.&lt;/p&gt;
&lt;h3 id="when-you-ask-claude-about-the-weather" style="position:relative;"&gt;When You Ask Claude About the Weather&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#when-you-ask-claude-about-the-weather" aria-label="when you ask claude about the weather permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;So what happens if you ask Claude, “Given this pressure pattern, compute how the temperature in Seoul will change tomorrow”? As we saw, what Claude does is ultimately pick the next token. So Claude doesn’t actually compute the atmosphere’s temperature and pressure. It generates sentences that look like the result of a computation.&lt;/p&gt;
&lt;p&gt;What Claude learned from is an enormous amount of text written by people. There’s surely plenty of writing about the weather in there, but that’s a far cry from numerical data recording how the Earth’s atmosphere actually moved over time. Reading tens of thousands of weather articles doesn’t make you able to compute tomorrow’s weather.&lt;/p&gt;
&lt;p&gt;Of course, if you ask Claude to write weather simulation code, it’ll write pretty convincing code. So to be precise, it’s less that Claude “can’t” compute the weather and more that it “doesn’t” compute it when you ask directly.&lt;/p&gt;
&lt;p&gt;There’s one more problem here: a model that learned by looking at patterns has no guarantee of obeying the laws of physics. A language model produces plausible answers based on patterns it saw in its training data. In language, that level of plausibility is usually enough, and if it’s wrong, you can ask again or have a person check.&lt;/p&gt;
&lt;p&gt;But in the physical world, there are strict rules called conservation laws, so you can’t compute things this way. The water or energy inside a region can go up or down, but the change has to exactly match the sum of what flowed in and out across the boundary plus sources like heat or rain.&lt;/p&gt;
&lt;p&gt;A model that learned only from patterns doesn’t know these rules, though, so it can make water that never flowed in or out slowly disappear, or conjure up energy from nowhere bit by bit. What’s worse is that these small errors pile up over time. If you feed the 1-hour forecast back in as input to predict 2 hours ahead, then feed that back in to predict 3 hours ahead, and keep rolling it forward, errors that were invisible at first end up producing completely bogus weather a few days later. It’s a lot like photocopying a photocopy over and over until the text gets mushier and mushier.&lt;/p&gt;
&lt;p&gt;This isn’t a problem unique to Claude. Every model that learns patterns from data carries it, and it’s also the problem PINO tackles head-on later in this post.&lt;/p&gt;
&lt;h3 id="some-transformers-are-good-at-forecasting-the-weather" style="position:relative;"&gt;Some Transformers Are Good at Forecasting the Weather&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#some-transformers-are-good-at-forecasting-the-weather" aria-label="some transformers are good at forecasting the weather permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Does that mean Transformers can’t predict the weather at all? Not at all. If anything, weather is a field where Transformers have been a big success.&lt;/p&gt;
&lt;p&gt;Pangu-Weather, which Huawei researchers published in Nature in 2023, is a weather model built on the Transformer architecture, and it reported more accurate results than the numerical forecasts of the European Centre for Medium-Range Weather Forecasts (ECMWF) on key metrics, including 5-day forecasts. Even ECMWF, which ran those numerical forecasts, put AIFS, a Transformer-based AI forecasting model, into actual operation starting in February 2025.&lt;/p&gt;
&lt;p&gt;Of course, they didn’t just take a language model and use it as-is. They reworked it quite a bit to fit weather data. Predicting the weather means computing how values like the atmosphere’s temperature, pressure, and wind change over time, and these values aren’t chopped into discrete pieces like word tokens. For example, the temperature between Seoul and Pangyo, a city just south of it, doesn’t suddenly change at some point. It flows smoothly as a continuous value. Moving from Seoul to Pangyo doesn’t mean the temperature is 20 degrees one moment and then, ta-da, 19 degrees the next.&lt;/p&gt;
&lt;p&gt;The problem is that computers are digital and can’t handle these continuous values as-is. A value being continuous is the same as saying there are infinitely many values between 0 and 1, and a computer can’t perform that kind of infinite computation.&lt;/p&gt;
&lt;p&gt;So when computing the weather on a computer, we chop space into a fine grid and store a value in each cell. ERA5, one of the most widely used datasets in meteorology, divides the Earth into 1,440 cells horizontally and 721 cells vertically, which alone comes to about 1.04 million cells for a single point in time.&lt;/p&gt;
&lt;p&gt;If you treated each of these cells as a token and applied attention, it’d be like a meeting room with 1.04 million people where everyone checks everyone’s key, and the scoring for a single point in time alone would come to about 1 trillion operations.&lt;/p&gt;
&lt;p&gt;And that’s just for one variable at one altitude. Throw in all the altitudes and variables, and you’re in the sorry situation where no number of GPUs is enough. So models that use Transformers for weather cut costs by bundling several cells into a chunk and using that as one token, or by applying attention only between nearby cells.&lt;/p&gt;
&lt;p&gt;Besides cost, resolution is another headache. When the resolution, meaning the number of cells the Earth is divided into, changes, the rules the model learned can fall out of alignment.&lt;/p&gt;
&lt;p&gt;Transformers that handle grid data usually attach a cell index, something like “which position am I,” to each token, and based on that index they learn relationships like “reference the cell right next to you this much, and the cell three away this much.”&lt;/p&gt;
&lt;p&gt;But think about it carefully. The “cell right next to you” learned on a 64x64 grid was some number of kilometers away in real distance, while on a 256x256 grid that same “cell right next to you” is only a quarter of that distance. In other words, even when showing the same weather, if the way you divide the cells changes, the learned rules fall out of alignment, so you need to apply separate corrections or retrain the model. This is less a problem with attention itself and more a problem with representing position by cell index, and there are techniques that compensate for it, but either way it’s something any model handling grid data has to worry about.&lt;/p&gt;
&lt;p&gt;The real secret to why these models predict the weather so well lies elsewhere, though. It’s the data. ERA5, which I mentioned earlier, is a dataset in which ECMWF gathered past observation records and reconstructed the state of the Earth’s atmosphere on an hourly basis, and it now covers 1940 to the present. Both the Transformer-based Pangu-Weather and Google DeepMind’s GraphCast, which uses a different architecture called a graph neural network, were trained on 39 years of it, from 1979 to 2017. They essentially learned by watching, wholesale, how the Earth’s atmosphere moved every hour for 39 years.&lt;/p&gt;
&lt;h3 id="what-about-problems-without-data" style="position:relative;"&gt;What About Problems Without Data?&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#what-about-problems-without-data" aria-label="what about problems without data permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;The problem is that a field like weather, with decades of records neatly piled up, is actually the exception.&lt;/p&gt;
&lt;p&gt;For the airflow around a car that was just designed, there can’t be any observation records, since the car hasn’t even come into the world yet. Plasma inside a fusion reactor is hard to measure in the first place, and so is how bacteria move inside a thin tube placed in the body. In the end, if you need training data, you have to make it yourself by running a simulator thousands of times, and I’ll come back later to how painful that is.&lt;/p&gt;
&lt;p&gt;What these problems do have instead of data is one thing to lean on: the equations that govern the phenomenon. How air flows and how heat spreads have been written down as equations for nearly 200 years. So instead of showing the model a pile of answer data, why not teach it those equations? The answer to that question is the star of this post, PINO.&lt;/p&gt;
&lt;h2 id="what-problem-does-pino-solve" style="position:relative;"&gt;What Problem Does PINO Solve?&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#what-problem-does-pino-solve" aria-label="what problem does pino solve permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;So what would a tool look like that fits problems where data is scarce but the equations are known? Before taking PINO’s principles apart, let’s first look at what PINO ends up doing for you.&lt;/p&gt;
&lt;p&gt;For example, when you want to know the airflow around a car body, you run a physics simulator, a program that honestly computes the laws of physics one step at a time. This program is accurate but slow, so evaluating a single design can take anywhere from a few hours to a few days. If you have to wait days every time you tweak the shape of the car body a little, the number of designs you can try computing is bound to be pretty limited.&lt;/p&gt;
&lt;p&gt;PINO is a neural network that mimics this kind of simulator. Once trained, when you feed it new conditions, it approximates the result hundreds of times faster than the simulator, and in some cases close to a million times faster. Hearing just this much, you might think, “Isn’t that just a neural network trained on simulation results?” but PINO has a few more unusual properties.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;First, it needs little training data. At the extreme, it can learn with no answer data at all, as long as you tell it the laws of physics.&lt;/p&gt;
&lt;p&gt;Second, it directly scores how well the model’s answers obey the laws of physics during training. So rather than answers that merely look plausible, it moves toward answers that make physical sense.&lt;/p&gt;
&lt;p&gt;Third, it works regardless of resolution. Give a model trained on a coarse grid a much finer grid, and it still makes predictions as-is. It doesn’t have the problem of being tied to cell indices that we saw earlier.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A real example makes this easier to grasp. Caltech researchers used Neural Operator technology, the foundation of PINO, to redesign a catheter, a thin tube used in hospitals.&lt;/p&gt;
&lt;p&gt;When a catheter stays in the body for a long time, bacteria can swim up the tube and cause infections. Taking a cue from how bacteria swim, the researchers came up with the idea that putting triangular bumps shaped like shark fins on the inner wall of the tube would make it harder for bacteria to swim upstream.&lt;/p&gt;
&lt;p&gt;The problem is that figuring out the most effective size, height, and spacing for the bumps means running simulations for countless shapes. The researchers built a neural network that, given a bump shape, predicts how bacteria will spread inside the tube, and used it to refine the bump shape. When they tested 3D-printed prototypes, they reported that 10 to more than 100 times fewer bacteria made it upstream compared to a smooth tube.&lt;/p&gt;
&lt;p&gt;How is this possible? To answer that, we need to break the name PINO into a few pieces. In this post, I’ll follow these four questions in order.&lt;/p&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li&gt;Why are physics simulations slow in the first place?&lt;/li&gt;
&lt;li&gt;How can you build a neural network that works regardless of resolution?&lt;/li&gt;
&lt;li&gt;Why do you need the Fourier transform for that?&lt;/li&gt;
&lt;li&gt;How can you score answers without answer data?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;In this section, I’ll cover the first two questions, and then work through the Fourier transform and the scoring method one at a time in the sections that follow.&lt;/p&gt;
&lt;h3 id="why-are-physics-simulations-slow" style="position:relative;"&gt;Why Are Physics Simulations Slow?&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#why-are-physics-simulations-slow" aria-label="why are physics simulations slow permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;If you’ve ever done game development, you’re probably familiar with a physics engine’s &lt;code class="language-text"&gt;update&lt;/code&gt; function. Every frame, it takes “the current state,” computes “the state after a very short amount of time,” and repeats this 60 times a second. A ball falling and a character jumping are all just repetitions of these small updates.&lt;/p&gt;
&lt;p&gt;Physics simulations are essentially the same. The difference is that instead of one ball, you run &lt;code class="language-text"&gt;update&lt;/code&gt; on every cell in space. Physicists write this &lt;code class="language-text"&gt;update&lt;/code&gt; rule down as a formula, and that kind of formula is called a partial differential equation. The name sounds intimidating, but it’s enough to understand it as “a rule describing how each cell’s value changes over one frame.”&lt;/p&gt;
&lt;p&gt;The simplest example is the rule for how heat spreads. Heat the middle of an iron rod with a lighter and then take it away, and the hot part gradually cools while the surroundings warm up, until eventually the whole rod reaches an even temperature. Written as a formula, it looks like this.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\frac{\partial u}{\partial t} = \alpha \frac{\partial^2 u}{\partial x^2}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:2.0574em;vertical-align:-0.686em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.3714em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.686em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:2.1771em;vertical-align:-0.686em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.4911em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.7401em;"&gt;&lt;span style="top:-2.989em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8141em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.686em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;In this formula, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;u&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is temperature, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;t&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6151em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is time, and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;x&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is the position along the rod. So the left side means “how fast is this cell’s temperature changing right now,” and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;∂&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\frac{\partial^2 u}{\partial x^2}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.3629em;vertical-align:-0.345em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.0179em;"&gt;&lt;span style="top:-2.655em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.7463em;"&gt;&lt;span style="top:-2.786em;margin-right:0.0714em;"&gt;&lt;span class="pstrut" style="height:2.5em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.394em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight" style="margin-right:0.05556em;"&gt;∂&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8913em;"&gt;&lt;span style="top:-2.931em;margin-right:0.0714em;"&gt;&lt;span class="pstrut" style="height:2.5em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.345em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; on the right side means “how much hotter, on average, are the cells on either side than this cell.” &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\alpha&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is a material constant that’s large for materials that conduct heat well, like copper, and small for materials that don’t, like wood.&lt;/p&gt;
&lt;p&gt;Put into words, this formula is saying something completely obvious: “a cell whose neighbors are hotter warms up, and a cell whose neighbors are colder cools down.”&lt;/p&gt;
&lt;p&gt;And to put it in terms developers are familiar with, this is similar to an image blur filter. Blur is an operation that nudges each pixel toward the average of its surrounding pixels, so if you apply blur over and over, the image gets mushier and mushier until it eventually converges to the average value and becomes unrecognizable. Heat spreading is actually the exact same operation.&lt;/p&gt;
&lt;p&gt;Since it’s hard to grasp from the formula alone, let’s move it into code. I’ve handed off the small array chores to es-toolkit functions like &lt;code class="language-text"&gt;range&lt;/code&gt; and &lt;code class="language-text"&gt;zipWith&lt;/code&gt;.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;import&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; range&lt;span class="token punctuation"&gt;,&lt;/span&gt; zipWith &lt;span class="token punctuation"&gt;}&lt;/span&gt; &lt;span class="token keyword"&gt;from&lt;/span&gt; &lt;span class="token string"&gt;'es-toolkit'&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;Samples&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; neighborPull &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;center&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token keyword"&gt;const&lt;/span&gt; left &lt;span class="token operator"&gt;=&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;%&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;const&lt;/span&gt; right &lt;span class="token operator"&gt;=&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell &lt;span class="token operator"&gt;+&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;%&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;left &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; center &lt;span class="token operator"&gt;+&lt;/span&gt; right&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;cellSize &lt;span class="token operator"&gt;*&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; heatStep &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;zipWith&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token function"&gt;neighborPull&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;,&lt;/span&gt; pull&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; value &lt;span class="token operator"&gt;+&lt;/span&gt; alpha &lt;span class="token operator"&gt;*&lt;/span&gt; pull &lt;span class="token operator"&gt;*&lt;/span&gt; timeStep&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;simulateHeat&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;initial&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; steps&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;steps&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;reduce&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token function"&gt;heatStep&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; initial&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;Samples&lt;/code&gt; is an array of temperature values measured and recorded at each cell along the rod. &lt;code class="language-text"&gt;neighborPull&lt;/code&gt; computes how much the cells on either side pull on this cell. &lt;code class="language-text"&gt;(left - 2 * center + right)&lt;/code&gt; is shorthand for &lt;code class="language-text"&gt;(left - center) + (right - center)&lt;/code&gt;, so it comes out positive if the neighbors are hotter and negative if they’re colder.&lt;/p&gt;
&lt;p&gt;&lt;code class="language-text"&gt;heatStep&lt;/code&gt; is a one-frame &lt;code class="language-text"&gt;update&lt;/code&gt; that nudges the temperature by the amount of the pull, and &lt;code class="language-text"&gt;simulateHeat&lt;/code&gt; repeats it &lt;code class="language-text"&gt;steps&lt;/code&gt; times. Here I’ve assumed the two ends of the rod are joined into a ring.&lt;/p&gt;
&lt;p&gt;The formula looked terrifying, but once it’s in code, all it does is add and subtract a measly three neighboring values. So if the computation is this simple, why does weather prediction need a supercomputer?&lt;/p&gt;
&lt;p&gt;The first reason is that you can’t make &lt;code class="language-text"&gt;timeStep&lt;/code&gt;, the time interval of a single frame, as large as you’d like. In games, too, if frames are computed too far apart, you get bugs where a fast bullet passes right through a wall, and physics simulations are similar.&lt;/p&gt;
&lt;p&gt;If you make &lt;code class="language-text"&gt;timeStep&lt;/code&gt; too large in this code, the values of neighboring cells start bouncing up and down in alternation, the swings grow every frame, and eventually they get so large that the whole thing blows up. To run it stably, you need to satisfy roughly the following condition.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;Δ&lt;/mi&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;mo&gt;≤&lt;/mo&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;Δ&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\Delta t \le \frac{\Delta x^2}{2\alpha}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8193em;vertical-align:-0.136em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;≤&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:2.1771em;vertical-align:-0.686em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.4911em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8141em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.686em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;Δ&lt;/mi&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\Delta t&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is &lt;code class="language-text"&gt;timeStep&lt;/code&gt; in the code, and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;Δ&lt;/mi&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\Delta x&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is &lt;code class="language-text"&gt;cellSize&lt;/code&gt;. The key point is that &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi mathvariant="normal"&gt;Δ&lt;/mi&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\Delta x&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;Δ&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; appears squared on the right side, which means that if you split cells to half their size, you have to cut the time interval to a quarter. The number of cells doubles and the number of frames quadruples, so the total computation goes up 8 times. The more precisely you try to look, the more steeply the cost climbs.&lt;/p&gt;
&lt;p&gt;The second reason is that real physics is far more complicated than heat spreading. Airflow is three-dimensional, and as wind carries wind along, small eddies and large eddies influence each other. To see this properly, you need very small cells and very short time intervals, which is how you end up in the tear-jerking situation of waiting days to check a single design.&lt;/p&gt;
&lt;p&gt;In other words, physics simulations are slow not because we don’t know the rules. We know the one-frame rule exactly, but that simple rule has to be repeated an enormous number of times at a very fine granularity. So couldn’t we have a neural network learn a shortcut that skips all this repetition, where you put in the initial state and the final state comes right out? This is where we move on to the second question.&lt;/p&gt;
&lt;h3 id="a-neural-network-that-works-regardless-of-resolution" style="position:relative;"&gt;A Neural Network That Works Regardless of Resolution&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#a-neural-network-that-works-regardless-of-resolution" aria-label="a neural network that works regardless of resolution permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;The neural networks we usually know are, simply put, functions that take a fixed-length array of numbers and return an array of numbers. An image classification model takes a 224x224 array of pixels and returns an array with one probability per class, like “cat 0.9, dog 0.08, fox 0.02.” The key point is that the size of the input is baked into the model’s design.&lt;/p&gt;
&lt;p&gt;What a Neural Operator wants to do is a bit different. Comparing them as TypeScript types makes the difference clear.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token comment"&gt;// A typical neural network&lt;/span&gt;

&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;NeuralNetwork&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token comment"&gt;// Neural Operator&lt;/span&gt;

&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;Field&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;position&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;NeuralOperator&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token operator"&gt;:&lt;/span&gt; Field&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Field&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;While &lt;code class="language-text"&gt;NeuralNetwork&lt;/code&gt; takes an array, &lt;code class="language-text"&gt;NeuralOperator&lt;/code&gt; takes a &lt;code class="language-text"&gt;Field&lt;/code&gt;, “a function that tells you the value at a position when you give it the position,” and returns another &lt;code class="language-text"&gt;Field&lt;/code&gt;. For heat spreading, the input is “the initial temperature distribution” and the output is “the temperature distribution 10 seconds later.” If the earlier &lt;code class="language-text"&gt;Samples&lt;/code&gt; is an array of temperatures measured and recorded at each cell along the rod, you can think of &lt;code class="language-text"&gt;Field&lt;/code&gt; as the temperature distribution itself, before any measuring. Reading values at regular intervals from a continuous distribution like this to make an array is called sampling. &lt;small&gt;(I covered sampling in detail using sound as the example in an earlier post, &lt;a href="https://evan-moon.github.io/2019/07/10/javascript-audio-waveform/en/"&gt;How Do Computers Hear Sound?&lt;/a&gt;)&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;In math, something that takes a function and returns a function is called an operator. You can think of it as the math version of what developers usually call a higher-order function. So the name Neural Operator ultimately means “a neural network that learns a higher-order function.” That means you can use the same neural network whether you sample the input temperature distribution into 64 cells or 256 cells, and how many cells to sample the result into is up to whoever is using it.&lt;/p&gt;
&lt;p&gt;To use an analogy frontend developers will know, it’s similar to the difference between PNG and SVG. PNG represents an image as pixels, so it breaks up when you zoom in, while SVG stores rules like “draw a curve from here to there,” so it’s sharp at any size, regardless of resolution. In the end, you can think of a Neural Operator as wanting to treat physical phenomena like SVG rather than PNG.&lt;/p&gt;
&lt;p&gt;Of course, since actual training still happens on data sitting on a grid, the analogy isn’t a perfect fit, but the direction of “learning the underlying rule rather than a specific resolution” is quite similar.&lt;/p&gt;
&lt;p&gt;Why is this a natural goal? Think back to the heat-spreading rule we saw earlier. The rule “if the neighbors are hotter, you warm up” is the same whether you divide the rod into 10 cells or 1,000 cells. The rule itself has no concept of resolution. So it makes sense that a model that learned that rule should also work regardless of resolution.&lt;/p&gt;
&lt;p&gt;But that’s easier said than done. A neural network is ultimately a machine that multiplies and adds arrays of numbers, so how on earth do you make it apply the same rule regardless of array size? This is where the Fourier transform comes in.&lt;/p&gt;
&lt;h2 id="seeing-the-world-as-a-sum-of-sine-waves-with-the-fourier-transform" style="position:relative;"&gt;Seeing the World as a Sum of Sine Waves With the Fourier Transform&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#seeing-the-world-as-a-sum-of-sine-waves-with-the-fourier-transform" aria-label="seeing the world as a sum of sine waves with the fourier transform permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The Fourier transform, the star of the third question, is also the F in FNO (Fourier Neural Operator), the foundation of PINO. That makes it the most important ingredient in this post. Here, let’s first look at what the Fourier transform does, and then follow what happens when we look at heat spreading again through the lens of the Fourier transform.&lt;/p&gt;
&lt;h3 id="getting-a-spectrum-from-a-waveform" style="position:relative;"&gt;Getting a Spectrum From a Waveform&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#getting-a-spectrum-from-a-waveform" aria-label="getting a spectrum from a waveform permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Picture the equalizer in a music app. On screen, bars from bass to treble bounce up and down, with a row of sliders beneath them. Raise the bass slider and the thumping bass gets stronger; lower the treble slider and the sound gets muffled. But if you think about it, it’s strange. The sound coming out of the speaker is ultimately just one wiggly wave of vibrating air, so how can you pick out just the bass from it and boost it?&lt;/p&gt;
&lt;p&gt;This is where the Fourier transform comes in. In the early 19th century, the French mathematician Joseph Fourier proposed the idea that any waveform, no matter how complicated, can be expressed as simple sine waves stacked on top of each other. Interestingly, the reason Fourier came up with this idea was precisely to solve the heat-spreading rule we saw earlier.&lt;/p&gt;
&lt;center&gt;
  &lt;span class="gatsby-resp-image-wrapper" style="position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 640px; "&gt;
      &lt;a class="gatsby-resp-image-link" href="https://evan-moon.github.io/static/59b6365dd013753c1fa1b3bca1393b9b/5a190/fourier.png" style="display: block" target="_blank" rel="noopener"&gt;
    &lt;span class="gatsby-resp-image-background-image" style="padding-bottom: 70%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAIAAACgpqunAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB7klEQVR42o1TS5LTMBD1ao7ADVhyDg7AOWDLSaiCDSsWLNjNZqqgoIodn/AZppJMSIIdx1iS1ZKsv63YRkmoBIah4G2sT7/W69ftZPgDfddHDP+BpO9/Cd19g25MBp0P/yb/ZO348U1V1sG2Nmf1uHRl3YfuqKi/qijRRiup4sq7NoSNJpJMkaHKU63mlbxELTcx6T56s9nEmCOZMVZizDg3xnLQ1jSGG/oV8wwM1Z6oelLqJe2aYJ0rEQKAg9JEiJqnKyVERQGVGKhU0jnbmErBomIpBO3FEuNvBRX8O+V1XR/JHAC/eS+nc7MuhBAoX2NEGSglrTcNy6p8klWMUS619QUTWoqDtwlQyhGWH875ZCaXmZzNKVAA5kxLEGBCBESGZdYqa++/zl/M8bb4btvPxHmPS8TytUpz9u6jiu9XFaM0RmBEcMVs27SNezKmt54ubjw6HxU8XnV72XsPhZQEISmlpUBGnyDL43ldC6Ply0zcOctPHny5+3yBmVAcVvm6KApCSHLwvWkaEIKucvR2RFfreBJrv/dqdfJwfPvZ9KLkQx9UzO68i4Za673/bUjatuXRsy22hZ3N0M3Hn08v8dBvexu6iP6aCTtMZ+xE1BMLiVumrLR2V+Fw7bAnV/ZRT5qmIYRDuu7vP8kPSTUZqTEntNIAAAAASUVORK5CYII='); background-size: cover; display: block;"&gt;&lt;/span&gt;
  &lt;img class="gatsby-resp-image-image" alt="fourier" title="" src="https://evan-moon.github.io/static/59b6365dd013753c1fa1b3bca1393b9b/6af66/fourier.png" srcset="/static/59b6365dd013753c1fa1b3bca1393b9b/69538/fourier.png 160w,
/static/59b6365dd013753c1fa1b3bca1393b9b/72799/fourier.png 320w,
/static/59b6365dd013753c1fa1b3bca1393b9b/6af66/fourier.png 640w,
/static/59b6365dd013753c1fa1b3bca1393b9b/5a190/fourier.png 800w" sizes="(max-width: 640px) 100vw, 640px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;" loading="lazy" decoding="async"&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;small&gt;The core of the Fourier transform is that any waveform in the world can be expressed as a stack of simple sine waves&lt;/small&gt;
&lt;/center&gt;
&lt;p&gt;The left side of the figure is the wiggly waveform we actually see, the middle is the sine waves that make up that waveform, and the right is a bar graph showing how much of each sine wave is mixed in. The Fourier transform is the tool that extracts the right side from the left, and the bar graph on the equalizer screen is exactly this result. This bar graph is called the frequency spectrum, or spectrum for short. The inverse transform goes the other way, looking at the spectrum and restoring the original waveform exactly.&lt;/p&gt;
&lt;p&gt;As the figure shows, what distinguishes the sine waves from each other is how densely they oscillate. In this post, treating the whole signal as a single interval, I’ll call the number of times a sine wave oscillates over that interval &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;. This count is the frequency, and a sine wave with small &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is called low-frequency while one with large &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is called high-frequency. In music terms, low-frequency is bass and high-frequency is treble.&lt;/p&gt;
&lt;p&gt;The formula for getting the spectrum looks like this.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;munderover&gt;&lt;mo&gt;∑&lt;/mo&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;/mrow&gt;&lt;/munderover&gt;&lt;msub&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/msub&gt;&lt;mi&gt;cos&lt;/mi&gt;&lt;mo&gt;⁡&lt;/mo&gt;&lt;mrow&gt;&lt;mo fence="true"&gt;(&lt;/mo&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;mi&gt;π&lt;/mi&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mfrac&gt;&lt;mo fence="true"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;a_k = \sum_{n=0}^{N-1} u_n \cos\left(2\pi \frac{kn}{N}\right)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.5806em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:3.0954em;vertical-align:-1.2671em;"&gt;&lt;/span&gt;&lt;span class="mop op-limits"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.8283em;"&gt;&lt;span style="top:-1.8829em;margin-left:0em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;span class="mrel mtight"&gt;=&lt;/span&gt;&lt;span class="mord mtight"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.05em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="mop op-symbol large-op"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-4.3em;margin-left:0em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;span class="mbin mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.2671em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.1514em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mop"&gt;cos&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;π&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.3714em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;kn&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.686em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;b&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;munderover&gt;&lt;mo&gt;∑&lt;/mo&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;/mrow&gt;&lt;/munderover&gt;&lt;msub&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/msub&gt;&lt;mi&gt;sin&lt;/mi&gt;&lt;mo&gt;⁡&lt;/mo&gt;&lt;mrow&gt;&lt;mo fence="true"&gt;(&lt;/mo&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;mi&gt;π&lt;/mi&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mfrac&gt;&lt;mo fence="true"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;b_k = -\sum_{n=0}^{N-1} u_n \sin\left(2\pi \frac{kn}{N}\right)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8444em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;b&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:3.0954em;vertical-align:-1.2671em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;−&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mop op-limits"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.8283em;"&gt;&lt;span style="top:-1.8829em;margin-left:0em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;span class="mrel mtight"&gt;=&lt;/span&gt;&lt;span class="mord mtight"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.05em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="mop op-symbol large-op"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-4.3em;margin-left:0em;"&gt;&lt;span class="pstrut" style="height:3.05em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;span class="mbin mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.2671em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.1514em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mop"&gt;sin&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;π&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.3714em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;kn&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.686em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size3"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;There are several symbols, so let’s take them apart one by one. Starting with the ingredients, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/msub&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;u_n&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.5806em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.1514em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is the value of the original signal at cell &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;n&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;, and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;N&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is the total number of cells.&lt;/p&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mo&gt;∑&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sum&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mop op-symbol small-op" style="position:relative;top:0em;"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; means to vary &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;n&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; from 0 to the end and add everything up. In code, it’s the same as computing a total with &lt;code class="language-text"&gt;reduce&lt;/code&gt;, and the code below uses es-toolkit’s &lt;code class="language-text"&gt;sumBy&lt;/code&gt;, which does that in one go. &lt;code class="language-text"&gt;sumBy(array, fn)&lt;/code&gt; applies the function to each element of the array and adds up all the results.&lt;/p&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;cos&lt;/mi&gt;&lt;mo&gt;⁡&lt;/mo&gt;&lt;mrow&gt;&lt;mo fence="true"&gt;(&lt;/mo&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;mi&gt;π&lt;/mi&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mfrac&gt;&lt;mo fence="true"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\cos\left(2\pi \frac{kn}{N}\right)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.2301em;vertical-align:-0.35em;"&gt;&lt;/span&gt;&lt;span class="mop"&gt;cos&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="minner"&gt;&lt;span class="mopen delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size1"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;π&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8801em;"&gt;&lt;span style="top:-2.655em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.394em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;kn&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.345em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose delimcenter" style="top:0em;"&gt;&lt;span class="delimsizing size1"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is the value at cell &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;n&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; of a reference sine wave that oscillates &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; times over the whole interval. The part in parentheses looks complicated, but you can think of it as just a device tuned so that it oscillates exactly &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; times over &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;N&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; cells.&lt;/p&gt;
&lt;p&gt;Splitting the computation into two lines, cosine and sine, like &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;a_k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.5806em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;b&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;b_k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8444em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;b&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;, is for recording not only the strength of the sine wave but also how far it’s shifted sideways, but for this post it’s enough to care only about strength.&lt;/p&gt;
&lt;p&gt;So what this formula does is “multiply the signal and the reference sine wave cell by cell and add everything up.” If the signal contains a lot of that sine wave, their peaks and troughs line up well and the sum gets large; if not, the positives and negatives cancel out and it gets close to 0. It’s pattern matching: holding up each reference sine wave one at a time and measuring how well it lines up.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;import&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; sumBy &lt;span class="token punctuation"&gt;}&lt;/span&gt; &lt;span class="token keyword"&gt;from&lt;/span&gt; &lt;span class="token string"&gt;'es-toolkit'&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;Component&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt; sine&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt; &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;waveAngle&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellCount&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;2&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;PI&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; index &lt;span class="token operator"&gt;*&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; toComponents &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Component&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; cellCount &lt;span class="token operator"&gt;=&lt;/span&gt; signal&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cellCount &lt;span class="token operator"&gt;/&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
    cosine&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token function"&gt;sumBy&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; value &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;cos&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function"&gt;waveAngle&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    sine&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token operator"&gt;-&lt;/span&gt;&lt;span class="token function"&gt;sumBy&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; value &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;sin&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function"&gt;waveAngle&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; fromComponents &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;components&lt;span class="token operator"&gt;:&lt;/span&gt; Component&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; cellCount &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;components&lt;span class="token punctuation"&gt;.&lt;/span&gt;length &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;weight&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;index &lt;span class="token operator"&gt;===&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt; &lt;span class="token operator"&gt;||&lt;/span&gt; index &lt;span class="token operator"&gt;===&lt;/span&gt; components&lt;span class="token punctuation"&gt;.&lt;/span&gt;length &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt; &lt;span class="token operator"&gt;?&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt; &lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    &lt;span class="token punctuation"&gt;(&lt;/span&gt;cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="token function"&gt;sumBy&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;components&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; index&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        &lt;span class="token keyword"&gt;const&lt;/span&gt; angle &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;waveAngle&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
        &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token function"&gt;weight&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;index&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;cosine &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;cos&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;angle&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;-&lt;/span&gt; sine &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;sin&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;angle&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
      &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;toComponents&lt;/code&gt; is the function that gets the spectrum, and &lt;code class="language-text"&gt;fromComponents&lt;/code&gt; is the function that combines the spectrum back into a waveform. &lt;code class="language-text"&gt;cosine&lt;/code&gt; and &lt;code class="language-text"&gt;sine&lt;/code&gt; in &lt;code class="language-text"&gt;Component&lt;/code&gt; correspond to &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;a&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;a_k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.5806em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;b&lt;/mi&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;b_k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8444em;vertical-align:-0.15em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;b&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; in the formula, and the formula’s &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; became &lt;code class="language-text"&gt;index&lt;/code&gt;, &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;n&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;n&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; became &lt;code class="language-text"&gt;cell&lt;/code&gt;, and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;N&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.10903em;"&gt;N&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; became &lt;code class="language-text"&gt;cellCount&lt;/code&gt;. The formula’s &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mo&gt;∑&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sum&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mop op-symbol small-op" style="position:relative;top:0em;"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; just became &lt;code class="language-text"&gt;sumBy&lt;/code&gt;, and it’s a double loop that sweeps the entire signal once for each cell of the spectrum.&lt;/p&gt;
&lt;p&gt;One thing worth noticing is that &lt;code class="language-text"&gt;toComponents&lt;/code&gt; only counts &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; up to half the number of cells. Within a 64-cell signal, 32 is the maximum number of times a sine wave can oscillate, so there’s no need to look beyond that. In exchange, when &lt;code class="language-text"&gt;fromComponents&lt;/code&gt; combines them back, it counts every sine wave except the very first and very last twice. Real FNO implementations use this kind of half spectrum too.&lt;/p&gt;
&lt;p&gt;Let’s run an experiment. We’ll make a signal mixing a low-frequency sine wave that oscillates 3 times with a high-frequency sine wave that oscillates 10 times, and get its spectrum.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;const&lt;/span&gt; cellCount &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token number"&gt;64&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;const&lt;/span&gt; grid &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;2&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;PI&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; cellCount&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;const&lt;/span&gt; signal &lt;span class="token operator"&gt;=&lt;/span&gt; grid&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;position&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;sin&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;3&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; position&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt; &lt;span class="token number"&gt;0.3&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;sin&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;10&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; position&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; strengths &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;toComponents&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;hypot&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cosine&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;Math.sin(3 * position)&lt;/code&gt; is a sine wave that oscillates 3 times over the whole interval, and &lt;code class="language-text"&gt;strengths&lt;/code&gt; holds how strong each sine wave is, for each cell of the spectrum.&lt;/p&gt;
&lt;p&gt;Print out &lt;code class="language-text"&gt;strengths&lt;/code&gt; and you’ll see values spike only at cells 3 and 10 while the rest are nearly 0. A signal that looked like a wiggly line to the eye turns, in the spectrum, into very simple information: “this much of sine wave 3, a little of sine wave 10.”&lt;/p&gt;
&lt;p&gt;For the record, this &lt;code class="language-text"&gt;toComponents&lt;/code&gt; is the simplest version I wrote for the sake of understanding, so it gets painfully slow as the number of cells grows. In practice, people use an algorithm called FFT (Fast Fourier Transform), which reuses intermediate computations to produce the same result much faster. With a million cells, my version does close to a trillion operations, while FFT gets it done in roughly 20 million, so the difference is huge.&lt;/p&gt;
&lt;p&gt;But what do we gain from knowing the spectrum? This is where the single most important observation in this whole post comes in.&lt;/p&gt;
&lt;h3 id="blur-wipes-out-high-frequencies-first" style="position:relative;"&gt;Blur Wipes Out High Frequencies First&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#blur-wipes-out-high-frequencies-first" aria-label="blur wipes out high frequencies first permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Earlier, I said heat spreading is just like a blur filter. So when you apply blur to a photo, what disappears first? The sharp, fine details, like a single strand of hair or the edges of text. On the other hand, the big picture, like “the left side is bright and the right side is dark,” survives even after quite a few rounds of blur.&lt;/p&gt;
&lt;p&gt;Explained again in terms of sine waves, you can understand blur as wiping out high frequencies quickly and low frequencies slowly. Exactly the same thing happens when heat spreads. If you actually apply the heat-spreading rule to the spectrum and solve it, the strength of each sine wave changes over time like this.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mover accent="true"&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;msub&gt;&lt;mover accent="true"&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mtext&gt; &lt;/mtext&gt;&lt;msup&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mrow&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\hat{u}_k(t) = \hat{u}_k(0) \, e^{-\alpha k^2 t}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.6944em;"&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="accent-body" style="left:-0.2222em;"&gt;&lt;span class="mord"&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.2869em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.6944em;"&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="accent-body" style="left:-0.2222em;"&gt;&lt;span class="mord"&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.1667em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.0369em;"&gt;&lt;span style="top:-3.113em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8913em;"&gt;&lt;span style="top:-2.931em;margin-right:0.0714em;"&gt;&lt;span class="pstrut" style="height:2.5em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mover accent="true"&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\hat{u}_k(0)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.6944em;"&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="accent-body" style="left:-0.2222em;"&gt;&lt;span class="mord"&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is the strength of sine wave &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; in the initial spectrum, and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mover accent="true"&gt;&lt;mi&gt;u&lt;/mi&gt;&lt;mo&gt;^&lt;/mo&gt;&lt;/mover&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\hat{u}_k(t)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord accent"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.6944em;"&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;u&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="accent-body" style="left:-0.2222em;"&gt;&lt;span class="mord"&gt;^&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3361em;"&gt;&lt;span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is its strength after time &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;t&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6151em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; has passed. The &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mrow&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;msup&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;e^{-\alpha k^2 t}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.9869em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.9869em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8913em;"&gt;&lt;span style="top:-2.931em;margin-right:0.0714em;"&gt;&lt;span class="pstrut" style="height:2.5em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size3 size1 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; multiplied in between is the fraction of sine wave &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; that remains after time &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;t&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6151em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi&gt;e&lt;/mi&gt;&lt;mrow&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mtext&gt;something&lt;/mtext&gt;&lt;/mrow&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;e^{-\text{something}}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8491em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8491em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;something&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is a value that’s 1 when “something” is 0 and gets closer to 0 as it grows. So this formula is saying “high-frequency waves that oscillate often disappear faster as time passes.”&lt;/p&gt;
&lt;p&gt;The numbers make it more vivid. With &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\alpha = 1&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0.1&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;t = 0.1&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6151em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;, about 90% of the low-frequency wave that oscillates once remains. Only about 8% of the sine wave that oscillates 5 times remains. The high-frequency wave that oscillates 10 times is down to 0.005%, so you can consider it effectively gone.&lt;/p&gt;
&lt;p&gt;And when you move this formula into code, the simulation that earlier required the grunt work of running &lt;code class="language-text"&gt;update&lt;/code&gt; thousands of times ends with a single multiplication on the spectrum.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;const&lt;/span&gt; solveHeat &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;initial&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; time&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;fromComponents&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    &lt;span class="token function"&gt;toComponents&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;initial&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; index&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
      &lt;span class="token keyword"&gt;const&lt;/span&gt; remaining &lt;span class="token operator"&gt;=&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;exp&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token operator"&gt;-&lt;/span&gt;alpha &lt;span class="token operator"&gt;*&lt;/span&gt; index &lt;span class="token operator"&gt;*&lt;/span&gt; index &lt;span class="token operator"&gt;*&lt;/span&gt; time&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
      &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token operator"&gt;:&lt;/span&gt; cosine &lt;span class="token operator"&gt;*&lt;/span&gt; remaining&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine&lt;span class="token operator"&gt;:&lt;/span&gt; sine &lt;span class="token operator"&gt;*&lt;/span&gt; remaining &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Since the spectrum’s &lt;code class="language-text"&gt;index&lt;/code&gt; is the formula’s &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;k&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;k&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03148em;"&gt;k&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;, all it does is get the spectrum, multiply each sine wave’s strength by the fraction that remains, and combine them back.&lt;/p&gt;
&lt;p&gt;This is where it really sinks in that the Fourier transform was originally built to solve the heat-spreading problem. A problem that, viewed on the cells, requires endlessly repeating “compare with your neighbors and adjust a little,” turns into “adjust one slider per sine wave” when viewed on the spectrum. The same data can make a problem this much easier or harder depending on how you look at it.&lt;/p&gt;
&lt;p&gt;So couldn’t we solve the weather this way too? Unfortunately, clean shortcuts like this only show up for rules as simple as heat spreading. In heat spreading, sine waves don’t interfere with each other and each fades at its own pace, so adjusting a separate slider for each sine wave is all it takes to get the right answer.&lt;/p&gt;
&lt;p&gt;But complex problems like airflow are a different story. Because wind carries wind along, large eddies break apart and create small eddies, and small eddies gather and in turn influence the larger flow. Seen as a spectrum, low frequencies and high frequencies get thoroughly mixed together.&lt;/p&gt;
&lt;p&gt;In problems like this, even if you know the one-frame rule exactly, you can’t derive a formula for the shortcut that jumps straight to the result of repeating it tens of thousands of times. That’s why you need a neural network that learns that shortcut from data.&lt;/p&gt;
&lt;p&gt;Even so, the observations from heat spreading give us two important hints.&lt;/p&gt;
&lt;p&gt;The first hint is that you can often throw away the high frequencies. There’s no need to carefully compute high frequencies that will vanish soon anyway. Handle a few low frequencies well, and you capture most of the information. The way JPEG compression shrinks photo file sizes works similarly.&lt;/p&gt;
&lt;p&gt;JPEG converts a photo into frequency components and then boldly throws away the high-frequency information that the human eye barely notices. To the human eye, it all looks pretty much the same anyway. Of course, in problems where high frequencies matter, like turbulence where eddies keep breaking down into ever smaller ones, this becomes a weakness and can be the reason predictions come out blurry.&lt;/p&gt;
&lt;p&gt;The second hint is that the sliders are attached to sine waves, not cells. Whether you sample a signal into 64 cells or 256 cells, a sine wave that oscillates 3 times is still a sine wave that oscillates 3 times. So a rule like “keep only 40% of sine wave 3” can be applied regardless of resolution.&lt;/p&gt;
&lt;p&gt;This might sound familiar if, like me, you’ve done some audio work. Whether a file was recorded at 44.1kHz or 96kHz, an equalizer setting like “boost the 100Hz band by 3dB” applies exactly the same way. The sample rate is only a matter of how densely the sound was recorded; it doesn’t change the nature of the 100Hz sound itself.&lt;/p&gt;
&lt;p&gt;These two hints are exactly where FNO starts.&lt;/p&gt;
&lt;h2 id="assembling-the-ingredients-into-a-neural-network" style="position:relative;"&gt;Assembling the Ingredients Into a Neural Network&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#assembling-the-ingredients-into-a-neural-network" aria-label="assembling the ingredients into a neural network permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;We’ve gathered all the ingredients, so now it’s time to assemble them. The assembly happens in two steps. First, we build FNO, which carries the two hints from heat spreading straight into a neural network architecture, and then we put the answer to the fourth question, checking the answer, on top of it to get PINO.&lt;/p&gt;
&lt;h3 id="fno-an-equalizer-that-tunes-its-own-sliders" style="position:relative;"&gt;FNO, an Equalizer That Tunes Its Own Sliders&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#fno-an-equalizer-that-tunes-its-own-sliders" aria-label="fno an equalizer that tunes its own sliders permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;FNO, published in 2020 by Zongyi Li, Anima Anandkumar, and others at Caltech, is an architecture that carries the ideas so far straight into a neural network. To sum it up in one line, FNO is “an equalizer that learns its own slider positions.”&lt;/p&gt;
&lt;p&gt;In the heat-spreading problem, we knew exactly where each sine wave’s slider should go from the remaining-fraction formula. In problems where sine waves mix with each other, like airflow or plasma, that value can’t be derived from a formula. So what do you do? You show the model lots of examples where putting in the initial state gives you the later state, and have it learn the slider positions. That’s the core idea of FNO.&lt;/p&gt;
&lt;p&gt;Written as a formula, one layer looks like this.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;out&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mi&gt;σ&lt;/mi&gt;&lt;mo fence="false" stretchy="true" minsize="1.8em" maxsize="1.8em"&gt;(&lt;/mo&gt;&lt;mi&gt;W&lt;/mi&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;in&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;msup&gt;&lt;mi mathvariant="script"&gt;F&lt;/mi&gt;&lt;mrow&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;/mrow&gt;&lt;/msup&gt;&lt;mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em"&gt;(&lt;/mo&gt;&lt;mi&gt;R&lt;/mi&gt;&lt;mo&gt;⋅&lt;/mo&gt;&lt;mi mathvariant="script"&gt;F&lt;/mi&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;in&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo fence="false" stretchy="true" minsize="1.2em" maxsize="1.2em"&gt;)&lt;/mo&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;mo fence="false" stretchy="true" minsize="1.8em" maxsize="1.8em"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;v_{\text{out}}(x) = \sigma\Big( W v_{\text{in}}(x) + \mathcal{F}^{-1}\big( R \cdot \mathcal{F}(v_{\text{in}}) \big)(x) \Big)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.2806em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;out&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.8em;vertical-align:-0.65em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;σ&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size2"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.13889em;"&gt;W&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3175em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;in&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.2141em;vertical-align:-0.35em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathcal" style="margin-right:0.09931em;"&gt;F&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8641em;"&gt;&lt;span style="top:-3.113em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size1"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.00773em;"&gt;R&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;⋅&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.8em;vertical-align:-0.65em;"&gt;&lt;/span&gt;&lt;span class="mord mathcal" style="margin-right:0.09931em;"&gt;F&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3175em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;in&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size1"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="delimsizing size2"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;It looks like a lot of symbols, but you already know all of them. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;in&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;v_{\text{in}}(x)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3175em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;in&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is the value at position &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;x&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; among the values coming into the layer, and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;out&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;v_{\text{out}}(x)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.2806em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;out&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is the value going out of the layer. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi mathvariant="script"&gt;F&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\mathcal{F}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathcal" style="margin-right:0.09931em;"&gt;F&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is getting the spectrum, and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi mathvariant="script"&gt;F&lt;/mi&gt;&lt;mrow&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;/mrow&gt;&lt;/msup&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\mathcal{F}^{-1}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8141em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathcal" style="margin-right:0.09931em;"&gt;F&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8141em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mtight"&gt;−&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is combining the spectrum back. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;R&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;R&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.00773em;"&gt;R&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is the slider values to be learned. So the right-hand term inside the parentheses means: get the spectrum, adjust the strengths by the sliders, and combine them back.&lt;/p&gt;
&lt;p&gt;The left-hand term &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;W&lt;/mi&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;in&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;W v_{\text{in}}(x)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.13889em;"&gt;W&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3175em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;in&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is the part that slightly transforms each cell by looking only at its own value, and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;σ&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sigma&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;σ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is a nonlinear transformation called an activation function. Why are these two needed?&lt;/p&gt;
&lt;p&gt;The &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;W&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;W&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.13889em;"&gt;W&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; term is a path that passes each cell’s original value straight through without going through the spectrum. Since the equalizer cuts off high frequencies, it tends to lose fine detail, and it’s enough to understand the &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;W&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;W&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.13889em;"&gt;W&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; term as making up for that lost information.&lt;/p&gt;
&lt;p&gt;&lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;σ&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sigma&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;σ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is needed because there are things an equalizer alone can’t do. An equalizer only boosts or cuts the strength of each sine wave; it can’t turn sine wave 3 into sine wave 10. So applying an equalizer ten times in a row ends up the same as applying a single equalizer with different slider values, and stacking layers does nothing no matter how many you add.&lt;/p&gt;
&lt;p&gt;But to solve problems like the airflow we saw earlier, where large eddies create small eddies, you need an operation where sine waves mix and give rise to new sine waves. That’s the role &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;σ&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sigma&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;σ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; plays. Here, we use its simplest form, “a function that turns negative values into 0.”&lt;/p&gt;
&lt;p&gt;How does such a simple function mix sine waves? Take a single sine wave 1 and turn its negative part into 0, and you get a shape with its bottom shaved flat. That shape can no longer be represented by a single sine wave, so if you get its spectrum, you’ll find new sine waves like 2 and 4 that weren’t there originally. This is also why the next layer can recreate the high frequencies the equalizer cut off.&lt;/p&gt;
&lt;p&gt;If you’ve roughly got the idea, let’s move the formula into code. First, the equalizer part.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;const&lt;/span&gt; applyEqualizer &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; sliders&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;fromComponents&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    &lt;span class="token function"&gt;toComponents&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;signal&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; index&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
      &lt;span class="token keyword"&gt;const&lt;/span&gt; gain &lt;span class="token operator"&gt;=&lt;/span&gt; sliders&lt;span class="token punctuation"&gt;[&lt;/span&gt;index&lt;span class="token punctuation"&gt;]&lt;/span&gt; &lt;span class="token operator"&gt;??&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
      &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; cosine&lt;span class="token operator"&gt;:&lt;/span&gt; cosine &lt;span class="token operator"&gt;*&lt;/span&gt; gain&lt;span class="token punctuation"&gt;,&lt;/span&gt; sine&lt;span class="token operator"&gt;:&lt;/span&gt; sine &lt;span class="token operator"&gt;*&lt;/span&gt; gain &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This function looks almost identical to &lt;code class="language-text"&gt;solveHeat&lt;/code&gt;, which we wrote above, except that instead of the remaining-fraction formula, it pulls values from the &lt;code class="language-text"&gt;sliders&lt;/code&gt; array.&lt;/p&gt;
&lt;p&gt;The length of the &lt;code class="language-text"&gt;sliders&lt;/code&gt; array is the number of sine waves to keep. With only 12 sliders, only the low frequencies from 0 to 11 survive, and every higher frequency gets caught by &lt;code class="language-text"&gt;?? 0&lt;/code&gt; and thrown away. The first hint, “you can throw away the high frequencies,” lives in this single line. Real FNO learns not only the strength but also how far to shift each sine wave sideways, but strength alone is enough to understand the principle.&lt;/p&gt;
&lt;p&gt;Now that we have the equalizer, we can build one layer, and the whole model made by stacking multiple layers.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;FourierLayer&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
  sliders&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  scale&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  bias&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;relu&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;max&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; value&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; applyLayer &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; sliders&lt;span class="token punctuation"&gt;,&lt;/span&gt; scale&lt;span class="token punctuation"&gt;,&lt;/span&gt; bias &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; FourierLayer&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;zipWith&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token function"&gt;applyEqualizer&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token punctuation"&gt;,&lt;/span&gt; sliders&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;,&lt;/span&gt; equalized&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token function"&gt;relu&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;scale &lt;span class="token operator"&gt;*&lt;/span&gt; value &lt;span class="token operator"&gt;+&lt;/span&gt; bias &lt;span class="token operator"&gt;+&lt;/span&gt; equalized&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;fourierNeuralOperator&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;layers&lt;span class="token operator"&gt;:&lt;/span&gt; FourierLayer&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;input&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  layers&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;reduce&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;applyLayer&lt;span class="token punctuation"&gt;,&lt;/span&gt; input&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The formula’s &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;W&lt;/mi&gt;&lt;msub&gt;&lt;mi&gt;v&lt;/mi&gt;&lt;mtext&gt;in&lt;/mtext&gt;&lt;/msub&gt;&lt;mo stretchy="false"&gt;(&lt;/mo&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo stretchy="false"&gt;)&lt;/mo&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;W v_{\text{in}}(x)&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1em;vertical-align:-0.25em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.13889em;"&gt;W&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;v&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.3175em;"&gt;&lt;span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord text mtight"&gt;&lt;span class="mord mtight"&gt;in&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.15em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mopen"&gt;(&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mclose"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; became &lt;code class="language-text"&gt;scale * value + bias&lt;/code&gt;, the equalizer part became &lt;code class="language-text"&gt;applyEqualizer&lt;/code&gt;, and &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;σ&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\sigma&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;σ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; became &lt;code class="language-text"&gt;relu&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Of course, real FNO is more complex, with dozens of channels per cell instead of a single number, but the core skeleton can be implemented with this much. The formula in the paper looked pretty terrifying, but once moved into code, it boils down to three simple steps: “get the spectrum, adjust a few low frequencies with sliders, and transform back.”&lt;/p&gt;
&lt;p&gt;So what exactly does training do? It’s similar to how most AI models learn. You compute how far the model’s prediction is from the answer as a single number, then gradually adjust the &lt;code class="language-text"&gt;sliders&lt;/code&gt;, &lt;code class="language-text"&gt;scale&lt;/code&gt;, and &lt;code class="language-text"&gt;bias&lt;/code&gt; values in the direction that makes that number smaller. This number is called the loss.&lt;/p&gt;
&lt;p&gt;You can tell which direction to adjust by nudging a single value very slightly and seeing whether the loss goes up or down. Neural network libraries have a feature that automatically computes this direction for all values at once, and it’s called automatic differentiation. This feature will show up again later when we talk about the catheter example.&lt;/p&gt;
&lt;p&gt;Let’s see how this architecture handles the problems with grid data we talked about earlier.&lt;/p&gt;
&lt;p&gt;First, computation cost. Thanks to FFT, getting the spectrum and transforming back is fast, and the slider adjustment only needs to happen for the 12 sine waves we kept. &lt;small&gt;(Real FNO has multiple channels, so it does a small matrix multiplication once per sine wave)&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;Next, field of view. Sine wave 1, which has the lowest frequency, oscillates once in a big sweep across the whole interval. Adjusting this sine wave’s strength is the same as mixing information from the whole interval all at once. So you can say FNO sees the entire space at once with just a single layer.&lt;/p&gt;
&lt;p&gt;In that sense it resembles attention, which also sees every token at once. The difference is that attention computes “who references whom” fresh for every input, while FNO learns a fixed rule: “which sine waves to keep, and by how much.”&lt;/p&gt;
&lt;p&gt;Finally, the most interesting part: resolution independence. Since the sliders are attached to sine waves rather than cells, the same sliders can be used as-is on a 64-cell input and on a 256-cell input. Let’s check it ourselves.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;const&lt;/span&gt; sample &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;field&lt;span class="token operator"&gt;:&lt;/span&gt; Field&lt;span class="token punctuation"&gt;,&lt;/span&gt; resolution&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;resolution&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token function"&gt;field&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;2&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;PI&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; resolution&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; initialTemperature&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token function-variable function"&gt;Field&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;position&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;exp&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token operator"&gt;-&lt;/span&gt;&lt;span class="token number"&gt;4&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;position &lt;span class="token operator"&gt;-&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token constant"&gt;PI&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;**&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; sliders &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token number"&gt;12&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;waves&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Math&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;exp&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token operator"&gt;-&lt;/span&gt;&lt;span class="token number"&gt;0.05&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; waves &lt;span class="token operator"&gt;*&lt;/span&gt; waves&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; coarse &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;applyEqualizer&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function"&gt;sample&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;initialTemperature&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;64&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; sliders&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;const&lt;/span&gt; fine &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;applyEqualizer&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function"&gt;sample&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;initialTemperature&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;256&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; sliders&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We sampled the same temperature distribution into 64 cells and 256 cells, then applied the same sliders to each. Instead of going through training here, I plugged the values from the earlier remaining-fraction formula with &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;α&lt;/mi&gt;&lt;mi&gt;t&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0.05&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\alpha t = 0.05&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6151em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal" style="margin-right:0.0037em;"&gt;α&lt;/span&gt;&lt;span class="mord mathnormal"&gt;t&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.05&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; directly into the sliders. If FNO were trained on heat-spreading data, ideally these are exactly the values it should find.&lt;/p&gt;
&lt;p&gt;Compare &lt;code class="language-text"&gt;coarse[cell]&lt;/code&gt; with &lt;code class="language-text"&gt;fine[4 * cell]&lt;/code&gt; and you get the same values down to a dozen or so decimal places. That means sliders tuned on 64 cells produce the same result when used as-is on a 256-cell input. Of course, this compares only the equalizer part in isolation. Real FNO has nonlinear operations like &lt;code class="language-text"&gt;relu&lt;/code&gt; between layers, so results shift a little when the resolution changes, and it’s not as if it discovers, at 256 cells, details that were never in the 64-cell data to begin with.&lt;/p&gt;
&lt;p&gt;You might be thinking, “Well, of course, you sampled the same temperature distribution,” so let’s set up a control group too. This time, it’s a rule that defines relationships based on cell indices, as we discussed earlier.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;const&lt;/span&gt; cellRule &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;map&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;,&lt;/span&gt; cell&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token keyword"&gt;const&lt;/span&gt; left &lt;span class="token operator"&gt;=&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt; &lt;span class="token operator"&gt;+&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;%&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;const&lt;/span&gt; right &lt;span class="token operator"&gt;=&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cell &lt;span class="token operator"&gt;+&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;%&lt;/span&gt; temperatures&lt;span class="token punctuation"&gt;.&lt;/span&gt;length&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token keyword"&gt;return&lt;/span&gt; &lt;span class="token number"&gt;0.5&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; value &lt;span class="token operator"&gt;+&lt;/span&gt; &lt;span class="token number"&gt;0.25&lt;/span&gt; &lt;span class="token operator"&gt;*&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;left &lt;span class="token operator"&gt;+&lt;/span&gt; right&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;applyTimes&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function-variable function"&gt;rule&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; times&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; input&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;range&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;times&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;reduce&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token function"&gt;rule&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;temperatures&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; input&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; coarseCell &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;applyTimes&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cellRule&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;10&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token function"&gt;sample&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;initialTemperature&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;64&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token keyword"&gt;const&lt;/span&gt; fineCell &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;applyTimes&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;cellRule&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;10&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token function"&gt;sample&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;initialTemperature&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;256&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;cellRule&lt;/code&gt; is a rule defined in units of cells: “mix half of your own value with a quarter each of the cells right next to you.” Apply this rule 10 times on 64 cells, and the highest temperature at the middle of the rod drops from 1 to about 0.85, but on 256 cells it stays nearly the same at about 0.99. Since “the cell right next to you” on 256 cells is actually only a quarter of the distance, applying the same rule spreads the heat far less. This is exactly why a model that learned relationships based on cell indices gets tied to resolution.&lt;/p&gt;
&lt;p&gt;The FNO paper called this property zero-shot super-resolution. It showed experiments where a model trained at a coarse resolution was run as-is at a fine resolution it had never seen, and the follow-up PINO paper reported that accuracy didn’t drop even when the resolution was raised this way.&lt;/p&gt;
&lt;h3 id="pino-a-way-to-score-without-answers" style="position:relative;"&gt;PINO, a Way to Score Without Answers&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#pino-a-way-to-score-without-answers" aria-label="pino a way to score without answers permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;With FNO, we can build a model that’s fast and free from resolution constraints. But one problem remains: FNO still learns by looking at answers.&lt;/p&gt;
&lt;p&gt;To train it, you need examples on the order of thousands saying “given these initial conditions, the result is this,” and who makes those answers? The same slow simulator I said earlier takes days.&lt;/p&gt;
&lt;p&gt;Wait a minute. We’re building a neural network because simulation is slow, but to train that neural network we have to run the slow simulation over a thousand times. It’s a strange situation. On top of that, a model that just imitates examples still has no guarantee of obeying the laws of physics.&lt;/p&gt;
&lt;p&gt;So couldn’t we score without answers?&lt;/p&gt;
&lt;p&gt;Think back to math tests in school. If you solve &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;msup&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/msup&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;5&lt;/mn&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;mn&gt;6&lt;/mn&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;x^2 - 5x + 6 = 0&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8974em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8141em;"&gt;&lt;span style="top:-3.063em;margin-right:0.05em;"&gt;&lt;span class="pstrut" style="height:2.7em;"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;5&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;6&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; and get &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;x&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;x = 2&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.4306em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;x&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;, there’s a way to check whether you got it right even without an answer key. Just plug 2 back into the original equation. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mn&gt;4&lt;/mn&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mn&gt;10&lt;/mn&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;mn&gt;6&lt;/mn&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mn&gt;0&lt;/mn&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;4 - 10 + 6 = 0&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;4&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;−&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7278em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;10&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;6&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6444em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;, so it’s the right answer. That’s checking your answer.&lt;/p&gt;
&lt;p&gt;This is exactly PINO’s idea. Earlier, we said we don’t know the shortcut for airflow, but we know the one-frame rule exactly. So we can plug the model’s prediction directly into that rule and add how much it violated the rule to the loss. For heat spreading, you look at the temperature change the model predicted and check, cell by cell, “did the cell whose neighbors are hotter really warm up by that much?” This scoring needs no answer data. All you need is the one-frame rule.&lt;/p&gt;
&lt;p&gt;But there’s a trap here: a lazy answer that just puts 0 in every cell somehow passes the check too. If the temperature is 0 everywhere, the difference with the neighbors is 0 and the change is 0, so it obeys the rule perfectly.&lt;/p&gt;
&lt;p&gt;This trap can be blocked with the initial state, though. The rule only tells you how the temperature changes, not where it starts. In the earlier &lt;code class="language-text"&gt;simulateHeat&lt;/code&gt;, too, the rule &lt;code class="language-text"&gt;heatStep&lt;/code&gt; alone didn’t determine the result. You needed &lt;code class="language-text"&gt;initial&lt;/code&gt;, the starting value passed to &lt;code class="language-text"&gt;reduce&lt;/code&gt;, for the result to be pinned down to one.&lt;/p&gt;
&lt;p&gt;PINO is the same. Even if we don’t know the final result, we know the initial temperature distribution, because we put it in ourselves. So if we also score “does the start of the prediction match the initial state we gave it?”, the countless answers that obey the rule get narrowed down to the one that starts from the starting point we gave.&lt;/p&gt;
&lt;p&gt;Written out in words, PINO’s loss looks like this.&lt;/p&gt;
&lt;div class="math math-display"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mtext&gt;loss&lt;/mtext&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mtext&gt;gap from the initial state&lt;/mtext&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;mtext&gt;gap from the answer&lt;/mtext&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;mi&gt;λ&lt;/mi&gt;&lt;mo&gt;×&lt;/mo&gt;&lt;mtext&gt;degree of rule violation&lt;/mtext&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\text{loss} = \text{gap from the initial state} + \text{gap from the answer} + \lambda \times \text{degree of rule violation}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;loss&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2778em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;gap from the initial state&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;gap from the answer&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.7778em;vertical-align:-0.0833em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;λ&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace" style="margin-right:0.2222em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"&gt;&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;degree of rule violation&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;p&gt;The gap from the answer is a term that’s only scored when answer data exists, and the degree of rule violation is the newly added check term. &lt;span class="math math-inline"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;λ&lt;/mi&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\lambda&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:0.6944em;"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;λ&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; is a ratio that sets how much weight to give the check term. The formula in the actual PINO paper has the same structure, adding up several terms with weights attached.&lt;/p&gt;
&lt;p&gt;Strictly speaking, checking the answer needs more than the result at a single point in time; you also have to see how the temperature changes as time flows. Real PINO treats time as a continuous function too and gets the rate of temperature change directly through differentiation, but moving all of that into code is too much hassle, so here let’s just say the model predicts temperature distributions at multiple points in time all at once and returns them as an array. I’ll call this array &lt;code class="language-text"&gt;Trajectory&lt;/code&gt;, since it’s a trajectory.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;import&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt; meanBy&lt;span class="token punctuation"&gt;,&lt;/span&gt; windowed &lt;span class="token punctuation"&gt;}&lt;/span&gt; &lt;span class="token keyword"&gt;from&lt;/span&gt; &lt;span class="token string"&gt;'es-toolkit'&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;Trajectory&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;meanSquare&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;values&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token function"&gt;meanBy&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;values&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;value&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; value &lt;span class="token operator"&gt;*&lt;/span&gt; value&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;gap&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;predicted&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;,&lt;/span&gt; expected&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;meanSquare&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token function"&gt;zipWith&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;predicted&lt;span class="token punctuation"&gt;,&lt;/span&gt; expected&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;a&lt;span class="token punctuation"&gt;,&lt;/span&gt; b&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; a &lt;span class="token operator"&gt;-&lt;/span&gt; b&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;lawViolation&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;trajectory&lt;span class="token operator"&gt;:&lt;/span&gt; Trajectory&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="token function"&gt;meanSquare&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;
    &lt;span class="token function"&gt;windowed&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;trajectory&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;2&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;flatMap&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;[&lt;/span&gt;current&lt;span class="token punctuation"&gt;,&lt;/span&gt; next&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="token function"&gt;zipWith&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;current&lt;span class="token punctuation"&gt;,&lt;/span&gt; next&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token function"&gt;neighborPull&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;current&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;now&lt;span class="token punctuation"&gt;,&lt;/span&gt; later&lt;span class="token punctuation"&gt;,&lt;/span&gt; pull&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;later &lt;span class="token operator"&gt;-&lt;/span&gt; now&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;/&lt;/span&gt; timeStep &lt;span class="token operator"&gt;-&lt;/span&gt; alpha &lt;span class="token operator"&gt;*&lt;/span&gt; pull&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class="language-text"&gt;gap&lt;/code&gt; is a function that measures how different two temperature distributions are. It squares the difference in each cell and takes the average, so that coming out too hot and coming out too cold count toward the loss equally.&lt;/p&gt;
&lt;p&gt;&lt;code class="language-text"&gt;lawViolation&lt;/code&gt; pairs up consecutive points in time, then for each cell computes the difference between “how fast the temperature actually changed” and “how fast it should have changed according to the rule.” The point is that &lt;code class="language-text"&gt;neighborPull&lt;/code&gt;, which we used in the simulation, gets reused here as-is. The simulator used this rule to produce the next state, and PINO uses the same rule to examine the model’s answer. If the model obeyed the rule perfectly, this value is 0.&lt;/p&gt;
&lt;div class="gatsby-highlight" data-language="typescript"&gt;&lt;pre style="counter-reset: linenumber NaN" class="language-typescript line-numbers"&gt;&lt;code class="language-typescript"&gt;&lt;span class="token keyword"&gt;type&lt;/span&gt; &lt;span class="token class-name"&gt;LossInput&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
  prediction&lt;span class="token operator"&gt;:&lt;/span&gt; Trajectory&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  initial&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  answer&lt;span class="token operator"&gt;?&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; Samples&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  alpha&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  timeStep&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  cellSize&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  lambda&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token builtin"&gt;number&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

&lt;span class="token keyword"&gt;const&lt;/span&gt; &lt;span class="token function-variable function"&gt;pinoLoss&lt;/span&gt; &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt; prediction&lt;span class="token punctuation"&gt;,&lt;/span&gt; initial&lt;span class="token punctuation"&gt;,&lt;/span&gt; answer&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;,&lt;/span&gt; lambda &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token operator"&gt;:&lt;/span&gt; LossInput&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; startGap &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;gap&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;prediction&lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; initial&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; answerGap &lt;span class="token operator"&gt;=&lt;/span&gt; answer &lt;span class="token operator"&gt;?&lt;/span&gt; &lt;span class="token function"&gt;gap&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;prediction&lt;span class="token punctuation"&gt;[&lt;/span&gt;prediction&lt;span class="token punctuation"&gt;.&lt;/span&gt;length &lt;span class="token operator"&gt;-&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; answer&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;return&lt;/span&gt; startGap &lt;span class="token operator"&gt;+&lt;/span&gt; answerGap &lt;span class="token operator"&gt;+&lt;/span&gt; lambda &lt;span class="token operator"&gt;*&lt;/span&gt; &lt;span class="token function"&gt;lawViolation&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;prediction&lt;span class="token punctuation"&gt;,&lt;/span&gt; alpha&lt;span class="token punctuation"&gt;,&lt;/span&gt; timeStep&lt;span class="token punctuation"&gt;,&lt;/span&gt; cellSize&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;&lt;/code&gt;&lt;span aria-hidden="true" class="line-numbers-rows" style="white-space: normal; width: auto; left: 0;"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Notice that &lt;code class="language-text"&gt;initial&lt;/code&gt; is required while &lt;code class="language-text"&gt;answer&lt;/code&gt; is an optional argument. We always know the initial state because we put it in ourselves, but the answer may or may not exist. If there’s an answer, it also looks at the gap from the answer; if not, it scores using only the initial state and the check. That’s exactly the flexibility PINO has.&lt;/p&gt;
&lt;p&gt;The “needs little data” property I introduced earlier comes straight from this scoring method. You prepare just a small amount of expensive simulation answers at a coarse resolution, and do the checking at a much finer resolution. Since FNO is free from resolution constraints, you can mix the two.&lt;/p&gt;
&lt;p&gt;At the extreme, it can learn with no answers at all, using only the initial state and the check, and the paper reported solving even flow problems with intricately tangled eddies this way. Of course, this method isn’t a silver bullet either. Reportedly, for the problem of predicting the same flow over a much longer time span, PINO couldn’t solve it properly without answer data.&lt;/p&gt;
&lt;p&gt;The check is useful in practice too. When a never-before-seen problem comes in, you don’t know the answer, but you do know the rule, so after making a prediction you can refine the model a bit more for that one problem in the direction of “obeying the rule better.”&lt;/p&gt;
&lt;p&gt;The idea of scoring with the laws of physics didn’t actually start with PINO. It became widely known through a method called PINN (Physics-Informed Neural Network), published in 2019, but PINN had to train a neural network from scratch every time it solved a single problem.&lt;/p&gt;
&lt;p&gt;On top of that, training often didn’t go well in problems where low and high frequencies are intricately mixed, such as flows where large and small eddies are tangled together. That’s because neural networks naturally tend to learn low frequencies easily and struggle to learn high frequencies. PINO put this checking approach on top of FNO, so it learns a rule that solves a whole family of similar problems rather than just one. Once trained, it produces an answer in a single inference pass even when new conditions come in.&lt;/p&gt;
&lt;p&gt;Now we can gather the answers to the original question, “How is this possible?” Simulations were slow because a simple one-frame rule has to be repeated at a very fine granularity. Seen on the spectrum, that repetition turns into slider adjustment, and since the sliders are attached to sine waves rather than cells, they don’t care about resolution. FNO is an architecture that learns from data the slider values that can’t be derived from a formula, and PINO attaches a check using the one-frame rule to it, so that it produces physically sensible answers even when answers are scarce.&lt;/p&gt;
&lt;h2 id="neural-operators-in-the-real-world" style="position:relative;"&gt;Neural Operators in the Real World&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#neural-operators-in-the-real-world" aria-label="neural operators in the real world permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Once you understand the principles, two things naturally make you curious: where it’s actually being used, and how much you can trust the results.&lt;/p&gt;
&lt;p&gt;There’s one thing to point out up front. Many of the examples introduced here are FNO-family models trained only on simulation data, without the check. PINO is an extension that adds the check to FNO, so you can read the examples below as showing how the technology underlying PINO is being used in the real world.&lt;/p&gt;
&lt;h3 id="where-is-it-being-used" style="position:relative;"&gt;Where Is It Being Used?&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#where-is-it-being-used" aria-label="where is it being used permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Having understood this much, you can roughly guess where this technology would be used. What they have in common is work that used to require running simulations for hours or days, and work that only becomes meaningful when repeated thousands of times.&lt;/p&gt;
&lt;p&gt;The most prominent field is weather prediction. FourCastNet, which NVIDIA published in 2022, is a model that predicts global weather, and interestingly, it uses an architecture where the attention slot in a Transformer skeleton is replaced with an FNO-style Fourier operation. The paper reported generating a week’s worth of forecasts in under 2 seconds, several orders of magnitude faster than traditional numerical forecasting.&lt;/p&gt;
&lt;p&gt;That said, this speed is the time for prediction alone with a model that has already finished training, so the cost of training isn’t included, and its accuracy was also a bit lower than ECMWF’s numerical forecasts at the time. And since this model too was trained on the decades of atmospheric records we talked about earlier, it reconfirms that weather is a data-rich field.&lt;/p&gt;
&lt;p&gt;When prediction gets this fast, you can run hundreds or thousands of forecasts with slightly perturbed initial conditions and compute even the uncertainty, like “there’s a 70% chance the typhoon takes this path.” With traditional methods, cost limited this to a few dozen or so.&lt;/p&gt;
&lt;p&gt;It’s also used in car and aircraft design. Predicting how air resistance changes with the shape of a car body has traditionally been the domain of slow simulations.&lt;/p&gt;
&lt;p&gt;A model called GINO (Geometry-Informed Neural Operator), published by Caltech and NVIDIA researchers, takes complex 3D car body shapes and predicts the pressure on their surfaces, and it reported being about 26,000 times faster for drag computation than an existing GPU-optimized simulator. When the time to evaluate a single design shrinks this much, sweeping through thousands of candidates, rather than a few designs a person picked by intuition, becomes a realistic option.&lt;/p&gt;
&lt;p&gt;The catheter example I introduced earlier goes in a slightly different direction, because it didn’t stop at making predictions fast but ran prediction in reverse.&lt;/p&gt;
&lt;p&gt;In the FNO section, I said neural network libraries can automatically compute which direction the loss changes when you nudge a value slightly. Apply that same feature to the input instead of the model’s weights, and you can compute “which way should the size or spacing of the bumps change so fewer bacteria swim up?”, and the researchers actually refined the bump shape this way.&lt;/p&gt;
&lt;p&gt;They flipped a tool for predicting outcomes into a tool for finding designs that produce the outcome you want. Traditional simulators do have techniques that trace things backward, like the adjoint method, but they have to be implemented separately for each simulator and are computationally heavy.&lt;/p&gt;
&lt;p&gt;Beyond these, research is expanding into a wide range of fields: carbon storage research predicting how carbon dioxide buried underground will spread over decades, research predicting plasma changes inside a fusion reactor roughly a million times faster than traditional simulations, seismic wave propagation prediction, predicting warping during 3D printing, and digital twins that replicate factory equipment as real-time physical models.&lt;/p&gt;
&lt;h3 id="so-is-it-a-silver-bullet" style="position:relative;"&gt;So Is It a Silver Bullet?&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#so-is-it-a-silver-bullet" aria-label="so is it a silver bullet permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;By now, you might be thinking, “So PINO is the answer to physics simulation.” But just as LLMs like Claude aren’t all-powerful, PINO isn’t either. Every neural network architecture comes with both strengths and limitations.&lt;/p&gt;
&lt;p&gt;PINO’s first limitation is that it doesn’t necessarily obey the laws of physics. PINO’s check term is just a value added to the loss, not a hard constraint. The model will of course learn in the direction that reduces the loss, but there’s no guarantee it will never violate the rules.&lt;/p&gt;
&lt;p&gt;In other words, PINO is a model that “mostly obeys” the laws of physics, not one that “always obeys” them. Some articles introducing PINO give off the impression that it solves physics problems without error, but if you follow the principles, it’s more accurate to say it approximates them much faster than existing methods, with fairly small errors.&lt;/p&gt;
&lt;p&gt;The second is a constraint of the Fourier transform itself. The basic Fourier transform assumes the signal repeats endlessly in the same shape, and it works well only when the cells are laid out on a neat, regular grid. But real cars and blood vessels have irregular shapes and don’t repeat. So follow-up research keeps coming out to compensate, such as methods that unfold complex shapes onto a regular grid or methods that handle the edges more accurately.&lt;/p&gt;
&lt;p&gt;The third is that it’s weak in situations outside the range it learned. Ask a model that only learned from slow water flow about supersonic airflow, and it’ll have a hard time answering properly. This is actually a limitation of every machine learning model, Transformers included. So in practice, the more realistic use is to quickly filter thousands of candidates with a model like PINO, then carefully verify only the final few candidates with a traditional simulator.&lt;/p&gt;
&lt;p&gt;And the relationship with Transformers isn’t black and white either. Earlier I said FNO sees the whole thing at once like attention, and in fact there’s been a lot of recent research redesigning attention within the Neural Operator framework.&lt;/p&gt;
&lt;p&gt;In the end, I think the key isn’t whether it’s a Transformer or not, but how much data you have and how much of the rules you already know about the problem you can build into the model. If data is abundant, learn from the data; if data is scarce, teach it the rules you know.&lt;/p&gt;
&lt;h2 id="closing-thoughts" style="position:relative;"&gt;Closing Thoughts&lt;a href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/#closing-thoughts" aria-label="closing thoughts permalink" class="anchor after"&gt;&lt;svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"&gt;&lt;path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I started digging into this topic because of a simple question. These days everyone talks about LLMs as if they can do anything, but there’s no way they really can, right? So what can’t LLMs do, and where do those limits come from?&lt;/p&gt;
&lt;p&gt;Following the thread, the answer turned out to be simpler than I expected: a model only knows as much as it has seen. Claude can’t compute the weather not because the Transformer architecture falls short, but because it has never seen the atmosphere, and the same Transformer, once it has seen decades of atmospheric records, can even predict the weather better than traditional numerical forecasting. In other words, the question “Can AI do this?” is really closer to “Has AI ever seen this?”&lt;/p&gt;
&lt;p&gt;So what do you do about problems it has never seen? PINO’s answer was not to simply trust a plausible answer, but to check it against rules you already know. You may not have answer data, but you do know the equations, so you measure how much the model’s answer violates them and pull it toward answers that make physical sense.&lt;/p&gt;
&lt;p&gt;I actually think this isn’t so different from what developers deal with every day working with LLMs. An LLM has never seen our service’s codebase or domain rules, so in those areas it has no choice but to produce code that merely looks plausible.&lt;/p&gt;
&lt;p&gt;So what matters is less agonizing over whether to trust an LLM’s answer, and more building a structure where you can check that answer with types, tests, and domain rules, the way PINO scores itself with equations. And in the end, I believe making AI work properly where data is scarce is the job of the people who know the rules of that problem.&lt;/p&gt;
&lt;p&gt;And with that, I’ll bring this post on why Claude can’t compute the weather to a close.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/</id>
    <link href="https://evan-moon.github.io/2026/10/03/why-claude-cannot-compute-weather/en/"/>
    <summary type="html">In this post, I want to talk about what actually happens when you ask an AI like Claude or GPT to compute the weather, and about PINO (Physics-Informed Neural Operator), an approach that emerged to tackle physics problems where data is scarce. Looking around these days, quite a few people seem to treat LLMs like Claude or GPT as an all-purpose tool. And in a world where AI writes code, summarizes papers, and reviews contracts, it’s not hard to see why. I spend a good chunk of my day working alongside these AIs too, and I can definitely feel how much better they’ve gotten compared to before. But there’s a catch. If we ask an LLM to “compute how the temperature in Seoul will change tomorrow,” it doesn’t actually solve the equations for how the atmosphere’s temperature and pressure will evolve. Instead, it produces a plausible-sounding sentence that looks like the result of a computation. That’s because an LLM is a model that learned language by reading an enormous amount of text, not a model that learned by watching the Earth’s atmosphere move. To be fair, models built on the same Transformer architecture as Claude are already posting results in weather forecasting that rival traditional numerical weather prediction. But those models were trained on decades of records of the Earth’s atmosphere, and a field like weather, where training data is plentiful, is actually closer to the exception. Most physics problems, like the airflow around a car or the plasma inside a fusion reactor, have nowhere near enough data to train on. What these problems do have is one crucial thing going for them: we already know the equations that govern the phenomenon. So in this post, I’ll start from why LLMs can’t compute the weather, then introduce PINO, an approach that learns by scoring itself against physics equations instead of answer data, and walk through how it actually works.</summary>
    <title>Why Can't Claude Compute the Weather?</title>
    <updated>2026-10-04T05:31:22+09:00</updated>
    <dc:date>2026-10-04T05:31:22+09:00</dc:date>
  </entry>
  <dc:date>2026-10-08T14:42:01+09:00</dc:date>
</feed>
