<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>유승완</title>
    <link>https://imnotadevleoper.tistory.com/</link>
    <description>글을 작성하는 시간은 인생에서 가장 의미있는 시간 중에 하나라고 생각합니다.</description>
    <language>ko</language>
    <pubDate>Wed, 6 May 2026 23:51:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>유승완</managingEditor>
    <image>
      <title>유승완</title>
      <url>https://tistory1.daumcdn.net/tistory/5094360/attach/fcc4b5b8def4401fa5984a20042178db</url>
      <link>https://imnotadevleoper.tistory.com</link>
    </image>
    <item>
      <title>[번역] startTransition은 언제 정말로 필요할까요?</title>
      <link>https://imnotadevleoper.tistory.com/383</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://tigerabrodi.blog/when-do-you-really-need-starttransition&quot;&gt;When Do You Really Need startTransition?&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- 실제 포스팅 할 때는 나루토 이미지 추가 예정 --&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EqayR/dJMcajaUBFv/kvZGkQM2FjPH4d8oAVWpvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EqayR/dJMcajaUBFv/kvZGkQM2FjPH4d8oAVWpvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EqayR/dJMcajaUBFv/kvZGkQM2FjPH4d8oAVWpvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEqayR%2FdJMcajaUBFv%2FkvZGkQM2FjPH4d8oAVWpvk%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;432&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 개발자 대부분은 startTransition에 대해 들어본 적이 있습니다. 하지만 이것이 실제로 어떤 역할을 하는지 제대로 이해하는 사람은 드뭅니다. 언제 사용해야 하는지 아는 사람은 더욱 적습니다. 이제 기초부터 차근차근 설명해 드리겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;startTransition 없이 리액트가 렌더링하는 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startTransition을 이해하려면, 먼저 리액트가 일반적으로 상태 업데이트를 어떻게 처리하는지 알아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setState를 호출하면 리액트는 렌더링을 예약합니다. 렌더링이란 리액트가 컴포넌트 함수를 실행하고, 새로운 요소 트리를 생성한 뒤, 이를 이전 트리와 비교하여 차이점을 DOM에 적용하는 과정을 의미합니다. 이를 &quot;재조정(reconciliation)&quot;이라고 합니다. 재조정이란 단순히 리액트가 무엇이 변경되었는지 파악하는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점이 있습니다. 기본적으로 모든 상태 업데이트는 긴급한(urgent) 작업으로 간주됩니다. 리액트는 모든 업데이트를 동일하게 취급합니다. 업데이트는 순서대로 처리되며, 각 업데이트가 완료되어야만 다음 업데이트가 시작됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 검색창에 텍스트를 입력한다고 가정해 봅시다. 키를 누를 때마다 새로운 텍스트를 전달받아 setState가 호출됩니다. 리액트는 컴포넌트를 다시 렌더링합니다. 만약 해당 컴포넌트가 10,000개의 항목으로 구성된 목록을 필터링하는 중이라면, 키를 누를 때마다 10,000개 항목 전체에 대한 전체 렌더링이 트리거됩니다. 리액트는 다음 키 입력을 처리하기 전에 10,000개 항목의 렌더링을 모두 완료해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 &quot;hello&quot;라고 입력합니다. 키 입력 5회. 렌더링 5회. 각 렌더링은 10,000개의 항목을 처리합니다. 리액트가 목록을 렌더링하느라 바쁘기 때문에 키 입력 사이에 입력 필드가 멈춥니다. 사용자는 텍스트가 한 글자씩 표시되는 대신 덩어리 단위로 나타나는 것을 보게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 바로 쟁크(jank)입니다. &quot;쟁크&quot;란 인터페이스가 끊기거나 반응이 느리게 느껴지는 현상을 말합니다. 이는 메인 스레드가 너무 오래 걸리는 작업으로 인해 차단될 때 발생합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;startTransition의 실제 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startTransition은 리액트에 특정 상태 업데이트가 긴급하지 않음을 알립니다. 리액트는 이를 백그라운드에서 처리할 수 있으며, 더 중요한 작업이 들어오면 이를 중단할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 이에 대한 개념적 모델입니다. 리액트에는 이제 두 개의 레인이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴급 레인(urgent lane). 입력, 클릭, 키 입력 같은 작업들입니다. 이러한 작업들은 화면을 즉시 업데이트해야 하며, 그렇지 않으면 앱이 제대로 작동하지 않는 것처럼 느껴집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전환 레인(transition lane). 필터링된 목록 업데이트, 큰 차트 렌더링, 검색 결과 처리 같은 작업들입니다. 이러한 작업들도 중요하지만, 사용자가 눈치채지 못할 정도로 몇 프레임 정도는 기다릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setState 호출을 startTransition으로 감싸면, 리액트에게 해당 업데이트를 전환 레인으로 보내라고 지시하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import { useState, useTransition } from &quot;react&quot;;

function Search() {
  const [query, setQuery] = useState(&quot;&quot;);
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    // 긴급. 입력을 즉시 업데이트합니다.
    setQuery(e.target.value);

    // 긴급하지 않음. 리액트가 시간이 될 때 필터링된 목록을 업데이트합니다.
    startTransition(() =&amp;gt; {
      setResults(filterItems(e.target.value));
    });
  }

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input value={query} onChange={handleChange} /&amp;gt;
      {isPending &amp;amp;&amp;amp; &amp;lt;span&amp;gt;Filtering...&amp;lt;/span&amp;gt;}
      &amp;lt;ItemList items={results} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 사용자가 &quot;hello&quot;를 입력하면 다음과 같은 일이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;h&quot; 키를 입력합니다. 리액트는 즉시 입력 필드에 &quot;h&quot;가 표시되도록 업데이트합니다. 그런 다음 전환 레인에서 목록 필터링 작업을 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트가 아직 필터링 중인 동안 &quot;e&quot; 키 입력이 도착합니다. 리액트는 &quot;h&quot;에 대한 필터링을 중단합니다. 즉시 입력 필드에 &quot;he&quot;가 표시되도록 업데이트한 다음, 대신 &quot;he&quot;에 대한 필터링을 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 입력한 키가 즉시 화면에 나타나는 것을 볼 수 있습니다. 리액트가 잠시 숨을 돌릴 틈이 생기면 목록이 업데이트됩니다. 끊김 현상 없이 매끄럽게 진행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 단어는 &quot;중단(abandons)&quot;입니다. 리액트는 새로운 업데이트가 들어오면 진행 중인 트랜지션 렌더링을 버릴 수 있습니다. 반드시 완료할 필요는 없습니다. 이를 중단 가능한 렌더링(interruptible rendering)이라고 합니다. startTransition을 사용하지 않으면 리액트는 이를 수행할 수 없습니다. 시작한 작업을 반드시 완료해야만 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리액트가 렌더링을 중단해야 할 시점을 어떻게 알까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트가 렌더링 도중에 작업을 중단할 수 있는 이유가 궁금할 수 있습니다. 그 이유는 리액트 18에서 내부적인 렌더링 방식이 변경되었기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 18 이전에는 렌더링이 동기식으로 이루어졌습니다. 리액트는 렌더링을 시작하면 완료될 때까지 중단하지 않았습니다. 이 기간 동안 메인 스레드는 계속 차단된 상태였습니다. 렌더링 중에는 어떤 사용자 이벤트도 처리할 수 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 18에서는 동시 렌더링 기능이 도입되었습니다. 리액트는 렌더링 작업을 작은 단위로 나눕니다. 각 단위가 처리된 후에는 더 시급한 작업이 있는지 확인합니다. 시급한 작업이 있다면 현재 작업을 일시 중지하고 시급한 작업을 먼저 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startTransition은 리액트에 &amp;ldquo;이 업데이트는 일시 중지해도 안전하다&amp;rdquo;고 알리는 방법입니다. 리액트는 시급한 업데이트를 일시 중지하지 않습니다. 오직 트랜지션만 일시 중지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useTransition의 isPending 값은 현재 전환이 진행 중인지 여부를 알려줍니다. 이 값을 사용하여 백그라운드에서 대용량 작업이 진행되는 동안 로딩을 표시할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제로 필요한 사용 사례&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI 스트리밍&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 토큰을 빠르게 전송합니다. 초당 10~30개 정도의 토큰이 전송될 수 있습니다. 각 토큰이 도착할 때마다 마크다운 문자열이 늘어납니다. 이를 블록 단위로 나누어 다시 렌더링합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startTransition을 사용하지 않으면, 각 토큰이 동기식 렌더링을 트리거합니다. 렌더링에 30ms가 걸리고 토큰이 33ms마다 도착한다면, 렌더링 작업이 쌓이게 됩니다. 브라우저는 화면을 그릴 기회를 전혀 얻지 못하게 되고, 페이지가 멈춰 버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startTransition을 사용하면 블록 상태 업데이트를 감싸게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const newBlocks = splitIntoBlocks(content);
  startTransition(() =&amp;gt; {
    setBlocks(newBlocks);
  });
}, [content]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 1이 도착합니다. 리액트는 전환 레인에서 새로운 블록을 렌더링하기 시작합니다. 33ms 후에 토큰 2가 도착합니다. 리액트는 토큰 1에 대한 렌더링을 중단하고 토큰 2로 새로 시작합니다. 토큰 3이 도착합니다. 다시 중단합니다. 토큰 전송이 잠시 멈춘 시점에 리액트는 가장 최근의 렌더링을 완료합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 가장 최근의 상태를 보게 됩니다. 중간 상태는 모두 볼 수 없습니다. UI는 이 모든 과정에서 반응성을 유지합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대규모 리스트 필터링 또는 검색&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 대표적인 예시입니다. 수천 개의 항목이 있는 상황에서 사용자가 검색어를 입력한다고 가정해 봅시다. startTransition을 사용하지 않으면, 리액트가 수천 개의 항목을 다시 렌더링하는 동안 키 입력 하나하나가 메인 스레드를 차단하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startTransition을 사용하면 입력 반응이 빠릅니다. 리액트에 여유가 생길 때 목록이 업데이트됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대용량 콘텐츠가 포함된 탭 전환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 탭을 클릭합니다. 새로운 탭에는 복잡한 컴포넌트 트리가 있습니다. 차트, 표, 방대한 양의 데이터 등이 포함되어 있죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startTransition을 사용하지 않으면, 새로운 콘텐츠 전체가 렌더링될 때까지 클릭한 탭이 시각적으로 강조 표시되지 않습니다. 마치 클릭이 아무런 반응도 일으키지 않은 것처럼 느껴집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startTransition을 사용하면, 활성 탭을 긴급 업데이트로 즉시 업데이트합니다. 그런 다음 탭 콘텐츠를 전환 효과와 함께 로드합니다. 탭은 즉시 강조 표시되고, 콘텐츠는 잠시 후에 나타납니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Tabs({ tabs }) {
  const [activeTab, setActiveTab] = useState(0);
  const [content, setContent] = useState(tabs[0].content);
  const [isPending, startTransition] = useTransition();

  function handleClick(index) {
    setActiveTab(index); // 긴급. 탭을 즉시 강조 표시합니다.
    startTransition(() =&amp;gt; {
      setContent(tabs[index].content); // 전환. 콘텐츠를 준비될 때 렌더링합니다.
    });
  }

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;div className=&quot;tabs&quot;&amp;gt;
        {tabs.map((tab, i) =&amp;gt; (
          &amp;lt;button key={i} onClick={() =&amp;gt; handleClick(i)} className={i === activeTab ? &quot;active&quot; : &quot;&quot;}&amp;gt;
            {tab.label}
          &amp;lt;/button&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
      {isPending ? &amp;lt;Spinner /&amp;gt; : &amp;lt;TabContent data={content} /&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;단일 페이지 애플리케이션(SPA)에서의 탐색&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 링크를 클릭했을 때, 탐색이 즉각적으로 이루어지는 것처럼 느껴지도록 해야 합니다. 하지만 새 페이지가 로딩에 시간이 걸릴 수 있습니다. 이 경우 경로 업데이트를 startTransition 메서드 안에 감싸세요. 그러면 기존 페이지는 그대로 표시된 채로, 새 페이지는 백그라운드에서 렌더링됩니다. 화면이 텅 비는 현상이 발생하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Router와 Next.js는 이미 내부적으로 이러한 방식을 사용하고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;startTransition이 필요하지 않은 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 상태 업데이트에 startTransition이 필요한 것은 아닙니다. 대부분은 필요하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setState로 트리거된 렌더링이 16ms 미만으로 빠르다면 startTransition이 필요하지 않습니다. 브라우저는 프레임 드롭 없이 이를 처리할 수 있습니다. 빠른 업데이트에 startTransition을 추가하면 이득 없이 복잡성만 가중됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 업데이트가 사용자 입력을 직접 제어하는 경우, startTransition으로 감싸지 마십시오. 입력은 즉시 업데이트되어야 합니다. 업데이트를 분리할 수 있습니다. 입력 값은 시급합니다. 그 값의 하류 효과는 전환입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 업데이트가 단 하나뿐이고 그 규모가 작다면, startTransition은 오버헤드를 발생시킵니다. 스케줄링 메커니즘은 무료가 아닙니다. 사소한 업데이트의 경우, 단순히 동기식으로 렌더링하는 것보다 더 느립니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;startTransition vs 디바운싱&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람들은 때때로 디바운싱을 통해 같은 문제를 해결하기도 합니다. &amp;ldquo;사용자가 300ms 동안 키 입력을 멈출 때까지 기다린 다음, 목록을 업데이트한다.&amp;rdquo; 이 방법은 작동하지만 인위적인 지연을 유발합니다. 사용자는 마지막 키 입력 후 300ms를 기다려야 결과를 볼 수 있습니다. 리액트가 더 빨리 렌더링할 수 있다 하더라도 말이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startTransition은 지연을 추가하지 않기 때문에 더 낫습니다. 리액트는 즉시 작업을 시작합니다. 단지 중단될 수 있는 방식으로 작업을 수행할 뿐입니다. 다음 키 입력 전에 리액트가 렌더링을 완료하면, 사용자는 결과를 즉시 볼 수 있습니다. 300ms를 기다릴 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디바운싱은 작업 시작을 지연시킵니다. startTransition은 작업을 즉시 수행하지만 중단 가능하게 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 가지를 결합할 수 있습니다. 키 입력마다 API 호출을 실행하고 싶지 않은 네트워크 요청에는 디바운싱을 사용하세요. 그 뒤를 잇는 렌더링에는 startTransition을 사용하세요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;startTransition vs requestIdleCallback&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람들이 자주 비교하는 또 다른 개념입니다. requestIdleCallback은 &amp;ldquo;브라우저가 유휴 상태일 때 이 함수를 실행하라&amp;rdquo;는 브라우저 API입니다. 이름은 비슷해 보이지만 실제로는 매우 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;requestIdleCallback은 리액트 외부에서 작업을 스케줄링하기 위한 것입니다. 리액트의 렌더링과는 전혀 연동되지 않습니다. 리액트는 이 기능을 인식하지 못하며, 리액트의 렌더링을 중단시킬 수도 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startTransition은 리액트의 자체적인 기능입니다. 이는 reconciler와 연동됩니다. &quot;Reconciler&quot;는 무엇을 다시 렌더링할지 결정하는 리액트의 구성 요소입니다. 리액트는 전체 프로세스를 제어하므로 트랜지션을 중단하고 재개할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석이나 로깅과 같은 리액트와 무관한 작업에는 requestIdleCallback을 사용하세요. 비용이 많이 드는 렌더링을 유발하는 리액트 상태 업데이트에는 startTransition을 사용하세요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 문장으로 요약하면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startTransition은 리액트에 &quot;이 업데이트는 중요하지만 잠시 미뤄도 된다&quot;고 알립니다. 리액트는 긴급한 업데이트가 들어올 때 전환 렌더링을 중단함으로써 UI의 반응성을 유지합니다. 상태 업데이트로 인해 비용이 많이 드는 렌더링이 발생하지만, 사용자가 렌더링이 끝날 때까지 기다리며 작업이 차단되어서는 안 되는 경우에 이 메서드를 사용하세요.&lt;/p&gt;</description>
      <category>개발/번역</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/383</guid>
      <comments>https://imnotadevleoper.tistory.com/383#entry383comment</comments>
      <pubDate>Sun, 26 Apr 2026 14:02:22 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 이것이 리눅스다(개정4판)</title>
      <link>https://imnotadevleoper.tistory.com/382</link>
      <description>&lt;h3 id=&quot;%22%ED%95%9C%EB%B9%9B%EB%AF%B8%EB%94%94%EC%96%B4%20%EC%84%9C%ED%8F%89%EB%8B%A8%20%3C%EB%82%98%EB%8A%94%20%EB%A6%AC%EB%B7%B0%EC%96%B4%EB%8B%A4%3E%20%ED%99%9C%EB%8F%99%EC%9D%84%20%EC%9C%84%ED%95%B4%EC%84%9C%20%EC%B1%85%EC%9D%84%20%ED%98%91%EC%B0%AC%EB%B0%9B%EC%95%84%20%EC%9E%91%EC%84%B1%EB%90%9C%20%EC%84%9C%ED%8F%89%EC%9E%85%EB%8B%88%EB%8B%A4.%22-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&quot;한빛미디어 서평단 &amp;lt;나는 리뷰어다&amp;gt; 활동을 위해서 책을 협찬받아 작성된 서평입니다.&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도서 링크: &lt;a style=&quot;color: #1155cc; text-align: start;&quot; href=&quot;https://www.hanbit.co.kr/store/books/look.php?p_code=B2498421283&quot; data-saferedirecturl=&quot;https://www.google.com/url?q=https://www.hanbit.co.kr/store/books/look.php?p_code%3DB2498421283&amp;amp;source=gmail&amp;amp;ust=1777216821403000&amp;amp;usg=AOvVaw2ms5mdbZxefiRUFgaw1W4Y&quot;&gt;https://www.&lt;span&gt;&lt;span&gt;&lt;span&gt;hanbit&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;.co.kr/store/books/look.php?p_code=B2498421283&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;2426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKpB2K/dJMcaayj7Sp/L8UDCndIyRKSFguO5xC49K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKpB2K/dJMcaayj7Sp/L8UDCndIyRKSFguO5xC49K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKpB2K/dJMcaayj7Sp/L8UDCndIyRKSFguO5xC49K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKpB2K%2FdJMcaayj7Sp%2FL8UDCndIyRKSFguO5xC49K%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;593&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;2426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 이 책을 읽게 되었을 때는 &amp;lsquo;리눅스 명령어를 배우는 입문서&amp;rsquo; 정도로 가볍게 읽을 수 있겠다고 생각했다. 기본적인 명령어와 개념을 중심으로 설명하는 책일 것이라고 예상했는데, 실제로 읽어보니 이 책의 방향은 조금 달랐다. 단순히 리눅스를 설명하는 데 그치기보다, 하나의 서버 환경을 직접 구성하고 운영해보는 흐름 속에서 자연스럽게 리눅스를 이해하게 만드는 구조였다. 그래서 이 책을 읽는 경험은 &amp;lsquo;리눅스를 공부한다&amp;rsquo;기보다 &amp;lsquo;서버를 직접 만들어보면서 익힌다&amp;rsquo;는 느낌에 가까웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책이 흥미로웠던 점은 설명의 출발점이 항상 &amp;ldquo;실제 환경&amp;rdquo;에 있다는 데 있다. 단순히 명령어를 나열하는 방식이 아니라, 가상머신을 활용해 여러 대의 서버를 구성하고, 그 위에서 사용자 관리, 네트워크 설정, 서비스 운영을 단계적으로 경험하게 만든다. 덕분에 각각의 개념이 독립적으로 떠 있는 것이 아니라, 하나의 시스템 안에서 어떻게 연결되는지를 함께 이해하게 된다. 리눅스를 처음 접하는 입장에서는 &amp;ldquo;이걸 왜 배우지?&amp;rdquo;라는 질문이 자연스럽게 해소되는 구조다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;2154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CbIkl/dJMcaaE6p2J/7CfuvPGCjWycYPmyYoQkbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CbIkl/dJMcaaE6p2J/7CfuvPGCjWycYPmyYoQkbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CbIkl/dJMcaaE6p2J/7CfuvPGCjWycYPmyYoQkbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCbIkl%2FdJMcaaE6p2J%2F7CfuvPGCjWycYPmyYoQkbk%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;554&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;2154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책 전반의 흐름은 비교적 친절하게 구성되어 있지만, 그렇다고 해서 내용을 가볍게만 다루지는 않는다. 설치부터 시작해 파일 시스템, 사용자 관리, 네트워크, 서버 구축까지 점진적으로 확장되며, 하나의 인프라를 만들어가는 과정을 따라가게 된다. 특히 단순한 CLI 사용을 넘어서 웹 서버, 메일 서버, DNS 같은 실제 서비스까지 다루는 부분은 이 책이 단순 입문서를 넘어선다는 느낌을 준다. 그래서 빠르게 읽기보다는, 직접 실습을 따라가며 천천히 읽는 것이 더 적합한 책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽으면서 자연스럽게 떠오른 생각은, 우리가 평소에 사용하는 많은 서비스들이 얼마나 복잡한 시스템 위에 올라가 있는지에 대한 것이었다. 개발을 하다 보면 API를 호출하고 결과를 받는 것에 익숙해지지만, 그 뒤에서 서버가 어떻게 구성되고 동작하는지는 깊이 생각해 볼 기회가 많지 않다. 이 책은 그런 부분을 한 단계 내려가서 보게 만든다. 거창한 이론을 설명하기보다는, 실제로 손을 움직이면서 시스템을 이해하게 만든다는 점에서 의미가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천하고 싶은 독자도 비교적 명확하다. 리눅스를 처음 접하는 입문자이면서도, 단순히 명령어만 배우는 것이 아니라 서버가 어떻게 구성되는지까지 알고 싶은 사람에게 적합하다. 특히 개발자라면, 백엔드나 인프라를 직접 다루지 않더라도 시스템에 대한 이해를 넓히는 데 도움이 될 수 있다. 반대로 빠르게 핵심만 훑거나 개념만 정리하고 싶은 독자에게는 다소 무겁게 느껴질 수도 있다. 이 책은 무엇을 요약해주는 책이라기보다, 직접 만들어보면서 이해하게 만드는 책이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적으로 보면, 이 책은 &amp;lsquo;리눅스를 설명하는 책&amp;rsquo;이라기보다 &amp;lsquo;서버 환경을 직접 구성해보며 시스템을 이해하게 만드는 책&amp;rsquo;에 가깝다. 읽고 나면 모든 내용을 완전히 익혔다고 말하기는 어렵지만, 적어도 리눅스가 단순한 운영체제가 아니라 하나의 시스템을 구성하는 핵심 요소라는 점을 체감하게 만든다는 점에서 의미 있는 경험이었다.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/382</guid>
      <comments>https://imnotadevleoper.tistory.com/382#entry382comment</comments>
      <pubDate>Sun, 26 Apr 2026 00:18:23 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 코드 리뷰를 잘한다면, AI 에이전트를 잘 활용하게 될 것입니다</title>
      <link>https://imnotadevleoper.tistory.com/381</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://www.seangoedecke.com/ai-agents-and-code-review/&quot;&gt;If you are good at code review, you will be good at using AI agents&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 에이전트를 올바르게 사용하는 일은 코드 리뷰 과정과 닮아 있습니다. 코드 리뷰에 익숙하다면 Claude Code, Codex, Copilot 같은 코딩 에이전트 도구도 더 잘 활용할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그럴까요? 대규모 언어 모델은 많은 양의 코드를 생성하는 데는 뛰어나지만, 숙련된 소프트웨어 엔지니어가 내리는 깊이 있는 판단까지는 아직 충분히 갖추지 못했습니다. 감독 없이 맡겨두면 잘못된 설계 결정을 따라가느라 많은 시간을 허비하게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AI 에이전트와 나쁜 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난주 저는 &lt;a href=&quot;https://github.com/sgoedecke/vicflora-offline&quot;&gt;VicFlora Offline&lt;/a&gt;을 만들었습니다. (식물 분류 키를 사용할 수 있도록 VicFlora 데이터 일부를 호스팅하는 오프라인 친화적인 PWA입니다.) 인터넷 수신이 나쁜 야외 환경에서도 키를 계속 사용할 수 있도록 하기 위해서입니다. Codex는 &lt;a href=&quot;https://simple.wikipedia.org/wiki/Dichotomous_key&quot;&gt;이분법적 키&lt;/a&gt;를 위한 VicFlora 프런트엔드 코드를 리버스 엔지니어링하는 데 많은 노력을 기울였습니다. 솔직히 지켜보는 것만으로도 꽤 인상적이었습니다. 다만 저는 원시 데이터에 접근하는 더 쉬운 방법이 있을 거라고 생각했는데, 실제로 그게 맞았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 코딩 에이전트를 사용할 때마다 비슷한 일이 반복됩니다. 한 시간에 한 번쯤 에이전트가 수상한 방향으로 가는 걸 발견하고, 조금만 자세히 살펴보면 올바른 방향으로 돌려놓음으로써 수 시간의 낭비를 막을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 저는 AI로 학습을 돕는 앱도 개발 중입니다. &amp;lsquo;무한히 자동 조정되는 간격 반복 학습 피드&amp;rsquo;라고 생각하시면 됩니다. 병렬 작업을 수행할 때(예: 백그라운드에서 학습 계획 생성), Codex와 Claude Code는 모두 작업 엔티티, 결과 폴링 등 완전한 백그라운드 작업 인프라를 구축하려고 합니다. 백그라운드 작업 자체는 유용하지만, 평범한 단기 병렬 작업에는 명백히 과잉 설계입니다. 프런트엔드에서 비차단 요청만 보내도 충분하니까요. 단순함을 꾸준히 지키지 않았다면 제 코드베이스는 훨씬 복잡해졌고, 이해하기도 어려웠을 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덧붙여 말하자면, 저는 이것이 순수한 &amp;lsquo;바이브 코딩&amp;rsquo;만으로 유용한 앱이 폭발적으로 늘어나지 못한 이유라고 생각합니다. LLM이 잘못된 방향으로 가고 있다는 신호를 감지할 기술적 능력이 없다면 금방 막히게 됩니다. 잘못 설계된 솔루션을 억지로 작동시키려 하면 시간과 토큰, 그리고 코드베이스 복잡성을 소모하게 됩니다. 이 모든 요소가 에이전트의 실제 문제 해결 능력을 갉아먹습니다. 두세 가지 문제가 쌓이기 시작하면 앱은 더 이상 에이전트가 처리할 수 있는 수준을 벗어나고, 결국 모든 것이 멈춰 버립니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 리뷰&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 사례는 열정 넘치는 신입들과 엔지니어링 팀에서 충분히 일해본 사람이라면 익숙할 겁니다. 초기 아이디어에 곧바로 뛰어들어 순수한 노력만으로 어떻게든 작동시키려 하는 건 매우 흔한 실수입니다. 이런 상황을 제어하는 것이 나머지 팀원들의 역할이죠. AI 에이전트와 일하는 경험은, 시간이 지나도 실제 인간이 갖게 되는 판단력을 발달시키지 못하는 열정 넘치는 신입과 함께 일하는 것과 비슷합니다. [1]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;[1]: 이 주장을 볼 때마다 궁금해집니다. 저는 2022년 초에 Copilot 같은 AI 코딩 도구를 사용하기 시작했는데, 2025년이 된 지금도 최첨단 AI 도구를 사용하고 있습니다. 이 흐름을 보면 도구가 인간과 비슷한 속도로 성장한 것 같은 느낌이 들지 않나요? 초기 Copilot을 신입 엔지니어로, 현재 Claude Code(또는 유사 도구)를 경력 3년 차 엔지니어로 비유한다면 너무 과장일까요? 앞으로 3년 후에는 AI 도구와 협업하는 일이 경력 6년 차 엔지니어와 일하는 것에 가까워질까요?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 저는, 엔지니어들이 코드 리뷰에서 저지르는 가장 큰 실수 하나를 이야기해볼 좋은 기회라고 생각합니다. &lt;b&gt;작성된 코드만 보고, 작성될 수도 있었던 코드는 보지 않는 것&lt;/b&gt;입니다. 경험 많은 엔지니어조차도 변경 사항을 꼼꼼히 훑으면서도, &amp;ldquo;이 코드가 과연 여기에 있어야 하는가?&amp;rdquo; 같은 질문은 거의 하지 않는 장면을 종종 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 보기에 가장 효과적인 코드 리뷰는 구조적입니다. 즉, diff에 직접 등장하지 않는 코드베이스의 다른 부분에서 맥락을 끌어와 판단합니다. 이상적으로는 그 맥락이 diff를 더 짧고 우아하게 만들기도 합니다. 예를 들어, 작업 X를 위해 새로운 시스템을 만드는 대신 이미 존재하는 시스템을 재사용할 수 있습니다. 프런트엔드 SPA 코드에서 이분법적 키 ID를 추출하는 취약한 스크래핑 파이프라인을 만드는 대신, 이분법적 키가 명시적으로 제공되는 다른 곳에서 그냥 다운로드하면 됩니다. 전체 백그라운드 작업 시스템을 구축하는 대신, 웹사이트가 동시에 두 가지 작업을 수행하기 위해 이미 갖추고 있는 메커니즘을 활용해 클라이언트 측 병렬 작업으로 끝낼 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로, 지나치게 디테일에 매달리는 코드 리뷰어는 AI 도구를 효과적으로 활용하기 어려울 수 있습니다. 개별 코드 줄을 끝없이 수정하고, &lt;code&gt;.map().filter()&lt;/code&gt; 대신 &lt;code&gt;.reduce()&lt;/code&gt;를 요구하며, 함수 이름을 두고 불필요한 논쟁을 벌이는 식으로 시간을 쓰게 됩니다. 그러는 사이 AI가 아키텍처의 막다른 골목으로 빠지지 않도록 안내할 기회를 놓치게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로, 무조건 승인하는 코드 리뷰어는 AI 도구에 지나치게 의존하게 됩니다. 이 접근은 유능한 동료들과는 어느 정도 통할 수 있지만, 신입 엔지니어를 교육할 때는 효과적이지 않고, AI 코딩 에이전트와 협업할 때도 잘 작동하지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마지막 생각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 &amp;ldquo;AI를 잘한다&amp;rdquo;는 건 무엇을 의미할까요? 예를 들어 git 같은 도구는 기준이 비교적 명확합니다. 저장소의 기본 트리 구조를 이해하고, 대부분의 git 작업을 익혔다면 git을 잘 다룬다고 말할 수 있죠. 하지만 AI의 기본 구조는 헤아릴 수 없는 모델 가중치의 집합이고, 수행할 수 있는 &amp;ldquo;작업&amp;rdquo;은 &amp;ldquo;컴퓨터로 할 수 있는 거의 모든 것&amp;rdquo;입니다. 이와 비슷한 소프트웨어 공학 도구는 존재하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 낙관적인 AI 지지자들은 &amp;ldquo;AI를 잘 활용한다&amp;rdquo;는 것이 삶의 모든 측면에 AI 도구를 최대한 도입하는 것이라고 말합니다. 그들의 관점에서 AI는 제프 베조스의 직원들과 비슷한 역할을 합니다. 막대한 자원과 뛰어난 역량을 가진 직원을 활용하는 데는 특별한 기술이 필요하지 않습니다. 원하는 것을 요청하기만 하면, 수많은 사람들의 노력이 그것을 제공하기 위해 투입될 테니까요. 하지만 오늘날 제가 갑자기 그의 자리에 텔레포트된다 해도, 저는 분명 베조스보다 훨씬 덜 효과적으로 스태프를 활용할 겁니다. 제가 원하는 것의 절반도 요청하지 않을 테니까요. 예를 들어, 개인 전용기에서 내릴 때 뜨거운 &lt;a href=&quot;https://lunecroissanterie.com/&quot;&gt;루네 크루아상&lt;/a&gt;이 기다리고 있을 거라는 생각 자체를 떠올리지 못할 겁니다. 비록 실제로는 꽤 즐겼을지라도 말입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 신봉자들은 AI 도구도 이와 비슷하다고 봅니다. 개인 AI 비서에게 원하는 프로그램을 코드로 만들어 달라고 하거나, 어떤 양의 데이터든 분류해 달라고 하거나, 모든 이메일의 초안을 작성해 달라고 요청할 수 있다는 사실을 진정으로 내면화하면, AI를 훨씬 더 자주 활용하게 되고 그만큼 이익을 볼 거라는 주장입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 아직은 그 단계까지 오지 않은 것 같습니다. 저는 에이전트형 코딩 도구를 많이 사용합니다. 업무에서는 GitHub Copilot을, 개인 프로젝트[2]에서는 Codex와 Claude Code를 모두 사용합니다. 이 도구들은 놀라울 정도로 많은 작업을 스스로 수행하지만, 상당히 세심한 감독이 필요합니다. 현재 주류 프로그래밍 모델은 숙련된 인간과 컴퓨터 보조자가 협력하는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Advanced_chess&quot;&gt;&amp;lsquo;켄타우로스 체스&amp;rsquo;&lt;/a&gt;와 유사합니다. 코드 리뷰 능력, 즉 특정 소프트웨어 접근법이 타당한지 평가하는 능력이 뛰어날수록 에이전트형 AI 도구를 활용하는 데도 더 능숙해질 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;[2]: Codex와 Claude Code를 사용한다고 해서 Copilot보다 더 낫다고 생각하는 것은 아닙니다. 제 생각에 다양한 AI 도구를 활용하는 것은 제 업무의 일부입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편집: 이 글이 &lt;a href=&quot;https://news.ycombinator.com/item?id=45310529&quot;&gt;Hacker News&lt;/a&gt;에서 주목을 받았습니다. 대부분의 댓글은 &amp;ldquo;AI 에이전트가 실제로 작동하는가&amp;rdquo;라는 질문을 되풀이했는데, 이는 제게 그다지 흥미롭지 않습니다(제 견해는 &amp;ldquo;물론 일부 코드베이스에서는 유용하지만, 완전히 쓸모없는 코드베이스도 존재할 수 있다는 것&amp;rdquo;입니다). 저는 훌륭한 코드 리뷰를 통해서 AI 에이전트가 리눅스 커널에 실질적으로 기여할 수 있게 해줄 거라고는 생각하지 않습니다. &lt;a href=&quot;https://news.ycombinator.com/item?id=45311422&quot;&gt;몇몇&lt;/a&gt; &lt;a href=&quot;https://news.ycombinator.com/item?id=45312278&quot;&gt;댓글&lt;/a&gt;에서는 &amp;ldquo;코드 스멜과 함수 명명&amp;rdquo; 스타일의 코드 리뷰를 옹호하며, 코드 리뷰에서 설계를 논의한다면 사전에 기능을 충분히 계획하지 못한 것이라고 주장했습니다. 저는 그 견해에 상당히 강하게 반대하며, 언젠가 이에 관한 글도 따로 쓰려고 합니다.&lt;/p&gt;</description>
      <category>개발/번역</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/381</guid>
      <comments>https://imnotadevleoper.tistory.com/381#entry381comment</comments>
      <pubDate>Sun, 8 Mar 2026 17:09:45 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] AI를 움직이는 수학 이야기</title>
      <link>https://imnotadevleoper.tistory.com/380</link>
      <description>&lt;h3 id=&quot;%22%ED%95%9C%EB%B9%9B%EB%AF%B8%EB%94%94%EC%96%B4%20%EC%84%9C%ED%8F%89%EB%8B%A8%20%3C%EB%82%98%EB%8A%94%20%EB%A6%AC%EB%B7%B0%EC%96%B4%EB%8B%A4%3E%20%ED%99%9C%EB%8F%99%EC%9D%84%20%EC%9C%84%ED%95%B4%EC%84%9C%20%EC%B1%85%EC%9D%84%20%ED%98%91%EC%B0%AC%EB%B0%9B%EC%95%84%20%EC%9E%91%EC%84%B1%EB%90%9C%20%EC%84%9C%ED%8F%89%EC%9E%85%EB%8B%88%EB%8B%A4.%22-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a style=&quot;color: #353638;&quot; href=&quot;https://imnotadevleoper.tistory.com/376#%22%ED%95%9C%EB%B9%9B%EB%AF%B8%EB%94%94%EC%96%B4%20%EC%84%9C%ED%8F%89%EB%8B%A8%20%3C%EB%82%98%EB%8A%94%20%EB%A6%AC%EB%B7%B0%EC%96%B4%EB%8B%A4%3E%20%ED%99%9C%EB%8F%99%EC%9D%84%20%EC%9C%84%ED%95%B4%EC%84%9C%20%EC%B1%85%EC%9D%84%20%ED%98%91%EC%B0%AC%EB%B0%9B%EC%95%84%20%EC%9E%91%EC%84%B1%EB%90%9C%20%EC%84%9C%ED%8F%89%EC%9E%85%EB%8B%88%EB%8B%A4.%22-1&quot;&gt;&quot;한빛미디어 서평단 &amp;lt;나는 리뷰어다&amp;gt; 활동을 위해서 책을 협찬받아 작성된 서평입니다.&quot;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5n9Pe/dJMcahQ3JdW/QCKjJpyQRgC7WiBsrVG39K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5n9Pe/dJMcahQ3JdW/QCKjJpyQRgC7WiBsrVG39K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5n9Pe/dJMcahQ3JdW/QCKjJpyQRgC7WiBsrVG39K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5n9Pe%2FdJMcahQ3JdW%2FQCKjJpyQRgC7WiBsrVG39K%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;552&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 이 책을 읽게 되었을 때는 &amp;lsquo;AI를 설명하는 수학 교양서&amp;rsquo; 정도로 가볍게 읽을 수 있겠다고 생각했다. 그런데 책을 조금 보다 보니 이 책의 목적이 단순히 수학 개념을 소개하는 소개하는 것보다는 우리가 매일 사용하는 검색, 추천, 음성 인식, 생성형 AI 같은 기술이 어떤 원리 위에서 작동하는지를 따라가다 보면, 자연스럽게 그 배경에 있는 수학과 만나게 되는 구조다. 그래서 책을 읽는 경험이 &amp;lsquo;수학을 공부한다&amp;rsquo;기보다 &amp;lsquo;기술을 이해하는 과정에서 수학을 다시 만난다&amp;rsquo;는 느낌에 가까웠던 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;2226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lE0yS/dJMcahwMkG4/RLAK00UnkV2K9NNLlbanAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lE0yS/dJMcahwMkG4/RLAK00UnkV2K9NNLlbanAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lE0yS/dJMcahwMkG4/RLAK00UnkV2K9NNLlbanAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlE0yS%2FdJMcahwMkG4%2FRLAK00UnkV2K9NNLlbanAK%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;518&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;2226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책이 읽기에 흥미로웠던 점은 설명의 출발점이 항상 현실의 기술이라는 데 있다. 예를 들어 추천 시스템이나 LLM 같은 익숙한 주제에서 시작해 &amp;ldquo;왜 이런 방식으로 동작할까?&amp;rdquo;라는 질문을 던지고, 그 질문에 답하기 위해 필요한 개념들을 차근차근 끌어온다. 덕분에 수식이나 개념이 등장해도 뜬금없다는 느낌이 적고, 기술을 구성하는 하나의 언어처럼 받아들이게 된다. 수학을 잘 아는 독자에게는 구조를 정리해 보는 시간이 될 것이고, 오랫동안 수학과 거리를 두고 지냈던 독자에게는 &amp;ldquo;아, 이런 식으로 연결되는구나&amp;rdquo; 하고 흐름을 따라가 볼 수 있는 계기가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책 전반의 분위기는 어렵게 보일 수 있는 내용을 최대한 친절하게 풀어내려는 쪽에 가깝다. 그렇다고 해서 내용을 지나치게 단순화하거나 가볍게 만들지는 않는다. 필요한 계산 과정이나 개념 사이의 연결을 비교적 성실하게 보여주기 때문에, 한 번에 빠르게 읽기보다는 중간중간 멈춰 생각하면서 읽게 되는 책이다. 그래서 가볍게 훑고 지나가기보다는, 관심이 가는 부분을 중심으로 다시 펼쳐 보게 되는 유형에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽으면서 자연스럽게 떠오르는 생각은, 우리가 평소에 사용하는 많은 기술을 &amp;lsquo;결과&amp;rsquo; 중심으로만 받아들이고 있었다는 점이다. 검색 결과가 잘 나온다거나 추천이 정확하다는 사실은 알고 있었지만, 그것이 어떤 원리 위에서 가능한지는 깊이 생각해 볼 기회가 많지 않았기 때문이다. 이 책은 그런 기술들을 조금 다른 각도에서 바라보게 만들어 준다. 거창한 깨달음을 준다기보다, 익숙한 것들을 한 번 더 이해해 보고 싶게 만드는 쪽에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천하고 싶은 독자는 분명하다. AI나 데이터 과학을 전공하지 않았더라도, 지금 우리가 쓰는 기술이 어떤 구조 위에서 만들어졌는지 궁금했던 사람이라면 충분히 흥미롭게 읽을 수 있다. 특히 개발자처럼 실제로 도구를 사용하고 있는 독자라면, 구현이나 사용법과는 또 다른 층위의 이해를 얻는 경험이 될 것이다. 반대로 빠르게 실습을 따라 하거나 즉각적인 활용 방법을 기대하는 독자에게는 조금 느리게 느껴질 수도 있다. 이 책은 무엇을 만드는 방법을 알려주기보다는, 이미 만들어진 것들을 다른 시선으로 보게 만드는 책이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적으로 보면, 이 책은 &amp;lsquo;수학을 설명하는 책&amp;rsquo;이라기보다 &amp;lsquo;기술을 이해하는 또 하나의 길&amp;rsquo;을 보여주는 책에 가깝다. 읽고 나면 모든 내용을 완전히 이해했다고 말하기는 어렵지만, 적어도 지금 쓰고 있는 기술들이 어떤 기반 위에 서 있는지 한 번쯤 생각해 보게 만든다는 점에서 의미 있는 경험이었다.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/380</guid>
      <comments>https://imnotadevleoper.tistory.com/380#entry380comment</comments>
      <pubDate>Mon, 2 Mar 2026 22:36:07 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 장애허용성</title>
      <link>https://imnotadevleoper.tistory.com/379</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbgdln/dJMcahDkvtE/RrwByZiPLM8T3NWJyw6W7K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbgdln/dJMcahDkvtE/RrwByZiPLM8T3NWJyw6W7K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbgdln/dJMcahDkvtE/RrwByZiPLM8T3NWJyw6W7K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcbgdln%2FdJMcahDkvtE%2FRrwByZiPLM8T3NWJyw6W7K%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;333&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://www.brandondail.com/posts/fault-tolerance-react&quot;&gt;Fault Tolerance&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대적인 웹 애플리케이션을 구축하는 일은 많은 구성 요소가 맞물려 있는 복잡한 과정입니다. 때로는 이러한 구성 요소들이 멈추면서 문제가 발생하기 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.brandondail.com/error.svg&quot; alt=&quot;error&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 이러한 상황을 예방하기 위해 최선을 다하지만, 현실적으로 애플리케이션을 완전히 에러 없이 유지하는 것은 불가능합니다. 즉 &lt;b&gt;예기치 않은 방식으로 문제가 발생할 수 있음&lt;/b&gt;을 항상 염두에 두고, 그러한 상황을 우아하게 처리할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해, &lt;a href=&quot;https://en.wikipedia.org/wiki/Fault_tolerance&quot;&gt;장애허용성&lt;/a&gt;이 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장애허용성이란 시스템을 구성하는 일부 구성 요소에서 장애가 발생하더라도(하나 이상의 결함이 존재하더라도) 시스템이 정상적으로 동작을 계속할 수 있도록 해주는 특성입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 경험상 웹 애플리케이션에서 장애허용성은 종종 간과되거나 과소평가됩니다. 수백 개의 테스트를 통해 문제가 발생하지 않을 것이라는 어느 정도의 확신은 가질 수 있지만, 불가피한 실패가 실제로 발생했을 때 어떤 일이 일어나는지에 대해서는 충분히 고민하지 않는 경우가 많습니다. 특히 &lt;a href=&quot;https://en.wikipedia.org/wiki/High_availability&quot;&gt;고가용성&lt;/a&gt;이 중요한 상황에서는 이 점이 더욱 중요합니다. (그리고 대부분의 시스템에서는 그렇습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 리액트 애플리케이션이 장애허용성을 갖도록 하려면 어떻게 해야 할까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 바운더리&lt;/h2&gt;
&lt;!-- 에러 바운더리 링크가 예전 리액트 문서이긴 한데 거기에도 최신 문서로 통하는 링크가 있어서 원문 그대로 유지해주었어요! --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 대답은 &lt;a href=&quot;https://legacy.reactjs.org/docs/error-boundaries.html&quot;&gt;에러 바운더리&lt;/a&gt;입니다. 현재 이 API는 클래스 컴포넌트에서만 사용할 수 있으며, 대략 다음과 같은 형태입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;componentDidCatch(error) {
  // 이 메서드가 호출되면 에러가 발생했음을 알 수 있습니다!
  // 장애 감지에 성공하였습니다!

  // 이제 에러가 발생했음을 사용자에게 알리기 위해 setState를 호출하여 폴백 UI를 렌더링할 수 있습니다.
  // 사용자는 이제 에러가 발생했음을 알 수 있습니다.
  this.setState({ error, showFallback: true });
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트에서 에러 바운더리는 componentDidCatch 메서드를 가진 클래스 컴포넌트에 불과합니다. 시작하기에 좋은 출발점이 필요하다면 &lt;a href=&quot;https://github.com/bvaughn/react-error-boundary&quot;&gt;react-error-boundary&lt;/a&gt;를 참고하면 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 공식 문서는 에러 바운더리가 무엇인지, 그리고 이를 어떻게 사용하는지에 대해 매우 잘 설명하고 있으므로 여기서는 그 부분을 깊이 다루지 않겠습니다. 기본 개요를 원한다면 먼저 공식 문서를 읽고, 이후에 다시 돌아오면 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;적절한 경계선 그리기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에 에러 바운더리를 추가하는 일은 쉽습니다. 몇 줄의 코드만 있으면 됩니다. 까다로운 부분은 이를 어디에 배치할지 &lt;b&gt;적절한 위치를 찾는 일&lt;/b&gt;입니다. 앞으로 살펴보겠지만, 보통은 &lt;a href=&quot;https://en.wikipedia.org/wiki/Goldilocks_principle&quot;&gt;골디락스 원칙&lt;/a&gt;을 따라 &amp;ldquo;딱 적당한 정도&amp;rdquo;의 에러 바운더리를 구현하는 것이 좋습니다. 하지만 &amp;ldquo;딱 적당한 정도&amp;rdquo;란 무엇일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 양 극단 두 가지를 살펴보면서 각각의 단점을 확인해 보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 바운더리가 충분하지 않은 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 극단은 애플리케이션의 최상단에 에러 바운더리 하나만 두는 방식입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// ⚠️ 이 예제에서는 react-error-boundary를 사용하겠습니다.
import ErrorBoundary from &quot;react-error-boundary&quot;;
import App from &quot;./App.js&quot;;

ReactDOM.render(
  &amp;lt;ErrorBoundary&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/ErrorBoundary&amp;gt;,
  document.getElementById(&quot;root&quot;)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 아마도 대부분의 사람이 하는 방식과 비슷할 것입니다. 서버 사이드 렌더링 애플리케이션이 실패할 때 일어나는 일과 본질적으로 동일합니다. 최악의 경험은 아니지만, 우리가 제공할 수 있는 최선의 경험은 아닙니다. 문제는 &lt;b&gt;한 부분에서 문제가 발생하면 애플리케이션 전체가 함께 무너진다는 점입니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 접근은 애플리케이션의 어느 부분에서든 실패가 발생하면 전체가 사용할 수 없게 되는 경우라면 타당합니다. 실제로 그런 경우도 분명히 존재하지만, 일반적인 경우는 아니라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애허용성의 정의로 돌아가 보면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장애허용성이란 장애가 발생하더라도, 시스템이 정상적으로 동작을 이어갈 수 있게 해주는 특성입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 보면 에러 바운더리 하나만 두는 방식은 사실 장애허용성을 제공하지도 못한다는 것을 알 수 있습니다. &lt;b&gt;실패가 한 번 발생하면 애플리케이션 전체가 함께 내려가기 때문입니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 바운더리가 너무 많은 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대 극단으로, 모든 컴포넌트를 에러 바운더리로 감싸는 방법을 시도할 수도 있습니다. 이 접근의 문제는 더 미묘하므로, 왜 이것이 에러 바운더리 하나만 두는 방식보다 더 나쁠 수 있는지 이해하기 위해 더 구체적인 예제를 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 장바구니에 담긴 상품을 확인하고, 신용카드 정보를 입력하고, 결제를 완료할 수 있게 해주는 &lt;code&gt;CheckoutForm&lt;/code&gt; 컴포넌트가 있다고 가정해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;function CheckoutForm(props) {
  return (
    &amp;lt;form&amp;gt;
      &amp;lt;CartDescription items={props.items} /&amp;gt;
      &amp;lt;CreditCardInput /&amp;gt;
      &amp;lt;CheckoutButton cartId={props.id} /&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 여기 있는 모든 컴포넌트를 에러 바운더리로 감싸 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;// 모든 컴포넌트에 에러 바운더리를 추가합니다!
&amp;lt;form&amp;gt;
  &amp;lt;ErrorBoundary&amp;gt;
    &amp;lt;CartDescription items={props.items} /&amp;gt;
  &amp;lt;/ErrorBoundary&amp;gt;
  &amp;lt;ErrorBoundary&amp;gt;
    &amp;lt;CreditCardInput /&amp;gt;
  &amp;lt;/ErrorBoundary&amp;gt;
  &amp;lt;ErrorBoundary&amp;gt;
    &amp;lt;CheckoutButton cartId={props.id} /&amp;gt;
  &amp;lt;/ErrorBoundary&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사례에서는 각 컴포넌트를 이런 식으로 인라인으로 감싸기보다는, 보통 자신의 export를 에러 바운더리로 감싸는 방식으로 구현할 것입니다(예: react-error-boundary의 withErrorBoundary HOC). 그 부분은 그냥 무시하면 됩니다  &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이것이 괜찮은 아이디어처럼 보일 수 있습니다. 에러 바운더리를 더 세밀하게 나눌수록 단일 실패가 애플리케이션 전체에 미치는 영향이 줄어들기 때문입니다. 이것은 장애허용성처럼 들립니다. 하지만 문제는 &lt;b&gt;에러의 영향을 최소화하는 것이 장애허용성을 갖춘 것과 같지는 않다는 점&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CreditCardInput&lt;/code&gt; 컴포넌트의 무언가가 고장 났다고 가정해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;form&amp;gt;
  &amp;lt;ErrorBoundary&amp;gt;
    &amp;lt;CartDescription items={props.items} /&amp;gt;
  &amp;lt;/ErrorBoundary&amp;gt;
  &amp;lt;ErrorBoundary&amp;gt;
    {/* 앗! 여기서 무언가 고장 났습니다   */}
    &amp;lt;CreditCardInput /&amp;gt;
  &amp;lt;/ErrorBoundary&amp;gt;
  &amp;lt;ErrorBoundary&amp;gt;
    &amp;lt;CheckoutButton cartId={props.id} /&amp;gt;
  &amp;lt;/ErrorBoundary&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 사용자 경험(UX)에 어떤 의미를 갖는지 자세히 풀어 보면, 사용자에게는 매우 고통스러운 상황이 될 수 있음을 알 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;반쯤 깨진 UI는 완전히 깨진 UX입니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CreditCardInput&lt;/code&gt;이 자체 에러 바운더리를 가지고 있으므로 에러는 &lt;code&gt;CheckoutForm&lt;/code&gt;의 나머지 부분으로 전파되지 않습니다. 하지만 &lt;code&gt;CheckoutForm&lt;/code&gt;은 &lt;code&gt;CreditCardInput&lt;/code&gt; 없이 사용할 수 없습니다  . &lt;code&gt;CheckoutButton&lt;/code&gt;과 &lt;code&gt;CardDescription&lt;/code&gt; 컴포넌트는 여전히 마운트되어 있으므로 사용자는 여전히 상품을 확인하고 결제를 시도할 수 있습니다. 하지만 신용카드 정보 입력을 끝내지 못했다면 어떻게 될까요? &lt;code&gt;CreditCardInput&lt;/code&gt;이 크래시 나기 전에 신용카드 정보를 입력했다면 그 상태는 유지되었을까요? 사용자가 결제를 시도하면 무슨 일이 일어날까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에 어떤 일이 일어날지 컴포넌트 작성자조차도 알기 어려울 것이라고 봅니다. 하물며 사용자는 더더욱 알 수 없습니다. 이는 사용자에게 혼란과 좌절을 안겨줄 가능성이 큽니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;범용 에러 바운더리, 범용 폴백&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 얼마나 답답하고 혼란스러운 경험이 되는지는 폴백으로 무엇을 렌더링하느냐에 따라서도 달라집니다. 컴포넌트가 아무 경고 없이 화면에서 그냥 사라질까요? 이는 대부분의 사용자에게 매우 혼란스러운 경험이 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않다면 아마도 공통으로 쓰는 폴백 UI를 사용하고 있을 것입니다. 예를 들어 슬픈 얼굴과 함께 에러에 대한 유용한 정보를 보여주는 형태일 수 있습니다. 이는 아무것도 없는 것보다는 낫지만, 모든 컴포넌트를 이 에러 바운더리로 감싼다면 이 폴백은 가능한 모든 UI 요소에 대해 올바르게 렌더링되어야 합니다. 하지만 요소마다 필요한 레이아웃 요구사항이 다르기 때문에 이를 제대로 처리하는 것은 사실상 불가능에 가깝습니다. 헤더처럼 페이지 수준의 섹션에는 적절한 폴백이 작은 아이콘 버튼에는 부적절할 수 있으며, 그 반대도 마찬가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 문제는 명확해졌을 것입니다. &lt;b&gt;모든 컴포넌트를 에러 바운더리로 감싸면 혼란스럽고 망가진 사용자 경험으로 이어질 수 있습니다.&lt;/b&gt; 이는 애플리케이션을 일관되지 않은 상태로 만들기 쉽고, 그 결과 사용자는 답답함과 혼란을 느끼게 됩니다. 흥미로운 점은 &amp;ldquo;손상된 상태(corrupted state)&amp;rdquo;를 피하는 것이 &lt;a href=&quot;https://legacy.reactjs.org/blog/2017/07/26/error-handling-in-react-16.html#new-behavior-for-uncaught-errors&quot;&gt;에러 바운더리가 존재하는 주요 이유 중 하나라는 사실입니다.&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 결정에 대해서는 많은 논의가 있었지만, 우리의 경험상 손상된 UI를 그대로 남겨두는 것보다 이를 완전히 제거하는 편이 더 낫습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 패널티: 에러 바운더리에는 본질적인 오버헤드가 일부 존재하므로, 이를 과도하게 사용하면 성능에 부정적인 영향을 줄 수 있습니다. 다만 이는 에러 바운더리를 애플리케이션 전반에 걸쳐 모든 곳에 사용하는 경우에만 문제가 됩니다. 따라서 이 점 때문에 에러 바운더리 사용 자체를 꺼릴 필요는 없습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;적절한 수의 에러 바운더리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면, 에러 바운더리가 충분하지 않으면 에러가 필요 이상으로 애플리케이션의 더 큰 부분을 무너뜨리게 되고, 에러 바운더리가 너무 많으면 손상된 UI 상태로 이어질 수 있습니다. 그렇다면 에러 바운더리는 어느 정도가 적절할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션마다 다르기 때문에 &amp;ldquo;이 정도가 적절한 수이다&amp;rdquo;라고 단일한 숫자를 제시할 수는 없습니다. 제가 찾은 가장 좋은 접근은 &lt;b&gt;애플리케이션에서 기능 경계를 식별하고, 그 경계에 에러 바운더리를 배치하는 것입니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기능 찾기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임의의 애플리케이션을 살펴보고 그 경계를 식별하는 데 사용할 수 있는 &amp;ldquo;기능&amp;rdquo;에 대한 보편적인 정의는 없습니다. 대부분의 경우 &lt;a href=&quot;https://en.wikipedia.org/wiki/I_know_it_when_I_see_it&quot;&gt;&amp;ldquo;보면 안다&amp;rdquo;&lt;/a&gt;라는 수준이 최선이지만, 가이드라인으로 삼을 수 있는 몇 가지 공통 패턴은 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 애플리케이션은 여러 개의 개별 섹션으로 구성되며, 이들이 서로 조합되어 하나의 애플리케이션을 이룹니다. 헤더, 내비게이션, 메인 콘텐츠, 사이드바, 푸터 등이 그 예입니다. 이들 각각은 사용자 경험 전체에 기여하지만, 동시에 일정 수준의 독립성도 유지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예로 &lt;a href=&quot;https://twitter.com&quot;&gt;트위터&lt;/a&gt;를 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.brandondail.com/twitter.png&quot; alt=&quot;twitter&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지에는 서로 구분되는 섹션과 기능들이 즉시 드러납니다. 트윗의 메인 타임라인, 팔로워 추천 영역, 트렌드 섹션, 내비게이션 바가 그 예입니다. 이러한 섹션들의 레이아웃과 스타일링만 보더라도 섹션 간에 분리가 존재함을 알 수 있으며, 이는 매우 좋은 출발점입니다. &lt;b&gt;시각적으로 독립적인 섹션은 대개 기능적으로도 독립적인 경우가 많고&lt;/b&gt;, 바로 이런 지점이 에러 바운더리를 두기에 적절한 위치입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이들 섹션 중 하나에서 컴포넌트가 에러를 던지더라도, 다른 섹션까지 함께 크래시되어서는 안 된다고 보는 것이 타당합니다. 예를 들어 팔로워 추천 섹션의 팔로우 버튼에서 에러가 발생하더라도, 메인 타임라인까지 함께 내려가서는 안 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;재귀적인 질문의 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI는 종종 &lt;a href=&quot;https://en.wikipedia.org/wiki/Recursion_(computer_science)&quot;&gt;재귀적인&lt;/a&gt; 구조를 가집니다. 페이지 수준에서는 사이드바나 타임라인 같은 큰 섹션이 있고, 그 안에는 다시 헤더나 리스트와 같은 하위 섹션이 있으며, 이들 또한 또 다른 섹션을 포함하는 식으로 이어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 바운더리를 배치할 적절한 위치를 식별할 때 스스로에게 던져볼 수 있는 좋은 질문은 &lt;b&gt;&amp;ldquo;이 컴포넌트에서 발생한 에러가 형제 컴포넌트에 어떤 영향을 미쳐야 하는가?&amp;rdquo;&lt;/b&gt;입니다. &lt;code&gt;CheckoutForm&lt;/code&gt; 예제에서도 바로 이 질문을 고려했습니다. &lt;code&gt;CreditCardInput&lt;/code&gt;이 실패했다면, 그 실패는 &lt;code&gt;CheckoutButton&lt;/code&gt;과 &lt;code&gt;CardDescription&lt;/code&gt;에 어떤 영향을 미쳐야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 질문을 컴포넌트 트리에 대해 재귀적으로 적용하면, 기능 경계가 어디에 있는지를 빠르게 식별할 수 있고, 그 경계를 기준으로 에러 바운더리를 배치할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트위터 심층 분석: 페이지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 트위터를 예로 들어 이 방식이 어떻게 동작하는지 살펴보겠습니다. 먼저 페이지 최상단에서 시작한 뒤, 팔로워 추천 섹션으로 더 깊이 들어가 보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 분석에는 해당 기능들이 어떻게 동작하는지에 대한 제 인식과 기대를 바탕으로 한 의견과 편향이 적지 않게 포함되어 있습니다. 이는 &amp;ldquo;정답&amp;rdquo;을 제시하기 위한 것이 아니라, 단지 사고 과정 자체를 살펴보는 것이 목적입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.brandondail.com/twitter-section-1.png&quot; alt=&quot;twitter-section-1&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상단에서 시작하면 세 가지 주요 콘텐츠 섹션을 식별할 수 있습니다. &lt;b&gt;Home&lt;/b&gt;, &lt;b&gt;Trends for you&lt;/b&gt;, 그리고 &lt;b&gt;Who to follow&lt;/b&gt;입니다. 이제 &lt;b&gt;Who to follow&lt;/b&gt; 섹션을 더 자세히 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 다음과 같은 질문을 던집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 컴포넌트에서 발생한 에러는 형제 컴포넌트에 어떤 영향을 미쳐야 할까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문은 다음과 같이 조금 더 구체적으로 바꿔볼 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 컴포넌트가 크래시된다면, 형제 컴포넌트도 함께 크래시되어야 할까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;Who to follow&lt;/b&gt; 섹션을 고려할 때 우리는 이렇게 질문하게 됩니다. &lt;b&gt;Who to follow 섹션이 크래시되었을 때 Home과 Trends 섹션도 함께 크래시되어야 할까요?&lt;/b&gt; 이는 분명 그렇지 않은 경우라고 생각합니다. 다른 섹션들은 서로 의존하는 것처럼 보이지 않으므로, 이 지점은 에러 바운더리를 배치하기에 매우 적절한 위치입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트위터 심층 분석: Who to follow&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 동일한 질문의 흐름을 &lt;b&gt;Who to follow&lt;/b&gt; 섹션에도 그대로 적용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.brandondail.com/twitter-section-2.png&quot; alt=&quot;twitter-section-2&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Who to follow에 초점을 맞추면 세 가지 분명한 섹션을 확인할 수 있습니다. 제목, 팔로우할 사용자 목록, 그리고 &amp;ldquo;show more&amp;rdquo; 버튼입니다. 사용자 목록을 더 자세히 살펴보면서 다시 동일한 질문을 던집니다. &lt;b&gt;팔로워 목록이 크래시된다면 제목과 &amp;ldquo;show more&amp;rdquo; 버튼도 함께 크래시되어야 할까요?&lt;/b&gt; 이 경우에는 다소 애매할 수 있지만, 아마도 그렇지 않은 편이 더 타당하다고 생각합니다. 제목을 그대로 유지한다고 해서 큰 문제가 생기지는 않으며, &amp;ldquo;Show more&amp;rdquo; 버튼은 다른 페이지로 연결되는데 그 페이지 자체는 정상적으로 동작하고 있을 수도 있습니다. 따라서 여기서도 답은 다시 한 번 &amp;lsquo;예&amp;rsquo;이며, 또 하나의 에러 바운더리를 추가하는 것이 적절합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트위터 심층 분석: 팔로워 추천&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 팔로워 추천 영역을 대상으로 한 번 더 같은 과정을 적용해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.brandondail.com/twitter-section-3.png&quot; alt=&quot;twitter-section-3&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는 두 개의 섹션만 있으므로 다음과 같은 질문만 던지면 됩니다. &lt;b&gt;사용자의 이름과 핸들이 크래시된다면 Follow 버튼도 함께 크래시되어야 할까요? 반대로 Follow 버튼이 크래시된다면 사용자의 이름과 핸들도 함께 크래시되어야 할까요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에는 그 답이 &amp;lsquo;예&amp;rsquo;라고 느껴집니다. 사용자의 이름과 핸들이 사라진다면 우리가 누구를 팔로우하고 있는지 알 수 없게 됩니다. 반대로 Follow 버튼이 사라진다면, 아무런 행동도 할 수 없는 추천을 받게 되어 사용자에게 답답한 경험이 될 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장애허용성 테스트하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 에러 바운더리와 장애허용성을 사용하는 방법에 대해 조금 더 알게 되었으니, 이 전체 주제에서 제가 가장 좋아하는 부분 중 하나를 공유하고자 합니다. 바로 이를 어떻게 테스트할 수 있는가입니다. 제가 찾은 장애허용성을 테스트하는 가장 간단하고 수고가 적은 방법은, 애플리케이션에 &lt;b&gt;직접 들어가서 의도적으로 무언가를 망가뜨려 보는 것입니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function CreditCardInput(props) {
  // 여기서 제가 실수를 했다면 무슨 일이 일어날까요? 직접 확인해 봅시다.
  throw new Error(&quot;oops, I made a mistake!&quot;);
  return &amp;lt;input className=&quot;credit-card&quot; /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 제가 새로운 컴포넌트를 추가할 때마다 하기 시작한 방법이며, 애플리케이션이 실패를 어떻게 처리하는지 확인하는 데 매우 도움이 되었습니다. 다만 이러한 &lt;code&gt;throw&lt;/code&gt; 구문을 커밋하지 않도록 주의해야 합니다  &lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애허용성을 테스트하기 위해 의도적으로 에러를 던지는 것은 &lt;a href=&quot;https://en.wikipedia.org/wiki/Chaos_engineering&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;카오스 엔지니어링&lt;/a&gt;의 아주 기본적인 예시입니다. 임의로 일부 컴포넌트를 랜덤하게 고장 내면서 장애허용성을 테스트할 수 있도록 해주는 React.ChaosMode 같은 것이 있다면 어떨까요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면, 이 글에서 말하고자 한 핵심은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;애플리케이션 최상단에 에러 바운더리 하나만 두는 방식은 피해야 합니다.&lt;/b&gt; 실패를 처리하는 최선의 방법인 경우는 드뭅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 바운더리를 과도하게 사용하는 것도 피해야 합니다.&lt;/b&gt; 이는 사용자 경험을 저해하고, 잠재적으로 성능에 악영향을 줄 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;애플리케이션의 기능 경계를 식별하고, 그 경계에 에러 바운더리를 배치해야 합니다.&lt;/b&gt; 리액트 애플리케이션은 트리 구조로 이루어져 있으므로, 최상단에서 시작해 아래로 내려가며 살펴보는 방식이 효과적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&amp;ldquo;이 컴포넌트가 크래시된다면, 형제 컴포넌트도 함께 크래시되어야 할까?&amp;rdquo;라는 질문을 재귀적으로 던져 보아야 합니다.&lt;/b&gt; 이는 기능 경계를 찾는 데 유용한 휴리스틱입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 상태를 전제로 애플리케이션을 의도적으로 설계해야 합니다.&lt;/b&gt; 기능 경계에 에러 바운더리를 배치하면 보기 좋은 커스텀 폴백 UI를 만들기 쉬워지고, 사용자에게 무언가 잘못되었음을 명확하게 전달할 수 있습니다. 나아가 페이지 전체를 새로 고치지 않고도 특정 섹션만 다시 시도할 수 있도록 기능별 재시도 로직을 구현할 수도 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의도적으로 무언가를 망가뜨려 보고, 어떤 일이 일어나는지 직접 확인해 보아야 합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발/번역</category>
      <category>error boundary</category>
      <category>Fault Tolerance</category>
      <category>장애허용성</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/379</guid>
      <comments>https://imnotadevleoper.tistory.com/379#entry379comment</comments>
      <pubDate>Mon, 9 Feb 2026 03:27:47 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 모든 것을 배열로 바꾸는 것을 그만두세요 (대신 일을 덜 하세요)</title>
      <link>https://imnotadevleoper.tistory.com/378</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://allthingssmitty.com/2026/01/12/stop-turning-everything-into-arrays-and-do-less-work-instead/&quot;&gt;Stop turning everything into arrays (and do less work instead)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 프론트엔드 코드는 데이터가 화면에 표시되기 훨씬 이전에 데이터를 처리합니다. 우리는 목록을 가져오고, 수정하고, 줄이고, 이 과정을 반복합니다. 하지만 보통 그 과정에서 우리가 얼마나 많은 작업을 하고 있는지 깊이 생각하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수년간 현대 자바스크립트는 우리를 익숙한 패턴으로 이끌어 왔습니다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;data
  .map(...)
  .filter(...)
  .slice(...)
  .map(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가독성이 좋고 표현력도 풍부합니다. 하지만 즉시 평가되고(eager), 여러 배열을 할당하고 &lt;b&gt;불필요한 작업&lt;/b&gt;을 자주 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트의 &lt;b&gt;이터레이터 헬퍼&lt;/b&gt;는 대규모 데이터셋, 스트림, UI 기반 로직을 처리할 때 특히 유용한 네이티브 지연 평가 방식의 대안을 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어디에나 배열이 있고 (필요 이상으로 훨씬 더 많은 작업이 발생합니다)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 UI 시나리오를 생각해 보세요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 데이터셋을 가져옵니다&lt;/li&gt;
&lt;li&gt;필터링합니다&lt;/li&gt;
&lt;li&gt;처음 몇 개의 결과를 추출합니다&lt;/li&gt;
&lt;li&gt;그 결과를 렌더링합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const visibleItems = items
  .filter(isVisible)
  .map(transform)
  .slice(0, 10);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나쁘지 않아 보이죠? 솔직히 말하면, 저도 이런 문구를 몇 번이나 썼는지 모를 정도입니다. 하지만 내부적으로는 꽤 많은 작업을 하고 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;filter&lt;/code&gt;는 새로운 배열을 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt;은 또 다른 배열을 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;slice&lt;/code&gt;도 역시 또 하나의 배열을 생성합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 10개 항목만 필요하더라도, 수천 개를 처리했을 수 있습니다. 바로 이 불일치가 문제의 핵심입니다. 그리고 이 지점에서 이터레이터 헬퍼가 빛을 발합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 이터레이터 헬퍼(Iterator Helpers)가 무엇인가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이테레이터 헬퍼는 배열이 아닌 &lt;b&gt;이테레이터 객체에 적용할 수 있는 체인 가능 메서드&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구분은 중요합니다. 그리고 처음에는 놓치기 쉽습니다. 배열이 마법처럼 이런 메서드를 갖게 되는 것은 아닙니다. &lt;code&gt;values()&lt;/code&gt;, &lt;code&gt;keys()&lt;/code&gt;, &lt;code&gt;entries()&lt;/code&gt; 또는 제너레이터를 통해 이터레이터를 얻어야 합니다. 그런 다음 그 위에 지연 파이프라인을 구축할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이터레이터 헬퍼를 사용하면 다음과 같은 작업을 수행할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;take&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;drop&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flatMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find&lt;/code&gt;, &lt;code&gt;some&lt;/code&gt;, &lt;code&gt;every&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reduce&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;toArray&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 헬퍼의 대부분은 &lt;b&gt;게으르게(lazzy)&lt;/b&gt; 동작하며, 즉 필요할 때만 값을 가져옵니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 참고: &lt;b&gt;reduce&lt;/b&gt;는 결과를 생성하기 위해 모든 값을 확인해야 하므로 이터레이터를 즉시 소모합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 게으름이란 다음을 의미합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중간 배열이 없습니다&lt;/li&gt;
&lt;li&gt;불필요한 작업이 발생하지 않습니다&lt;/li&gt;
&lt;li&gt;그리고 무엇보다도 가능한 한 빨리 작업을 중단합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 결과를 설명하면, 런타임은 필요한 경우에만 값을 가져옵니다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본적으로 게으르게&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이터레이터 헬퍼를 사용하면 동일한 논리를 다음과 같이 표현할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;const visibleItems = items
  .values()
  .filter(isVisible)
  .map(transform)
  .take(10)
  .toArray();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 여기서 실제로 무엇이 달라졌을까요?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;items.values()&lt;/code&gt;는 배열이 아니라 &lt;b&gt;이터레이터&lt;/b&gt;를 반환합니다&lt;/li&gt;
&lt;li&gt;각 단계는 &lt;b&gt;다음 값이 요청될 때만&lt;/b&gt; 실행됩니다&lt;/li&gt;
&lt;li&gt;10번 조건을 만족하면 처리가 중단됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 앱에서 이로 인해 얻는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순수한 속도만이 중요한 것은 아닙니다. 핵심은 불필요한 작업을 피하는 데 있습니다. 이터레이터 헬퍼는 &lt;b&gt;더 나은 UI 패턴&lt;/b&gt;을 가능하게 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대규모 목록 렌더링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 작업을 처리하고 있다고 가정해 봅시다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가상화된 목록&lt;/li&gt;
&lt;li&gt;무한 스크롤&lt;/li&gt;
&lt;li&gt;대규모 테이블&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연 반복은 화면에 표시되지 않는 항목을 처리하지 않는다는 의미입니다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;function* rows(data) {
  for (const row of data) {
    yield renderRow(row);
  }
}

const visibleRows = rows(data)
  .filter(isInViewport)
  .take(20)
  .toArray();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 것만 정확히 렌더링하고, 그 이상은 수행하지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스트리밍 및 비동기 데이터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비동기 이터러블(Async Iterables)&lt;/b&gt;은 자체적인 이터레이터 헬퍼를 제공하므로, 페이지 분할된 API나 스트림 처리에 매우 적합합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function* fetchPages() {
  let page = 1;
  while (true) {
    const res = await fetch(`/api/items?page=${page++}`);
    if (!res.ok) return;
    yield* await res.json();
  }
}

const firstTen = await fetchPages().filter(isValid).take(10).toArray();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 응답을 버퍼링할 필요도 없고, 수동으로 카운터를 관리할 필요도 없습니다. 파이프라인을 설명하기만 하면, 런타임이 필요한 부분만 가져옵니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  더 알아볼 준비가 되셨나요?&lt;br /&gt;루프에서 &lt;b&gt;await&lt;/b&gt;가 어떻게 동작하는지, 그리고 &lt;a href=&quot;https://allthingssmitty.com/2025/10/20/rethinking-async-loops-in-javascript/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;비동기 로직을 효율적으로 구성하는 방법&lt;/a&gt;을 살펴보세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깔끔한 데이터 파이프라인 (유틸리티 라이브러리 없이)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이터레이터 헬퍼가 등장하기 전에는 게으른 파이프라인을 구현하기 위해 별도의 라이브러리를 사용해야 했습니다. 이제는 언어 자체에 내장되었습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const ids = users
  .values()
  .map((u) =&amp;gt; u.id)
  .filter(Boolean)
  .toArray();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 쉽고, 네이티브이며, 의존성이 없습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이터레이터 헬퍼 vs 배열 메서드&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;&lt;b&gt;이터레이터 헬퍼&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;&lt;b&gt;배열 메서드&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;지연 처리&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;즉시 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;중간 배열 없음&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;중간 배열 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;가능한 한 빨리 작업 중단&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;모든 항목 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;약간의 러닝 커브&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;친숙함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;경험상&lt;/b&gt; 전체 배열이 필요하지 않다면, 배열을 생성하지 않는 것이 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이테레이터 헬퍼를 사용하지 &lt;i&gt;말아야&lt;/i&gt; 할 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이터레이터 헬퍼는 강력하지만, 모든 상황에서 배열을 대체할 수는 없습니다. 모든 경우에 억지로 적용하려 하면 오히려 코드 가독성만 떨어질 수 있습니다. 다음과 같은 경우에는 적합하지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임의 접근이 필요한 경우 (&lt;code&gt;items[5]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;배열 변형에 크게 의존하는 경우&lt;/li&gt;
&lt;li&gt;데이터 크기가 작아 단순성이 더 중요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;알아두어야 할 주의사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이터레이터 헬퍼는 몇 가지 중요한 측면에서 배열과 다르게 동작합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;주의사항&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;중요한 이유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;일회용 이터레이터&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;한 번 소비되면 다시 사용할 수 없습니다&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;동일한 파이프라인을 두 번 사용할 수 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;게으른 실행&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;소비되기 전까지 아무 작업도 실행되지 않습니다&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;부수 효과가 사라진 것처럼 보일 수 있습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;순차 접근만 가능&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;임의 접근이 불가능합니다&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;items[5]&lt;/b&gt;와 같은 패턴은 사용할 수 없습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;디버깅시 데이터 소모&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;로깅시 이터레이터가 이동합니다&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;b&gt;console.log&lt;/b&gt; 등의 로그가 동작 방식을 바꿀 수 있습니다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이터레이터는 이미 존재하는 데이터가 아니라 &lt;b&gt;아직 수행되지 않은 작업&lt;/b&gt;으로 생각해야 합니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금 당장 사용할 수 있나요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이터레이터 헬퍼는 모든 최신 브라우저와 Node 22 이상에서 지원됩니다. 현재 버전을 대상으로 한다면 문제없이 사용할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일부러 일을 덜 하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랫동안 자바스크립트는 모든 것을 열성적으로 배열로 변환하도록 우리를 훈련시켜 왔습니다. 반복자 헬퍼는 또 다른 선택지를 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 적은 작업을 수행하고&lt;/li&gt;
&lt;li&gt;더 적은 메모리를 할당하며&lt;/li&gt;
&lt;li&gt;UI가 실제로 작동하는 방식에 부합하는 파이프라인을 작성할 수 있게 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게으른 반복에 익숙해지면, 열성적인 체인으로 돌아가는 방식은 다소 낭비처럼 느껴질 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt; &amp;nbsp;한국어로&amp;nbsp;된&amp;nbsp;프런트엔드&amp;nbsp;아티클을&amp;nbsp;빠르게&amp;nbsp;받아보고&amp;nbsp;싶다면&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;background-color: #ffffff; color: #0070d1; text-align: left;&quot; href=&quot;https://kofearticle.substack.com/&quot;&gt;Korean&amp;nbsp;FE&amp;nbsp;Article&lt;/a&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;을&amp;nbsp;구독해주세요!&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/번역</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/378</guid>
      <comments>https://imnotadevleoper.tistory.com/378#entry378comment</comments>
      <pubDate>Sat, 24 Jan 2026 16:10:26 +0900</pubDate>
    </item>
    <item>
      <title>[번역] HTTP 범위 요청(Range Requests)을 통한 동영상 제공하기</title>
      <link>https://imnotadevleoper.tistory.com/377</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://smoores.dev/post/http_range_requests/&quot;&gt;Serving Video with HTTP Range Requests&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최근에&lt;/b&gt; 가족과 친구들을 위한 간단한 사진 공유 앱을 만들게 되었습니다.&lt;br /&gt;주변에서 근황을 놓치고 싶지 않은 일이 있었는데, 그걸 위해 클라우드 서비스에 의존하는 건 별로 마음에 들지 않았거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 Next.js로 작은 프로그레시브 웹 앱을 만들기로 결정했습니다. 몇 시간 만에 첫 버전을 완성했는데, 사진 디렉터리를 지정하면 그리드 형태의 썸네일과 섀도우박스 형태의 전체 크기 이미지로 렌더링하는 기본적인 자체 호스팅 앱이었습니다. 새 사진이 디렉터리에 추가되면 푸시 알림까지 받을 수 있도록 설정해 두기도 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다가, 이제 끝이라고 생각하고 이 프로젝트를 대성공으로 결론 내리려는 순간, 문득 한 가지가 생각났습니다. 비디오를 완전히 잊고 있었다는 점이었습니다! &quot;음, 문제없어,&quot;라고 생각했습니다, &quot;그냥 &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 태그를 쓰면 되잖아?&quot;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 요소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 요소는 컨트롤 표시, 자동 재생 같은 여러 옵션을 지원합니다. &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; 요소와 마찬가지로 &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 요소에도 &lt;code&gt;src&lt;/code&gt; 속성을 지정하여 삽입할 동영상의 URL을 명시할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 브라우저가 모든 비디오 형식을 지원하는 것은 아니기 때문에, &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 요소는 일반적으로 단일 &lt;code&gt;src&lt;/code&gt; 속성 대신 &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; 요소 목록을 자식으로 생성합니다. 브라우저는 지원하는 첫 번째 소스 유형을 찾을 때까지 이 소스 목록을 자동으로 순회합니다. 따라서 AV1 형식을 지원하는 사용자에게는 선명하고 용량이 작은 AV1 비디오를 제공하고, 그 외 사용자에게는 더 널리 지원되는 WEBM 또는 MP4 인코딩으로 대체할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MDN 문서에서 예시를 들어 설명해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;video controls width=&quot;250&quot;&amp;gt;
  &amp;lt;source src=&quot;/shared-assets/videos/flower.webm&quot; type=&quot;video/webm&quot; /&amp;gt;
  &amp;lt;source src=&quot;/shared-assets/videos/flower.mp4&quot; type=&quot;video/mp4&quot; /&amp;gt;
&amp;lt;/video&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://bfb64ee5-9861-47aa-b04a-e699002e3903.mdnplay.dev/shared-assets/videos/flower.webm&quot; width=&quot;430&quot; height=&quot;430&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 개발자 도구에서 네트워크 탭을 확인하면 여기서 무슨 일이 벌어지는지 볼 수 있습니다. 사용하는 브라우저에 따라 서로 다른 요청이 이루어지는 것을 확인할 수 있습니다. 현재 저는 WEBM을 지원하는 Zen/Firefox를 사용 중이라 WEBM 동영상만 요청합니다(최근 버전의 주요 브라우저 모두 마찬가지일 것입니다). &lt;code&gt;AV1&lt;/code&gt; 소스를 포함하면 구형 iPhone 및 macOS 기기에서는 이를 건너뛰고 WEBM 파일로 대체됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;범위 요청&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 탭을 아주 주의 깊게 살펴보셨다면 몇 가지 흥미로운 점을 발견하셨을 수도 있습니다. 첫째, &lt;code&gt;flower.webm&lt;/code&gt; 요청에 대한 응답 코드는 표준적인 &lt;code&gt;200&lt;/code&gt; 성공/OK 응답이 아닌 &lt;code&gt;206&lt;/code&gt;이었습니다. 브라우저에 따라서는 이 비디오 파일에 대한 요청이 여러 번 발생했고, 그 모든 요청이 &lt;code&gt;206&lt;/code&gt; 응답으로 돌아오는 모습을 보셨을 수도 있습니다. 또한 &lt;code&gt;Accept-Ranges: bytes&lt;/code&gt; 및 &lt;code&gt;Content-Range: bytes 0-554057/554058&lt;/code&gt;과 같은 추가 헤더도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것들이 합쳐져 &lt;b&gt;HTTP 범위 요청&lt;/b&gt;을 구성합니다. 이름에서 알 수 있듯이, 범위 요청은 클라이언트가 전체 리소스 대신 서버로부터 특정 바이트 범위를 요청할 수 있게 합니다. 이는 특히 비디오에 유용합니다. 비디오 파일은 길이가 임의적일 수 있고(따라서 용량이 클 수 있음) 클라이언트는 거의 항상 이를 조각(chunks) 단위로 로드하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 저의 작은 사진 공유 앱에 범위 요청을 지원할 생각이 전혀 없었습니다. 분명 유용하지만 Next.js에는 기본 지원이 없고, 직접 구현하는 것도 달갑지 않았습니다. 게다가 공유할 계획이었던 모든 동영상은 비교적 작아서 길이가 최대 1분에 불과했습니다. 로딩에 몇 초는 걸리겠지만 참을 수 없는 정도는 아니었죠. 동영상 파일을 200 응답 코드로 직접 제공하는 엔드포인트를 추가하고 커밋한 뒤, 작은 사이드 프로젝트를 마쳤다는 사실에 스스로를 칭찬했습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { FileHandle, open } from &quot;node:fs/promises&quot;;
import { join } from &quot;node:path&quot;;

import { createReadableStreamFromReadable } from &quot;@remix-run/node&quot;;

interface Props {
  params: Promise&amp;lt;{ album: string; file: string }&amp;gt;;
}

export async function GET(request: Request, props: Props) {
  const { album, file: filename } = await props.params;

  const rootDir = process.env.ROOT_DIR!;
  const filepath = join(rootDir, album, filename);

  let file: FileHandle;
  try {
    file = await open(filepath);
  } catch {
    return new Response(null, { status: 404 });
  }

  return new Response(createReadableStreamFromReadable(file.createReadStream()), {
    headers: { &quot;Content-Type&quot;: &quot;video/mp4&quot; },
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 제 휴대폰에서 앱을 테스트했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사파리 브라우저가 거부합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고보니 사파리는 비디오 소스가 HTTP 범위 요청을 반드시 &lt;i&gt;지원&lt;/i&gt;해야 했습니다. 사파리는 처음 두 바이트에 대한 요청을 보내고, 올바른 HTTP 범위 헤더가 포함된 응답을 받지 못하면 다음 소스로 넘어갑니다. 전체 목록을 다 확인해도 적절한 범위 응답을 받지 못하면, 아예 비디오를 렌더링하지 않습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 이미지나 동영상 같은 정적 자산은 CDN을 통해 제공되거나 적어도 적절한 정적 파일 서버를 사용합니다. 이런 환경에서는 모두 범위 요청을 지원하기 때문에, 개발자가 그 동작 방식까지 신경 써야 할 일은 거의 없습니다. 하지만 제가 직접 하드웨어에 자체 호스팅할 목적으로 앱을 구축 중이었기 때문에 CDN이나 별도의 정적 파일 서버에 의존할 수 없었습니다. 또한 사용자가 제공한 에셋을 제공해야 했기 때문에 HTTP 범위 요청을 지원하는 Next.js의 &lt;code&gt;public&lt;/code&gt; 폴더 규칙에도 의존할 수 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋습니다. 그럼, 저는 이걸 해야하고, 아마 재미있을 것 같습니다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 해결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 MDN에는 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests&quot;&gt;HTTP 범위 요청에 관한 비교적 유용한 가이드&lt;/a&gt;가 있습니다. 먼저 요청 헤더를 확인하여 범위 요청을 받았는지 살펴봐야 합니다. 가장 중요한 것은 &lt;code&gt;Range&lt;/code&gt; 헤더로, 클라이언트가 범위 요청을 하고 있음을 나타내는 신호이며 클라이언트가 요청하는 바이트 범위를 포함합니다. 기술적으로 Range 헤더는 향후 바이트 이외의 단위를 지원할 수 있지만, 현재로서는 바이트가 유일한 등록된 단위입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Range header 예시&lt;/b&gt;&lt;br /&gt;&lt;code&gt;Range: bytes=0-554057&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의해야 할 몇 가지 다른 헤더도 있습니다. 범위 요청은 모바일 앱이 백그라운드로 전환될 때 중단된 대용량 다운로드가 앱 재실행 시 재개되는 것처럼, 재개 가능한 다운로드에도 흔히 사용됩니다. 이 시나리오에서는 클라이언트가 요청하는 리소스가 이미 일부 다운로드한 것과 정확히 동일한지 확인할 수 있어야 합니다. 그렇지 않으면 파일이 손상될 수 있습니다. 이를 지원하기 위해 클라이언트는 &lt;code&gt;If-Range&lt;/code&gt; 헤더를 사용하여 ETag(일반적으로 리소스 내용의 해시) 또는 마지막 수정 타임스탬프를 지정할 수 있습니다. 서버는 이 검증자가 전송하려는 리소스와 일치하는지 확인할 수 있습니다. 일치하지 않으면 200 상태 코드와 함께 리소스 전체를 처음부터 응답할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;If-Range header 예시 (ETag 사용)&lt;/b&gt;&lt;br /&gt;&lt;code&gt;If-Range: &quot;67cf03ca-8744a&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 먼저 이러한 헤더를 확인하고 이를 활용하여 부분 응답 또는 전체 응답으로 응답할지 결정하는 것부터 시작해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;// import { FileHandle, open } from &quot;node:fs/promises&quot;;
// import { join } from &quot;node:path&quot;;

// import { createReadableStreamFromReadable } from &quot;@remix-run/node&quot;;

// interface Props {
//   params: Promise&amp;lt;{ album: string; file: string }&amp;gt;;
// }

// export async function GET(request: Request, props: Props) {
//   const { album, file: filename } = await props.params;

//   const rootDir = process.env.ROOT_DIR!;
//   const filepath = join(rootDir, album, filename);

const ifRange = request.headers.get(&quot;If-Range&quot;)?.valueOf();
const range = request.headers.get(&quot;Range&quot;)?.valueOf();
const rangesString = range?.replace(&quot;bytes=&quot;, &quot;&quot;);
const rangeStrings = rangesString?.split(&quot;,&quot;);

// let file: FileHandle;
// try {
//   file = await open(filepath);
// } catch {
//   return new Response(null, { status: 404 });
// }

const stats = await file.stat();
const lastModified = new Date(stats.mtime).toISOString();
const etagBase = `${stats.mtime.valueOf()}-${stats.size}`;
const etag = `&quot;${createHash(&quot;md5&quot;).update(etagBase).digest(&quot;hex&quot;)}&quot;`;

const partialResponse =
  range?.startsWith(&quot;bytes=&quot;) &amp;amp;&amp;amp;
  // We're only supporting single ranges for now
  rangeStrings?.length === 1 &amp;amp;&amp;amp;
  (!ifRange || ifRange === etag || ifRange === lastModified);

if (partialResponse) {
  // TODO
}

// return new Response(createReadableStreamFromReadable(file.createReadStream()), {
//   headers: { &quot;Content-Type&quot;: &quot;video/mp4&quot; },
// });
// }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 바이트 범위를 파싱하고 해당 바이트만 포함하도록 스트림을 업데이트해야 합니다. 바이트 범위는 시작점이나 끝점을 생략할 수 있으며, 파싱 후 NaN 값을 확인하여 이를 처리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// import { FileHandle, open } from &quot;node:fs/promises&quot;;
// import { join } from &quot;node:path&quot;;

// import { createReadableStreamFromReadable } from &quot;@remix-run/node&quot;;

// interface Props {
//   params: Promise&amp;lt;{ album: string; file: string }&amp;gt;;
// }

// export async function GET(request: Request, props: Props) {
//   const { album, file: filename } = await props.params;

//   const rootDir = process.env.ROOT_DIR!;
//   const filepath = join(rootDir, album, filename);

//   const ifRange = request.headers.get(&quot;If-Range&quot;)?.valueOf();
//   const range = request.headers.get(&quot;Range&quot;)?.valueOf();
//   const rangesString = range?.replace(&quot;bytes=&quot;, &quot;&quot;);
//   const rangeStrings = rangesString?.split(&quot;,&quot;);

//   let file: FileHandle;
//   try {
//     file = await open(filepath);
//   } catch {
//     return new Response(null, { status: 404 });
//   }

//   const stats = await file.stat();
//   const lastModified = new Date(stats.mtime).toISOString();
//   const etagBase = `${stats.mtime.valueOf()}-${stats.size}`;
//   const etag = `&quot;${createHash(&quot;md5&quot;).update(etagBase).digest(&quot;hex&quot;)}&quot;`;

let start = 0;
let end = stats.size - 1;

// const partialResponse =
//   range?.startsWith(&quot;bytes=&quot;) &amp;amp;&amp;amp;
//   // We're only supporting single ranges for now
//   rangeStrings?.length === 1 &amp;amp;&amp;amp;
//   (!ifRange || ifRange === etag || ifRange === lastModified);

// if (partialResponse) {
const firstRangeString = rangeStrings[0];

const [startString, endString] = firstRangeString.trim().split(&quot;-&quot;) as [string, string];

try {
  const parsedStart = parseInt(startString.trim(), 10);
  if (!Number.isNaN(parsedStart)) {
    start = parsedStart;
  }
  const parsedEnd = parseInt(endString.trim(), 10);
  if (!Number.isNaN(parsedEnd)) {
    end = parsedEnd;
  }
} catch {
  // If the ranges weren't valid, then leave the defaults
}
// }

// return new Response(
  createReadableStreamFromReadable(file.createReadStream({ start, end })), 
  {
//    headers: { &quot;Content-Type&quot;: &quot;video/mp4&quot; },
//  },
// );
// }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;file.createReadStream({ start, end })&lt;/code&gt; 호출에서 많은 작업을 수행합니다. 이 호출은 시작 오프셋부터 끝 오프셋까지 파일의 바이트를 반환하는 Node.js ReadStream(Readable 인터페이스를 확장함)을 생성합니다. 편리하게도 이 스트림은 Range 헤더와 동일한 범위 의미론(start와 end 모두 포함)을 가지므로, 해당 값들을 그대로 전달하기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 구현은 거의 완성되었습니다! 마지막 단계는 클라이언트에 올바른 헤더와 상태를 전송하여, 그들이 실제로 청크 또는 재개 가능한 다운로드를 구현할 수 있도록 하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 표준 HTTP 요청을 수신한 경우에도 응답에 &lt;code&gt;Accept-Ranges&lt;/code&gt; 헤더를 포함하도록 업데이트해야 합니다. 이를 통해 클라이언트는 향후 범위 요청을 사용할 수 있는지 여부를 판단할 수 있습니다(예: 대용량 파일 다운로드가 중단된 경우). 다음으로, 범위 요청에 응답할 때는 요청된 범위가 전체 리소스를 포함하더라도 기본 성공 코드인 &lt;code&gt;200&lt;/code&gt; 대신 &lt;code&gt;206&lt;/code&gt; 상태 코드를 반환해야 합니다. 또한 &lt;code&gt;Content-Range&lt;/code&gt; 헤더를 포함시켜야 합니다. 이 헤더는 응답에서 전송되는 바이트 범위를 명시하며, 요청의 &lt;code&gt;Range&lt;/code&gt; 헤더에서 지정한 범위에 대한 수락 확인 역할을 합니다. 마지막으로 &lt;code&gt;Last-Modified&lt;/code&gt; 및 &lt;code&gt;ETag&lt;/code&gt; 헤더를 추가하여 클라이언트가 두 검증자 중 하나로 &lt;code&gt;If-Range&lt;/code&gt; 조건을 구현할 수 있도록 합니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// import { FileHandle, open } from &quot;node:fs/promises&quot;;
// import { join } from &quot;node:path&quot;;
// import { createHash } from &quot;node:crypto&quot;;

// import { createReadableStreamFromReadable } from &quot;@remix-run/node&quot;;

// interface Props {
//   params: Promise&amp;lt;{ album: string; file: string }&amp;gt;;
// }

// export async function GET(request: Request, props: Props) {
//   const { album, file: filename } = await props.params;

//   const rootDir = process.env.ROOT_DIR!;
//   const filepath = join(rootDir, album, filename);

//   const ifRange = request.headers.get(&quot;If-Range&quot;)?.valueOf();
//   const range = request.headers.get(&quot;Range&quot;)?.valueOf();
//   const rangesString = range?.replace(&quot;bytes=&quot;, &quot;&quot;);
//   const rangeStrings = rangesString?.split(&quot;,&quot;);

//   let file: FileHandle;
//   try {
//     file = await open(filepath);
//   } catch {
//     return new Response(null, { status: 404 });
//   }

//   const stats = await file.stat();
//   const lastModified = new Date(stats.mtime).toISOString();
//   const etagBase = `${stats.mtime.valueOf()}-${stats.size}`;
//   const etag = `&quot;${createHash(&quot;md5&quot;).update(etagBase).digest(&quot;hex&quot;)}&quot;`;

//   let start = 0;
//   let end = stats.size - 1;

//   const partialResponse =
//     range?.startsWith(&quot;bytes=&quot;) &amp;amp;&amp;amp;
//     // We're only supporting single ranges for now
//     rangeStrings?.length === 1 &amp;amp;&amp;amp;
//     (!ifRange || ifRange === etag || ifRange === lastModified);

//   if (partialResponse) {
//     const firstRangeString = rangeStrings[0];

//     const [startString, endString] = firstRangeString.trim().split(&quot;-&quot;) as [string, string];

//     try {
//       const parsedStart = parseInt(startString.trim(), 10);
//       if (!Number.isNaN(parsedStart)) {
//         start = parsedStart;
//       }
//       const parsedEnd = parseInt(endString.trim(), 10);
//       if (!Number.isNaN(parsedEnd)) {
//         end = Math.min(parsedEnd, stats.size - 1);
//       }
//     } catch {
//       // If the ranges weren't valid, then leave the defaults
//     }
//   }

  if (start &amp;gt; stats.size - 1) {
    return new Response(null, {
      status: 416,
      headers: { &quot;Content-Range&quot;: `bytes */${stats.size}` },
    });
  }

//   // return new Response(createReadableStreamFromReadable(file.createReadStream({ start, end })), {
          status: partialResponse ? 206 : 200,
          headers: {
            &quot;Content-Type&quot;: &quot;video/mp4&quot;,
            &quot;Content-Length&quot;: `${end - start + 1}`,
            &quot;Accept-Ranges&quot;: &quot;bytes&quot;,
            &quot;Last-Modified&quot;: new Date(stats.mtime).toString(),
            Etag: etag,
            ...(partialResponse &amp;amp;&amp;amp; {
              &quot;Content-Range&quot;: `bytes ${start}-${end}/${stats.size}`,
            }),
          },
//   // });
// // }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기본 &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; 요소만으로 모든 주요 브라우저에서 동영상이 재생됩니다! 기술적으로는 멀티레인지 요청을 구현하지 않았지만, 실제로 이를 사용할 만한 사례를 찾거나 생각해내지 못했습니다(다행히 브라우저들은 파일 다운로드나 동영상 버퍼링에 이를 활용하지 않는 것 같습니다). 만약 이를 지원하려면, MDN의 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/206#receiving_a_206_response_for_multiple_requested_ranges&quot;&gt;206 부분 콘텐츠 상태 문서&lt;/a&gt;에 설명된 대로 여러 개의 읽기 가능한 스트림을 연결하고(Deno 표준 라이브러리에 훌륭한 &lt;code&gt;concatReadableStreams&lt;/code&gt; 구현체가 있습니다), 멀티파트 응답 구분자로 결합해야 합니다. 현재로서는 다중 범위 요청을 무시하고 비범위 요청처럼 처리합니다. 다중 범위 요청을 지원하지 않는 서버에 대해서는 일반적으로 허용 가능한 동작이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정: 416 조건을 잘못 구현했다는 점을 &lt;a href=&quot;https://www.reddit.com/r/javascript/comments/1ki1yb3/comment/mrhjkjk/&quot;&gt;지적해 주신 u/Pesthuf&lt;/a&gt; 님께 감사드립니다! 사양서의 관련 부분은 실제로 다음과 같이 명시하고 있습니다(강조는 제가 추가함).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GET 요청에서 유효한 바이트 범위 사양은 다음 중 하나에 해당할 경우 충족 가능합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선택된 표현의 현재 길이보다 작은 &lt;b&gt;첫 번째 위치&lt;/b&gt;(first-pos)를 가진 정수 범위(int-range)이거나&lt;/li&gt;
&lt;li&gt;0이 아닌 접미사 길이(suffix-length)를 가진 접미사 범위(suffix-range)입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;b&gt;end-pos&lt;/b&gt;가 현재 길이보다 작은지 확인했는데, 이는 범위가 만족 가능하기 위한 필수 조건이 아닙니다.&lt;/p&gt;</description>
      <category>개발/번역</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/377</guid>
      <comments>https://imnotadevleoper.tistory.com/377#entry377comment</comments>
      <pubDate>Wed, 31 Dec 2025 19:30:58 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 소프트웨어 아키텍처 The Basics (2판)</title>
      <link>https://imnotadevleoper.tistory.com/376</link>
      <description>&lt;h3 id=&quot;%22%ED%95%9C%EB%B9%9B%EB%AF%B8%EB%94%94%EC%96%B4%20%EC%84%9C%ED%8F%89%EB%8B%A8%20%3C%EB%82%98%EB%8A%94%20%EB%A6%AC%EB%B7%B0%EC%96%B4%EB%8B%A4%3E%20%ED%99%9C%EB%8F%99%EC%9D%84%20%EC%9C%84%ED%95%B4%EC%84%9C%20%EC%B1%85%EC%9D%84%20%ED%98%91%EC%B0%AC%EB%B0%9B%EC%95%84%20%EC%9E%91%EC%84%B1%EB%90%9C%20%EC%84%9C%ED%8F%89%EC%9E%85%EB%8B%88%EB%8B%A4.%22-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&quot;한빛미디어 서평단 &amp;lt;나는 리뷰어다&amp;gt; 활동을 위해서 책을 협찬받아 작성된 서평입니다.&quot;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-12-28-12-45-49 001.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s7iKI/dJMcaiPrs8I/mkeTUf3ZxSDksN3L9FazoK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s7iKI/dJMcaiPrs8I/mkeTUf3ZxSDksN3L9FazoK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s7iKI/dJMcaiPrs8I/mkeTUf3ZxSDksN3L9FazoK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs7iKI%2FdJMcaiPrs8I%2FmkeTUf3ZxSDksN3L9FazoK%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-12-28-12-45-49 001.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 아키텍처 The Basics (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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-12-28-12-46-10.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kMdJF/dJMcagqAWGD/mlgvOkUdFgeRarT8AjcNS0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kMdJF/dJMcagqAWGD/mlgvOkUdFgeRarT8AjcNS0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kMdJF/dJMcagqAWGD/mlgvOkUdFgeRarT8AjcNS0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkMdJF%2FdJMcagqAWGD%2FmlgvOkUdFgeRarT8AjcNS0%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-12-28-12-46-10.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책의 큰 특징은 &lt;span&gt;아키텍처를 하나의 기술 스택에 종속된 설계 지식으로 다루지 않고, 폭넓은 원칙과 사고 체계로 풀어낸다는 점입니다.&lt;/span&gt; 예를 들어, 계층형 아키텍처, 모듈형 모놀리스, 마이크로서비스 등 다양한 스타일을 비교하면서 어떤 상황에서 각 스타일이 적합한지 설계 기준과 사례 중심으로 설명합니다. 이러한 접근 방식은 단지 문법적 요소를 설명하는 것이 아니라 &lt;span&gt;독자가 아키텍처 결정을 스스로 분석하고 논리적으로 설명할 수 있도록 유도합니다.&lt;/span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;아키텍처 설계는 기술적 선택뿐 아니라 &lt;/span&gt;비즈니스 요구와 팀의 상호작용, 품질 속성 간의 트레이드오프를 고려하는 복합적 활동&lt;span&gt;입니다. 이 책은 기술적 결정과 동시에 &lt;/span&gt;협업, 커뮤니케이션, 발표, 문서화 등의 소프트 스킬&lt;span&gt;을 중요하게 다룹니다. 이는 아키텍트 역할이 단지 기술 결정자가 아니라 &lt;/span&gt;팀과 조직 전체의 방향성을 조율하는 리더 역할임을 이해하는 데 중요한 맥락을 제공합니다.&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 최신 클라우드 환경과 생성형 AI 중심의 현대적 엔지니어링 관행을 반영하여, &lt;span&gt;최근 기술 흐름과 아키텍처 사이의 상관관계를 이해하도록 구성&lt;/span&gt;되어 있습니다. 이러한 최신 사례 중심 설명은 전통적 아키텍처 지식에 머물지 않고 &lt;span&gt;실무 중심의 적용과 판단 능력을 키우는 데 도움이 됩니다.&lt;/span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정리하면, 『소프트웨어 아키텍처 The Basics (2판)』은 &lt;/span&gt;아키텍처의 본질적인 개념을 체계적으로 정리하고, 실전적인 설계 사고를 확장하도록 설계된 책입니다.&lt;span&gt; 아키텍처를 처음 접하는 개발자부터 이미 설계 책임을 맡고 있지만 방향성을 고민하는 테크 리더까지 &lt;/span&gt;아키텍처를 이해하고 실전에 적용하는 데 필요한 큰 그림과 구체적 판단 기준을 제공합니다.&lt;span&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/376</guid>
      <comments>https://imnotadevleoper.tistory.com/376#entry376comment</comments>
      <pubDate>Sun, 28 Dec 2025 12:53:27 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 왜 타입스크립트는 당신을 구해주지 못하는가</title>
      <link>https://imnotadevleoper.tistory.com/375</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://cekrem.github.io/posts/why-typescript-wont-save-you/&quot;&gt;Why TypeScript Won't Save You&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트는 결국 당신을 당신 자신으로부터 지켜주지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네릭, 조건부 타입, 매핑 타입 같은 고급 기능을 다루기 위해 많은 시간을 투자한 사람이라면 이 말이 다소 가혹하게 들릴 수 있습니다. 하지만 타입스크립트 컴파일러가 보여주는 초록색 체크표시는 당신의 코드가 스스로 모순 없이 일관된다는 것일 뿐, 그 코드가 정말로 올바르다는 보증은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 타입스크립트를 비난하려는 것이 아닙니다. 오히려 &amp;ldquo;타입이 곧 타입 안전성이다&amp;rdquo;라는 오해를 깨기 위한 이야기입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 도구가 아니라 사고방식입니다. 저는 이 장면을 현업에서 끊임없이 보게 됩니다. &quot;타입이 잡아주겠지&quot;라며 엣지 케이스를 더 이상 고민하지 않는 개발자, &quot;이미 타입이 있으니까&quot;라는 이유로 검증을 생략하는 코드, 그리고 컴파일러를 지나치게 신뢰하는 습관이 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트는 안전하다는 &lt;b&gt;느낌&lt;/b&gt;은 줄 수 있지만, 실제 안전을 보장해 주지는 않습니다.&lt;br /&gt;그리고 그 간극은 느낌과 현실 사이, 컴파일타임과 런타임 사이, 코드와 외부 세계 사이에 존재합니다. 바로 그 지점에서 프로덕션 버그가 발생합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안전하다는 착각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트는 훌륭한 도구입니다. 수많은 버그를 사전에 잡아주고, 리팩터링을 훨씬 안전하게 만들어주며, 개발 경험을 눈에 띄게 개선해 줍니다. 저 역시 매일 사용하고 있고, 중요한 프로덕션 코드라면 다시는 순수 자바스크립트로 돌아가고 싶지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 타입스크립트는 외부 세계로부터 당신을 보호해주지 않습니다. 그리고 더 큰 문제는, 이 언어 자체가 곳곳에 우회 경로(escape hatch)를 마련해 두어 상황을 오히려 악화시킨다는 점에 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;우회 경로의 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 말인지 예시로 보여드리겠습니다. 아래 코드는 타입스크립트 관점에서는 전혀 문제가 없습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;interface User {
  id: number;
  name: string;
  email: string;
}

async function getUser(id: number): Promise&amp;lt;User&amp;gt; {
  const response = await fetch(`/api/users/${id}`);
  return await response.json();
}

const firstUser = await getUser(1);

// 여기서 무슨 일이 벌어지나요?
console.log(`Greetings, ${firstUser.name}!`);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러는 아무 문제 없다는 듯 만족합니다. 코드 편집기 역시 오류를 보여주지 않습니다. (심지어 console.log 안에서 &lt;code&gt;User&lt;/code&gt;의 프로퍼티를 자동 완성까지 해줍니다!) 하지만 사실 우리는 이미 타입 시스템에게 명백한 거짓말을 한 셈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;as User&lt;/code&gt;나, 더 나쁜 형태인 &lt;code&gt;as unknown as User&lt;/code&gt; 같은 단언을 쓰지도 않았는데, 우리는 타입스크립트에게 이 함수가 실제로 무엇을 반환하든(아무것도 반환하지 않는 경우조차도) 항상 &lt;code&gt;User&lt;/code&gt;를 반환한다고 믿게 만들어 버린 것입니다. API는 &lt;code&gt;null&lt;/code&gt;을 반환할 수도 있고, 에러 객체를 보낼 수도 있고, 전혀 다른 구조의 데이터를 보낼 수도 있습니다. 타입스크립트는 이를 절대 알 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환 타입을 통해 암묵적으로 캐스팅되는 상황은 우회 경로 중 하나에 불과합니다. 타입스크립트는 더 많은 우회 경로를 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;any&lt;/code&gt; (사실상 핵무기)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ts-ignore&lt;/code&gt; (문제를 그냥 덮어버리는 방식)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;as unknown as T&lt;/code&gt; (항상 동작하는 이중 거짓말)&lt;/li&gt;
&lt;li&gt;검증할 수 없는 타입 단언&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규모가 큰 코드베이스에서 누군가 이런 우회 경로를 쓰지 않았다는 걸 어떻게 확신할 수 있을까요? 방법이 없습니다. &lt;b&gt;결국 코드의 안전성은 가장 취약한 부분(&lt;code&gt;any&lt;/code&gt;) 만큼만 안전할 뿐입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elm과 비교해 보면 차이는 더 분명합니다. Elm에서는 이런 속임수가 아예 불가능합니다. 우회 경로가 없습니다. 컴파일러가 안전하다고 말하면, 그건 실제로 안전합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;경계 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 깊이 들어가 보면, 핵심 문제는 여기에 있습니다. &lt;b&gt;타입스크립트는 당신의 코드 내부만 알고 있으며, 외부 세계에 대해서는 아무것도 모른다는 점입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 응답, 사용자 입력, 로컬스토리지 값, URL 파라미터 등 시스템으로 들어오는 모든 데이터는 본질적으로 믿을 수 없는 상태입니다. 타입스크립트는 이를 검증할 수 없습니다. 당신이 부여한 타입은, 실제 검증을 하기 전까지는 그냥 희망 사항에 불과합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이것뿐만이 아닙니다. 현대 프론트엔드 개발 대부분이 &lt;b&gt;프레임워크 로직과 인프라스트럭처 로직을 강하게 결합해 놓는다는 점에서 상&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;황은 더 나빠집니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 전형적인 리액트 컴포넌트의 예입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState&amp;lt;User | null&amp;gt;(null);

  useEffect(() =&amp;gt; {
    fetch(`/api/users/${userId}`)
      .then((res) =&amp;gt; res.json())
      .then((data) =&amp;gt; setUser(data as User)); // ??
  }, [userId]);

  if (!user) return &amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;;

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{user.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{user.email}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구조는 너무도 흔합니다. UI 로직, 상태 관리, 사이드 이펙트, 데이터 패칭이 한 컴포넌트 안에서 전부 뒤섞여 있습니다. 이 컴포넌트는 동시에 다음 역할을 수행하고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리액트 고유의 상태와 라이프사이클 관리&lt;/li&gt;
&lt;li&gt;네트워크에서 데이터 가져오기&lt;/li&gt;
&lt;li&gt;가져온 데이터를 변환하기 (타입 단언까지 곁들여서)&lt;/li&gt;
&lt;li&gt;UI 렌더링&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안전한 데이터와 안전하지 않은 데이터의 경계는 존재하지 않습니다. 인프라스트럭처 로직(fetching)은 프레임워크 로직(hooks, effects)에 결합되고, 이는 다시 프레젠테이션 로직과 섞여버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 타입스크립트만의 문제가 아니라, 아키텍처적인 문제입니다. 하지만 타입스크립트는 &lt;code&gt;data as User&lt;/code&gt; 같은 단언이 마치 안전한 것처럼 보이게 만들어 오히려 상황을 더 악화시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제대로 설계된 시스템에서는 도메인 레이어와 애플리케이션 레이어가 검증된 안전한 데이터만을 다룹니다. 오직 인프라스트럭처 레이어만이 외부 세계의 지저분하고, 구조가 보장되지 않은 데이터를 처리해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Elm: 기본적으로 안전한 언어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elm은 이런 아키텍처를 강제로 따르게 합니다. API 데이터를 다루는 방식만 봐도 확연히 드러납니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;type alias User =
    { id : Int
    , name : String
    , email : String
    }

userDecoder : Decoder User
userDecoder =
    Decode.map3 User
        (Decode.field &quot;id&quot; Decode.int)
        (Decode.field &quot;name&quot; Decode.string)
        (Decode.field &quot;email&quot; Decode.string)

-- Result Error User를 반환한다
-- 컴파일러가 성공/실패 두 경우 모두를 처리하도록 강제한다
decodeUser : String -&amp;gt; Result Error User
decodeUser json =
    Decode.decodeString userDecoder json
    ```&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 레이어에 &lt;code&gt;User&lt;/code&gt;가 도달한 순간, 그 값은 무조건 유효합니다. 타입 시스템이 유효하지 않은 데이터가 비즈니스 로직에 침투하는 것을 원천적으로 막아주기 때문입니다. 내부 레이어는 오직 안전한 데이터만을 다루게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타입스크립트: 경계가 없는 세계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 타입스크립트에는 이런 강제력이 없습니다. 검증되지 않은 데이터를 어디로든 그대로 흘려보낼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 인프라스트럭처 레이어 - raw 데이터를 얻음
async function fetchUser(id: number): Promise&amp;lt;User&amp;gt; {
  const response = await fetch(`/api/users/${id}`);
  return await response.json(); // ?? 실제로 User이기를 희망함
}

// 도메인 레이어 - user 데이터가 안전하다고 가정함
function sendWelcomeEmail(user: User) {
  // user가 null, 숫자 등 User가 아니라면 깨짐
  emailService.send(user.email, &quot;Welcome!&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트는 &lt;code&gt;fetchUser&lt;/code&gt;가 실제 &lt;code&gt;User&lt;/code&gt;를 반환하지 않을 수도 있다는 사실을 알려주지 못합니다. 또한 도메인 레이어가 잠재적으로 잘못된 데이터를 다루고 있다는 점도 감지하지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 타입스크립트에서도 경계를 올바르게 구축할 수는 있습니다. 바로 Zod나 io-ts 같은 라이브러리를 사용해 시스템의 가장자리에서 데이터를 검증하는 방식입니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { z } from &quot;zod&quot;;

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer&amp;lt;typeof UserSchema&amp;gt;;

async function fetchUser(id: number): Promise&amp;lt;User&amp;gt; {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return UserSchema.parse(data); // 실제로 검증됨!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 중요한 점이 있습니다. &lt;b&gt;이 검증을 직접 기억해서 해야 한다는 것입니다.&lt;/b&gt; 타입스크립트는 잊어버린다고 해서 경고해주지도 않고, 컴파일을 막지도 않습니다. 그리고 수십 명이 함께 작업하는 큰 코드베이스에서는, 누군가는 반드시 빠뜨립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(조금 더 전체적인 접근을 원한다면 &lt;a href=&quot;https://effect.website/&quot;&gt;Effect&lt;/a&gt; 같은 도구를 고려해볼 수도 있습니다. 하지만 이것은 또 다른 글의 주제입니다.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;런타임 vs 컴파일타임&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 지점에서 핵심적인 차이가 드러납니다. &lt;b&gt;타입스크립트는 런타임에 존재하지 않습니다.&lt;/b&gt; 그리고 &lt;b&gt;컴파일타임에도 많은 것을 모른 채로 지나갑니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로덕션 환경에서 코드가 실제로 실행될 때, 그 멋진 타입들은 모두 사라집니다. 남는 것은 동적이고, 타입이 없고, &lt;code&gt;undefined&lt;/code&gt;가 앱을 터뜨려도 아무렇지 않은 언어인 자바스크립트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트는 컴파일타임 도구일 뿐입니다. 코드가 스스로에게 모순되지 않는지만 확인할 뿐, 현실과 맞는지는 전혀 확인하지 못합니다. 그리고 당신이 알려주지 않는 이상, 아키텍처 레이어나 도메인과 외부 세계의 경계에는 관심도 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Elm의 타입은 아키텍처 전반에 걸쳐 일관되게 강제됩니다. 디코더는 단순히 타입을 주석처럼 붙이는 것이 아니라 실제로 데이터를 검증합니다. Maybe 타입은 &amp;ldquo;값이 없을 수도 있다&amp;rdquo;는 힌트를 주는 데서 끝나지 않고, 그 경우를 반드시 처리하지 않으면 컴파일 자체가 되지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 근본적인 문제: 마인드셋&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타입스크립트는 개발자에게 잘못된 안전감을 제공합니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 다음과 같은 개발자들의 모습을 자주 봅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;타입이 있으니까&quot;라며 검증을 건너뛰는 경우&lt;/li&gt;
&lt;li&gt;&quot;컴파일러가 확인했으니까&quot;라며 엣지 케이스 테스트를 생략하는 경우&lt;/li&gt;
&lt;li&gt;급하다는 이유로 &lt;code&gt;as&lt;/code&gt; 단언에 의존하는 경우&lt;/li&gt;
&lt;li&gt;에러를 없애려고 &lt;code&gt;any&lt;/code&gt;를 집어넣는 경우&lt;/li&gt;
&lt;li&gt;컴파일되면 동작한다는 잘못된 믿음을 갖는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 위험한 건 여기입니다. 타입스크립트가 나쁘다는 뜻은 아닙니다. 실제로 매우 훌륭한 도구입니다. 문제는 우리가 타입스크립트를 본래 역할 이상으로 과신한다는 데 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트는 고급 린터입니다. 코드 내부의 오타, 잘못된 API 사용, 리팩터링 오류를 잡는 데 뛰어나죠. 하지만 이것은 안전성에 대한 보증이 아닙니다. 생각을 대신해주는 도구도 아니고, 진정한 의미의 완전한 타입 안전성과는 거리가 멉니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진짜로 당신을 지켜주는 것은 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 타입스크립트가 당신을 지켜주지 못한다면, 무엇이 필요할까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;경계를 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트나 Elm 또는 어떤 언어로 작성된 시스템이든 상관없이 어디까지가 안전하지 않은 데이터이고, 어디서부터 안전해지는지를 명확하게 구분해야 합니다. 외부 세계에서 들어오는 데이터는 인프라스트럭처 레이어에서 검증되고, 도메인 레이어는 오직 검증된 값만을 다뤄야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elm은 이 구조를 언어 차원에서 강제로 따르게 합니다. 경계에서는 디코더가 데이터를 검증하고, 핵심 로직은 순수 함수로 유지되며, 부수효과는 바깥으로 밀려납니다. 속임수는 허용되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 타입스크립트에서는 이 규칙을 직접 만들어야 합니다. 다음과 같은 방식으로요.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;경계에서 데이터를 검증하거나 파싱하기&lt;/b&gt; - Zod, io-ts, Effect 같은 도구를 사용하고, 외부 데이터를 그대로 믿지 않기.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안전한 타입 만들기&lt;/b&gt; - 검증된 값으로만 생성 가능한 브랜드 타입이나 클래스를 사용하기.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;우회 경로를 차단하기&lt;/b&gt; - &lt;code&gt;any&lt;/code&gt;, &lt;code&gt;as&lt;/code&gt;, &lt;code&gt;@ts-ignore&lt;/code&gt;를 경고나 에러로 처리하도록 설정해 사실상 쓰기 어렵게 만들기.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;관심사 분리하기&lt;/b&gt; - 인프라스트럭처(fetching, parsing)와 도메인 로직을 분리하고, &lt;code&gt;useEffect&lt;/code&gt; 안에 비즈니스 로직을 섞지 않기.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실패 경로 테스트하기&lt;/b&gt; - 타입은 잘못된 데이터를 막아주지 못하지만, 테스트는 막아줄 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;타입 안전성의 공예(Craft)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 자연스럽게 한 가지 생각으로 돌아가게 됩니다. 바로 코딩을 공예로 바라보는 관점입니다. (이에 대해서는 &lt;a href=&quot;https://cekrem.github.io/posts/coding-as-craft-going-back-to-the-old-gym/&quot;&gt;Coding as Craft: Going Back to the Old Gym&lt;/a&gt;에서도 이야기한 바 있습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 장인은 자신이 사용하는 도구의 장점과 한계를 모두 이해합니다. 망치는 못을 박을 때 훌륭한 도구지만, 손에 쥐고 있다고 해서 나사에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 것은 올바른 선택이 아니죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트도 마찬가지입니다. 그 한계를 이해할 때 비로소 훌륭한 도구가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;bull; 코드베이스 내부의 버그를 잡아주고&lt;br /&gt;&amp;bull; 리팩터링을 훨씬 안전하게 만들어주며&lt;br /&gt;&amp;bull; 코드의 의도를 문서화하고&lt;br /&gt;&amp;bull; 개발 경험을 전반적으로 향상시킵니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 타입스크립트가 못 하는 일도 명확합니다.&lt;br /&gt;&amp;bull; 외부 데이터를 검증하지 못합니다&lt;br /&gt;&amp;bull; 런타임 오류를 막지 못합니다&lt;br /&gt;&amp;bull; 완전한 타입 안전성을 보장하지 못합니다&lt;br /&gt;&amp;bull; &lt;b&gt;잘못된 데이터로부터 코드를 보호하지 못합니다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트든 Elm이든, 어떤 언어를 사용하든 중요한 것은 당신이 실제로 무엇을 얻고 있는지 이해하는 것입니다. 도구는 훌륭하지만, 사고를 대신해주지는 않습니다. 그리고 (이 이야기는 다른 글에서 더 자세히 다루겠지만) &lt;b&gt;프런트엔드에는 단순히 &amp;ldquo;타입 선언&amp;rdquo;이 아니라 탄탄한 엔지니어링과 아키텍처가 필요합니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진짜 타입 안전성을 배우기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;컴파일되면 동작한다&amp;rdquo;가 밈이 아니라 현실이 되는 타입 안전성을 직접 느껴보고 싶다면, Elm을 시도해보세요. 물론 Elm만큼 안전한(그리고 함수형인) 언어들이 다른 곳에도 있지만, 프런트엔드 도메인(특히 리액트에 익숙하다면) Elm은 가장 짧고 직접적인 경로를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꼭 프로덕션에 사용하라는 말은 아닙니다. (저는 실제로 쓰고 있고 매우 좋아하지만요) 대신, 언어가 타입 안전성을 &amp;lsquo;진지하게&amp;rsquo; 받아들일 때 어떤 모습이 되는지 배우기 위한 도구로서 큰 의미가 있습니다. 우회 경로가 없고, 컴파일러가 진짜로 당신 편이며, 검증되지 않은 데이터가 시스템 내부까지 도달할 수 없는 환경입니다. 이런 진짜 타입 안전성을 경험하고 나면, 어떤 언어를 사용하든 자연스럽게 더 나은 경계를 설계하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이 주제는 &lt;a href=&quot;https://leanpub.com/elm-for-react-devs&quot;&gt;An Elm Primer for React Developers&lt;/a&gt;에서 더 깊이 다뤘습니다. Elm의 보장이 어떻게 사고방식과 아키텍처를 바꾸는지, 그리고 그 경험이 타입스크립트로 돌아왔을 때 어떤 영향을 주는지에 대해 이야기합니다.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트가 당신을 구해주지는 않습니다. 하지만 그 한계를 이해하는 것이 당신을 구할지도 모릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트를 사용하세요. 즐기세요. 하지만 맹신하지는 마세요. 경계에서 데이터를 검증하고, 실패 시나리오를 테스트하며, 제대로 된 아키텍처를 구축해야 합니다. 그리고 기억해야 합니다. 타입스크립트의 초록색 체크표시는 코드가 자기 자신과 일관적이라는 뜻이지, &lt;b&gt;정확하다&lt;/b&gt;는 의미는 아니라는 것을.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 코드는 생각하는 개발자, 자신의 공예를 갈고닦는 엔지니어와 아키텍트에게서 나옵니다. 매끈한 타입과 단단하게 결합된 코드에 취한 프레임워크나 하이프 추종자들에게서 나오는 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt; &amp;nbsp;한국어로&amp;nbsp;된&amp;nbsp;프런트엔드&amp;nbsp;아티클을&amp;nbsp;빠르게&amp;nbsp;받아보고&amp;nbsp;싶다면&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;background-color: #ffffff; color: #0070d1; text-align: left;&quot; href=&quot;https://kofearticle.substack.com/&quot;&gt;Korean&amp;nbsp;FE&amp;nbsp;Article&lt;/a&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;을&amp;nbsp;구독해주세요!&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/번역</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/375</guid>
      <comments>https://imnotadevleoper.tistory.com/375#entry375comment</comments>
      <pubDate>Wed, 3 Dec 2025 01:45:51 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 상태&amp;nbsp;기반&amp;nbsp;렌더링&amp;nbsp;vs&amp;nbsp;시그널&amp;nbsp;기반&amp;nbsp;렌더링</title>
      <link>https://imnotadevleoper.tistory.com/374</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://jovidecroock.com/blog/state-vs-signals/&quot;&gt;State-based vs Signal-based rendering&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프런트엔드 프레임워크에서 상태 관리를 떠올리면, 우리는 흔히 훅(hooks), 옵저버블(observables), 시그널(signals) 같은 API의 형태에 집중하곤 합니다. 하지만 더 근본적인 변화가 일어나고 있습니다. 바로 &lt;code&gt;렌더링이 어디서 일어나느냐&lt;/code&gt; 에 대한 관점입니다. 기존의 리액트 훅 기반 상태 관리는 상태를 생성한 지점에서 렌더링이 발생하는 반면, Preact나 Solid.js 같은 시그널 기반 접근 방식에서는 상태를 소비하는 지점에서만 렌더링이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;상태를 만든 곳에서 렌더링&quot; 하던 방식에서 &amp;ldquo;상태를 사용하는 곳에서 렌더링&amp;rdquo;하는 방식으로의 전환은 성능, 코드 구조, 그리고 사고방식 전반에 큰 변화를 가져옵니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심적인 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 리액트 훅 기반의 상태 관리에서는 &lt;code&gt;useState&lt;/code&gt;를 호출하면, 그 상태가 업데이트될 때 해당 컴포넌트뿐 아니라 모든 하위 컴포넌트까지 함께 리렌더링이 일어납니다. 하위 컴포넌트가 실제로 그 상태를 사용하고 있는지는 중요하지 않습니다. 단지 상태를 가진 컴포넌트의 자식이라는 이유만으로 렌더링의 파도에 휩쓸리게 되는 것이죠.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;const Parent = () =&amp;gt; {
  const [count, setCount] = useState(0);
  return (
    &amp;lt;&amp;gt;
      {/* count 값을 사용하지 않더라도 리렌더가 됨 */}
      &amp;lt;ChildA /&amp;gt;
      &amp;lt;ChildB /&amp;gt;
      {/* 실제로 count 값을 사용하고 리렌더가 됨 */}
      &amp;lt;ChildC count={count} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시그널 기반 렌더링에서는 이 패러다임이 완전히 뒤집힙니다. 시그널은 스스로의 의존성을 추적하는 반응형(reactive) 기본 단위입니다. 시그널을 생성하더라도, 그 시점에서 렌더링이 발생하지는 않습니다. 대신 해당 시그널의 값에 실제로 &lt;code&gt;접근&lt;/code&gt;하는 컴포넌트에서만 렌더링이 일어납니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;const Parent = () =&amp;gt; {
  const count = useSignal(0);
  return (
    &amp;lt;&amp;gt;
      {/* 리렌더가 되지 않음 */}
      &amp;lt;ChildA /&amp;gt;
      &amp;lt;ChildB /&amp;gt;
      {/* count 값을 읽는 경우에만 리렌더 됨 */}
      &amp;lt;ChildC count={count} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 세밀한 반응성(granular reactivity) 덕분에, 시그널이 업데이트되더라도 정확히 그 시그널에 의존하는 컴포넌트만 리렌더링됩니다. 이제 사고방식도 바뀝니다. &amp;ldquo;불필요한 리렌더링을 막자&amp;rdquo;에서 &amp;ldquo;필요한 곳에서만 리렌더링이 일어난다&amp;rdquo;로 전환되는 것이죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨텍스트(Context): 패러다임 전환이 더 극명해지는 순간&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이는 컨텍스트 API를 사용할 때 더욱 뚜렷하게 드러납니다. 리액트에서는 상태를 컨텍스트로 전달하고 그 값을 업데이트하면, 그 컨텍스트를 구독하고 있는 &lt;code&gt;모든 컴포넌트&lt;/code&gt;가 일괄적으로 리렌더링됩니다. 문제는, 그 컴포넌트들이 실제로 변경된 값을 읽고 있든 아니든 상관없이 모두 리렌더링이 일어난다는 점입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const CountContext = createContext();

const Provider = ({ children }) =&amp;gt; {
  const [count, setCount] = useState(0);
  const [name, setName] = useState(&quot;&quot;);
  return &amp;lt;CountContext.Provider value={{ count, name, setCount, setName }}&amp;gt;{children}&amp;lt;/CountContext.Provider&amp;gt;;
};

const ComponentA = () =&amp;gt; {
  const { name } = useContext(CountContext);
  // 오직 name 값만 사용하고 있지만,
  // count가 변경되었을때 리렌더 됩니다.
  return &amp;lt;div&amp;gt;{name}&amp;lt;/div&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시그널을 컨텍스트와 함께 사용하면, 반응성은 마치 외과 수술처럼 정교해집니다. 컨텍스트 안에 시그널을 저장할 수 있고, 그 시그널의 &lt;code&gt;.value&lt;/code&gt;를 실제로 호출하는 컴포넌트만 업데이트에 구독하게 됩니다. 즉, 시그널을 읽는 곳만 다시 렌더링되는 것이죠.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const CountContext = createContext();

const Provider = ({ children }) =&amp;gt; {
  const count = useSignal(0);
  const name = useSignal(&quot;&quot;);
  return &amp;lt;CountContext.Provider value={{ count, name }}&amp;gt;{children}&amp;lt;/CountContext.Provider&amp;gt;;
};

const ComponentA = () =&amp;gt; {
  const { name } = useContext(CountContext);
  // count가 변경될 때는 영향이 없고,
  // name이 바뀔 때만 리렌더링됩니다.
  return &amp;lt;div&amp;gt;{name.value}&amp;lt;/div&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 접근 방식은 대규모 애플리케이션에서 컨텍스트를 통해 상태를 여러 컴포넌트에 분산시킬 때 완전히 판을 바꾸는 개념입니다. 더 이상 불필요한 리렌더링을 막기 위해 컨텍스트를 여러 개로 쪼개거나, 복잡한 최적화 패턴을 적용할 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 컨텍스트는 상태를 배포하고 렌더링을 조율하는 수단이 아니라, 단순히 의존성을 주입하는(Dependency Injection) 도구로만 사용됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;렌더링 전파(Rendering Propagation)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 컴포넌트 트리를 기준으로 리렌더링이 어떻게 전파되는지 시각적으로 살펴보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상태 기반 렌더링 (리액트 훅)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 기반 렌더링에서는 상태가 업데이트될 때, 그 상태가 생성된 지점부터 시작해 해당 하위 트리 전체가 리렌더링됩니다. 이로 인해 불필요한 연산이 발생하기 쉬우며, 이를 방지하려면 &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;shouldComponentUpdate&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt; 같은 수동 최적화 기법을 직접 적용해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image1.png&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KCO2W/dJMcaaDBeWW/vLuFRpoX8z3HOVmPaZc8CK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KCO2W/dJMcaaDBeWW/vLuFRpoX8z3HOVmPaZc8CK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KCO2W/dJMcaaDBeWW/vLuFRpoX8z3HOVmPaZc8CK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKCO2W%2FdJMcaaDBeWW%2FvLuFRpoX8z3HOVmPaZc8CK%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;1112&quot; height=&quot;660&quot; data-filename=&quot;image1.png&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 하위 컴포넌트(빨간색으로 표시된 부분)가, 그 상태를 실제로 사용하지 않더라도 전부 리렌더링됩니다. 사실상 업데이트가 필요한 것은 &lt;code&gt;GC 2&lt;/code&gt; 단 하나뿐이지만, &lt;code&gt;Child 1&lt;/code&gt;, &lt;code&gt;Child 2&lt;/code&gt;, &lt;code&gt;Child 3&lt;/code&gt;, &lt;code&gt;GC 1&lt;/code&gt;, &lt;code&gt;GC 3&lt;/code&gt;까지 모두 불필요하게 다시 렌더링되는 것이죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시그널 기반 렌더링 (Preact Signals / Solid.js)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시그널 기반 렌더링에서는 실제로 시그널의 값을 읽는 컴포넌트만 리렌더링됩니다. 컴포넌트 계층 구조는 전혀 중요하지 않습니다. 핵심은 &lt;code&gt;컴포넌트가 어디에 위치하느냐&lt;/code&gt;가 아니라, &lt;code&gt;어떤 데이터에 의존하느냐&lt;/code&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image2.png&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beq4kA/dJMcagqhx6c/0Mg21Su1Elmgvy99U53Ik1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beq4kA/dJMcagqhx6c/0Mg21Su1Elmgvy99U53Ik1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beq4kA/dJMcagqhx6c/0Mg21Su1Elmgvy99U53Ik1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbeq4kA%2FdJMcagqhx6c%2F0Mg21Su1Elmgvy99U53Ik1%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;1112&quot; height=&quot;660&quot; data-filename=&quot;image2.png&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;signal.value&lt;/code&gt;를 실제로 참조하는 &lt;code&gt;GC 2&lt;/code&gt;(초록색으로 표시된 부분)만 리렌더링됩니다. 같은 컴포넌트 트리 안에 있더라도 다른 컴포넌트들(회색으로 표시된 부분)은 전혀 변경되지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제어 흐름을 통한 세밀한 렌더링 제어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Preact는 &lt;code&gt;Show&lt;/code&gt;, &lt;code&gt;For&lt;/code&gt;와 같은 제어 흐름용 컴포넌트를 통해 이 개념을 한 단계 더 확장합니다. 이런 컴포넌트들은 반응성의 범위를 훨씬 더 정밀하게 한정(scope) 하여, 필요한 부분만 최소 단위로 다시 렌더링할 수 있게 해줍니다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;const items = signal([]);
// items 시그널이 업데이트 되었을 때, 오직 영향을 끼치는 items만 리렌더됩니다.
&amp;lt;For each={items}&amp;gt;
  {(item) =&amp;gt; (
    &amp;lt;div&amp;gt;
      {/* item.value가 변경되었을 때 오직 이 아이템만 리렌더됩니다. */}
      &amp;lt;span&amp;gt;{item.name.value}&amp;lt;/span&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; item.count.value++}&amp;gt;{item.count.value}&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  )}
&amp;lt;/For&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 훅 기반 방식에서는 리스트의 한 항목이 변경되면, 그 항목뿐 아니라 형제 컴포넌트, 부모 컴포넌트, 심지어 다른 자식 컴포넌트들까지 리렌더링이 전파될 수 있습니다. 이 문제를 막으려면 모든 부분을 꼼꼼히 메모이제이션 처리해야 하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, For나 Show 같은 제어 흐름 컴포넌트는 시그널의 리렌더링 범위를 해당 JSX 블록 내부로만 제한합니다. 즉, 파생된 computed 시그널이든 단순한 시그널이든, 그 값을 사용하는 JSX 자식만 다시 렌더링되는 것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능에 미치는 영향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 패러다임의 전환은 단순한 개념 변화에 그치지 않습니다. 실제 성능 측면에서도 분명한 차이를 만들어냅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;연산량 감소&lt;/code&gt;: 리렌더링되는 컴포넌트가 줄어들면, 실행되는 자바스크립트의 양도 그만큼 감소합니다. 상태 변경과 무관한 컴포넌트에서는 render() 함수 호출, 가상 DOM 비교(diffing), useEffect 재실행 등이 전혀 일어나지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;번들 크기 감소&lt;/code&gt;: &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;shouldComponentUpdate&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;같은 메모이제이션 헬퍼들이 필요하지 않습니다. 프레임워크 자체의 반응형 시스템이 이미 자동으로 최적화를 처리하니까요. 결과적으로 애플리케이션 코드가 간결해지고 번들 크기도 작아집니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;예측 가능한 성능&lt;/code&gt;: 어떤 부분이 다시 렌더링될지는 컴포넌트 트리 구조가 아니라, 시그널이 읽히는 위치에 의해 결정됩니다. 즉, 성능의 변동성이 줄어들고 디버깅이 훨씬 쉬워집니다. &amp;ldquo;어떤 컴포넌트가 업데이트되었는가?&amp;rdquo;를 알고 싶다면 시그널이 어디에서 읽혔는지만 추적하면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prop drilling이 사라짐&lt;/code&gt;: 시그널은 컨텍스트를 거쳐 전달하거나, 심지어 직접 import 해서 사용할 수도 있습니다. 이 과정에서 불필요한 리렌더링이 발생하지 않습니다. 따라서 리액트에서 흔히 겪는 복잡한 Provider 피라미드도 만들 필요가 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;언제 상태 기반 렌더링이 더 적합한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 상태 기반 렌더링이 본질적으로 나쁘다는 뜻은 아닙니다. 규모가 작은 컴포넌트나, 리렌더링 비용이 거의 없는 애플리케이션이라면 훅 기반 모델만으로도 충분히 간단하고 실용적입니다. 수십 개 정도의 컴포넌트가 리렌더링되는 정도라면, 그 비용은 대부분의 경우 거의 무시할 수준이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 트레이드오프는 다음과 같은 상황에서 확연히 두드러집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;규모가 크고 중첩이 깊은 컴포넌트 트리&lt;/li&gt;
&lt;li&gt;업데이트 빈도가 높은 애플리케이션 &amp;mdash; 예: 애니메이션, 실시간 데이터 처리&lt;/li&gt;
&lt;li&gt;상태가 여러 컨텍스트나 전역 상태로 복잡하게 분산된 애플리케이션의 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 기반 렌더링에서 시그널 기반 렌더링으로의 전환은 단순한 성능 최적화 이상의 의미를 가집니다. 이는 반응성을 바라보는 사고방식 자체의 전환입니다. 이제 우리는 리렌더링을 메모이제이션으로 &amp;lsquo;막는&amp;rsquo; 것이 아니라, &lt;code&gt;필요한 곳에서만 &amp;lsquo;발생시키는&amp;rsquo; 방식&lt;/code&gt;으로 사고해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 전환, 즉 &amp;ldquo;상태를 생성하는 곳에서 렌더링&amp;rdquo;에서 &amp;ldquo;상태를 사용하는 곳에서 렌더링&amp;rdquo;으로의 전환은 코드를 실제 데이터 흐름에 더 가깝게 만듭니다. 이 접근 방식은 기본적으로 애플리케이션을 더 빠르게 만들고, 개발자가 이해해야 하는 정신적 모델도 훨씬 단순화시킵니다. 시그널을 읽는다면, 그 값이 바뀔 때 리렌더링됩니다. 읽지 않는다면, 아무 일도 일어나지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Preact Signals와 Solid.js 같은 프레임워크가 보여주듯, 이는 단순한 이론상의 개선이 아니라 실질적인 생산성 향상으로 이어지는 변화입니다. 프론트엔드 반응성의 미래는 정교한 단위 제어에 있으며, 그 미래는 이미 우리 앞에 와 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt; &amp;nbsp;한국어로&amp;nbsp;된&amp;nbsp;프런트엔드&amp;nbsp;아티클을&amp;nbsp;빠르게&amp;nbsp;받아보고&amp;nbsp;싶다면&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: left;&quot; href=&quot;https://kofearticle.substack.com/&quot;&gt;Korean&amp;nbsp;FE&amp;nbsp;Article&lt;/a&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;을&amp;nbsp;구독해주세요!&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/번역</category>
      <category>preact</category>
      <category>상태</category>
      <category>시그널</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/374</guid>
      <comments>https://imnotadevleoper.tistory.com/374#entry374comment</comments>
      <pubDate>Thu, 6 Nov 2025 01:42:49 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 프런트엔드 레벨을 높이는 자바스크립트 퀴즈북</title>
      <link>https://imnotadevleoper.tistory.com/373</link>
      <description>&lt;h3 id=&quot;%22%ED%95%9C%EB%B9%9B%EB%AF%B8%EB%94%94%EC%96%B4%20%EC%84%9C%ED%8F%89%EB%8B%A8%20%3C%EB%82%98%EB%8A%94%20%EB%A6%AC%EB%B7%B0%EC%96%B4%EB%8B%A4%3E%20%ED%99%9C%EB%8F%99%EC%9D%84%20%EC%9C%84%ED%95%B4%EC%84%9C%20%EC%B1%85%EC%9D%84%20%ED%98%91%EC%B0%AC%EB%B0%9B%EC%95%84%20%EC%9E%91%EC%84%B1%EB%90%9C%20%EC%84%9C%ED%8F%89%EC%9E%85%EB%8B%88%EB%8B%A4.%22-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&quot;한빛미디어 서평단 &amp;lt;나는 리뷰어다&amp;gt; 활동을 위해서 책을 협찬받아 작성된 서평입니다.&quot;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-10-27-02-03-18 001.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqDabb/dJMb9gRtfb1/n1UjyP9ttk0KXYOS7JyBD0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqDabb/dJMb9gRtfb1/n1UjyP9ttk0KXYOS7JyBD0/img.jpg&quot; data-alt=&quot;표지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqDabb/dJMb9gRtfb1/n1UjyP9ttk0KXYOS7JyBD0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqDabb%2FdJMb9gRtfb1%2Fn1UjyP9ttk0KXYOS7JyBD0%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-10-27-02-03-18 001.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;표지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 프런트엔드 개발자들이 실무에서 일을 하다보면 React나 Next.js와 같은 프레임워크에 집중하게 되면서 상대적으로 간과하기 쉬운 바닐라 자바스크립트의 &quot;본질적인 개념&quot;을 퀴즈라는 컨셉으로 정리한 책입니다. 기술 면접 대비부터 현업 프런트엔드 개발자로서 갖춰야 할 실무 역량까지 폭넓게 다루고 있으며, 국내 대표 IT 기업의 프런트엔드 개발자 분들께서 공동으로 집필했습니다. 시중에 많은 자바스크립트 책이 있지만 더욱 더 깊은 학습을 하고 싶은 분들을 위한 책입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책의 구성은 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 챕터는 셀프 실력 점검 &amp;rarr; 워밍업 퀴즈 &amp;rarr; 핵심 개념 파헤치기 &amp;rarr; 실전 레벨업 퀴즈 챌린지 &amp;rarr; 리얼 현장 인터뷰 라는 구조로 이루어져 있습니다.&lt;/li&gt;
&lt;li&gt;또한 각 챕터마다 &amp;lsquo;Ask-AI 질문 플레이북&amp;rsquo;이 포함되어 있어서, AI 기반 코드 생성 또는 분석 시 어떤 질문을 던지면서 공부를 하면 좋은지에 대해서도 가이드를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무언가를 학습하는 데 있어서 인출을 하는 과정이 정말 중요한데, 이를 퀴즈를 통해서 자연스럽게 실천할 수 있게끔 되어있는 책이라서 정말 좋다고 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-10-27-02-03-19 002.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rwr8O/dJMb9XRTqtV/H6Sq7fXO4ulR5zSarlyXuK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rwr8O/dJMb9XRTqtV/H6Sq7fXO4ulR5zSarlyXuK/img.jpg&quot; data-alt=&quot;비동기 챕터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rwr8O/dJMb9XRTqtV/H6Sq7fXO4ulR5zSarlyXuK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRwr8O%2FdJMb9XRTqtV%2FH6Sq7fXO4ulR5zSarlyXuK%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-10-27-02-03-19 002.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;비동기 챕터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 이 책에서 얻을 수 있는 것들은 아래와 같은 것들입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;퀴즈 형식의 학습 구조&lt;/b&gt;&lt;/span&gt;: 단순히 개념을 나열하는 대신, 워밍업&amp;ndash;퀴즈&amp;ndash;해설의 흐름으로 학습자가 스스로 생각하고 체득하도록 설계되어 있어 적극적인 학습 방식에 잘 맞습니다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;현업 개발자의 인터뷰 수록&lt;/b&gt;&lt;/span&gt;: 각 챕터마다 실제 프런트엔드 개발자가 &amp;ldquo;해당 개념을 실무에서 어떻게 적용했는가&amp;rdquo;를 이야기해주므로, 단순 개념 이해를 넘어서 실전 감각까지 얻을 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;자바스크립트의 본질을 다룬다&lt;/b&gt;&lt;/span&gt;: 프레임워크 중심이 아니라 언어 자체의 동작 원리(스코프, 클로저, 비동기, 이벤트 등)를 다룸으로써, 자바스크립트의 기초를 제대로 배울 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;기술면접 대비&lt;/b&gt;&lt;/span&gt;: 면접에서 자주 나오는 주제들을 퀴즈로 구성해두어, &amp;ldquo;왜 이렇게 동작하는가?&amp;rdquo;까지 묻는 심화된 질문에 대비하기 적절합니다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;AI 코드 분석의 대비책&lt;/b&gt;&lt;/span&gt;: &amp;ldquo;AI가 작성한 코드를 어떻게 비판적으로 볼 것인가&amp;rdquo;라는 관점까지 제시하고 있어, AI 시대에 스스로 생각할 수 있는 힘을 기를 수 있도록 도와줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프런트엔드 개발자라면 한 번쯤은 겪게 되는 기초가 부족하다는 고민이 있다면 이 책이 도움이 될 것 같습니다. 특히 자바스크립트의 심화 개념을 체계적으로 다지고 싶거나, 기술면접 준비 또는 실무 역량을 한 단계 올리고 싶다면 추천드립니다.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/373</guid>
      <comments>https://imnotadevleoper.tistory.com/373#entry373comment</comments>
      <pubDate>Mon, 27 Oct 2025 02:14:18 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 정지훈의 양자 컴퓨터 강의</title>
      <link>https://imnotadevleoper.tistory.com/372</link>
      <description>&lt;h3 id=&quot;%22%ED%95%9C%EB%B9%9B%EB%AF%B8%EB%94%94%EC%96%B4%20%EC%84%9C%ED%8F%89%EB%8B%A8%20%3C%EB%82%98%EB%8A%94%20%EB%A6%AC%EB%B7%B0%EC%96%B4%EB%8B%A4%3E%20%ED%99%9C%EB%8F%99%EC%9D%84%20%EC%9C%84%ED%95%B4%EC%84%9C%20%EC%B1%85%EC%9D%84%20%ED%98%91%EC%B0%AC%EB%B0%9B%EC%95%84%20%EC%9E%91%EC%84%B1%EB%90%9C%20%EC%84%9C%ED%8F%89%EC%9E%85%EB%8B%88%EB%8B%A4.%22-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&quot;한빛미디어 서평단 &amp;lt;나는 리뷰어다&amp;gt; 활동을 위해서 책을 협찬받아 작성된 서평입니다.&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;표지.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5yrzE/btsQSzRH4gM/Wid0fWzyxK5Dmt0C6l8Phk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5yrzE/btsQSzRH4gM/Wid0fWzyxK5Dmt0C6l8Phk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5yrzE/btsQSzRH4gM/Wid0fWzyxK5Dmt0C6l8Phk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5yrzE%2FbtsQSzRH4gM%2FWid0fWzyxK5Dmt0C6l8Phk%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;표지.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양자 컴퓨터에 대해서는 정말 단순하게 이해하고 있었는데 이 책을 통해서 조금 더 자세히 알 수 있게 재밌었습니다. 설명이 어렵지 않고 비즈니스, 투자등과 관련해서도 설명하고 있어서 실제로 당장 써먹을 수 있는 지식을 얻을 수 있다는 점도 좋았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저자는 지금으로부터 10년전인 2015년에 AI가 산업을 바꿀 수 있다고 생각하여 관련된 스타트업에 투자를 했었고, 그때 거대한 물결이 시작되고 있음을 직감했다고 얘기합니다. 10년이 지난 지금 생각해보면 AI는 우리의 삶에서 빠질 수 없는 하나의 요소가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 지금은 양자 컴퓨팅이라는 새로운 기술로부터 10년 전에 AI에게 느꼈던 설레임을 느끼고 있다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 설레임은 이 책을 읽으면서 저에게도 찾아오게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-09-29-01-37-50.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhwZf6/btsQTUVaaBe/8239TKoOUaPf9jfg91lJsk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhwZf6/btsQTUVaaBe/8239TKoOUaPf9jfg91lJsk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhwZf6/btsQTUVaaBe/8239TKoOUaPf9jfg91lJsk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhwZf6%2FbtsQTUVaaBe%2F8239TKoOUaPf9jfg91lJsk%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-09-29-01-37-50.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 불가능 했던 것을 현실로 만들 수 있는 기술이라는 것이 정말 매력적이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슈퍼컴퓨터로도 풀기 어려웠던 문제들을 해결할 수 있고 과학, 산업, 금융, 환경 등 다양한 영역에서 혁신적인 변화를 만들 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 양자 기술의 핵심 메커니즘을 자세하게 설명해주고 이후에 이 기술이 현재 산업에서 어떤 영향을 끼치고 있는지, 마지막으로는 투자에 관심이 있는 사람들을 위해 중요하면서도 핵심이 될 가이드를 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양자 컴퓨터에 대해 관심이 있고 이 기술에 대해서 더 알고 싶은 분들에게 이 책을 추천드릴 수 있을 것 같습니다.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/372</guid>
      <comments>https://imnotadevleoper.tistory.com/372#entry372comment</comments>
      <pubDate>Mon, 29 Sep 2025 01:49:07 +0900</pubDate>
    </item>
    <item>
      <title>[번역] CSS 길이 단위 이해하기</title>
      <link>https://imnotadevleoper.tistory.com/371</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://blog.scottlogic.com/2025/08/22/css-length-units.html&quot;&gt;Making Sense of CSS Length Units&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS를 막 시작한 주니어 개발자라면 온갖 길이 단위들 때문에 머리가 아플 겁니다. 절대 단위는 반응형 디자인에서 변하지 않기 때문에 &lt;code&gt;px&lt;/code&gt;를 쓰지 말라고 하는 사람들이 있는데, 정말 그럴까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;절대 단위&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;px&lt;/code&gt;(픽셀)은 화면에서 표현할 수 있는 가장 작은 점입니다. 하지만 고해상도의 레티나 디스플레이가 등장하면서, 오늘날의 &lt;code&gt;픽셀&lt;/code&gt;은 실제 모니터의 물리적 픽셀이 아니라 가상의 픽셀을 의미하게 되었습니다. 이는 매우 중요한 변화인데, 이제 &lt;code&gt;픽셀&lt;/code&gt;이 더 이상 절대적인 단위가 아니라는 뜻이기 때문입니다. 이 부분에 대해서는 뒤에서 다시 다루겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqRugR/btsQhw9culh/BL4PaLQfQaLEB2lNavEHx0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqRugR/btsQhw9culh/BL4PaLQfQaLEB2lNavEHx0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqRugR/btsQhw9culh/BL4PaLQfQaLEB2lNavEHx0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqRugR%2FbtsQhw9culh%2FBL4PaLQfQaLEB2lNavEHx0%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;316&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;cm&lt;/code&gt;, &lt;code&gt;mm&lt;/code&gt;, &lt;code&gt;q&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt;, &lt;code&gt;pc&lt;/code&gt;, &lt;code&gt;pt&lt;/code&gt;(센티미터, 밀리미터, 쿼터밀리미터, 인치, 피카(1/6인치), 포인트(1/72인치))는 주로 인쇄용으로 권장되는 단위입니다. 초창기 컴퓨터 프린터는 인치당 1/72의 작은 점을 인쇄할 수 있었기 때문에, 저해상도 프린터에서는 &lt;code&gt;1pt&lt;/code&gt;가 1도트에 해당했습니다. 요즘 프린터는 인치당 150, 300, 600, 심지어 1200포인트까지 인쇄할 수 있으니, 1포인트가 얼마나 작은지 짐작할 수 있습니다. 따라서 프린터의 해상도와 관계없이 페이지가 의도한 대로 인쇄되길 원한다면, 다른 측정 방법이나 상대 단위를 사용하는 것이 더 현명할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnRzdo/btsQgdJl5ui/gYBkEXoKVlQy4YWEPM7dq0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnRzdo/btsQgdJl5ui/gYBkEXoKVlQy4YWEPM7dq0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnRzdo/btsQgdJl5ui/gYBkEXoKVlQy4YWEPM7dq0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cnRzdo/btsQgdJl5ui/gYBkEXoKVlQy4YWEPM7dq0/img.gif&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;320&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상대 단위&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;%&lt;/code&gt;(퍼센트)는 부모 요소를 기준으로 하므로 비교적 이해하기 쉽습니다. 다만, 무엇이 부모로 간주되는지 상황에 따라 다를 수 있어 주의가 필요합니다. 보통은 명확하지만 해당 요소의 position이 &lt;code&gt;absolute&lt;/code&gt;일 때는 조금 까다로워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;em&lt;/code&gt;은 부모의 글꼴 크기를 기준으로 한 비율 단위입니다(특히 &quot;m&quot; 문자의 너비를 참조). 예를 들어, &lt;code&gt;2em&lt;/code&gt;은 현재 글꼴 크기의 두 배가 됩니다. 이해하기 쉽지만 CSS 컴포넌트를 만들 때 크기가 부모 컨테이너의 영향을 받는다는 문제가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;rem&lt;/code&gt;은 &lt;code&gt;em&lt;/code&gt;과 비슷하지만, 기준이 부모가 아니라 루트(&lt;code&gt;html&lt;/code&gt;) 요소의 글꼴 크기입니다. 따라서 컴포넌트가 어디에 있든 항상 동일한 크기를 유지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 브라우저의 기본 글꼴 크기는 &lt;code&gt;16px&lt;/code&gt;이지만, 아래와 같이 설정하면 &lt;code&gt;1rem이 10px&lt;/code&gt;이 되어 계산이 쉬워집니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;html {
  font-size: 62.5%;
}
body {
  font-size: 1.6rem;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 그냥 &lt;code&gt;픽셀&lt;/code&gt;을 쓰지 않을까요? &lt;code&gt;16px&lt;/code&gt;이 기본 크기이지만, 사용자가 접근성 설정에서 글꼴 크기를 변경할 수 있기 때문입니다. 레이아웃이 &lt;code&gt;px&lt;/code&gt; 기준이면 사용자의 설정을 무시하게 되고, 레이아웃이 깨지거나 글자가 너무 작아질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;[s,l,d]v[h,w,i,b,min,max]&lt;/code&gt;는 웹 페이지의 표시 영역(뷰포트)을 기준으로 하는 24가지 단위입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;*vh&lt;/code&gt;는 뷰포트의 높이, &lt;code&gt;*vw&lt;/code&gt;는 너비를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;*vmin&lt;/code&gt;과 &lt;code&gt;*vmax&lt;/code&gt;는 각각 뷰포트 치수 중 작은 값과 큰 값을 기준으로 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;*vi&lt;/code&gt;(인라인)와 &lt;code&gt;*vb&lt;/code&gt;(블록)는 텍스트 방향에 따라 너비 또는 높이를 나타냅니다. 영어처럼 가로쓰기 언어에서는 &lt;code&gt;vi&lt;/code&gt;가 &lt;code&gt;vh&lt;/code&gt;, &lt;code&gt;vb&lt;/code&gt;가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;vw&lt;/code&gt;와 같지만, 일본어처럼 세로쓰기 언어에서는 반대가 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sv*&lt;/code&gt;(small)은 브라우저 컨트롤이 표시되는 디바이스의 뷰포트 크기입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lv*&lt;/code&gt;(large)은 컨트롤이 숨겨져 있을 때의 뷰포트 크기입니다. &lt;code&gt;sv*&lt;/code&gt;와 &lt;code&gt;lv*&lt;/code&gt;는 컨트롤의 표시 여부와 관계없이 고정된 값이라는 점을 기억하세요. &lt;code&gt;dv*&lt;/code&gt;(dynamic)은 현재 뷰포트(작든 크든)를 나타냅니다. 이는 뷰포트 전체 높이에 꼭 맞는 레이아웃을 만들려고 할 때 중요하지만, 컨트롤이 사라지는 즉시(예를 들어 사용자가 스크롤을 시작했기 때문에) 변경됩니다. &lt;code&gt;s|l|d&lt;/code&gt;를 지정하지 않으면 원래 &lt;code&gt;v*&lt;/code&gt;는 &lt;code&gt;lv*&lt;/code&gt;(large)와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beivyw/btsQgObsZZK/WJobRqNprCduSi11p9vjz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beivyw/btsQgObsZZK/WJobRqNprCduSi11p9vjz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beivyw/btsQgObsZZK/WJobRqNprCduSi11p9vjz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbeivyw%2FbtsQgObsZZK%2FWJobRqNprCduSi11p9vjz1%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;480&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특이한 단위(모든 브라우저에서 지원되는 것은 아님)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 특수한 용도의 단위들도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ch&lt;/code&gt;: &quot;0&quot; 문자의 너비&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ex&lt;/code&gt;: &quot;x&quot; 문자의 높이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;cap&lt;/code&gt;: 대문자 높이(텍스트 옆에 아이콘을 맞출 때 유용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ic&lt;/code&gt;: 한중일 문자의 '물(水)' 너비&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lh&lt;/code&gt;: 부모 요소의 줄 높이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;rlh&lt;/code&gt;: 루트 요소의 줄 높이&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여전히 복잡하게 느껴진다면, 아래 세 가지 정도만 기억해도 충분합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인쇄용 디자인에는 정확한 크기(&lt;code&gt;mm&lt;/code&gt; 등)를 사용하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 페이지에서는 &lt;code&gt;rem&lt;/code&gt;을 쓰면 크기 조정이 쉽고, &lt;code&gt;1rem=10px&lt;/code&gt;로 설정하는 것도 고려해볼 만합니다(선택 사항).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션에서는 뷰포트 단위를 활용해 레이아웃을 잡는 것이 좋습니다. 만약 이 외의 단위를 써야 한다면, 정말 특별한 경우일 테니 자신감을 가져도 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt; &amp;nbsp;한국어로&amp;nbsp;된&amp;nbsp;프런트엔드&amp;nbsp;아티클을&amp;nbsp;빠르게&amp;nbsp;받아보고&amp;nbsp;싶다면&amp;nbsp;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://kofearticle.substack.com/&quot;&gt;Korean&amp;nbsp;FE&amp;nbsp;Article&lt;/a&gt;을&amp;nbsp;구독해주세요!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/번역</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/371</guid>
      <comments>https://imnotadevleoper.tistory.com/371#entry371comment</comments>
      <pubDate>Tue, 2 Sep 2025 22:56:37 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 한 입 크기로 잘라 먹는 Next.js</title>
      <link>https://imnotadevleoper.tistory.com/370</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-08-30-23-44-37 001.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqKbKK/btsQdz6wLJI/b55y0yFf3Q6VNlZiFDumD0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqKbKK/btsQdz6wLJI/b55y0yFf3Q6VNlZiFDumD0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqKbKK/btsQdz6wLJI/b55y0yFf3Q6VNlZiFDumD0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqKbKK%2FbtsQdz6wLJI%2Fb55y0yFf3Q6VNlZiFDumD0%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-08-30-23-44-37 001.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 정환님의 Next.js책이 출판되었고, 서평단을 모집하신다는 얘기를 듣고 지원해서 책을 읽어볼 수 있게 되었는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 인프콘에서 정환님께서 타입스크립트에 대한 발표를 하시는 것을 시작으로 정환님의 컨텐츠를 처음 알게 되었는데 전달력이 워낙 좋으셔서 발표가 재미있었고 끝나고나서 간단하게 이야기를 나누는 시간에도 여러가지 질문에 정성넘치게 답변해주시는 모습이 되게 인상깊었던 기억이 있습니다. 그래서 더욱 기대하면서 책을 읽을 수 있었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 현대 프론트엔드 생태계에서 가장 많이 사용되는 프레임워크인 Next.js에 대해서 설명하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 온라인상에서 출판하셨던 강의를 바탕으로 출판이 되었는데 책에서도 정환님의 강점이라고 생각하는 친절한 설명이 되게 돋보여서 설명이 따뜻하다는 느낌을 많이 받을 수 있었습니다. 예를 들면, 아래와 같은 느낌입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EBFyy/btsQfgdvt4E/KeFK2LaXR6DGwsDVA7UEDK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EBFyy/btsQfgdvt4E/KeFK2LaXR6DGwsDVA7UEDK/img.jpg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2025-08-30-23-44-38 002.jpeg&quot; width=&quot;430&quot; height=&quot;573&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EBFyy/btsQfgdvt4E/KeFK2LaXR6DGwsDVA7UEDK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEBFyy%2FbtsQfgdvt4E%2FKeFK2LaXR6DGwsDVA7UEDK%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;4284&quot; height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/104qX/btsQdPVxt37/whkjEIo5sJ1k7Va5IWvGhK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/104qX/btsQdPVxt37/whkjEIo5sJ1k7Va5IWvGhK/img.jpg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_Photo_2025-08-30-23-44-39 003.jpeg&quot; width=&quot;430&quot; height=&quot;573&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/104qX/btsQdPVxt37/whkjEIo5sJ1k7Va5IWvGhK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F104qX%2FbtsQdPVxt37%2FwhkjEIo5sJ1k7Va5IWvGhK%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;4284&quot; height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 개발에 대해서 경험이 적다면 잘 모를 수 있는 용어들에 대한 설명도 꾸준히 해주시고 실습을 따라하던 중간에 혹시나 착오로 인해서 에러가 발생할 수 있을만한 부분도 미리 고려하여 내용들이 남겨져있어서 하나하나 신경써주려고하는 따뜻한 회사 선배같은 느낌이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 책을 펼쳐서 처음 읽기를 시작하고 모든 내용을 읽고 책을 닫기까지 계속해서 프로젝트를 기반으로 진행하고 있는데 어떻게하면 이 책을 익으면서 공부하는 개발자들이 개념을 쉽게 이해하고 써먹을 수 있을지 고민을 하신 결과라고 느껴져서 인상 깊었습니다. 마치 내 옆자리에 있는 선배랑 페어프로그래밍을 하면서 Next.js를 공부한다는 느낌을 받을 수 있을 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따뜻하며 친절한 설명이 좋고 코드를 계속해서 작성하면서 학습을 하고 싶은 분들에게 꼭 추천하고 싶은 책입니다.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <category>Next.js</category>
      <category>이정환</category>
      <category>한입크기로잘라먹는next.js</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/370</guid>
      <comments>https://imnotadevleoper.tistory.com/370#entry370comment</comments>
      <pubDate>Sun, 31 Aug 2025 00:09:32 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 소플의 처음 만난 Next.js</title>
      <link>https://imnotadevleoper.tistory.com/369</link>
      <description>&lt;h3 id=&quot;%22%ED%95%9C%EB%B9%9B%EB%AF%B8%EB%94%94%EC%96%B4%20%EC%84%9C%ED%8F%89%EB%8B%A8%20%3C%EB%82%98%EB%8A%94%EB%A6%AC%EB%B7%B0%EC%96%B4%EB%8B%A4%3E%20%ED%99%9C%EB%8F%99%EC%9D%84%20%EC%9C%84%ED%95%B4%EC%84%9C%20%EC%B1%85%EC%9D%84%20%ED%98%91%EC%B0%AC%20%EB%B0%9B%EC%95%84%20%EC%9E%91%EC%84%B1%EB%90%9C%20%EC%84%9C%ED%8F%89%EC%9E%85%EB%8B%88%EB%8B%A4.%22-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&quot;한빛미디어 서평단 &amp;lt;나는 리뷰어다&amp;gt; 활동을 위해서 책을 협찬받아 작성된 서평입니다.&quot;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-08-30-23-39-18 001.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3bfWi/btsQedBOyaw/AnsDZqnu52vr1uve1lnHp0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3bfWi/btsQedBOyaw/AnsDZqnu52vr1uve1lnHp0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3bfWi/btsQedBOyaw/AnsDZqnu52vr1uve1lnHp0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3bfWi%2FbtsQedBOyaw%2FAnsDZqnu52vr1uve1lnHp0%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-08-30-23-39-18 001.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 읽게 된 책은 &amp;lt;소플의 처음 만난 Next.js&amp;gt; 라는 책입니다. 이전에 React, AWS 시리즈도 있었는데 최근에는 Next.js에 대한 책이 나왔고 한번 읽어보고 싶었던 책이기 때문에 재밌게 읽을 수 있었습니다. 책의 저자이신 소플이라는 닉네임을 사용하시는 이인제 멘토님과는 이전에 교육과정에서 멘토-멘티 관계로 만났었는데 그때 도움을 되게 많이 받았기 때문에 더 기대하면서 읽을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책의 표지를 보면 &quot;개념은 쉽게, 실습은 탄탄하게!&quot;라고 책을 소개하고 있는데 정확한 설명이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js라는 프레임워크를 사용하며 개발하기 위해 필수적인 개념들을 순차적으로 소개하고 있고, 각 챕터의 마지막에는 그 개념에 대해서 직접 코드를 작성하면서 실습을 해볼 수 있습니다. 그리고 개인적으로는 책을 읽으면서 회사에서 되게 겉으로는 살짝 무뚝뚝할 때도 있지만 뒤에서는 생각보다 나를 챙겨주려고 신경 쓰는 선배가 개발을 알려준다는 느낌이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-08-30-23-39-19 002.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PZSx0/btsQeV8vugh/Blst6zE2KLapILfGFUCw1K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PZSx0/btsQeV8vugh/Blst6zE2KLapILfGFUCw1K/img.jpg&quot; data-alt=&quot;레이아웃 꿀팁!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PZSx0/btsQeV8vugh/Blst6zE2KLapILfGFUCw1K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPZSx0%2FbtsQeV8vugh%2FBlst6zE2KLapILfGFUCw1K%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-08-30-23-39-19 002.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;레이아웃 꿀팁!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책의 구성은 단계식으로 학습할 수 있도록 되어 있다고 생각하고 Next.js를 처음 사용하게 되거나, 혹은 오래전에 사용해 보고 최근에는 사용하지 않아서 다시 한번 개념을 익혀야 하는 상황에 적합한 책이라고 생각합니다. 꼭 필요한 개념들을 간결하게 설명해 주시고 알아두면 좋은 내부 동작이나 실무 활용 팁들도 볼드 처리로 강조되어 있어서 저도 모르는 내용을 알게 되는 재미가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 결국에 개발은 이론적인 지식에만 머물러서는 한계가 있기 때문에 꼭 무언가를 만들어보는 과정이 필요하다고 생각하는데요. 바로 내가 만들고 싶은 제품을 만들어봐도 좋겠지만, 그전에 한번 간단한 토이프로젝트를 해보고 싶은 분을 위해서 책의 마지막에는 미니 프로젝트 챕터가 존재합니다. 여기서는 직접 데이터베이스도 설계하고 API를 개발하기 위해 ORM도 사용해 보며 최종적으로는 서비스를 배포까지 진행하는 하나의 웹서비스를 만드는 플로우를 진행해 볼 수 있습니다. 이다음에 실제로 회사에서 Next.js로 개발을 하거나 내가 개인적으로 만들고 싶은 제품을 만드는 흐름으로 넘어가면 도움이 많이 될 것이라고 생각합니다.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <category>Next.js</category>
      <category>넥스트</category>
      <category>소플</category>
      <category>소플의처음만난next.js</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/369</guid>
      <comments>https://imnotadevleoper.tistory.com/369#entry369comment</comments>
      <pubDate>Sat, 30 Aug 2025 23:40:42 +0900</pubDate>
    </item>
    <item>
      <title>[번역] React.memo 완벽 해부: 언제 쓸모 있고 언제 쓸모없는가</title>
      <link>https://imnotadevleoper.tistory.com/368</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://cekrem.github.io/posts/react-memo-when-it-helps-when-it-hurts/&quot;&gt;React.memo Demystified: When It Helps and When It Hurts&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모이제이션의 약속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 애플리케이션의 속도가 느려지기 시작하면, 많은 개발자가 가장 먼저 &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;과 같은 도구를 떠올립니다. 불필요한 리렌더링을 막는 것이 성능을 높이는 가장 직관적인 방법처럼 보이기 때문입니다. 하지만 리액트 생태계에서 메모이제이션은 생각보다 훨씬 더 복잡합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 이러한 도구가 실제로 내부에서 어떻게 동작하고 예상치 못한 방식으로 실패할 수 있는 미묘한 지점들을 알아보고, 언제 이 도구들이 정말 도움이 되는지와 단순히 불필요한 복잡성만 추가하는지에 대해 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 읽어보지 않으셨다면, &lt;a href=&quot;https://cekrem.github.io/posts/beyond-react-memo-smarter-performance-optimization/&quot;&gt;메모이제이션을 사용하지 않고 최적화하는 방법에 대한 제 이전 글들&lt;/a&gt;도 꼭 확인해 보시기 바랍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 이해하기: 자바스크립트의 참조 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본질적으로 리액트에서 메모이제이션이 필요한 이유는 자바스크립트가 객체, 배열, 함수 등을 비교하는 방식에 있습니다. 문자열, 숫자, 불리언과 같은 원시값은 실제 값으로 비교되지만, 객체는 참조로 비교됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 값으로 비교되는 원시값
const a = 1;
const b = 1;
a === b; // true

// 참조로 비교되는 객체
const objA = { id: 1 };
const objB = { id: 1 };
objA === objB; // false, 다른 참조값

// true로 비교가 되려면, 같은 객체의 참조를 바라봐야 함
const objC = objA;
objA === objC; // true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 점이 리액트에서 문제가 되는 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트는 자신의 상태가 변경되거나 부모 컴포넌트가 리렌더링 될 때 다시 렌더링 됩니다.&lt;/li&gt;
&lt;li&gt;컴포넌트가 리렌더링 되면, 모든 지역 변수(객체와 함수 포함)는 새로운 참조로 다시 생성됩니다.&lt;/li&gt;
&lt;li&gt;이러한 새로운 참조가 프로퍼티로 전달되거나 훅의 의존성 배열에 사용되면, 불필요한 리렌더링이나 이펙트 실행이 발생할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useMemo와 useCallback의 내부 동작 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 리액트는 렌더링 간의 참조를 보존해 주는 메모이제이션 훅을 제공합니다. 그렇다면 이 훅들은 실제로 어떻게 동작할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;useMemo&lt;/code&gt;와 &lt;code&gt;useCallback&lt;/code&gt;은 주로 리렌더링이 반복될 때에도 참조의 안정성을 유지하기 위해 존재합니다. 이 훅들은 값을 캐싱하고, 지정한 의존성이 변경될 때만 해당 값을 다시 계산합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 훅들이 내부적으로 수행하는 동작은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// useCallback의 개념적 구현
let cachedCallback;
const useCallback = (callback, dependencies) =&amp;gt; {
  if (dependenciesHaventChanged(dependencies)) {
    return cachedCallback;
  }
  cachedCallback = callback;
  return callback;
};

// useMemo의 개념적 구현
let cachedResult;
const useMemo = (factory, dependencies) =&amp;gt; {
  if (dependenciesHaventChanged(dependencies)) {
    return cachedResult;
  }
  cachedResult = factory();
  return cachedResult;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 차이점은 &lt;code&gt;useCallback&lt;/code&gt;은 함수 자체를 캐싱하고, &lt;code&gt;useMemo&lt;/code&gt;는 전달받은 함수의 반환값을 캐싱한다는 점입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가장 흔한 오해: 프로퍼티를 메모이제이션하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 널리 퍼진 오해 중 하나는 &lt;code&gt;useCallback&lt;/code&gt;이나 &lt;code&gt;useMemo&lt;/code&gt;로 프로퍼티를 메모이제이션 하면 자식 컴포넌트의 리렌더링을 막을 수 있다고 생각하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Component = () =&amp;gt; {
  // 사람들은 이 코드가 자식 컴포넌트의 리렌더링을 막는다고 생각합니다
  const onClick = useCallback(() =&amp;gt; {
    console.log(&quot;clicked&quot;);
  }, []);

  return &amp;lt;button onClick={onClick}&amp;gt;Click me&amp;lt;/button&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 사실이 아닙니다. 부모 컴포넌트가 리렌더링 되면, 자식 컴포넌트는 프로퍼티가 변경되지 않았더라도 기본적으로 모두 리렌더링 됩니다. 프로퍼티를 메모이제이션 하는 것은 다음 두 가지 특정 상황에서만 도움이 됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;해당 프로퍼티가 자식 컴포넌트의 훅 의존성 배열에 사용될 때&lt;/li&gt;
&lt;li&gt;자식 컴포넌트가 &lt;code&gt;React.memo&lt;/code&gt;로 감싸져 있을 때&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React.memo의 실제 동작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;React.memo&lt;/code&gt;는 컴포넌트 렌더링 결과를 메모이제이션 하는 고차 컴포넌트입니다. 프로퍼티의 얕은 비교를 통해 리렌더링이 필요한지 판단합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const ChildComponent = ({ data, onClick }) =&amp;gt; {
  // 컴포넌트 구현
};

const MemoizedChild = React.memo(ChildComponent);

const ParentComponent = () =&amp;gt; {
  // 메모이제이션이 없다면, 매 렌더링마다 새로운 참조를 가짐
  const data = { value: 42 };
  const onClick = () =&amp;gt; console.log(&quot;clicked&quot;);

  // MemoizedChild 컴포넌트는 ParentComponent가 렌더 될 때 마다 리렌더링 됨
  // React.memo로 래핑되어 있지만, 프로퍼티의 참조가 변경되기 때문
  return &amp;lt;MemoizedChild data={data} onClick={onClick} /&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시에서 &lt;code&gt;React.memo&lt;/code&gt;는 프로퍼티의 참조가 계속 변경되기 때문에 리렌더링을 막지 못합니다. 이런 상황에서 &lt;code&gt;useMemo&lt;/code&gt;와 &lt;code&gt;useCallback&lt;/code&gt;이 유용하게 쓰입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const ParentComponent = () =&amp;gt; {
  // 렌더링간의 동일한 참조를 가짐
  const data = useMemo(() =&amp;gt; ({ value: 42 }), []);
  const onClick = useCallback(() =&amp;gt; console.log(&quot;clicked&quot;), []);

  // 이제 MemoizedChild 컴포넌트는 프로퍼티의 참조가 실제로 변경되었을 때만 리렌더링 됨
  return &amp;lt;MemoizedChild data={data} onClick={onClick} /&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React.memo의 숨겨진 함정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;React.memo&lt;/code&gt;를 효과적으로 사용하는 것은 생각보다 어렵습니다. 메모이제이션을 조용히 깨뜨릴 수 있는 몇 가지 흔한 함정들을 살펴보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프로퍼티 스프레드 연산자 문제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Child = React.memo(({ data }) =&amp;gt; {
  // 컴포넌트 구현
});

// 프로퍼티가 변경되기 때문에 메모이제이션이 깨짐
const Parent = (props) =&amp;gt; {
  return &amp;lt;Child {...props} /&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 프로퍼티를 펼쳐서 전달하면, &lt;code&gt;Child&lt;/code&gt; 컴포넌트가 프로퍼티로 받는 속성들이 안정적인 참조를 유지하는지 제어할 수 없습니다. 누군가 &lt;code&gt;Parent&lt;/code&gt; 컴포넌트를 사용할 때 의도치 않게 메모이제이션을 깨뜨릴 수도 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. children 프로퍼티 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마도 가장 놀라운 함정은 JSX의 children도 단순히 또 다른 프로퍼티일 뿐이며, 이 역시 메모이제이션이 필요하다는 점입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const MemoComponent = React.memo(({ children }) =&amp;gt; {
  // 컴포넌트 구현
});

const Parent = () =&amp;gt; {
  // 이것은 메모이제이션을 깨뜨립니다! children은 매 렌더마다 새로 생성
  return (
    &amp;lt;MemoComponent&amp;gt;
      &amp;lt;div&amp;gt;Some content&amp;lt;/div&amp;gt;
    &amp;lt;/MemoComponent&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하려면, children도 메모이제이션 해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Parent = () =&amp;gt; {
  const content = useMemo(() =&amp;gt; &amp;lt;div&amp;gt;Some content&amp;lt;/div&amp;gt;, []);

  return &amp;lt;MemoComponent&amp;gt;{content}&amp;lt;/MemoComponent&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 중첩된 Memo 컴포넌트 문제&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const InnerChild = React.memo(() =&amp;gt; &amp;lt;div&amp;gt;Inner&amp;lt;/div&amp;gt;);
const OuterChild = React.memo(({ children }) =&amp;gt; &amp;lt;div&amp;gt;{children}&amp;lt;/div&amp;gt;);

const Parent = () =&amp;gt; {
  // OuterChild의 메모이제이션은 깨집니다!
  return (
    &amp;lt;OuterChild&amp;gt;
      &amp;lt;InnerChild /&amp;gt;
    &amp;lt;/OuterChild&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 컴포넌트 모두 메모이제이션되어 있더라도, &lt;code&gt;InnerChild&lt;/code&gt; JSX 엘리먼트가 매 렌더마다 새로운 객체 참조를 생성하기 때문에 OuterChild는 여전히 리렌더링 됩니다. 해결 방법은 자식 엘리먼트도 메모이제이션 하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Parent = () =&amp;gt; {
  const innerChild = useMemo(() =&amp;gt; &amp;lt;InnerChild /&amp;gt;, []);

  return &amp;lt;OuterChild&amp;gt;{innerChild}&amp;lt;/OuterChild&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제로 언제 메모이제이션을 사용해야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 다양한 복잡함을 고려할 때, 실제로 언제 리액트의 메모이제이션 도구들을 사용해야 할까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React.memo를 사용해야 하는 경우.&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;동일한 프로퍼티가 주어졌을 때 항상 같은 결과를 렌더링하는 순수한 함수 컴포넌트일 때&lt;/li&gt;
&lt;li&gt;동일한 프로퍼티로 자주 렌더링되는 경우&lt;/li&gt;
&lt;li&gt;렌더링 자체가 계산 비용이 많이 드는 경우&lt;/li&gt;
&lt;li&gt;프로파일링을 통해 실제로 성능 병목임을 확인한 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useMemo를 사용해야 하는 경우.&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;매 렌더마다 다시 계산할 필요가 없는 비용이 큰 연산이 있을 때&lt;/li&gt;
&lt;li&gt;메모이제이션 된 컴포넌트에 전달되는 객체나 배열의 참조를 안정적으로 유지해야 할 때&lt;/li&gt;
&lt;li&gt;실제로 해당 연산이 비용이 크다는 것을 측정하고 확인한 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useCallback을 사용해야 하는 경우.&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;참조 동일성(reference equality)에 의존하는 최적화된 자식 컴포넌트에 콜백을 전달할 때&lt;/li&gt;
&lt;li&gt;해당 콜백이 useEffect 훅의 의존성 배열에 포함되어 있을 때&lt;/li&gt;
&lt;li&gt;메모이제이션 된 컴포넌트에서 이벤트 핸들러의 함수 참조를 안정적으로 유지해야 할 때&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;합성(Composition)이라는 대안&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모이제이션을 적용하기 전에, 컴포넌트 구조를 합성을 통해 개선할 수 있는지 먼저 고려해 보세요. 컴포넌트 합성은 종종 메모이제이션보다 더 우아하게 성능 문제를 해결해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 아래와 같이 비용이 많이 드는 컴포넌트를 매번 리렌더링하는 것은 좋지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const ParentWithState = () =&amp;gt; {
  const [count, setCount] = useState(0);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;Increment&amp;lt;/button&amp;gt;
      &amp;lt;ExpensiveComponent /&amp;gt; {/* count가 변경될 때마다 리렌더링 됨 */}
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 상태를 분리하여 더 구체적인 컨테이너에 넣는 방식으로 개선할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const CounterButton = () =&amp;gt; {
  const [count, setCount] = useState(0);

  return &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;Count: {count}&amp;lt;/button&amp;gt;;
};

const Parent = () =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;CounterButton /&amp;gt;
      &amp;lt;ExpensiveComponent /&amp;gt; {/* count가 변경될 때 더 이상 리렌더링 되지 않음 */}
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트에서의 메모이제이션은 강력한 최적화 기법이지만, 숙련된 개발자조차도 실수할 수 있는 미묘한 부분이 많습니다. &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;을 코드 전체에 무분별하게 적용하기 전에 다음을 꼭 고려해 보세요.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;먼저 프로파일링하세요&lt;/b&gt;: 실제 성능 병목이 어디인지 React DevTools Profiler로 확인하세요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;합성을 고려하세요&lt;/b&gt;: 컴포넌트 구조를 재설계하면 메모이제이션이 필요 없을 수도 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함정에 유의하세요&lt;/b&gt;: 메모이제이션이 조용히 무너질 수 있는 다양한 경우를 인지하세요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다시 측정하세요&lt;/b&gt;: 최적화가 실제로 성능을 개선하는지 반드시 검증하세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모이제이션을 신중하고 올바르게 사용하면 리액트 애플리케이션의 성능을 크게 향상시킬 수 있습니다. 하지만 주의 깊게 적용하지 않으면 복잡성만 높아지고, 오히려 성능이 저하될 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;섣부른 최적화(premature optimization)는 소프트웨어 개발에서 많은 문제의 근원임을 기억하세요. 함수형 프로그래밍 원칙에 따라 깔끔하게 컴포넌트를 합성하는 것부터 시작하고, 성능을 측정한 뒤, 메모이제이션이 정말 필요하다는 명확한 근거가 있을 때만 적용하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분은 리액트의 메모이제이션 도구들을 사용하면서 어떤 경험을 하셨나요? 불필요한 리렌더링을 방지하는 데 도움이 되었던 다른 패턴이 있다면 공유해 주세요. (오른쪽의 피드백 위젯을 통해 의견을 남겨주시면 감사하겠습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt; &amp;nbsp;한국어로&amp;nbsp;된&amp;nbsp;프런트엔드&amp;nbsp;아티클을&amp;nbsp;빠르게&amp;nbsp;받아보고&amp;nbsp;싶다면&amp;nbsp;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://kofearticle.substack.com/&quot;&gt;Korean&amp;nbsp;FE&amp;nbsp;Article&lt;/a&gt;을&amp;nbsp;구독해주세요!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/번역</category>
      <category>memo</category>
      <category>Memoization</category>
      <category>react</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/368</guid>
      <comments>https://imnotadevleoper.tistory.com/368#entry368comment</comments>
      <pubDate>Mon, 11 Aug 2025 01:52:59 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 개발자를 위한 IT 영어 온보딩 가이드</title>
      <link>https://imnotadevleoper.tistory.com/367</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&quot;한빛미디어 서평단 &amp;lt;나는리뷰어다&amp;gt; 활동을 위해서 책을 협찬 받아 작성된 서평입니다.&quot;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-07-27-19-22-57 001.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4onAp/btsPCmE1tS3/73CZ3VcF5iiCNuBVPnBQZk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4onAp/btsPCmE1tS3/73CZ3VcF5iiCNuBVPnBQZk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4onAp/btsPCmE1tS3/73CZ3VcF5iiCNuBVPnBQZk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4onAp%2FbtsPCmE1tS3%2F73CZ3VcF5iiCNuBVPnBQZk%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-07-27-19-22-57 001.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;개발자에게 가장 중요한 언어가 무엇이냐는 질문에 Java, C, Python이 아닌 영어가 1순위로 뽑혔던 설문조사를 본 기억이 있습니다. 아마도 그 이유는 특정 프로그래밍 언어를 알면 그것을 이용해서 특정 프로그램을 만들 수 있지만, 영어를 알면 영어를 통해서 무엇이든 공부할 수 있기 때문일 것 같습니다. 그러나 영어를 공부한다는 것은 되게 방대하고 목적에 따라서 그 방식이 달라져야만 하는데요. 특히 저도 개발자로서 일을 해오다보니 영어를 학습하는 방식도 일반적인 방식과는 달라야 한다고 생각합니다. 여기서 설명하려고 하는 &amp;lt;개발자를 위한 IT 영어 온보딩 가이드&amp;gt;는 &quot;온보딩 가이드&quot;라는 책의 제목처럼 개발자로서 어떻게 영어를 학습할 수 있을지 체계적으로 설명하고 있습니다. 또한 개발자도 영어를 학습하게 되면 다른 기술들을 공부하는 것 외에도 다양한 취업의 기회를 얻을 수 있는 면도 강조하고 있는데 개발자라면 누구나 한번은 생각해볼 해외 취업에 대한 내용도 포함하고 있습니다. 이 외에도 별도로 정리 노트를 제공하기도 하고, 추가적인 학습을 위해 온라인 자료들을 제공하고 있어서 실습도 해볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-07-27-19-22-57 002.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OKRHB/btsPzUw27s4/DN0eg0XsUBUS3lC5fg2A70/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OKRHB/btsPzUw27s4/DN0eg0XsUBUS3lC5fg2A70/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OKRHB/btsPzUw27s4/DN0eg0XsUBUS3lC5fg2A70/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOKRHB%2FbtsPzUw27s4%2FDN0eg0XsUBUS3lC5fg2A70%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-07-27-19-22-57 002.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;책에서 배울 수 있는 것들&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책은 크게 네 가지 파트로 나뉘어 있는데, 각각 다음과 같은 질문에 대답해 줍니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;왜 영어를 배워야 하는가?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 느끼는 현실적인 벽부터 영어에 대한 편견까지 짚어줍니다. 막연하게 개발자에게도 영어는 &amp;ldquo;필요하다&amp;rdquo;가 아니라 &amp;ldquo;이래서 커리어가 달라진다&amp;rdquo;는 동기부여가 확실합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실무에서 영어는 어떻게 쓰이는가?&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;매일 일을 하면서도 보는 용어와 상황들&lt;/span&gt;이 등장해서, 만약에 영어로 대화를 해야 한다면 써먹을 수 있겠다는 생각이 들었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제로 개발 문서에서 어떻게 쓰이는가?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커밋 메시지, API 문서, 깃허브 이슈, PR, 블로그의 글도 같이 살펴봅니다.&lt;/li&gt;
&lt;li&gt;리액트, NestJS, 쿠버네티스, 스마트 컨트랙트 등 &lt;span&gt;최신 기술 스택에 대한 영어 표현도 다루고 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해외 취업을 꿈꾼다면 어떻게 준비해야 할까?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이력서, 포트폴리오, 링크드인 작성부터 면접 준비, 코딩 테스트 팁까지 &lt;span&gt;해외 취업을 위한 방법도 다룹니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;추천 대상&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 다음과 같은 분들께 추천드립니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;개발자로 커리어를 확장&lt;/span&gt;하고 싶은 분&lt;/li&gt;
&lt;li&gt;해외 취업을&amp;nbsp;&lt;span&gt;고려 중인 개발자&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;개발자 영어를 &lt;/span&gt;재미있고 실용적으로 배우고 싶은 분&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 단순하게는 개발자 영어를 학습하는 용도로 읽을 수도 있지만 조금 더 확장한다면 글로벌한 개발자로 살아가는 방법에 대한 가이드가 될 것 같습니다. 그리고 개발자 영어는 일반적으로 영어를 배우는 것보다는 훨씬 쉽게 접근할 수 있다고 생각하기 때문에 조금이라도 관심이 생기셨다면 한번 읽어보기를 권장드리고 싶습니다.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/367</guid>
      <comments>https://imnotadevleoper.tistory.com/367#entry367comment</comments>
      <pubDate>Sun, 27 Jul 2025 19:23:59 +0900</pubDate>
    </item>
    <item>
      <title>[독서] You Don't Know JS Yet 1~2장 간단 정리</title>
      <link>https://imnotadevleoper.tistory.com/366</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;PART 1&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Chapter 1 자바스크립트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바스크립트라는 언어는 완벽히 이해하기보다 꾸준히 학습한다고 생각하는 것이 바람직하다. 사실 무엇을 완벽히 이해한다는 것은 굉장히 어려운 일이기 때문에 (그럴 필요도 없고?) 필요에 의해서 학습하는 루틴을 만드는 것이 좋다고 생각한다.&lt;/li&gt;
&lt;li&gt;JS를 ES6, ES8 같은 이름으로 부르는 것은 혼란스럽다. ES20xx나 JS라고 부르는 방향을 제안함.&lt;/li&gt;
&lt;li&gt;TC39는 JS를 관리하는 기술 운영 위원회이고 모든 제안은 다섯 단계(stage)로 이루어진 절차를 거친다. (단계는 0부터 시작함)&lt;/li&gt;
&lt;li&gt;JS는 버전이라는 개념이 없다. 브라우저나 디바이스 제조사는 단 하나뿐인 명세서를 기준으로 JS 구현체를 만든다.&lt;/li&gt;
&lt;li&gt;ECMAScript 명세서에는 웹에서 돌아가는 JS의 차이가 상세히 기록되어 있는 부록 B, 'Additional ECMAScript Features for Web Browsers'가 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 브라우저마다 ECMAScript 명세서는 없지만 구현되어 있는 JS 문법이나 API들이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Host(Browser, Node)에 따라 달라지는 API도 존재한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS로 보이지만 웹에서만 지원되는 fetch, alert와 같은 것들&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;브라우저내의 개발자 도구 콘솔이나 REPL과 같은 것들이 JS의 프로그램 처리 방식을 엄격하게 준수하지 않기 때문에 이는 주의할 필요가 있음. 간단한 문만 작성해서 테스트하기에 적합하다.&lt;/li&gt;
&lt;li&gt;멀티패러다임 프로그래밍 언어이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;절차적 프로그래밍&lt;/li&gt;
&lt;li&gt;객체 지향 프로그래밍&lt;/li&gt;
&lt;li&gt;함수형 프로그래밍&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;하위 호환성과 상위 호환성의 개념
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS는 하위 호환성을 보장한다. 명세서가 변경되더라도 예전 문법은 항상 보장된다.&lt;/li&gt;
&lt;li&gt;상휘 호환성은 보장하지 않는다. 구형 JS 엔진에서 최신 JS 명세서에 존재하는 문법은 사용할 수 없다. 이 문제를 해결하기 위해 새로운 JS 문법을 오래된 문법으로 바꿔주는 Babel과 같은 트랜스파일러를 사용하게 됨.&lt;/li&gt;
&lt;li&gt;명시적으로 최신 문법을 추가해야 할 때는 폴리필-polyfill(shim)을 이용하여 최신 API 메서드가 오래된 환경에도 있었던 것처럼 지원할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JS는 컴파일 언어인가, 인터프리터 언어인가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저자는 컴파일 언어에 가깝다고 얘기하고 있음.&lt;/li&gt;
&lt;li&gt;개발자가 작성한 코드는 바벨이 트랜스파일하고, 번들러를 거쳐 번들링 되고, 그 결과가 JS 엔진에 전달.&lt;/li&gt;
&lt;li&gt;JS에서 엔진은 코드를 파싱해 추상 구문 트리로 바꿈.&lt;/li&gt;
&lt;li&gt;추상 구문 트리를 이진 바이트 코드로 바꾸는데, 이 과정에서 JIT(Just-in-Time) 컴파일러가 작동하며 최적화 진행.&lt;/li&gt;
&lt;li&gt;JS 가상 머신이 프로그램 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;웹어셈블리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS가 아닌 언어를 웹 브라우저에서 실행할 수 있는 방법&lt;/li&gt;
&lt;li&gt;웹 전용 기술은 아니고, JS도 아님&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;엄격 모드(strict mode)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS 엔진이 코드를 최적화하고 효율적으로 실행할 수 있게 해주는 가이드 역할을 하는 모드&lt;/li&gt;
&lt;li&gt;파일 단위, 함수 단위로 적용할 수 있는데 점진적으로 적용하는 것이 아니라면 파일 단위로 권장&lt;/li&gt;
&lt;li&gt;ES6 모듈은 기본이 엄격모드이고 대부분 트랜스파일 처리된 코드는 엄격 모드를 준수함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Chapter 2 자바스크립트 조망하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일은 프로그램이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.js 파일 각각을 프로그램으로 바라보는 게 중요함&lt;/li&gt;
&lt;li&gt;파일 하나에만 오류가 있어도 다음 파일이 처리되지 않을 수 있음, 오류는 우아하게 처리할 수 있어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;원시 타입과 객체 타입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리터럴은 변수를 통하지 않고 코드 그 자체로 프로그램에 값을 주입할 때 사용함&lt;/li&gt;
&lt;li&gt;백틱은 보간 표현식을 사용하기 위한 목적, 일반 문자열을 표현하는 것은 권장하지 않음&lt;/li&gt;
&lt;li&gt;null과 undefined중에 뭘 사용하는지에 대해서는 의견이 다양한데, undefined도 권장하는 편&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;변수 선언 키워드는 var, let, const
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;var를 사용하지 말아야 한다가 아닌, 상황에 맞게 사용하면 된다라고 설명함&lt;/li&gt;
&lt;li&gt;var는 function scope, let/const는 block scope&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const로 객체를 정의하는 것은 좋지 않은 방식이라고 표현하는데, 의도는 이해했으나 일반적으로는 const로 정의하는 게 표준이라고 생각함&lt;/li&gt;
&lt;li&gt;함수
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수 선언식은 식별자와 실제 함수를 나타내는 값의 연관이 컴파일 단계에서 맺어짐&lt;/li&gt;
&lt;li&gt;함수 표현식은 함수 식별자가 코드가 실행되기 전까지는 관계를 맺지 않음&lt;/li&gt;
&lt;li&gt;함수가 값이라는 특징은 중요함, 할당 가능하고 전달 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비교
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일치 비교(===)와 동등 비교(==)의 차이를 알고 있어야 한다.&lt;/li&gt;
&lt;li&gt;NaN, 0, -0등을 비교하기 위해서는 &lt;a href=&quot;http://Object.is&quot;&gt;Object.is&lt;/a&gt;() 메서드 권장&lt;/li&gt;
&lt;li&gt;일치 비교는 값의 본질이나 내용을 비교하는데, 비교 대상이 객체인 경우에는 독자성 일치(identity equality)를 비교함, 즉 참조를 비교한다는 말&lt;/li&gt;
&lt;li&gt;객체의 구조가 같은지 비교하려면 직접 코드를 작성해야 한다, 함수 비교는 클로저등을 고려할 수 없어 불가능함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;강제 변환(coercion)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;피연산자가 같은 타입이면 ==와 ===는 완전히 동일하게 작동&lt;/li&gt;
&lt;li&gt;피연산자의 타입이 다른 경우 == 연산자는 비교 이전에 강제로 타입을 맞추는 작업을 수행, 따라서 저자는 강제 변환 동등 비교 연산자라고 설명하는게 적합하다고 생각&lt;/li&gt;
&lt;li&gt;== 연산자는 숫자형 피연산자를 선호함. 숫자와 문자가 있으면 문자를 숫자로 변경하기 비교&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;코드 구조화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터와 데이터를 조작하는 동작의 집합&lt;/li&gt;
&lt;li&gt;구체적인 값이 필요하다면 new 연산자를 통해 인스턴스를 생성&lt;/li&gt;
&lt;li&gt;상속과 다형성은 클래스 지향 설계의 중요한 부분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모듈
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스와 동일하게 데이터와 행동을 그룹화&lt;/li&gt;
&lt;li&gt;클래식 모듈은 클로저를 활용하여 인스턴스를 반환하는 개념&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ES 모듈
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ES6에서 도입된 문법, 파일이라는 맥락에서 구현됨&lt;/li&gt;
&lt;li&gt;import, export 키워드를 통해 퍼블릭 API를 정의하고, 다른 모듈에서 가져옴&lt;/li&gt;
&lt;li&gt;인스턴스화 하지 않아도 import 키워드를 통해 단일 인스턴스처럼 사용할 수 있음, 사실상 싱글턴&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발 서적/일일 독서</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/366</guid>
      <comments>https://imnotadevleoper.tistory.com/366#entry366comment</comments>
      <pubDate>Mon, 7 Jul 2025 01:12:45 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 리액트의 개방-폐쇄 원칙: 확장 가능한 컴포넌트 만들기</title>
      <link>https://imnotadevleoper.tistory.com/365</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문 : &lt;a href=&quot;https://cekrem.github.io/posts/open-closed-principle-in-react/&quot;&gt;Open-Closed Principle in React: Building Extensible Components&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cekrem.github.io/posts/clean-architecture-and-plugins-in-go/&quot;&gt;의존성 역전(Dependency Inversion)&lt;/a&gt;, &lt;a href=&quot;https://cekrem.github.io/posts/interface-segregation-in-practice/&quot;&gt;인터페이스 분리(Interface Segregation)&lt;/a&gt;, &lt;a href=&quot;https://cekrem.github.io/posts/liskov-substitution-the-real-meaning-of-inheritance/&quot;&gt;리스코프 치환(Liskov Substitution)&lt;/a&gt; 원칙에 대해 살펴본 후, 이번에는 현대 리액트 애플리케이션의 관점에서 개방-폐쇄 원칙(Open-Closed Principle, OCP)에 대해 다뤄보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 한번, 좋은 소프트웨어 아키텍처의 중요성을 일깨워준 Uncle Bob의 명저 &lt;a href=&quot;https://www.amazon.com/dp/0134494164&quot;&gt;클린 아키텍처&lt;/a&gt;에 찬사를 보냅니다! 이 시리즈는 주로 그 책에서 영감을 받았습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개방-폐쇄 원칙은 소프트웨어 구성 요소가 확장에는 열려 있고, 변경에는 닫혀 있어야 한다고 말합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 관점에서 보면, 컴포넌트는 기존 코드를 변경하지 않고도 쉽게 확장할 수 있어야 합니다. 실제로 어떻게 적용되는지 함께 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;폐쇄적인 컴포넌트의 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 흔히 볼 수 있는 안티패턴입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 이렇게 하지 마세요
const Button = ({ label, onClick, variant }: ButtonProps) =&amp;gt; {
  let className = &quot;button&quot;;

  // 각 변형(variant)마다 직접 수정
  if (variant === &quot;primary&quot;) {
    className += &quot; button-primary&quot;;
  } else if (variant === &quot;secondary&quot;) {
    className += &quot; button-secondary&quot;;
  } else if (variant === &quot;danger&quot;) {
    className += &quot; button-danger&quot;;
  }

  return (
    &amp;lt;button className={className} onClick={onClick}&amp;gt;
      {label}
    &amp;lt;/button&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 개방-폐쇄 원칙을 위반합니다. 그 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;새로운 variant를 추가할 때마다 컴포넌트를 수정해야 합니다.&lt;/li&gt;
&lt;li&gt;컴포넌트가 모든 variant에 대해 알고 있어야 합니다.&lt;/li&gt;
&lt;li&gt;variant가 추가될수록 테스트가 점점 더 복잡해집니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개방적인 컴포넌트 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개방-폐쇄 원칙을 따르도록 리팩터링해봅시다.&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;type ButtonBaseProps = {
  label: string;
  onClick: () =&amp;gt; void;
  className?: string;
  children?: React.ReactNode;
};

const ButtonBase = ({ label, onClick, className = &quot;&quot;, children }: ButtonBaseProps) =&amp;gt; (
  &amp;lt;button className={`button ${className}`.trim()} onClick={onClick}&amp;gt;
    {children || label}
  &amp;lt;/button&amp;gt;
);

// 변형된 컴포넌트가 기본 컴포넌트를 확장합니다
const PrimaryButton = (props: ButtonBaseProps) =&amp;gt; &amp;lt;ButtonBase {...props} className=&quot;button-primary&quot; /&amp;gt;;

const SecondaryButton = (props: ButtonBaseProps) =&amp;gt; &amp;lt;ButtonBase {...props} className=&quot;button-secondary&quot; /&amp;gt;;

const DangerButton = (props: ButtonBaseProps) =&amp;gt; &amp;lt;ButtonBase {...props} className=&quot;button-danger&quot; /&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기존 코드를 변경하지 않고도 새로운 변형을 쉽게 추가할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 기존 컴포넌트에 손대지 않고 새로운 변형 추가하기
const OutlineButton = (props: ButtonBaseProps) =&amp;gt; &amp;lt;ButtonBase {...props} className=&quot;button-outline&quot; /&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴포넌트 합성 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합성(Composition)을 활용한 조금 더 복잡한 예제를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;type CardProps = {
  title: string;
  children: React.ReactNode;
  renderHeader?: (title: string) =&amp;gt; React.ReactNode;
  renderFooter?: () =&amp;gt; React.ReactNode;
  className?: string;
};

const Card = ({ title, children, renderHeader, renderFooter, className = &quot;&quot; }: CardProps) =&amp;gt; (
  &amp;lt;div className={`card ${className}`.trim()}&amp;gt;
    {renderHeader ? renderHeader(title) : &amp;lt;div className=&quot;card-header&quot;&amp;gt;{title}&amp;lt;/div&amp;gt;}
    &amp;lt;div className=&quot;card-content&quot;&amp;gt;{children}&amp;lt;/div&amp;gt;
    {renderFooter &amp;amp;&amp;amp; renderFooter()}
  &amp;lt;/div&amp;gt;
);

// 기존 코드를 변경하지 않고 확장하기
const ProductCard = ({ product, onAddToCart, ...props }: ProductCardProps) =&amp;gt; (
  &amp;lt;Card {...props} renderFooter={() =&amp;gt; &amp;lt;button onClick={onAddToCart}&amp;gt;Add to Cart - ${product.price}&amp;lt;/button&amp;gt;} /&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;확장을 위한 고차 컴포넌트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고차 컴포넌트는 개방-폐쇄 원칙을 따르는 또 다른 방법을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;type WithLoadingProps = {
  isLoading?: boolean;
};

const withLoading = &amp;lt;P extends object&amp;gt;(WrappedComponent: React.ComponentType&amp;lt;P&amp;gt;) =&amp;gt; {
  return ({ isLoading, ...props }: P &amp;amp; WithLoadingProps) =&amp;gt; {
    if (isLoading) {
      return &amp;lt;div className=&quot;loader&quot;&amp;gt;Loading...&amp;lt;/div&amp;gt;;
    }

    return &amp;lt;WrappedComponent {...(props as P)} /&amp;gt;;
  };
};

// 사용법
const UserProfileWithLoading = withLoading(UserProfile);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개방-폐쇄 원칙을 따르는 커스텀 훅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 훅 역시 개방-폐쇄 원칙을 따를 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const useDataFetching = &amp;lt;T&amp;gt;(url: string) =&amp;gt; {
  const [data, setData] = useState&amp;lt;T | null&amp;gt;(null);
  const [error, setError] = useState&amp;lt;Error | null&amp;gt;(null);
  const [loading, setLoading] = useState(true);

  useEffect(() =&amp;gt; {
    fetchData();
  }, [url]);

  const fetchData = async () =&amp;gt; {
    try {
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
    } catch (e) {
      setError(e as Error);
    } finally {
      setLoading(false);
    }
  };

  return { data, error, loading, refetch: fetchData };
};

// 기존 코드의 수정 없이 확장하기
const useUserData = (userId: string) =&amp;gt; {
  const result = useDataFetching&amp;lt;User&amp;gt;(`/api/users/${userId}`);

  // 사용자와 관련된 기능 추가하기
  const updateUser = async (data: Partial&amp;lt;User&amp;gt;) =&amp;gt; {
    // 업데이트 로직
  };

  return { ...result, updateUser };
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트에서의 이점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개방-폐쇄 원칙을 따르면 테스트가 훨씬 더 간단해집니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;describe(&quot;ButtonBase&quot;, () =&amp;gt; {
  it(&quot;renders with custom className&quot;, () =&amp;gt; {
    render(&amp;lt;ButtonBase label=&quot;Test&quot; onClick={() =&amp;gt; {}} className=&quot;custom&quot; /&amp;gt;);
    expect(screen.getByRole(&quot;button&quot;)).toHaveClass(&quot;button custom&quot;);
  });
});

describe(&quot;PrimaryButton&quot;, () =&amp;gt; {
  it(&quot;includes primary styling&quot;, () =&amp;gt; {
    render(&amp;lt;PrimaryButton label=&quot;Test&quot; onClick={() =&amp;gt; {}} /&amp;gt;);
    expect(screen.getByRole(&quot;button&quot;)).toHaveClass(&quot;button button-primary&quot;);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;변경보다는 합성을 사용하세요&lt;/b&gt; &amp;mdash; 프로퍼티와 렌더 함수를 통해 확장하세요.&lt;/li&gt;
&lt;li&gt;확장하기 쉬운 &lt;b&gt;기본 컴포넌트&lt;/b&gt;를 만드세요.&lt;/li&gt;
&lt;li&gt;재사용 가능한 확장을 위해 &lt;b&gt;고차 컴포넌트와 커스텀 훅을 적극적으로 활용하세요&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장 포인트를 염두에 두세요&lt;/b&gt; &amp;mdash; 앞으로 무엇이 변경될 수 있을지 생각해보세요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타입스크립트&lt;/b&gt;로 확장에 타입 안정성을 확보하세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개방-폐쇄 원칙과 &amp;ldquo;상속보다 합성&amp;rdquo;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 팀이 권장하는 &lt;a href=&quot;https://legacy.reactjs.org/docs/composition-vs-inheritance.html&quot;&gt;&amp;ldquo;상속보다 합성&amp;rdquo;&lt;/a&gt; 원칙은 개방-폐쇄 원칙과 완벽하게 일치합니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;// 상속 기반 접근 방식 (유연성 낮음)
class Button extends BaseButton {
  render() {
    return (
      &amp;lt;button className={this.getButtonClass()}&amp;gt;
        {this.props.icon &amp;amp;&amp;amp; &amp;lt;Icon name={this.props.icon} /&amp;gt;}
        {this.props.label}
      &amp;lt;/button&amp;gt;
    );
  }
}

// 합성 기반 접근 방식(더 유연하며, 개방-폐쇄 원칙을 따름)
const Button = ({ label, icon, renderPrefix, renderSuffix, ...props }: ButtonProps) =&amp;gt; (
  &amp;lt;ButtonBase {...props}&amp;gt;
    {renderPrefix?.()}
    {icon &amp;amp;&amp;amp; &amp;lt;Icon name={icon} /&amp;gt;}
    {label}
    {renderSuffix?.()}
  &amp;lt;/ButtonBase&amp;gt;
);

const DropdownButton = ({ items, ...props }: DropdownButtonProps) =&amp;gt; (
  &amp;lt;Button {...props} renderSuffix={() =&amp;gt; &amp;lt;DropdownIcon /&amp;gt;} onClick={() =&amp;gt; setIsOpen(true)} /&amp;gt;
);

const LoadingButton = ({ isLoading, ...props }: LoadingButtonProps) =&amp;gt; (
  &amp;lt;Button {...props} renderPrefix={() =&amp;gt; isLoading &amp;amp;&amp;amp; &amp;lt;Spinner /&amp;gt;} disabled={isLoading} /&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합성 기반 접근 방식은 다음과 같은 장점이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로퍼티와 렌더 함수를 통해 컴포넌트를 확장할 수 있어 개방-폐쇄 원칙을 지킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;기본 컴포넌트는 변경 없이 유지됩니다.&lt;/li&gt;
&lt;li&gt;동작의 다양한 조합을 무한히 허용합니다.&lt;/li&gt;
&lt;li&gt;타입 안전성과 프로퍼티의 투명성을 유지할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 팀이 합성을 선호하는 이유는 단순한 스타일의 문제가 아닙니다. 합성을 통해 자연스럽게 개방-폐쇄 원칙을 따르는 확장 가능하고 유지보수가 쉬운 컴포넌트를 만들 수 있기 때문입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개방-폐쇄 원칙은 다소 추상적으로 느껴질 수 있지만, 리액트에서는 컴포넌트를 더 유지보수하기 쉽고 유연하게 만들어주는 실질적인 패턴으로 이어집니다. 앞서 살펴본 SOLID 원칙들과 결합하면, 확장과 유지보수가 쉬운 견고한 아키텍처를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈의 마지막 글에서는 단일 책임 원칙(Single Responsibility Principle)에 대해 다룰 예정이니 많은 기대 부탁드립니다!&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;꿀팁&lt;/b&gt;: 다양한 변형이나 동작을 위해 if/else 문을 많이 사용하고 있다면, 아마도 개방-폐쇄 원칙을 위반하고 있는 것일 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;업데이트: 친근한 안내 및 알림&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 여러분이 소프트웨어 아키텍처에 대한 종합적인 가이드를 기대하셨다면, 이 글은 그런 목적이 아닙니다. 최근 소프트웨어 아키텍처에 대해 연재하고 있는 글들의 목적은, 그동안 제가 너무 쉽게 무시하거나 적용하기를 미뤄왔던 몇 가지 원칙들을 실제로 어떻게 활용할 수 있는지 탐구해보는 데 있습니다.&lt;br /&gt;저는 이 개념들에 대해 완벽하게 이해하고 있다고 주장하지도 않고, 이 원칙들을 모든 상황에 무조건적으로 적용해야 한다고 말하는 것도 아닙니다. 심지어, 제가 제시한 간단한 예시들이 이 원칙들을 구현하거나 설명하는 최선의 방법이라고 생각하지도 않습니다.&lt;br /&gt;그보다는, 고전적인 소프트웨어 엔지니어링 원칙과 현대 개발 실무 사이의 간극을 좁히려는 저의 시도를 기록하고 있을 뿐입니다. 사실 저 자신도 &amp;lsquo;클린 아키텍처&amp;rsquo;에 얼마나 가까이 다가갈지, 혹은 얼마나 실용적으로 접근할지 아직 결정을 내리지 못했습니다. 하지만 지금은 (대체로) 배우고 탐구하는 과정 자체를 즐기고 있습니다. 그러니 혹시라도 저나 Uncle Bob, 혹은 다른 분들에게 이와 관련해 레딧 등에서 뭐라고 하시기 전에 이 점을 꼭 기억해 주세요  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 저와 의미 있고 존중하는 방식으로 의견을 나눠주신 모든 분들께 감사드립니다. 저에게 정말 큰 배움의 기회가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다 :)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt; &amp;nbsp;한국어로&amp;nbsp;된&amp;nbsp;프런트엔드&amp;nbsp;아티클을&amp;nbsp;빠르게&amp;nbsp;받아보고&amp;nbsp;싶다면&amp;nbsp;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://kofearticle.substack.com/&quot;&gt;Korean&amp;nbsp;FE&amp;nbsp;Article&lt;/a&gt;을&amp;nbsp;구독해주세요!&lt;/blockquote&gt;</description>
      <category>개발/번역</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/365</guid>
      <comments>https://imnotadevleoper.tistory.com/365#entry365comment</comments>
      <pubDate>Sun, 29 Jun 2025 23:19:27 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] UX/UI 디자이너를 위한 실무 피그마</title>
      <link>https://imnotadevleoper.tistory.com/364</link>
      <description>&lt;h1&gt;&quot;한빛미디어 서평단 &amp;lt;나는리뷰어다&amp;gt; 활동을 위해서 책을 협찬 받아 작성된 서평입니다.&quot;&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;승완-책이미지.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpJge2/btsOWgMGUv6/XG7B4x6KKgCjoepwv8gI20/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpJge2/btsOWgMGUv6/XG7B4x6KKgCjoepwv8gI20/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpJge2/btsOWgMGUv6/XG7B4x6KKgCjoepwv8gI20/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpJge2%2FbtsOWgMGUv6%2FXG7B4x6KKgCjoepwv8gI20%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;승완-책이미지.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 책에 대한 간단한 소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;『피그마 완전 정복』은 UX/UI 디자이너, 기획자, 개발자 등 협업에 관여하는 모든 실무자들이 피그마(Figma)를 실질적으로 활용할 수 있도록 돕는 실전형 가이드북입니다. 단순한 툴 사용법을 넘어서 협업 관점에서의 실전 예제와 디자인 시스템 구축까지 폭넓게 다루며, 초보자부터 중급 사용자까지 폭넓게 수용할 수 있도록 구성된 책입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⸻&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;승완-책본문.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3CP9q/btsOUBYRn9Z/4V7CtidPC74EEhVkoK3cuK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3CP9q/btsOUBYRn9Z/4V7CtidPC74EEhVkoK3cuK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3CP9q/btsOUBYRn9Z/4V7CtidPC74EEhVkoK3cuK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3CP9q%2FbtsOUBYRn9Z%2F4V7CtidPC74EEhVkoK3cuK%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;승완-책본문.jpeg&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 목차 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책은 총 두 개의 Part로 구성되어 있습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Part 1. 피그마 활용하기&lt;br /&gt;기초적인 인터페이스와 설치부터 시작하여 그래픽 스타일, 레이아웃, 컴포넌트 활용, 오토레이아웃, 디자인 시스템, 프로토타이핑, 버전 관리까지 실무에서 바로 사용할 수 있는 피그마의 전반적인 기능을 체계적으로 소개합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Part 2. 피그마로 디자인하기&lt;br /&gt;iOS/안드로이드 앱, 반응형 웹, 디자인 시스템, 글로벌 NFT 마켓 등 다양한 실제 사례를 기반으로 프로젝트를 하나씩 따라 하며 배운 내용을 실습할 수 있도록 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부록 A. 개발 전달과 파일 관리&lt;br /&gt;디자인 애셋 내보내기, 데브 모드 사용, 브랜치 관리, 단축키 등 개발 협업과 생산성을 높이는 실용적인 팁들도 빠짐없이 정리되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⸻&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 서평&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피그마를 처음 접하거나 막 실무에 투입된 디자이너, 기획자, 개발자에게 이 책은 강력한 나침반 역할을 합니다. 이 책의 가장 큰 장점은 실무 중심의 시각입니다. 단순한 기능 설명에 그치지 않고, 각 기능이 실제 협업 과정에서 어떻게 활용되는지를 &amp;lsquo;디자인 노하우&amp;rsquo; 코너와 실습 예제를 통해 구체적으로 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &amp;lsquo;디자인 시스템&amp;rsquo;이나 &amp;lsquo;오토레이아웃&amp;rsquo;처럼 초보자들이 어렵게 느끼는 개념도 예제와 함께 차근차근 설명하여 이해를 돕습니다. 또한 각 파트에 흩어져 있는 [실습]과 [디자인 노하우] 박스는 현업에서 바로 써먹을 수 있는 팁들로 가득해 단순한 학습서를 넘어 워크플로우 개선서로서의 가치를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후반부에는 iOS 앱, 안드로이드 앱, 반응형 웹 등 실제 사례를 직접 따라 하며 디자인하며 자연스럽게 책에서 배운 내용을 복습하고 확장할 수 있도록 되어 있어, 학습의 흐름과 응용의 폭이 자연스럽게 이어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⸻&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 추천 대상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피그마를 처음 접하는 입문자: UI/UX에 관심 있는 디자이너 또는 기획자, 개발자&lt;br /&gt;피그마를 독학하며 막히는 부분이 많았던 사람: 실습과 함께 기능을 익히고 싶은 실무자&lt;br /&gt;디자인 시스템과 협업 기능에 대해 실무에서 고민하는 디자이너&lt;br /&gt;개발자와 협업 시 디자인 전달에 어려움을 겪는 팀 전체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⸻&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 발전된 디자인 협업 환경을 만들고 싶은 분들께 이 책은 많은 도움이 될 수 있을 것 같아요.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/364</guid>
      <comments>https://imnotadevleoper.tistory.com/364#entry364comment</comments>
      <pubDate>Fri, 27 Jun 2025 23:00:56 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 두 대의 컴퓨터를 위한 리액트</title>
      <link>https://imnotadevleoper.tistory.com/363</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;(이 글은 원문을 세 명의 개발자가 공동 번역한 글입니다. (&lt;a href=&quot;https://velog.io/@typo/posts&quot;&gt;@Saetbyeol Ahn&lt;/a&gt;, &lt;a href=&quot;https://ricki-lee.medium.com/&quot;&gt;@Siyeon Lee&lt;/a&gt;)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문 : &lt;a href=&quot;https://overreacted.io/react-for-two-computers/&quot;&gt;React for Two Computers&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 쓰려고 적어도 열두 번은 시도했습니다. 비유적으로 말하는 것이 아니라, 한때는 데스크톱 폴더에 버려진 초고가 수십 개나 쌓여 있었을 정도였습니다. 그 글들은 엄격한 것부터 혼란스러울 정도로 난해하고 참을 수 없을 정도로 메타적인 것까지 매우 다양한 스타일이었는데, 모두 갑자기 시작해서 스스로를 갉아먹다가 결국 아무 소용 없는 것으로 끝나곤 했죠. 하나하나 다 형편없어서 모두 버렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고 보니, 사실 저는 글을 쓰고 있었던 게 아니라 발표를 준비하고 있었던 것이었습니다. 지금 이 글을 꽤 오랫동안 쓰고 있을 때쯤 그 사실을 깨달았죠. 이런! 다행히 React Conf 주최측에서 짧은 시간에 새로운 발표를 할 수 있게 해 주셔서, 8개월 전에 발표를 마쳤습니다. 아래에서 &lt;code&gt;두 대의 컴퓨터를 위한 리액트&lt;/code&gt;를 시청할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- 블로그에서 이 영역에는 React for Two Computers 발표에 대한 유튜브 영상을 첨부할 예정입니다. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두가 좋아하는 주제인 리액트 서버 컴포넌트에 관한 것입니다. (아닐 수도 있습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 발표를 글 형태로 변환하는 것은 이제 포기했고, 그럴 수도 없을 것 같기도 합니다. 하지만 발표의 내용을 보완할 수 있는 몇 가지 메모를 적어두고 싶었습니다. 이 글은 발표를 이미 보셨다는 전제하에 쓰였습니다. 발표에 담기에 정리가 덜 되었던, 마무리를 짓지 못한 느슨한 생각들의 조각들을 모았습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제1막&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레시피 및 청사진&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그와 함수 호출 간의 차이점은 무엇일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 태그이고&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;p&amp;gt;Hello&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 함수 호출입니다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;alert(&quot;Hello&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 차이점은 &lt;code&gt;&amp;lt;&lt;/code&gt;와 &lt;code&gt;&amp;gt;&lt;/code&gt;는 딱딱하고 뾰족하며, &lt;code&gt;(&lt;/code&gt; 와 &lt;code&gt;)&lt;/code&gt;는 부드럽고 둥글다는 것입니다. 하지만 제가 얘기하려는 것은 그게 아닙니다. 이는 단지 시각적인 차이일 뿐입니다. 하지만 작동 방식, 의미, 우리가 기대하는 것에는 어떤 차이가 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 사용하는 언어를 지정하지 않으면 태그나 함수 호출에 특별한 의미가 없습니다. 예를 들어, 자바스크립트 함수 호출은 하스켈 함수 호출과 다르게 동작할 수 있고, HTML 태그는 콜드퓨전 태그와 다르게 동작할 수 있습니다. 그럼에도 불구하고, 우리는 널리 사용되는 언어에서 태그나 함수가 어떻게 작동하는지 잘 알고 있기 때문에, 태그나 함수 호출에서 기대하는 몇 가지 특징이 있습니다. 뾰족한 &lt;code&gt;&amp;lt;&lt;/code&gt; 및 &lt;code&gt;&amp;gt;&lt;/code&gt;은 부드러운 &lt;code&gt;(&lt;/code&gt; 및 &lt;code&gt;)&lt;/code&gt;과 마찬가지로 일련의 연관성과 직관을 전달합니다. 저는 이러한 직관을 파헤치고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;alert('Hello')&lt;/code&gt;와 &lt;code&gt;&amp;lt;p&amp;gt;Hello&amp;lt;/p&amp;gt;&lt;/code&gt;가 어떤 공통점이 있는지부터 살펴보겠습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;우리는 함수나 태그를 그 이름으로 지칭합니다.&lt;/b&gt; 관례상 함수 호출은 동사로 시작하는 경우가 많지만(&lt;code&gt;createElement&lt;/code&gt;, &lt;code&gt;printMoney&lt;/code&gt;, &lt;code&gt;querySelectorAll&lt;/code&gt;), 태그는 보통 명사(예: 단락의 경우 &lt;code&gt;p&lt;/code&gt;)로 이름을 지정합니다. 이것은 엄격한 규칙은 아니지만(&lt;code&gt;alert&lt;/code&gt;는 둘 다, &lt;code&gt;b&lt;/code&gt;는 굵게 표시됨), 대체로 사실입니다. (왜 그럴까요?)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함수나 태그에 정보를 전달할 수 있습니다.&lt;/b&gt; 앞서 태그와 함수에 텍스트(&lt;code&gt;'Hello'&lt;/code&gt;)를 전달했습니다. 하지만 단일 문자열만 전달할 수 있는 것은 아닙니다. 자바스크립트 함수 호출에서는 문자열, 숫자, 불리언, 객체 등 여러 인수를 전달할 수 있습니다. HTML 태그 내에서는 여러 속성을 전달할 수 있지만, 객체나 기타 풍부한 데이터를 값으로 전달할 수는 없으므로 상당히 제한적입니다. 다행히 JSX(및 많은 HTML 기반 템플릿 언어)의 태그를 사용하면 객체나 기타 풍부한 값을 전달할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함수 호출과 태그는 모두 깊게 중첩될 수 있습니다.&lt;/b&gt; 예를 들어, &lt;code&gt;alert('Hello, ' + prompt('Who are you?'))&lt;/code&gt;처럼 두 함수 호출 간의 관계를 표현하는 코드를 작성할 수 있습니다. 내부 &lt;code&gt;prompt&lt;/code&gt; 호출의 결과는 문자열과 결합되어 외부 &lt;code&gt;alert&lt;/code&gt; 호출에 전달됩니다(이것이 어떤 기능을 하는지 잘 모르겠다면 콘솔에서 사용해 보세요). 함수 호출에서는 중첩이 매우 일반적이지만, 태그에서는 중첩이 핵심입니다. 태그가 다른 태그에 둘러싸여 있지 않고 완전히 혼자 있는 경우는 거의 없습니다. (왜 그럴까요?)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명히 함수 호출과 태그는 매우 유사합니다. 함수는 명명된 대상에 정보를 전달할 수 있고, 필요한 경우 중첩을 통해 더 많은 정보를 전달하여 정교하게 만들 수 있습니다(필요한 만큼 중첩).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 이 둘의 근본적인 차이점에 대한 몇 가지 힌트를 얻기 시작했습니다. 우선 함수 호출은 동사인 반면 태그는 명사인 경향이 있습니다. 그리고 깊게 중첩된 태그는 자주 보지만, 깊게 중첩된 함수 호출은 상대적으로 드뭅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그럴까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 복잡하게 중첩된 구조를 표현할 때 굳이 태그를 사용하는 이유는, 모든 태그의 &lt;code&gt;&amp;lt;/end&amp;gt;&lt;/code&gt;를 눈으로 직접 확인할 수 있어서일지도 모릅니다. 어떤 괄호 &lt;code&gt;)&lt;/code&gt;가 닫는 괄호인지 일일이 추측하지 않아도 되니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그 자체가 깊은 중첩을 유도하기보다는, 오히려 우리가 깊은 중첩을 위해 태그를 선택합니다. (1~2년 동안은 거의 보편적으로 외면당했지만 결국 자바스크립트 커뮤니티가 얼마나 광범위하게 JSX를 채택했는지 기억하세요. 태그 중첩은 포기하기 어렵습니다!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중첩에 태그를 사용하는 것을 선호한다고 가정해 보겠습니다. 그런데 왜 태그는 동사가 아닌 명사가 되는 경향이 있을까요? 우연의 일치일까요, 아니면 더 깊은 이유가 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추측에 가깝지만 동사보다 명사가 분해하기 쉽기 때문이라고 생각합니다. 명사는 사물을 묘사하며, 사물은 다른 사물의 구성만으로 적절하게 설명할 수 있는 경우가 많습니다. 예를 들어 건물은 층으로, 층은 방으로, 방은 사람으로, 사람은 물로 이루어져 있습니다. 이 설명은 한 시점의 스냅숏(snapshot)을 묘사한다는 점에서 '시간에 구애받지 않는다'고 볼 수 있습니다. 영화의 한 프레임이나 설계도처럼, 시간이라는 요소를 빼고 생각하더라도 그 자체로 건물이나 구조에 대해 중요한 정보를 전달할 수 있기 때문에 충분히 유용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 동사는 시간이 지남에 따라 일어나는 과정을 설명하는 경향이 있습니다. 요리 레시피를 생각해 보세요: &amp;ldquo;프라이팬을 데우고 버터를 얹고 버터가 녹을 때까지 기다렸다가 이제 달걀을 부어주세요.&amp;rdquo; 여전히 조합할 수 있는 기회가 있지만(달걀을 어떻게 깨는가?), 여기서는 순서가 매우 중요합니다! 어떤 단계가 먼저 진행되고, 다음 단계는 무엇이며, 각 단계 사이에 어떤 결정을 내려야 하는지 끊임없이 인지하고 있어야 합니다. 청사진과 달리 레시피에는 순서가 있고, 어느 정도 긴박감도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이것이 태그 및 함수 호출과 어떤 관련이 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레시피는 순서대로 수행해야 하는 일련의 단계를 규정합니다. 동사로 구성되어 있지만 표현이 중첩되는 경우는 거의 없습니다. (사실 중첩은 순서를 모호하게 만들 수 있습니다.) 각 단계는 무언가를 변경하거나 이전 단계에 의존할 수 있으므로 레시피를 위에서 아래로 작성된 정확한 순서대로 실행하는 것이 중요합니다. 명령형 프로그램이라고도 하는 이러한 레시피는 함수 호출로 작성됩니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;const eggs = crackEggs();
heat(fryingPan);
put(fryingPan, butter);
await delay(30000);
put(fryingPan, eggs);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 청사진은 사물이 어떤 명사로 구성되어 있는지를 설명합니다. 특정 작업 순서를 규정하는 것이 아니라 전체가 어떻게 부분으로 나뉘는지를 설명할 뿐입니다. 그렇기 때문에 이를 '선언적 프로그램'이라고 하죠. 이러한 청사진은 자연스럽게 깊게 중첩되기 때문에, 태그를 사용하여 작성하는 것이 더 편리합니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;Building&amp;gt;
  &amp;lt;Roof /&amp;gt;
  &amp;lt;Floor&amp;gt;
    &amp;lt;Room /&amp;gt;
    &amp;lt;Room&amp;gt;
      &amp;lt;Person name=&quot;Alice&quot; /&amp;gt;
      &amp;lt;Person name=&quot;Bob&quot; /&amp;gt;
    &amp;lt;/Room&amp;gt;
  &amp;lt;/Floor&amp;gt;
  &amp;lt;Basement /&amp;gt;
&amp;lt;/Building&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 많은 프로그램들은 두 가지 기술을 결합합니다. 예를 들어, 일반적인 리액트 컴포넌트는 명령형 레시피(이벤트 핸들러의 함수 호출 시퀀스 등)와 선언형 청사진(반환된 JSX 태그 등)을 결합합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 궁극적으로 프로그램은 무언가를 수행해야 합니다. 레시피는 실행할 준비가 되어 있으므로 다음에 무엇을 수행해야 할지에 대한 모호함이 없습니다. 이 단계를 수행한 다음, 이 단계를 수행하고, 이 단계를 수행한 다음, 이 단계를 수행하면 완료됩니다. 반면, 청사진은 무언가를 구성하기 위한 세부적인 계획일 뿐입니다. 이 청사진이 실제로 작동하려면, 어떤 레시피가 그것을 보고 구성하겠다고 결정하고 실제 행동에 옮겨야만 합니다. (예를 들어, 리액트는 JSX 청사진에 기술된 DOM을 구성합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 의미에서 청사진은 레시피와 비슷하지만, 더 수동적이고 비활성적이며 향후 해석에 열려 있습니다. 청사진은 마치 시간이라는 요소만 빠진 레시피라고도 할 수 있습니다. 시간을 제외하면 사물, 명사, 태그와 같은 구조만 남게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청사진은 잠재적인 레시피입니다. 어떤 레시피가 결국 그 계획을 실행할지 여부에 따라 일어날 수도 있고 일어나지 않을 수도 있는 계획입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청사진은 태그로 이루어집니다. 레시피는 함수 호출로 이루어지죠. 만약 청사진이 &lt;i&gt;잠재적인&lt;/i&gt; 레시피라고 한다면, 태그는 &lt;i&gt;잠재적인&lt;/i&gt; 함수 호출이라고 할 수 있겠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠깐, 뭐라고요?&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Await 및 RPC&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 호출은 다음과 같이 쉽게 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;alert(&quot;Hello&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 함수의 실행이 끝나면 다음 줄이 즉시 실행될 것이라고 확신할 수 있습니다. 특히, 한 함수 호출의 결과를 바로 다음 함수 호출에 사용할 수 있다는 점이 매우 유용합니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;const name = prompt(&quot;Who are you?&quot;);
alert(&quot;Hello, &quot; + name);
console.log(&quot;Done.&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 호출하려는 함수가 다른 컴퓨터에 있다고 가정해 보세요. 정말 짜증 나겠죠? 하지만 실제로 그런 일이 일어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황을 처리하는 표준적인 방법은 일종의 네트워크 요청을 보내는 것일 겁니다. 이 분야에는 HTTP 같은 방식은 물론, 그보다 더 저수준의 방법들도 이미 많이 존재합니다. 우리 대부분은 바이트가 해저 케이블을 따라 어떻게 이동하는지조차 모른 채 평생을 개발자로 살아갑니다. 정말 놀라운 일이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 문제는 네트워크 호출이 끝날 때까지 프로그램을 이어서 진행할 수 없다는 것입니다. 다른 컴퓨터와 통신하지 않고는 상대방의 &lt;code&gt;name&lt;/code&gt;을 알 수 없다면, &lt;code&gt;alert&lt;/code&gt; 함수를 호출하기 전에 코드 실행을 &quot;일시 중지&quot;해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분이 이 문제를 처음 겪었다고 가정해 보세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 아이디어는 콜백 함수를 받는 &lt;code&gt;callNetwork&lt;/code&gt; API를 개발하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;callNetwork(&quot;https://another-computer/?fn=prompt&amp;amp;args=Who+are+you?&quot;, (response) =&amp;gt; {
  const name = response;
  alert(&quot;Hello, &quot; + name);
  console.log(&quot;Done.&quot;);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답이 도착하면 &lt;code&gt;callNetwork&lt;/code&gt; API는 &lt;code&gt;응답&lt;/code&gt;과 함께 전달된 콜백 함수를 호출하고, 나머지 코드가 실행되게 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 첫 번째 아이디어치고는 나쁘지 않습니다. 하지만 좋은 것도 아닙니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;네트워크 호출로 인해 코드의 흐름이 꼬였습니다.&lt;/b&gt; 이전에는 코드가 위에서 아래로 순차적으로 실행되었지만 이제 코드 실행 흐름에 변화가 생겼습니다. 개념적으로 &lt;code&gt;alert('Hello' + name)&lt;/code&gt; 은 우리가 전달하고자 하는 레시피에서 &amp;ldquo;다음에 일어날 일&amp;rdquo;입니다. 하지만 컴퓨터가 그 내용을 &quot;기다려야 할 작업&quot; 으로 인식할 수 있도록, 우리는 이 코드를 &lt;code&gt;callNetwork&lt;/code&gt; 호출 안에 집어 넣어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;두 코드 조각 사이의 연결이 끊어졌습니다.&lt;/b&gt; 일반적으로 함수를 호출하고 싶을 때는 함수를 호출하기만 하면 됩니다. 만약 같은 파일 안에 있다면 말이죠. 만약 함수가 다른 파일에 있다면 &lt;code&gt;export&lt;/code&gt;를 하고 여기에서는 &lt;code&gt;import&lt;/code&gt;를 합니다. 그러나 이 경우에는 더 이상 함수 호출이 아니라 HTTP 호출을 다루고 있습니다. 수십 년 동안 REST API를 다뤄온 사람이라면 이해하기 어려울 수도 있지만, 사실 이 전환 과정에서 본질적인 부분을 잃어버렸습니다. 우선, 타입 체크가 되지 않습니다! 해당 엔드포인트가 존재하지 않을 수도 있습니다. 해당 호출에 대해 IDE에서 코드 따라가기를 통해 함수가 어디에 정의되어 있고 어떤 기능을 하는지 확인할 수 없습니다. 호출되는 함수와 함수를 호출하는 위치 사이에는 직접적이고 강한 연결이 있었지만, 더 이상 존재하지 않습니다. 개념적으로 멋진 분리를 하고 싶어서가 아니라 그 연결을 유지할 수 있는 다른 수단이 없기 때문이죠.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;alert&lt;/code&gt; 기능도 다른 컴퓨터에 있다고 상상하면 문제가 더 쉽게 이해됩니다. 이제 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;callNetwork(&quot;https://another-computer/?fn=prompt&amp;amp;args=Who+are+you?&quot;, (response) =&amp;gt; {
  const name = response;
  callNetwork(&quot;https://yet-another-computer/?fn=alert&amp;amp;args=Hello,+&quot; + name, () =&amp;gt; {
    console.log(&quot;Done.&quot;);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이 두 가지 문제를 어떻게 해결할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 아이디어가 떠오릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 문제인 코드의 흐름이 꼬이는 문제를 해결하기 위해 &lt;code&gt;async&lt;/code&gt; 키워드라는 새로운 개념을 도입할 수 있습니다. &lt;code&gt;async&lt;/code&gt; 키워드를 붙인 함수는 호출되자마자 끝까지 실행된다고 보장되지 않으며, 오히려 네트워크 요청과 같은 작업으로 인해 실행 도중 일시적으로 중단(pause)될 수 있도록 의도된 함수입니다. 이러한 실행 방식에 따라, 호출하는 쪽에서는 &lt;code&gt;await&lt;/code&gt;을 사용해 그 사실을 명시적으로 인지하고 기다려야 하며, 그래야 실행 흐름이 예상치 못하게 끊기는 상황을 방지할 수 있습니다. 즉, 호출하는 함수 쪽도 결국 실행을 일시 중단할 수 있게 된다는 뜻이며, 따라서 해당 함수 역시 &lt;code&gt;async&lt;/code&gt;로 선언되어야 합니다. 이렇게 해서 &lt;code&gt;async&lt;/code&gt;와 &lt;code&gt;await&lt;/code&gt;은 호출 체계를 따라 위쪽으로 전파되며, 어느 누구도 실행 도중 코드가 일시적으로 중단되는 상황에 당황하지 않도록 합니다. 적어도 아이디어는 이렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 아이디어는 나쁘지 않습니다. 사실 어떤 식으로든 이러한 아이디어는 요즘 새 프로그래밍 언어에서는 기본 중의 기본입니다. 사람들에게 그게 좋은 것인지 &lt;a href=&quot;https://tirania.org/blog/archive/2013/Aug-15.html&quot;&gt;설득할 필요&lt;/a&gt;조차 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 코드가 다음과 같이 바뀝니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const name = await callNetwork(&quot;https://another-computer/fn=prompt&amp;amp;args=Who+are+you?&quot;);
await callNetwork(&quot;https://yet-another-computer/fn=alert&amp;amp;args=Hello,+&quot; + name);
console.log(&quot;Done.&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 두 번째 문제가 눈에 들어왔습니다. 한 컴퓨터에서는 &lt;code&gt;prompt&lt;/code&gt;라는 함수를 호출하고 다른 컴퓨터에서는 &lt;code&gt;alert&lt;/code&gt;라는 함수를 호출하려고 합니다. 이러한 함수가 실제로 코드베이스에 정의되어 있다고 가정해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 다른 컴퓨터에서 이 함수를 가져올 수 있다면 어떨까요?&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { prompt } from &quot;another-computer&quot;;
import { alert } from &quot;yet-another-computer&quot;;

const name = await prompt(&quot;Who are you?&quot;);
await alert(&quot;Hello, &quot; + name);
console.log(&quot;Done.&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠깐만요, 하지만 위에서 언급한 것처럼 실제로는 문제가 해결되지 않습니다. 예를 들어, 타입스크립트는 &lt;code&gt;another-computer&lt;/code&gt;가 무엇인지 알지 못합니다. 대신 코드베이스의 실제 위치에서 해당 함수를 가져올 수 있다고 가정해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { prompt, alert } from &quot;./stuff&quot;;

const name = await prompt(&quot;Who are you?&quot;);
await alert(&quot;Hello, &quot; + name);
console.log(&quot;Done.&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 잠깐만요, 그건 그냥 일반적인 import입니다. 이 컴퓨터의 프로그램으로 가져오는 것이지만, 여러분이 원한 것은 다른 컴퓨터에 배포하는 것이었습니다. 네트워크 경계를 넘어 HTTP를 통해 원격으로 호출되도록 하려면, 그 사실을 코드 어딘가에 표현해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 표현할 수 있는 특별한 구문을 만들어 봅시다. 이 구문은 나중에 수정할 수 있지만, 일단 지금은 수십 년 동안 &lt;a href=&quot;https://en.wikipedia.org/wiki/Remote_procedure_call&quot;&gt;RPC&lt;/a&gt; 또는 &quot;원격 프로시저 호출&quot;로 알려져 온 바에 따라 &lt;code&gt;import rpc&lt;/code&gt;라고 부르겠습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import rpc { prompt, alert } from './stuff';

const name = await prompt('Who are you?');
await alert('Hello, ' + name);
console.log('Done.');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 타입스크립트가 이러한 함수를 클릭할 수 있을 뿐만 아니라 이 함수가 원격 경계 뒤에 있다는 것을 인식하여, 강제로 &lt;code&gt;async&lt;/code&gt;로 선언하고, 입력과 출력의 유형을 직렬화할 수 있도록(따라서 실제로 네트워크를 통해 이동할 수 있도록) 보장한다고 상상해 보십시오.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; 및 &lt;code&gt;import rpc&lt;/code&gt;, 이 정도면 하루의 발명품으로 충분합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니면?..&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;혹시 전화 줄래? (Call Me Maybe)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동료가 문제를 가지고 찾아옵니다. &quot;&lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; 및 &lt;code&gt;import rpc&lt;/code&gt;는 정말 훌륭했어요. 그런데 만약, 상대 컴퓨터가 아예 &lt;i&gt;응답을 주지 않는&lt;/i&gt; 구조라면 어떻게 하죠? 즉, 호출이 성공했는지 실패했는지도 전혀 알 수 없는 상황이라면요? 그런 환경에서 서로 의존하는 함수 호출들을 어떻게 조합하고 표현할 수 있을까요?&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 말도 안 되는 질문처럼 들리지만 잠시 곰곰이 생각해 보세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 컴퓨터가 응답하지 않으면... 당연히 언제 완료되었는지 알 수 없으므로 &lt;code&gt;await&lt;/code&gt;로 일시 중지해도 소용없습니다. 따라서 다음과 같이 할 수 없습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;await alert(&quot;Hello, &quot; + name);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나쁜 점은, 함수가 있는 컴퓨터가 응답하지 않으면 함수 호출의 &lt;i&gt;결과&lt;/i&gt;도 얻을 수 없으므로 다음 역시 동작할 수 없다는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;const name = await prompt(&quot;Who are you?&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가망 없다고 보일 수도 있지만, 다시 비판적으로 살펴 보죠. 비록 다시 정보를 받을 수는 없지만, 최소한 상대방 컴퓨터에게 &lt;i&gt;전달할&lt;/i&gt; 수는 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 다음은 정보를 상대방 컴퓨터에게 전달하기만 합니다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;alert(&quot;Hello&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대방 컴퓨터가 응답하지 않더라도 여기서는 &lt;code&gt;'Hello'&lt;/code&gt; 문자열로 &lt;code&gt;alert&lt;/code&gt; 함수를 호출하도록 요청하는 것뿐입니다. 어떤 응답도 요구하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 호출은 가능해야 합니다! 다만... 일반 함수 호출처럼 작동하지 않으므로 일반 함수 호출과 동일한 구문을 사용하는 것은 잘못된 것 같습니다. 일반적으로는 함수 호출이 &lt;i&gt;완료&lt;/i&gt;된 후 다음 코드가 실행될 것으로 예상할 수 있지만, 여기서는 이를 보장할 수 없습니다. 실제로 호출이 성공할지 전혀 확신할 수 없으며, 네트워크 때문에 중간에 실패할 경우 이를 알 수 있는 방법이 없습니다. RPC와 달리 네트워크 오류에 대한 알림을 받지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 함수 호출이 아닙니다. 이것은... &lt;i&gt;잠재적인&lt;/i&gt; 함수 호출입니다. 미래에 일어날 수도 있고 일어나지 않을 수도 있는 호출입니다. 함수 호출의 청사진이라고 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 &quot;잠재적 호출&quot;을 위해 몇 가지 가상의 구문을 만들어 봅시다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;alert⧼'Hello'⧽;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 이 구문을 변경할 수도 있습니다. 하지만 지금은 이 구문의 의미, 즉 우리가 실제로 무엇을 하고 싶은지를 생각해 봅시다. 설계 과정에서 &amp;ldquo;상대방 컴퓨터가 응답할 수 없다&amp;rdquo;는 제한 사항부터 시작하여, 이러한 &quot;잠재적 호출&quot;의 의미에 불가피한 제약이 있는지 살펴보는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명히 이러한 &quot;잠재적 호출&quot;은 실행을 중단하지 않으며 나머지 코드에 영향을 미치지 않습니다. 기다릴 것이 없기 때문에 아무것도 &quot;기다리지&quot; 않습니다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;alert⧼'Hello'⧽;      //  성공/실패 여부를 알 수 없음
console.log('Done.') //  즉시 실행됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이런 의문이 생깁니다. 이러한 '잠재적 호출'은 무엇을 반환해야 할까요?&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;const name = prompt⧼'Who are you?'⧽;
console.log(name); // ???&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명히 &lt;code&gt;prompt⧼'Who are you?'⧽&lt;/code&gt; 는 상대 컴퓨터가 응답할 수 없기 때문에 &lt;code&gt;prompt&lt;/code&gt; 호출의 최종 실제 반환 값을 반환할 수 없습니다. 이 구문이 항상 &lt;code&gt;정의되지 않은 값(undefined)&lt;/code&gt;을 반환하도록 결정할 수도 있지만 이는 다소 제한적으로 느껴집니다. &quot;잠재적 호출&quot;이라는 &lt;code&gt;prompt&lt;/code&gt;와 &quot;잠재적 호출&quot;이라는 &lt;code&gt;alert&lt;/code&gt; 사이를 조율할 방법이 없으니까요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 달성하고자 하는 것은 바로 이런 것입니다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;const name = prompt⧼'Who are you?'⧽;
alert⧼'Hello, ' + name⧽;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는, 위의 코드가 성립하지 않는다는 점입니다. 왜냐하면 &lt;code&gt;prompt&lt;/code&gt;라는 &amp;ldquo;잠재적 호출&amp;rdquo;로부터는 아무런 값을 얻어올 수 없기 때문입니다. 따라서 &lt;code&gt;name&lt;/code&gt;이라는 변수에 값을 할당할 수도 없고, 이 컴퓨터에서는 그 반환값에 &lt;code&gt;+&lt;/code&gt; 연산을 적용할 수도 없습니다. 하지만 한 가지 아이디어가 있습니다. 아예 위의 두 줄을 전부 &amp;ldquo;잠재적 호출&amp;rdquo;&lt;i&gt;만으로&lt;/i&gt; 다시 표현해보는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;alert⧼
  concat⧼
    'Hello, ',
    prompt⧼'Who are you?'⧽
  ⧽
⧽;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(앞으로는 &lt;code&gt;concat&lt;/code&gt;이 &lt;code&gt;(a, b) =&amp;gt; a + b&lt;/code&gt;로 설정된 전역 함수라고 가정합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 코드를 재구성하면 두 가지 이점이 있습니다. 첫째, 어차피 값을 알 수 없는 &lt;code&gt;name&lt;/code&gt; 같은 무의미한 변수를 선언하지 않아도 됩니다. 그 &lt;code&gt;prompt&lt;/code&gt;같은 함수는 다른 컴퓨터에서 실행될 예정이기 때문에, 이쪽에서 그 결과인 &lt;code&gt;name&lt;/code&gt;값은 어차피 알 수 없습니다. 둘째, 이렇게 중첩된 &amp;ldquo;잠재적 호출&amp;rdquo;을 하나의 표현식으로 다룰 수 있게 해줍니다. 그리고 그 표현식을 JSON으로 쉽게 직렬화할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;{
  fn: 'alert',
  args: [{
    fn: 'concat',
    args: ['Hello, ', {
      fn: 'prompt',
      args: ['Who are you?']
    }]
  }]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 만들어진 JSON을 응답할 수 없는 다른 컴퓨터에 전달할 수 있고, JSON을 받은 컴퓨터는 우리가 보낸 지시를 다음과 같은 함수로 해석해 실행하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function interpret(json) {
  if (json &amp;amp;&amp;amp; json.fn) {
    // 함수 이름으로 전역 함수를 찾습니다
    let fn = window[json.fn];
    // 인자 안에 중첩된 호출이 있다면 그것도 해석합니다
    let args = json.args.map((arg) =&amp;gt; interpret(arg));
    // 실제로 함수를 호출합니다
    let result = fn(...args);
    // 반환값에 또 다른 호출이 포함되어 있다면 그것도 처리합니다
    return interpret(result);
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 JSON 객체를 &lt;code&gt;interpret()&lt;/code&gt;에 전달하면 원래 코드와 동일한 작업이 수행되는지 콘솔에서 확인할 수 있습니다. (&lt;code&gt;concat&lt;/code&gt;을 전역으로 정의하는 것을 잊지 마세요!).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해, 이 접근 방식은 작동합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구문을 다시 한번 살펴봅시다.&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;  concat⧼
    'Hello, ',
    prompt⧼'Who are you?'⧽
  ⧽
⧽;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;prompt&lt;/code&gt;와 &lt;code&gt;alert&lt;/code&gt; 사이의 &quot;잠재적 호출&quot;과 같은 의존관계는, 이 호출들을 서로 중첩시켜 표현해야 한다는 사실을 알게 되었습니다. 이들 사이에 일반적인 코드 형태를 끼워 넣을 수는 없습니다. 왜냐하면 이 코드는 직렬화되어 다른 컴퓨터에서 해석되고 실행될 예정이기 때문입니다. 따라서 이 호출들은 코드 형태라기보다는&amp;hellip; 선언적인 마크업(markup) 표현에 가깝다고 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출을 구성하는 다른 방법이 없기 때문에 중첩 수준이 깊을 것으로 예상됩니다. 따라서 구문을 좀 더 쉽게 읽을 수 있도록 만드는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;alert&amp;gt;
  &amp;lt;concat&amp;gt;
    Hello,
    &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
  &amp;lt;/concat&amp;gt;
&amp;lt;/alert&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특이한 점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 함수 호출의 경우 반환 값은 호출한 함수에 의해 결정됩니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;const result = prompt(&quot;Who are you?&quot;);
console.log(result); // 'Dan'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 &quot;잠재적&quot; 함수 호출의 경우 반환 값은 &lt;i&gt;데이터로서의 호출 자체&lt;/i&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const inner = &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;;
// { fn: 'prompt', args: ['Who are you?'] }

const outer = &amp;lt;concat&amp;gt;Hello, {inner}&amp;lt;/concat&amp;gt;;
// {
//   fn: 'concat',
//   args: ['Hello', { fn: 'prompt', args: ['Who are you?'] }]
// }

const outest = &amp;lt;alert&amp;gt;{outer}&amp;lt;/alert&amp;gt;;
// {
//   fn: 'alert',
//   args: [{
//     fn: 'concat',
//     args: ['Hello', { fn: 'prompt', args: ['Who are you?'] }]
//   }]
// }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 호출은 이루어지지 않았으며, 호출에 대한 &lt;i&gt;청사진&lt;/i&gt;만 작성하고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;alert&amp;gt;
  &amp;lt;concat&amp;gt;
    Hello,
    &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
  &amp;lt;/concat&amp;gt;
&amp;lt;/alert&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 &quot;잠재적 호출&quot;의 청사진은 코드처럼 보이지만 데이터처럼 작동합니다. 구조적으로 함수 호출과 유사하지만, 더 수동적이고 비활성적이며 해석의 여지가 있습니다. 아직 이 청사진을 실제로 해석할 다른 컴퓨터로 보내지는 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌든 &quot;잠재적 함수 호출&quot;을 너무 많이 쓰다 보니 신경이 쓰입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 태그라고 부르기로 하죠.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 분할 (Splitting a Function)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 함수가 있습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  alert(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행하면 일반적인 함수가 그러하듯, 한 번에 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행을 두 부분으로 나누고 싶다고 가정해 봅시다. 첫 번째 부분은 즉시 실행됩니다. 두 번째 부분은 호출자가 결정할 때 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 이를 수행하는 쉬운 방법입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return function resume() {
    alert(&quot;Hello, &quot; + name);
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 함수를 두 부분으로 실행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;const resume = greeting(); // 첫 번째 부분을 실행
resume(); // 두 번째 부분을 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다른 컴퓨터에서 두 번째 부분을 실행하려고 한다고 가정해 보겠습니다. 여전히 하나의 계산으로 생각하고 있습니다. 단지 물리적으로 분산되어 있을 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;쉬워요!&quot;라고 말합니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return `function resume() {
    alert('Hello, ' + name);
  }`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠깐, 뭐라고요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수의 나머지 코드를 다른 컴퓨터로 전송하여 계산을 완료할 수 있도록 반환하는 것이군요. 하지만 잠깐만요! 다른 컴퓨터의 입장에서는 &lt;code&gt;name&lt;/code&gt;이 정의되지 않았기 때문에 작동하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;function resume() {
  alert(&quot;Hello, &quot; + name); //   ReferenceError: name is not defined
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당신은 &amp;ldquo;문제없습니다.&quot;라고 말합니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return `function resume() {
    alert('Hello, ' + ${JSON.stringify(name)});
  }`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 바뀌었는지 알겠습니다. 여러분은 첫 번째 컴퓨터에서 얻은 &lt;code&gt;name&lt;/code&gt; 값을 다른 컴퓨터로 보낸 코드에 직접 포함시켰습니다. 그 컴퓨터의 관점에서 보면 &lt;code&gt;name&lt;/code&gt;이 마치 항상 있었던 것처럼 미리 계산된 것처럼 보일 것입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function resume() {
  alert(&quot;Hello, &quot; + &quot;Dan&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 기능은 더 큰 그림의 일부라는 사실을 전혀 모를 것입니다. 이 함수의 관점에서 보면 세상은 두 번째 컴퓨터에서 시작됩니다. 이 기능이 더 복잡해지면 이 기능이 전부라고 생각하기 시작할 수도 있습니다. 괜찮습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여러분은 전체를 &lt;i&gt;보셨으니까요&lt;/i&gt;.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return `function resume() {
    alert('Hello, ' + ${JSON.stringify(name)});
  }`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 흥미로운 형태인데, 프로그램이 네트워크를 통해 전송할 수 있는 형태로 나머지 부분을 반환하여 다른 컴퓨터에서 계속 실행할 수 있도록 합니다. 이를 네트워크를 통한 클로저라고 부를 수 있습니다. 작동 방식에 대해 몇 가지 알아두세요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터는 엄격하게 첫 번째 컴퓨터에서 두 번째 컴퓨터로 한 방향으로만 흐릅니다.&lt;/b&gt; 두 번째 컴퓨터는 첫 번째 컴퓨터의 값을 볼 수 있습니다(텍스트로 변환할 수 있는 한). 하지만 첫 번째 부분은 두 번째 부분에 대해 아무것도 알지 못합니다. 첫 번째 파트는 대본을 작성하고 두 번째 파트는 무대에서 대본을 공연합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;첫 번째 부분과 두 번째 부분은 완전히 분리되어 있습니다.&lt;/b&gt; 단일 개념 프로그램의 일부이긴 하지만 별도의 런타임 환경입니다. 시간과 공간으로 분리되어 있기 때문에 런타임에 서로 조율할 수 없습니다. 모듈 시스템은 서로 완전히 분리되어 있고, 각각 고유한 전역이 있으며, 심지어 다른 자바스크립트 엔진에서 실행될 수도 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;각 파트 사이의 경계는 확고하면서도 유동적입니다.&lt;/b&gt; 경계가 확고한 이유는 두 환경이 완전히 분리되어 있기 때문이며, 닫혀 있는 것 외에는 공유되는 것이 없기 때문입니다. 그러나 경계가 유동적인 이유는 두 세계 사이를 이동할 수 있기 때문입니다. 어느 쪽에서 어떤 줄을 실행할지, 두 번째 컴퓨터에서 더 많은 코드를 실행할지, 이미 미리 계산된 데이터를 전달할지 등을 선택할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 요점은 좀 더 자세히 설명할 필요가 있습니다. &lt;code&gt;1&lt;/code&gt;에서 &lt;code&gt;n&lt;/code&gt;까지의 숫자에 대한 알림을 표시하여 숫자가 3으로 나뉘면 &lt;code&gt;'Fizz'&lt;/code&gt;, 5로 나뉘면 &lt;code&gt;Buzz&lt;/code&gt;, 둘 다로 나뉘면 &lt;code&gt;FizzBuzz&lt;/code&gt;라고 경고하고 싶다고 가정해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;function fizzBuzz() {
  const n = Number(prompt(&quot;How many?&quot;));
  for (let i = 1; i &amp;lt;= n; i++) {
    if (i % 3 === 0 &amp;amp;&amp;amp; i % 5 === 0) {
      alert(&quot;FizzBuzz&quot;);
    } else if (i % 3 === 0) {
      alert(&quot;Fizz&quot;);
    } else if (i % 5 === 0) {
      alert(&quot;Buzz&quot;);
    } else {
      alert(i);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이것이 두 대의 컴퓨터용 프로그램이라고 가정해 보겠습니다. 여러 가지 방법으로 분할할 수 있습니다. 예를 들어 두 번째 컴퓨터에서 모든 작업을 수행하도록 선택할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;function fizzBuzz() {
  return `function resume() {
    const n = Number(prompt('How many?'));
    for (let i = 1; i &amp;lt;= n; i++) {
      if (i % 3 === 0 &amp;amp;&amp;amp; i % 5 === 0) {
        alert('FizzBuzz');
      } else if (i % 3 === 0) {
        alert('Fizz');
      } else if (i % 5 === 0) {
        alert('Buzz');
      } else {
        alert(i);
      }
    }
  }`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 첫 번째 컴퓨터에서 &lt;code&gt;prompt&lt;/code&gt;를 실행하고 싶을 수도 있습니다. &lt;code&gt;prompt&lt;/code&gt; 호출을 앞부분으로 이동한 다음 두 번째 부분으로 데이터로 &lt;code&gt;n&lt;/code&gt;을 전달할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;function fizzBuzz() {
  const n = Number(prompt(&quot;How many?&quot;));
  return `function resume() {
    const n = ${JSON.stringify(n)};
    for (let i = 1; i &amp;lt;= n; i++) {
      if (i % 3 === 0 &amp;amp;&amp;amp; i % 5 === 0) {
        alert('FizzBuzz');
      } else if (i % 3 === 0) {
        alert('Fizz');
      } else if (i % 5 === 0) {
        alert('Buzz');
      } else {
        alert(i);
      }
    }
  }`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 컴퓨터의 관점에서 보면 &lt;code&gt;n&lt;/code&gt;은 하드코딩된 것처럼 보일 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 첫 번째 컴퓨터에서 모든 메시지를 미리 계산할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;function fizzBuzz() {
  const n = Number(prompt(&quot;How many?&quot;));
  const messages = [];
  for (let i = 1; i &amp;lt;= n; i++) {
    if (i % 3 === 0 &amp;amp;&amp;amp; i % 5 === 0) {
      messages.push(&quot;FizzBuzz&quot;);
    } else if (i % 3 === 0) {
      messages.push(&quot;Fizz&quot;);
    } else if (i % 5 === 0) {
      messages.push(&quot;Buzz&quot;);
    } else {
      messages.push(i);
    }
  }
  return `function resume() {
    const messages = ${JSON.stringify(messages)};
    messages.forEach(alert);
  }`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 두 번째 컴퓨터의 입장에서는 메시지를 반복하는 것 외에는 할 수 있는 계산이 남지 않습니다. 예를 들어 &lt;code&gt;16&lt;/code&gt;을 &lt;code&gt;n&lt;/code&gt;으로 선택하면 두 번째 컴퓨터의 관점에서 전체 프로그램은 다음과 같이 보입니다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;function resume() {
  const messages = [1, 2, &quot;Fizz&quot;, 4, &quot;Buzz&quot;, &quot;Fizz&quot;, 7, 8, &quot;Fizz&quot;, &quot;Buzz&quot;, 11, &quot;Fizz&quot;, 13, 14, &quot;FizzBuzz&quot;, 16];
  messages.forEach(alert);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;messages&lt;/code&gt;를 미리 계산하는 것의 단점은 전송할 데이터의 크기가 &lt;code&gt;n&lt;/code&gt;이 커질수록 커진다는 것입니다. 피즈버즈 알고리즘은 간단하기 때문에 &lt;code&gt;n&lt;/code&gt; 자체를 전송하고 두 번째 컴퓨터가 피즈버즈 자체를 실행하도록 하는 것이 더 현명합니다. 중요한 부분은 데이터 전달과 코드 실행 사이의 절충점을 선택할 수 있다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 원래의 예제로 돌아가 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램을 두 대의 컴퓨터로 분할하면 연산을 유연하게 이동할 수 있다는 개념적 요점을 설명했습니다. 하지만 실제로는 코드의 절반을 문자열 안에 작성하고 싶지 않을 것입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return `function resume() {
    alert('Hello, ' + ${JSON.stringify(name)});
  }`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 다른 파일에 &lt;code&gt;resume&lt;/code&gt;를 작성하여 &lt;i&gt;가져오는&lt;/i&gt; 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { resume } from &quot;./stuff&quot;;

function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return resume(name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠깐만요, &lt;code&gt;resume&lt;/code&gt;는 일반적인 가져오기가 아니라 &lt;i&gt;이 함수의 코드를 다른 컴퓨터로 보내야 합니다!&lt;/i&gt; 따라서 지금 이 컴퓨터에서 함수 자체를 가져오거나 코드를 실행하는 것이 아니라 해당 함수를 &lt;i&gt;참조하고&lt;/i&gt; 싶을 것입니다. 이 경우 &lt;code&gt;import rpc&lt;/code&gt;를 만든 RPC를 떠올릴 수 있습니다. 다른 컴퓨터로 전송할 함수를 표시하는 또 다른 유사한 주석을 만들어 봅시다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import tag { resume } from './stuff';

function greeting() {
  const name = prompt('Who are you?');
  return resume(name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tag&lt;/code&gt;를 &lt;code&gt;import&lt;/code&gt;하는 이유는 무엇일까요? 이 기능은 &quot;응답&quot;하지 않는 컴퓨터에 있으므로 호출할 수 없습니다. 기껏해야 &quot;잠재적 호출&quot;, 즉 태그만 할 수 있습니다!&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import tag { resume } from './stuff';

function greeting() {
  const name = prompt('Who are you?');
  return &amp;lt;resume name={name} /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;code&gt;import rpc&lt;/code&gt; 및 &lt;code&gt;import tag&lt;/code&gt; 구문은 나중에 다시 살펴보고 수정하겠습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 분할된 프로그램을 흔히 클라이언트-서버 애플리케이션이라고 합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import tag { Client } from './stuff';

function Server() {
  const data = precomputeData();
  return &amp;lt;Client data={data} /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와 서버를 서로 통신하는 두 개의 개별 프로그램으로 보고 싶을 수 있습니다. 하지만 이제는 네트워크를 통해 나머지를 시공간을 넘나들며 보내는 하나의 기능이라는 것을 알고 계실 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 점을&lt;/i&gt; 잊지 마세요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;양쪽의 태그 (Tags on Both Sides)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 섹션 전에 태그를 발명했습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function greeting() {
  return (
    &amp;lt;alert&amp;gt;
      &amp;lt;concat&amp;gt;
        Hello,
        &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
      &amp;lt;/concat&amp;gt;
    &amp;lt;/alert&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해, 태그는 함수 호출과 매우 유사하지만 실제로는 아무것도 호출하지 않고 호출의 구조를 반영할 뿐입니다. 그렇기 때문에 태그는 지금 당장, 그리고 여기서는 아닐 수도 있지만 실행하고 싶은 계산을 표현하는 완벽한 방법입니다. 태그는 계산의 계획, 즉 청사진을 나타냅니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;function greeting() {
  return {
    fn: &quot;alert&quot;,
    args: [
      {
        fn: &quot;concat&quot;,
        args: [
          &quot;Hello, &quot;,
          {
            fn: &quot;prompt&quot;,
            args: [&quot;Who are you?&quot;],
          },
        ],
      },
    ],
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그 자체만으로는 아무것도 할 수 없습니다. 일부 코드는 태그가 말하는 내용을 실제로 &lt;i&gt;해석해야&lt;/i&gt; 합니다. 위의 예제에서 확인한 한 가지 방법은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function interpret(json) {
  if (json &amp;amp;&amp;amp; json.fn) {
    let fn = window[json.fn];
    let args = json.args.map((arg) =&amp;gt; interpret(arg));
    let result = fn(...args);
    return interpret(result);
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-part1-forked-h73wkn?file=%2Fsrc%2Findex.html&quot;&gt;코드를 실행하여&lt;/a&gt; &lt;code&gt;interpret(greeting())&lt;/code&gt;이 예상 결과를 생성하는지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 해석은 주관적이라는 점이 문제입니다. 무언가에 대한 해석은 여러 가지가 있을 수 있습니다. 이것이 바로 해석의 핵심입니다. 그런 종류의 유연성을 허용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 예제에서 &lt;code&gt;interpret&lt;/code&gt; 함수는 전역 &lt;code&gt;window&lt;/code&gt; 범위에서 각 태그를 직접 구현하는 함수를 찾고 있었습니다. 그래서 &lt;code&gt;window.alert&lt;/code&gt;와 &lt;code&gt;window.prompt&lt;/code&gt; 등을 찾을 수 있었습니다. 이제 약간 다른 버전의 &lt;code&gt;interpret&lt;/code&gt;을 만들어 보겠습니다. 이 버전은 이러한 함수와 함께 명시적인 &lt;code&gt;knownTags&lt;/code&gt; 딕셔너리를 사용합니다. 알 수 없는 태그는 건너뜁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같아요.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;function interpret(json, knownTags) {
  if (json &amp;amp;&amp;amp; json.fn) {
    if (knownTags[json.fn]) {
      let fn = knownTags[json.fn];
      let args = json.args.map((arg) =&amp;gt; interpret(arg, knownTags));
      let result = fn(...args);
      return interpret(result, knownTags);
    } else {
      let args = json.args.map((arg) =&amp;gt; interpret(arg, knownTags));
      return { fn: json.fn, args };
    }
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;interpret&lt;/code&gt;에 빈 &lt;code&gt;knownTags&lt;/code&gt;를 전달하면 원래 호출 트리를 얻을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;interpret(greeting(), {});

// {
//   fn: 'alert',
//   args: [{
//     fn: 'concat',
//     args: ['Hello, ', {
//       fn: 'prompt',
//       args: ['Who are you?']
//     }]
//   }]
// };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 &lt;code&gt;{ prompt: window.prompt }&lt;/code&gt;를 전달하면 &lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-part1-forked-2cx44l&quot;&gt;어떻게 되는지 주목하세요&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;interpret(greeting(), {
  prompt: window.prompt,
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이름을 먼저 묻고 (&lt;code&gt;prompt&lt;/code&gt; 실행됨) 이 트리를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;// {
//   fn: 'alert',
//   args: [{
//     fn: 'concat',
//     args: ['Hello, ', 'Dan' /* (or whatever you typed) */]
//   }]
// };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여전히 호출 트리가 다시 표시되지만 이번에는 &lt;code&gt;prompt&lt;/code&gt;가 &quot;분해&quot;되었습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험 삼아 &lt;code&gt;prompt&lt;/code&gt;와 &lt;code&gt;concat&lt;/code&gt;(&lt;code&gt;alert&lt;/code&gt;는 제외)을 모두 &quot;분해&quot;해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;interpret(greeting(), {
  prompt: window.prompt,
  concat: (a, b) =&amp;gt; a + b,
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 이전과 마찬가지로 &lt;code&gt;prompt&lt;/code&gt;가 실행되지만 &lt;code&gt;alert&lt;/code&gt; 호출을 위해 준비된 메시지가 이미 &lt;code&gt;연결되지&lt;/code&gt; 않은 상태로 표시됩니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;// {
//   fn: 'alert',
//   args: ['Hello, Dan']
// };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;alert&lt;/code&gt; 호출 자체를 제외한 모든 것을 &lt;i&gt;미리 계산했습니다&lt;/i&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 이전처럼 &lt;code&gt;alert&lt;/code&gt;, &lt;code&gt;prompt&lt;/code&gt; 및 &lt;code&gt;concat&lt;/code&gt;을 모두 &quot;분해&quot;해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;interpret(greeting(), {
  alert: window.alert,
  prompt: window.prompt,
  concat: (a, b) =&amp;gt; a + b,
});

// undefined&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 모든 단계가 실행되므로 더 이상 할 일이 남지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그의 청사진은 특정 연산 순서를 규정하지 않고 구조만 규정하기 때문에 그 순서를 자유롭게 조작할 수 있게 되었습니다. 예를 들어, 이제 하나의 연산을 여러 단계로 나눌 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const step1 = greeting();
// {
//   fn: 'alert',
//   args: [{
//     fn: 'concat',
//     args: ['Hello, ', {
//       fn: 'prompt',
//       args: ['Who are you?']
//     }]
//   }]
// };

const step2 = interpret(step1, {
  prompt: window.prompt,
  concat: (a, b) =&amp;gt; a + b,
});
// {
//   fn: 'alert',
//   args: ['Hello, Dan']
// };

interpret(step2, {
  alert: window.alert,
});
// undefined&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-part1-forked-wfvxhp?file=%2Fsrc%2Findex.mjs&quot;&gt;코드를 실행해보세요.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 아이디어를 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;step1&lt;/code&gt;과 &lt;code&gt;step2&lt;/code&gt;를 서로 다른 컴퓨터에서 실행하면 어떨까요? 즉, 첫 번째 컴퓨터에서 일부 태그를 먼저 해석하거나 &quot;분해&quot;한 다음 나머지는 나중에 두 번째 컴퓨터에서 해석하거나 &quot;분해&quot;하도록 보내면 어떨까요? 일부 태그가 두 컴퓨터 중 어느 쪽에서 해석하는 것이 더 적합한 경우(예: 두 컴퓨터의 기능이 서로 다른 경우) 이 방법이 유용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물의 상태 변화를 생각해 보세요. 먼저 산 정상에서 얼음이 녹아 물로 변합니다. 그런 다음 강이 흘러내립니다. 마지막으로 물이 증발합니다. 태그도 마찬가지입니다. 일부 태그는 첫 번째 컴퓨터에서 일찍 녹아내릴 수 있습니다. 나머지 태그는 네트워크를 통해 다른 컴퓨터로 이동하여 그곳에서 운명을 맞이할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2대의 컴퓨터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당신의 이론은 이해하기 어렵고 때로는 말도 안 된다고 생각하지만 그 큰 윤곽이 드러나기 시작했습니다. 지금까지의 과정을 요약해 달라고 한다면 이렇게 말할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 프로그램은 여러 컴퓨터에 걸쳐 분산 계산을 수행합니다. 특히 일부 프로그램은 두 대의 컴퓨터에 걸쳐 있는 함수로 표현될 수 있습니다(원칙적으로는 더 많을 수도 있지만). 이러한 함수 중 일부는 첫 번째 컴퓨터에서 일부 계산을 수행한 다음 나머지 코드를 두 번째 컴퓨터로 전송하여 나머지 계산을 &quot;핸드오프&quot;하는 특정 형태를 갖습니다. 이것이 바로 여러분의 이론이 집중하는 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 기계의 환경에 이름을 붙여 보겠습니다. 여러분의 프로그램은 &lt;i&gt;초기&lt;/i&gt; 세계, 즉 첫 번째 머신에서 시작됩니다. 일부 작업은 여기서 진행됩니다. 그런 다음 나머지 작업은 두 번째 기계인 후기 세계로 넘겨집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 세계와 후기 세계는 시간과 공간으로 완전히 분리된 두 개의 런타임 환경이므로 상태나 전역 변수를 공유하지 않습니다. 초기 세계는 후기 세계를 위해 일부 잔여 정보, 특히 실행할 남은 코드와 실행에 필요한 데이터를 남길 수 있지만 그 이상은 남기지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 세계와 후기 세계는 서로의 코드를 직접 &lt;code&gt;가져오지&lt;/code&gt; 않습니다. 왜냐하면 해당 코드를 import 하는 세계로 가져올 뿐이기 때문입니다. 하지만 서로의 코드를 참조하는 역할을 합니다. &lt;code&gt;import tag&lt;/code&gt;와 &lt;code&gt;import rpc&lt;/code&gt;는 모두 다른 컴퓨터의 코드를 참조하여(타입적으로 안전한 방식으로!) 실제로 가져오는 세계에 로드하지 않고도 유용한 작업을 수행하는 예입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘은 확고하게 분리되어 있기 때문에 초기 세계의 함수는 후기 세계의 함수를 호출할 수 없습니다. 결국 함수 호출은 호출자에게 정보를 다시 전달하기 위한 것이지만, 호출자가 양동이를 오래 걷어차 버린 경우에는 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 초기 세계에서 후기 세계로 정보를 전달하는 것은 여전히 의미가 있습니다. 이를 허용하기 위해 함수 호출보다 약한 개념인 태그를 발명했습니다. 태그는 함수 호출과 비슷하지만 수동적이고 비활성이며 해석의 여지가 있습니다. 태그는 구체화되기를 기다리는 잠재적 함수 호출입니다. 태그는 데이터로서의 함수 호출로, 지금 또는 더 나은 시점에 실행될 준비가 되어 있거나 전혀 실행되지 않을 수도 있습니다. 태그는 프로토-콜(proto-call)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이질적인 이론의 실타래가 처음으로 하나로 모이기 시작하는 것을 보며 승리의 기쁨을 느낍니다. 갑자기 보스 음악이 흘러나오기 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누가 타임이라고 했나요?&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시간이 반격을 가하다 (Time Strikes Back)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 보스는 시간 그 자체입니다. 시간을 이기기 위해서는 '시간에 구애받지 않는 청사진(timeless blueprints)'이 정말로 시간에 구애받지 않는다는 것을 증명해야 합니다. 즉, 계산의 순서를 바꿔도 프로그램이 엉망이 되지 않는다는 것을 보여줘야 합니다. 당신이 옳아야 하겠죠!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 설명하던 &lt;code&gt;greeting&lt;/code&gt; 함수입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function greeting() {
  return (
    &amp;lt;alert&amp;gt;
      &amp;lt;concat&amp;gt;
        Hello,
        &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
      &amp;lt;/concat&amp;gt;
    &amp;lt;/alert&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보스전을 좀 더 재미있고 무섭게 만들 수 있도록 조금 더 강화해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;alert&lt;/code&gt;와 &lt;code&gt;concat&lt;/code&gt;을 자주 조합하고 싶을 것 같아서 별도의 함수로 추출하겠습니다. 저는 이 함수를 &quot;단락&quot;을 뜻하는 &lt;code&gt;p&lt;/code&gt;라고 부르겠습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function p(...children) {
  return (
    &amp;lt;alert&amp;gt;
      &amp;lt;concat&amp;gt;{children}&amp;lt;/concat&amp;gt;
    &amp;lt;/alert&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;greeting&lt;/code&gt; 함수는 &lt;code&gt;p&lt;/code&gt; 태그만 반환하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello,
      &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 실행된 시간을 반환하는 새로운 &lt;code&gt;clock&lt;/code&gt; 함수도 추가할 것입니다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;function clock() {
  return new Date().toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;code&gt;greeting&lt;/code&gt;과 &lt;code&gt;p&lt;/code&gt; 내부의 &lt;code&gt;clock&lt;/code&gt;을 결합하는 &lt;code&gt;app&lt;/code&gt; 함수를 추가하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function app() {
  return [
    &amp;lt;greeting /&amp;gt;,
    &amp;lt;p&amp;gt;
      The time is: &amp;lt;clock /&amp;gt;
    &amp;lt;/p&amp;gt;,
  ];
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;interpret&lt;/code&gt;에서 배열을 지원하기에 좋은 시기입니다. 다행히도 이는 매우 쉽습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function interpret(json, knownTags) {
  if (json &amp;amp;&amp;amp; json.fn) {
    if (knownTags[json.fn]) {
      let fn = knownTags[json.fn];
      let args = json.args.map((arg) =&amp;gt; interpret(arg, knownTags));
      let result = fn(...args);
      return interpret(result, knownTags);
    } else {
      let args = json.args.map((arg) =&amp;gt; interpret(arg, knownTags));
      return { fn: json.fn, args };
    }
  } else if (Array.isArray(json)) {
    return json.map((item) =&amp;gt; interpret(item, knownTags));
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자, 이제 &lt;code&gt;interpret&lt;/code&gt;이 제대로 작동하는지 확인해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 모든 태그를 함께 해석해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;interpret(app(), {
  alert: window.alert,
  prompt: window.prompt,
  concat: (a, b) =&amp;gt; a + b,
  p: p,
  greeting: greeting,
  clock: clock,
});
// [undefined, undefined]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-part1-forked-slfzsc?file=%2Fsrc%2Findex.mjs&quot;&gt;이 코드를 실행하면&lt;/a&gt; 예상되는 결과가 생성됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내 이름을 묻는 &lt;code&gt;프롬프트&lt;/code&gt;가 표시됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;안녕하세요, 댄&lt;/code&gt;이라는 알림이 표시됩니다.&lt;/li&gt;
&lt;li&gt;또 다른 알림이 표시됩니다 &lt;code&gt;지금 시간은: 수 Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 여러분이 방어하고 있는 주장은 아직 호출로 전환되지 않은 태그인 청사진일 뿐이므로 이러한 태그는 어떤 순서로든 자유롭게 분해할 수 있다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 테스트해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 태그의 &lt;i&gt;절반&lt;/i&gt;(&lt;code&gt;p&lt;/code&gt;, &lt;code&gt;greeting&lt;/code&gt;, &lt;code&gt;clock&lt;/code&gt;)만 분해해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const step2 = interpret(app(), {
  // alert: window.alert,
  // concat: (a, b) =&amp;gt; a + b,
  // prompt: window.prompt,
  p: p,
  greeting: greeting,
  clock: clock,
});

// [
//   { fn: 'alert', args: [{ fn: 'concat', args: ['Hello', { fn: 'prompt', args: ['Who are you?'] }] }] },
//   { fn: 'alert', args: [{ fn: 'concat', args: ['The time is ', 'Wed Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)'] }] }
// ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상대로 프롬프트나 경고 없이 조용히 진행되었습니다... 이제 중간 결과를 가져와 나머지 태그(&lt;code&gt;alert&lt;/code&gt;, &lt;code&gt;concat&lt;/code&gt;, &lt;code&gt;prompt&lt;/code&gt;)를 분해할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;interpret(step2, {
  alert: window.alert,
  concat: (a, b) =&amp;gt; a + b,
  prompt: window.prompt,
  // p: p,
  // greeting: greeting,
  // clock: clock,
});
// [undefined, undefined]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 예상대로 &lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-part1-forked-vvhg65?file=%2Fsrc%2Findex.html&quot;&gt;작동합니다&lt;/a&gt;.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내 이름을 묻는 &lt;code&gt;프롬프트&lt;/code&gt;가 표시됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;안녕하세요, 댄&lt;/code&gt;이라는 알림이 표시됩니다.&lt;/li&gt;
&lt;li&gt;또 다른 알림이 표시됩니다 &lt;code&gt;지금 시간은: 수 Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;축하합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 호출이 아닌 태그로 이루어진 계산을 여러 단계로 분할하여 임의의 순서로 계산할 수 있으며, 이를 통해 시간 자체를 무력화할 수 있음을 증명하셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않다면?...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;concat&lt;/code&gt;을 제외한 모든 태그를 함께 분해해 보는 건 어떨까요?&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;interpret(app(), {
  alert: window.alert,
  prompt: window.prompt,
  // concat: (a, b) =&amp;gt; a + b,
  p: p,
  greeting: greeting,
  clock: clock,
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시대를 초월한 청사진에서 나중에 &lt;code&gt;concat&lt;/code&gt;을 실행해도 문제가 되지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-part1-forked-zyxyyz&quot;&gt;코드를 실행해 보세요&lt;/a&gt;.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내 이름을 묻는 &lt;code&gt;프롬프트&lt;/code&gt;가 표시됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[object Object]&lt;/code&gt;라고 표시되는 알림이 표시됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[object Object]&lt;/code&gt;라고 표시되는 또 다른 알림이 표시됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당신은 죽었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;치명적인 결함&lt;/h3&gt;
&lt;!-- What just happened? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 일이 일어났을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!--
Turns out, your theory has a flaw. Even if you describe your program with tags rather than function calls, time is actually important! For some functions, anyway. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고 보니 여러분의 이론에는 결함이 있습니다. 프로그램을 함수 호출이 아닌 태그로 기술하더라도, 시간은 여전히 중요합니다. 적어도 어떤 함수들에는 말입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제를 한번 봅시다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;concat&amp;gt;
  Hello,
  &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
&amp;lt;/concat&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;!-- When two tags are nested, in which order should they be intepreted? Should `&lt;prompt&gt;` be interpreted first, and the result of that be passed to the `concat` function? Or should the `concat` function receive `&lt;prompt&gt;` itself as a tag? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 태그가 중첩된 경우 어떤 순서로 해석해야 할까요? &lt;code&gt;&amp;lt;prompt&amp;gt;&lt;/code&gt;를 먼저 해석하고 그 결괏값을 &lt;code&gt;concat&lt;/code&gt; 함수에 전달해야 할까요? 아니면 &lt;code&gt;concat&lt;/code&gt; 함수가 &lt;code&gt;&amp;lt;prompt&amp;gt;&lt;/code&gt; &lt;i&gt;자체를 태그로&lt;/i&gt; 받아야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- We can start by considering the behavior of regular function calls: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 함수를 호출한다면 어떻게 동작하는지 생각해 보는 것도 좋을 것 같네요.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;concat(
  &quot;Hello, &quot;,
  prompt(&quot;Who are you?&quot;), // 이 코드가 먼저 실행됩니다
);&lt;/code&gt;&lt;/pre&gt;
&lt;!-- In case you’re not sure, when you call a function in JavaScript, its arguments are calculated first—and after those values are known, the function gets called: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 모르시는 분들을 위해 부연 설명하자면, 자바스크립트에서는 함수를 호출하면 인수를 &lt;i&gt;먼저&lt;/i&gt; 계산하여 값이 확정되고 나서 함수를 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function concat(a, b) {
  // a: 'Hello, '
  // b: 'Dan'
  return a + b;
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Our interpret function dealing with tags applies them in the same order. When it encounters a tag like &lt;concat&gt;, it first runs interpret on its arguments in case there are nested calls like that &lt;prompt&gt;. Only then it would call concat(): --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그를 처리하는 &lt;code&gt;interpret&lt;/code&gt; 함수도 동일한 순서를 따릅니다. &lt;code&gt;&amp;lt;concat&amp;gt;&lt;/code&gt; 같은 태그를 만나면 &lt;code&gt;&amp;lt;prompt&amp;gt;&lt;/code&gt;와 같이 중첩된 태그 호출이 있을 수 있으므로, 각 인수에 &lt;code&gt;interpret&lt;/code&gt;를 &lt;i&gt;먼저&lt;/i&gt; 실행합니다. 그 &lt;i&gt;다음에서야&lt;/i&gt; &lt;code&gt;concat()&lt;/code&gt; 함수가 호출됩니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function interpret(json, knownTags) {
  if (json &amp;amp;&amp;amp; json.fn) {
    if (knownTags[json.fn]) {
      let fn = knownTags[json.fn];
      let args = json.args.map((arg) =&amp;gt; interpret(arg, knownTags));
      let result = fn(...args);
      return interpret(result, knownTags);
    } else {
      // ...
    }
  } else {
    // ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- As a result, this code: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 결과로 아래 코드는&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;concat&amp;gt;
  Hello,
  &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
&amp;lt;/concat&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;!-- is currently equivalent to this: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 동일합니다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;concat(
  &quot;Hello, &quot;,
  prompt(&quot;Who are you?&quot;) // 이 코드가 먼저 실행됩니다&lt;/code&gt;&lt;/pre&gt;
&lt;!-- However, there’s something off about that. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 뭔가 이상하지 않습니까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Weren’t our tags supposed to be timeless blueprints, untethered from the pesky constraints of the tedious arguments-must-go-first JavaScript evaluation order? What good are these “tags” if in the end they behave exactly like function calls? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그는 본래 '인수를 먼저 평가해야 한다'는 따분한 자바스크립트 실행 규칙에 얽매이지 않으면서 선언된 그대로 해석되어야 하지 않나요? 그런데 함수 호출과 똑같이 동작한다면, &quot;태그&quot;에 무슨 의미가 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Okay, but how else could this work? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 과연 &lt;i&gt;다르게&lt;/i&gt; 작동할 수는 없을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Well, what if this: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글쎄요. 이런 건 어떨까요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;concat&amp;gt;
  Hello,
  &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
&amp;lt;/concat&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드가 아래 코드와 동일하다면요?&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;concat(
  // 이 코드가 먼저 실행됩니다
  &quot;Hello, &quot;,
  &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;,
);&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Imagine that tags were evaluated outside-in rather than inside-out. So, when you have &lt;concat&gt; with &lt;prompt /&gt; inside, you wouldn’t actually see the prompt call first. Instead, you’d step into concat with &lt;prompt /&gt; still being a tag: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그가 &lt;i&gt;안쪽부터&lt;/i&gt;가 아니라 &lt;i&gt;바깥쪽부터&lt;/i&gt; 평가된다고 상상해 보세요. 예를 들어, &lt;code&gt;&amp;lt;prompt /&amp;gt;&lt;/code&gt;가 안에 들어 있는 &lt;code&gt;&amp;lt;concat&amp;gt;&lt;/code&gt;을 사용할 때, 먼저 &lt;code&gt;&amp;lt;prompt&amp;gt;&lt;/code&gt; 호출이 일어나지 않는 거죠. 대신, &lt;code&gt;&amp;lt;prompt /&amp;gt;&lt;/code&gt;가 여전히 하나의 &lt;i&gt;태그&lt;/i&gt;로 &lt;code&gt;&amp;lt;concat&amp;gt;&lt;/code&gt; 내부에 들어가게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function concat(a, b) {
  // a: 'Hello'
  // b: { fn: 'prompt', args: ['Who are you?'] }
  return a + b;
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Of course, that would utterly break concat since it can only concatenate strings, not some arbitrary computations like &lt;prompt /&gt; which haven’t even run yet. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 그렇게 되면 &lt;code&gt;&amp;lt;concat&amp;gt;&lt;/code&gt;이 완전히 망가질 겁니다. &lt;code&gt;&amp;lt;concat&amp;gt;&lt;/code&gt;은 &lt;i&gt;문자열&lt;/i&gt;만 이어 붙일 수 있는데, &lt;code&gt;&amp;lt;prompt /&amp;gt;&lt;/code&gt;처럼 아직 실행되지도 않은 임의의 결괏값을 다룰 수는 없기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- This problem is not unique to concat. For example, the alert function also expects a string. It wouldn’t know how to handle an object representing a tag: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 &lt;code&gt;&amp;lt;concat&amp;gt;&lt;/code&gt;에만 국한되지 않습니다. 예를 들어, &lt;code&gt;alert&lt;/code&gt; 함수도 인수의 기댓값이 &lt;i&gt;문자열&lt;/i&gt;인데, 태그를 나타내는 객체가 넘어오면 처리할 수 없습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;alert({
  fn: &quot;concat&quot;,
  args: [
    /* ... */
  ],
});&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Or rather, it would handle it—by coercing it to a string like &quot;[object Object]&quot;. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엄밀히 말하면 &lt;i&gt;처리하긴&lt;/i&gt; 할 겁니다. 다만 객체를 문자열로 강제 변환해서 &lt;code&gt;&quot;[object Object]&quot;&lt;/code&gt; 같은 식이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- This explains what happened during the boss fight! --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이유를 알게 되었네요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Although our interpret function would normally handle the arguments first, we specifically delayed interpreting the &lt;concat&gt; tag to demonstrate that the ordering doesn’t matter. However, it does matter—both the concat and the alert functions need their arguments to be regular strings rather than tags. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래라면 &lt;code&gt;interpret&lt;/code&gt; 함수가 먼저 인자들을 처리했겠지만, 순서가 중요하지 않다는 걸 보여주려고 일부러 &lt;code&gt;&amp;lt;concat&amp;gt;&lt;/code&gt; 태그의 해석을 지연시켰습니다. 하지만 &lt;i&gt;실제로는&lt;/i&gt; 순서가 중요했던 거죠. &lt;code&gt;concat&lt;/code&gt;과 &lt;code&gt;alert&lt;/code&gt; 함수 모두 태그가 아니라 정상적인 문자열인 인수를 받아야 제대로 동작하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- It seems like your timeless blueprints aren’t so timeless after all. Functions need their arguments to be computed first. That’s where the time was hiding. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 시간의 제약을 받고 있었던 거군요. 함수의 인수들은 먼저 &lt;i&gt;계산되어야&lt;/i&gt; 합니다. 바로 그 과정에 시간이 숨어 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Your theory has a fatal flaw --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분의 이론에는 큰 결함이 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;새로운 희망&lt;/h3&gt;
&lt;!-- Your theory has a fatal flaw. There are three things you can do with that. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당신의 이론에는 치명적인 결함이 있습니다. 그렇다면 할 수 있는 일은 세 가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- You could pretend that it doesn’t exist. But that won’t fix your theory. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 그 결함이 존재하지 않는 척하는 것입니다. 하지만 그렇게 해도 이론이 나아지진 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- You could give up on your theory. But you were onto something, weren’t you? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로는 이 이론을 포기하는 것입니다. 하지만 이론이 뭔가 중요한 것을 &lt;i&gt;짚고 있던&lt;/i&gt; 건 맞지 않나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Finally, you could let that flaw guide you. Like a well-chosen failed experiment, it tells you something very important. You’ve made a mistake, but where exactly? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 그 결함을 &lt;i&gt;길잡이&lt;/i&gt;로 삼을 수 있습니다. 잘 설계된 실패한 실험처럼, 분명 중요한 무언가를 말해주고 있습니다. 실수를 한 건 맞지만 &lt;i&gt;정확히&lt;/i&gt; 어디서 잘못된 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- There’s a good way to find out. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인할 수 있는 좋은 방법이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Currently, we’re always eagerly interpreting nested tags before calling the parent tag’s function to ensure that the tag functions get called in the inside-out order: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 우리는 항상 중첩된 태그를 먼저 해석한 뒤 부모 태그의 함수를 호출하고 있습니다. 태그 함수들이 &lt;i&gt;안쪽부터 바깥쪽으로&lt;/i&gt; 호출되도록 하기 위해서입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function interpret(json, knownTags) {
  if (json &amp;amp;&amp;amp; json.fn) {
    if (knownTags[json.fn]) {
      let fn = knownTags[json.fn];
      let args = json.args.map((arg) =&amp;gt; interpret(arg, knownTags));
      let result = fn(...args);
      return interpret(result, knownTags);
    } else {
      let args = json.args.map((arg) =&amp;gt; interpret(arg, knownTags));
      return { fn: json.fn, args };
    }
  } else if (Array.isArray(json)) {
    return json.map((item) =&amp;gt; interpret(item, knownTags));
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- What if instead we just passed the raw arguments (even if they include tags)? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 태그가 포함되어 있더라도 가공하지 않은 인수 &lt;i&gt;그대로&lt;/i&gt;를 전달하면 어떻게 될까요?&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function interpret(json, knownTags) {
  if (json &amp;amp;&amp;amp; json.fn) {
    if (knownTags[json.fn]) {
      let fn = knownTags[json.fn];
      let args = json.args;
      let result = fn(...args);
      return interpret(result, knownTags);
    } else {
      let args = json.args.map((arg) =&amp;gt; interpret(arg, knownTags));
      return { fn: json.fn, args };
    }
  } else if (Array.isArray(json)) {
    return json.map((item) =&amp;gt; interpret(item, knownTags));
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Of course, this would completely break each of our previous examples. Remember, alert() can’t handle an object argument like &lt;concat&gt;—and concat() itself can’t handle an object argument like &lt;prompt&gt;. It wants two strings, not tags: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이렇게 하면 지금까지의 예제들에서 에러가 발생할 겁니다. 생각해 보세요. &lt;code&gt;alert()&lt;/code&gt;는 &lt;code&gt;&amp;lt;concat&amp;gt;&lt;/code&gt; 같은 객체 인수를 &lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-part1-forked-rnvdg2?file=%2Fsrc%2Findex.mjs&quot;&gt;처리할 수 없고&lt;/a&gt;, &lt;code&gt;concat()&lt;/code&gt; 자체도 &lt;code&gt;&amp;lt;prompt&amp;gt;&lt;/code&gt; 같은 객체 인수를 처리할 수 없습니다. 이 함수들은 태그가 아니라 문자열 두 개를 원하니까요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const tags = (
  &amp;lt;concat&amp;gt;
    Hello, &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
  &amp;lt;/concat&amp;gt;
);

interpret(tags, {
  concat: (a, b) =&amp;gt; a + b,
  prompt: window.prompt,
});

// 'Hello, [object Object]'&lt;/code&gt;&lt;/pre&gt;
&lt;!-- But fully embracing the “flaw” might also shine some light on what does work. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 &amp;ldquo;결함&amp;rdquo;을 온전히 받아들이면, 오히려 &lt;i&gt;잘&lt;/i&gt; 작동하는 부분이 무엇인지 실마리를 얻을 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- For example, replacing &lt;concat&gt; with &lt;p&gt; no longer leads to a broken output: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, &lt;code&gt;&amp;lt;concat&amp;gt;&lt;/code&gt;을 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;로 바꾸면 &lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-part1-forked-kw9cxj?file=%2Fsrc%2Findex.mjs&quot;&gt;더 이상 출력이 깨지지&lt;/a&gt; 않습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function p(...children) {
  return (
    &amp;lt;alert&amp;gt;
      &amp;lt;concat&amp;gt;{children}&amp;lt;/concat&amp;gt;
    &amp;lt;/alert&amp;gt;
  );
}

// ...

const tags = (
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
  &amp;lt;/p&amp;gt;
);

interpret(tags, {
  p: p,
  prompt: window.prompt,
});

// { fn: 'alert', args: [{ fn: 'concat', args: ['Hello, ', 'Dan'] }] }&lt;/code&gt;&lt;/pre&gt;
&lt;!-- This might seem insignificant (we still need to run concat later). But actually this is very important! Something is fundamentally different between functions concat and p. The outside-in call order breaks concat, but it doesn’t break p. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 나중에 &lt;code&gt;concat&lt;/code&gt;을 실행해야 하니 겉보기엔 별것 아닌 것처럼 보일 수도 있습니다.&lt;br /&gt;하지만 사실 매우 중요한 차이입니다! &lt;code&gt;concat&lt;/code&gt; 함수와 &lt;code&gt;p&lt;/code&gt; 함수 사이에는 근본적인 차이가 있습니다.&lt;br /&gt;&lt;i&gt;바깥에서 안쪽으로&lt;/i&gt; 호출하는 순서를 따르면 &lt;code&gt;concat&lt;/code&gt;을 문제를 일으키지만 &lt;code&gt;p&lt;/code&gt;는 그렇지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Why is that exactly? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;도대체&lt;/i&gt; 왜 그런 걸까요?&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;임베딩(embedding)과 속성 검사(introspecting)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 두 함수를 봅시다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;function concat(a, b) {
  return a + b;
}

function pair(a, b) {
  return [a, b];
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- How are they different? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 둘은 어떻게 다른 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Obviously, they’re different in purpose. One of them concatenates strings. The other one creates an array with the two provided elements. But there’s also a more subtle difference between how they behave with respect to their arguments. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 목적 자체가 다릅니다. 하나는 문자열을 이어 붙이고, 다른 하나는 주어진 두 요소로 배열을 만듭니다. 하지만 그보다 더 미묘한 차이가 있습니다. 바로 인수를 처리하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- To explain it, I’ll use an analogy. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 설명하기 위해 비유를 하나 들어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Suppose your job is to tie pieces of a rope together. That’s not terribly difficult. You take the two pieces and tie them together, job done. Now suppose that one day someone hands you a rope and… a pumpkin. Suddenly, you can’t do your job. You need to take the two pieces of rope by their ends, but a pumpkin has no end. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분이 밧줄 두 가닥을 묶는다고 가정해 봅시다. 별로 어려운 일은 아닙니다. 두 가닥을 받아서 묶으면 끝입니다. 그런데 어느 날, 누군가 밧줄 하나, 그리고... 호박 하나를 건넵니다. 호박을 받는 순간, 여러분은 더 이상 일을 할 수 없습니다. 두 밧줄 끝을 잡아야 묶을 수 있는데, 호박은 묶을 끝부분이 없으니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Now, you might conclude from this that arbitrarily replacing things with pumpkins leads to disasters, and indeed sometimes it does. But not always. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이렇게 결론 내릴지도 모릅니다. &amp;ldquo;무턱대고 뭔가를 호박으로 바꾸면 문제가 생긴다.&amp;rdquo; 실제로 종종 그런 일이 일어나긴 합니다. 하지만 항상 그렇지는 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Suppose that you have a new job wrapping up presents in a toy shop. You’d spend your day wrapping up various presents, be it a doll, or a car, or an entire toy house. Then one day someone hands you a pumpkin. Although you might refuse the request out of principle, technically you could wrap up a pumpkin just fine. When you wrap things up, you don’t rely on their properties (like the rope-iness of a rope). You merely put them in a box. You’re embedding, not introspecting. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분이 장난감 가게에서 선물을 포장하는 일을 맡았다고 가정해 보세요. 하루 종일 다양한 선물들을 포장하는 일을 합니다. 인형, 자동차, 혹은 장난감 집일 수도 있죠. 어느 날, 누군가 당신에게 호박을 건넵니다. &lt;i&gt;원칙적으로는&lt;/i&gt; 거절할 수도 있겠지만 사실 호박도 잘 포장할 수 있습니다. 포장할 때는 그 물건의 속성(밧줄의 구조같은)은 중요하지 않습니다. 그냥 상자에 넣으면 됩니다. 여러분은 임베딩을 하고 있는 것이지, 속성 검사를 하고 있는 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- The difference between concat and pair above is that concat cares about what’s being passed to it. It introspects. It wouldn’t work if you pass a pumpkin. But pair would happily accept ropes, toys, or pumpkins. It embeds, so it doesn’t care. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 &lt;code&gt;concat&lt;/code&gt;과 &lt;code&gt;pair&lt;/code&gt;의 차이점은 &lt;code&gt;concat&lt;/code&gt;은 전달된 값이 무엇인지가 중요하다는 점입니다. 즉, 속성 검사를 수행합니다. 그래서 호박을 전달하면 제대로 동작하지 않습니다. 하지만 &lt;code&gt;pair&lt;/code&gt;는 밧줄, 장난감, 호박 모두를 기꺼이 받아들입니다. 임베딩을 하므로 그 속성에 신경 쓰지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Let’s see how this connects to the order of execution. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이제 이 점이 실행 순서와 어떻게 연결되는지 살펴봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Since concat introspects arguments a and b (concretely, + turns them to strings), concat breaks if you pass an uninterpreted tag as an argument: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;concat&lt;/code&gt;은 인수인 &lt;code&gt;a&lt;/code&gt;와 &lt;code&gt;b&lt;/code&gt;의 &lt;i&gt;속성을 검사합니다&lt;/i&gt;(더 설명하자면 &lt;code&gt;+&lt;/code&gt; 연산자가 이를 문자열로 변환합니다). 따라서 &lt;code&gt;concat&lt;/code&gt;은 해석되지 않은 태그를 인수로 전달하면 문제가 발생합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;concat(&quot;Hello &quot;, &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;);
// 'Hello, [object Object]'&lt;/code&gt;&lt;/pre&gt;
&lt;!-- On the other hand, pair embeds its arguments a and b. It produces a new [a, b] array—and that works correctly no matter what you pass as a or b. So it’s happy to accept a tag as one of the arguments. It just embeds that tag in its output: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, &lt;code&gt;pair&lt;/code&gt;는 그 인수인 &lt;code&gt;a&lt;/code&gt;와 &lt;code&gt;b&lt;/code&gt;를 임베딩 합니다. &lt;code&gt;[a, b]&lt;/code&gt; 배열을 새로 생성하며 &lt;code&gt;a&lt;/code&gt;나 &lt;code&gt;b&lt;/code&gt;에 무엇을 전달하든 상관없이 올바르게 동작합니다. 따라서 &lt;code&gt;pair&lt;/code&gt;는 태그를 인수로 받아도 아무런 문제가 없습니다. 그냥 그 태그를 결과에 임베딩 해서 출력할 뿐입니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;const todo = pair(&quot;Hello &quot;, &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;);
// ['Hello, ', { fn: 'prompt', args: ['Who are you?'] }]&lt;/code&gt;&lt;/pre&gt;
&lt;!-- This lets you interpret that tag sometime after the pair call: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;code&gt;pair&lt;/code&gt; 호출 이후에도 그 태그를 해석할 수 있게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const result = interpret(todo, { prompt: window.prompt });
// ['Hello, ', 'Dan']&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Let’s summarize. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Generally, functions want to have their arguments computed before the call. However, if a function only embeds an argument in its output without introspecting it, you can delay computing it. You can call that function with that argument still uncomputed (a tag), and compute that tag later when it’s necessary or convenient. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 함수는 호출 전에 인수가 연산되기를 원합니다. 하지만 함수가 인수의 &lt;i&gt;속성을 검사하지&lt;/i&gt; 않고 &lt;i&gt;임베딩만&lt;/i&gt; 한다면, 인수 연산을 지연시킬 수 있습니다. 그 함수는 인수가 아직 연산되지 않은 상태(태그)로 호출할 수 있으며, 그 태그는 나중에 필요하거나 편할 때 계산할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- You may have found a way to beat Time after all. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쩌면 시간을 이길 방법을 찾았을지도 모릅니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생각하고 실행하기&lt;/h3&gt;
&lt;!-- Your program is still the same: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분의 프로그램은 여전히 동일합니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function app() {
  return [
    &amp;lt;greeting /&amp;gt;,
    &amp;lt;p&amp;gt;
      The time is: &amp;lt;clock /&amp;gt;
    &amp;lt;/p&amp;gt;,
  ];
}

function clock() {
  return new Date().toString();
}

function greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello,
      &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}

function p(...children) {
  return (
    &amp;lt;alert&amp;gt;
      &amp;lt;concat&amp;gt;{children}&amp;lt;/concat&amp;gt;
    &amp;lt;/alert&amp;gt;
  );
}

function alert(message) {
  window.alert(message);
}

function prompt(message) {
  return window.prompt(message);
}

function concat(a, b) {
  return a + b;
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- But your interpret function is simpler—it interprets tags outside-in. It doesn’t try to interpret the arguments before the call; rather, it passes tags to other tags. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 당신의 &lt;code&gt;interpret&lt;/code&gt; 함수는 더 간단합니다. 단지 태그를 &lt;i&gt;바깥에서 안쪽으로&lt;/i&gt; 해석합니다.&lt;br /&gt;호출 전에 인수를 해석하지 않고 &lt;i&gt;태그를 다른 태그에&lt;/i&gt; &lt;i&gt;전달합니다&lt;/i&gt;.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function interpret(json, knownTags) {
  if (json &amp;amp;&amp;amp; json.fn) {
    if (knownTags[json.fn]) {
      let fn = knownTags[json.fn];
      let args = json.args;
      let result = fn(...args);
      return interpret(result, knownTags);
    } else {
      let args = json.args.map((arg) =&amp;gt; interpret(arg, knownTags));
      return { fn: json.fn, args };
    }
  } else if (Array.isArray(json)) {
    return json.map((item) =&amp;gt; interpret(item, knownTags));
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Time smirks at you. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 당신을 비웃고 있네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- “This isn’t going to work, is it? Functions need to know their arguments.” --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&amp;ldquo;이건 잘 안 될 걸? 함수는 자신의 인수를 알아야 하잖아.&amp;rdquo;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- “Some of them do.” --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;그렇지 않은 함수도 있어.&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- You look over all the functions in your program to see whether they introspect the stuff you’re nesting inside their tags or merely embed it without introspection: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당신은 프로그램의 모든 함수를 살펴보며, 그 함수들이 태그 내 중첩된 내용을 &lt;i&gt;검사하는지&lt;/i&gt; 아니면 &lt;i&gt;임베딩만&lt;/i&gt; 하는지 확인합니다.&lt;/p&gt;
&lt;!-- - Clearly, alert and concat introspect the stuff you put inside their tags. --&gt;&lt;!-- - Some functions (app, clock, and greeting) take no arguments at all. --&gt;&lt;!-- - Although you do pass stuff into p, it merely embeds whatever you nest in it. --&gt;&lt;!-- - The case of prompt is ambiguous. Technically, it does introspect the message argument (because it passes message to the built-in window.prompt). However, so far, we haven’t had a temptation to nest any other tags inside &lt;prompt&gt;. So if we promise not to do that (e.g. by restricting the type somehow), it doesn’t matter. --&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분명히 &lt;code&gt;alert&lt;/code&gt;과 &lt;code&gt;concat&lt;/code&gt;은 태그 내부 내용의 속성을 검사합니다.&lt;/li&gt;
&lt;li&gt;몇몇 함수(&lt;code&gt;app&lt;/code&gt;, &lt;code&gt;clock&lt;/code&gt;, &lt;code&gt;greeting&lt;/code&gt;)는 인수를 받지 않습니다.&lt;/li&gt;
&lt;li&gt;비록 &lt;code&gt;p&lt;/code&gt;에 무언가를 &lt;i&gt;전달하긴&lt;/i&gt; 하지만, 단지 내부에 중첩된 내용을 임베딩만 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prompt&lt;/code&gt;의 경우는 애매합니다. 엄밀히 말하면 &lt;code&gt;prompt&lt;/code&gt;는 &lt;code&gt;message&lt;/code&gt; 인수의 속성을 검사합니다(내장된 &lt;code&gt;window.prompt&lt;/code&gt;에 전달해야 하므로). 하지만 지금까지 &lt;code&gt;&amp;lt;prompt&amp;gt;&lt;/code&gt; 안에 다른 태그를 중첩하려고 하지 않았습니다. 중첩하지 않겠다고 보장한다면(타입을 제한하는 방식처럼), 문제 될 건 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- To keeps things straight, you’ll introduce a new convention. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명확히 구분하고자 새로운 규칙을 추가하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Functions that won’t break when passed tags as arguments, i.e. functions that embed rather than introspect them, will now start their names with capital letters: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그를 인수로 받았을 때 문제가 생기지 않는 함수들, 즉 &lt;i&gt;속성을 검사하지&lt;/i&gt; 않고 &lt;i&gt;임베딩만&lt;/i&gt; 하는 함수들은 대문자로 시작하게 이름을 짓겠습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function App() {
  return [
    &amp;lt;Greeting /&amp;gt;,
    &amp;lt;P&amp;gt;
      The time is: &amp;lt;Clock /&amp;gt;
    &amp;lt;/P&amp;gt;,
  ];
}

function Clock() {
  return new Date().toString();
}

function Greeting() {
  return (
    &amp;lt;P&amp;gt;
      Hello,
      &amp;lt;prompt&amp;gt;Who are you?&amp;lt;/prompt&amp;gt;
    &amp;lt;/P&amp;gt;
  );
}

function P(...children) {
  return (
    &amp;lt;alert&amp;gt;
      &amp;lt;concat&amp;gt;{children}&amp;lt;/concat&amp;gt;
    &amp;lt;/alert&amp;gt;
  );
}

function alert(message) {
  window.alert(message);
}

function prompt(message) {
  return window.prompt(message);
}

function concat(a, b) {
  return a + b;
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Let’s give these capital letter functions a special name: Components. Components are the “brains” of our program—they figure out what needs to be done. Because they don’t introspect the stuff you nest inside of them, they can run in any order, in any number of steps, together or separately. In other words, Components are truly timeless. They are untethered from the future because they return tags, and they are untethered from the past because they accept tags as arguments. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 대문자로 시작하는 함수에 &quot;컴포넌트&quot;라는 특별한 이름을 붙여봅시다. 컴포넌트는 우리 프로그램의 &quot;두뇌&quot;입니다. 무엇을 해야 할지 결정합니다. 컴포넌트는 내부에 중첩된 내용이 무엇인지 검사하지 않습니다. 그러므로 순서나 단계와 관계없이, 어떤 방식으로든 함께 또는 따로 실행될 수 있습니다. 다시 말해, 컴포넌트는 정말 시간의 제약을 받지 &lt;i&gt;않습니다&lt;/i&gt;. 태그를 &lt;i&gt;반환하기&lt;/i&gt; 때문에 미래에 얽매이지 않으며, 태그를 인수로 &lt;i&gt;받기&lt;/i&gt; 때문에 과거에도 얽매이지 않습니다.&lt;/p&gt;
&lt;!-- What about the rest of the functions, like alert, prompt, and concat? Let’s call them Primitives. Primitives can be used as tags too, but they don’t merely embed stuff—they introspect it. They must know all their arguments. Primitives are the “muscles” of our program—they actually do stuff after most of the thinking has already been done by Components. Primitives run last: “think before you do”. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- 프리미티브.. 여기선 원시값이라고 하기도 좀 애매하군요 ㅠㅠ --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 나머지 함수들, 예를 들어 &lt;code&gt;alert&lt;/code&gt;, &lt;code&gt;prompt&lt;/code&gt;, &lt;code&gt;concat&lt;/code&gt;은 어떻게 될까요? 이 함수들은 프리미티브(Primitives)라고 부르겠습니다. 프리미티브도 태그로 사용될 수 있지만, 그들은 단순히 내용을 임베딩하는 것이 아니라, 속성을 검사합니다. 그래서 자신이 받은 모든 인수를 &lt;i&gt;알아야&lt;/i&gt; 합니다. 프리미티브는 우리 프로그램의 &quot;근육&quot;입니다. 컴포넌트가 대부분의 사고를 끝내면 실제로 &lt;i&gt;일을 처리하는&lt;/i&gt; 역할을 합니다. 따라서 프리미티브는 마지막에 실행됩니다. &lt;i&gt;&quot;행동하기 전에 생각하라.&quot;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- This distinction lets you naturally slice the program in two phases. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구분에 따르면 프로그램은 자연스레 두 단계로 나뉩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- First, you need to think—that is, to run the Components. Your existing interpret function can take care of that: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 당신은 &lt;i&gt;생각해야&lt;/i&gt; 합니다. 즉, 컴포넌트를 실행해야 합니다. 기존의 &lt;code&gt;interpret&lt;/code&gt; 함수가 그 일을 처리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;const primitives = interpret(&amp;lt;App /&amp;gt;, {
  App,
  Greeting,
  Clock,
  P,
});

// [
// { fn: 'alert', args: [{ fn: 'concat', args: ['Hello', { fn: 'prompt', args: ['Who are you?'] }] }] },
// { fn: 'alert', args: [{ fn: 'concat', args: ['The time is: ', 'Wed Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)'] }] }
// ]&lt;/code&gt;&lt;/pre&gt;
&lt;!-- After thinking, you need to do. The result of the “thinking” phase contains only the Primitives. Let’s create a new perform function that’ll look a lot like interpret, but it will handle Primitives instead of Components. Since Primitives introspect stuff and need to know their arguments, perform ensures they run inside-out: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;생각한&lt;/i&gt; 후에는, 이제 실행해야 합니다. &quot;생각하기&quot; 단계의 결과에는 오직 프리미티브만 포함되어 있습니다.&lt;br /&gt;&lt;code&gt;perform&lt;/code&gt;이라는 새로운 함수를 만들어 봅시다. 이 함수는 &lt;code&gt;interpret&lt;/code&gt;와 매우 비슷하지만, 컴포넌트 대신 프리미티브를 처리합니다. 프리미티브는 속성을 검사하고 인수를 &lt;i&gt;알아야&lt;/i&gt; 하므로, &lt;code&gt;perform&lt;/code&gt;은 프리미티브가 안쪽에서 바깥쪽으로 실행되도록 보장합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function perform(json, knownTags) {
  if (json &amp;amp;&amp;amp; json.fn) {
    let fn = knownTags[json.fn];
    let args = perform(json.args, knownTags);
    let result = fn(...args);
    return perform(result, knownTags);
  } else if (Array.isArray(json)) {
    return json.map((item) =&amp;gt; perform(item, knownTags));
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Notice perform doesn’t include any code for skipping unknown tags—it assumes knownTags contains all Primitives it may encounter. This is because perform is intended as the final step and does not let you split the computation any further. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 &lt;code&gt;perform&lt;/code&gt;에는 정의되지 않은 태그를 건너뛰는 코드가 없습니다. &lt;code&gt;knownTags&lt;/code&gt;에 포함된 모든 프리미티브만 처리한다고 가정합니다. 이는 &lt;code&gt;perform&lt;/code&gt;이 &lt;i&gt;최종&lt;/i&gt; 단계로서, 더 이상 연산을 분리할 수 없기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Now you can use perform to finish the computation: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;perform&lt;/code&gt;을 사용하여 연산을 마칠 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;perform(primitives, {
  alert,
  concat,
  prompt,
});

// undefined&lt;/code&gt;&lt;/pre&gt;
&lt;!-- This displays the prompt and the two expected alerts. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &lt;code&gt;prompt&lt;/code&gt;와 두 개의 예상되는 &lt;code&gt;alert&lt;/code&gt;를 표시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Run the code. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-forked-6f6jz7?file=%2Fsrc%2Findex.mjs&quot;&gt;코드를 실행해 보세요.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- So, did you beat Time? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자, 이제 시간으로부터 승리했나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Sort of. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 정도 승리했네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Previously, interpret was fragile because skipping some tags (like concat) broke the ordering that was implicitly assumed by some other tags (like alert). But this can no longer happen. Now interpret only deals with Components, and they don’t mind being run in any order (since they embed rather than introspect). --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 &lt;code&gt;interpret&lt;/code&gt;는 취약했습니다. &lt;i&gt;일부&lt;/i&gt; 태그(예: &lt;code&gt;concat&lt;/code&gt;)를 건너뛰면 다른 태그(예: &lt;code&gt;alert&lt;/code&gt;)가 암묵적으로 가정한 순서가 깨졌기 때문입니다. 하지만 이제는 그런 일이 발생할 수 없습니다. 이제 &lt;code&gt;interpret&lt;/code&gt;는 &lt;i&gt;오직&lt;/i&gt; 컴포넌트만 처리하며, 컴포넌트는 순서에 관계없이 실행되어도 괜찮습니다(&lt;i&gt;속성을 검사하지&lt;/i&gt; 않고 &lt;i&gt;임베딩만&lt;/i&gt; 하기 때문입니다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Primitives, on the other hand, are now being handled by perform, which always finishes the work in a single step. So the problem can’t come up there either. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, 프리미티브는 이제 &lt;code&gt;perform&lt;/code&gt;에서 처리되며, &lt;code&gt;perform&lt;/code&gt;은 항상 한 번에 작업을 끝냅니다. 그래서 그곳에서도 문제가 발생하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- If you ever extend your program to span two computers, it’s Components (rather than Primitives) that would be split between them. That is because Components don’t mind being run in a different order. Primitives, on the other hand, would have to run together at the very end—which puts them firmly into the Late world. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 프로그램이 두 대의 컴퓨터에 걸쳐 실행되도록 확장된다면, (프리미티브가 아닌) 컴포넌트는 두 대의 컴퓨터로 분리될 것입니다. 컴포넌트가 다른 순서로 실행되어도 괜찮기 때문입니다. 반면, 프리미티브는 반드시 끝에서 실행되어야 하므로 최종 단계에 속하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- If you have some control over the computers running the Late worlds, there is an interesting optimization you could make. You could preinstall the Primitives that you expect to be shared by all programs alongside the JavaScript runtime. Of course, such a collection of Primitives would have to be carefully curated so that it serves a broadest possible set of use cases. But you can already see some good candidates! For example, your P function might make more sense as a Primitive: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 최종 단계를 실행하는 컴퓨터를 어느 정도 제어할 수 있다면, 흥미로운 최적화를 할 수 있습니다. 모든 프로그램에서 공유되리라 예상되는 프리미티브를 자바스크립트 런타임과 함께 &lt;i&gt;미리 설치할&lt;/i&gt; 수 있습니다. 물론, 이러한 프리미티브 모음은 가능한 넓은 범위의 경우를 지원할 수 있도록 신중하게 선별되어야 합니다. 하지만 이미 몇 가지 좋은 후보가 보입니다! 예를 들어, &lt;code&gt;p&lt;/code&gt; 함수는 프리미티브로 처리하는 것이 더 합리적일 것 같습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function p(...children) {
  return (
    &amp;lt;alert&amp;gt;
      &amp;lt;concat&amp;gt;{children}&amp;lt;/concat&amp;gt;
    &amp;lt;/alert&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Arguably, a “paragraph” is something many programs might want to display! --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명히, &quot;단락(paragraph, 줄여서 p)&amp;rdquo;은 많은 프로그램에서 사용할 겁니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- If you think bigger, you might come up with a whole suite of such Primitives—some graphical (like making something &lt;b&gt;bold&lt;/b&gt; or &lt;i&gt;italic&lt;/i&gt;) and some behavioral (like expanding &lt;details&gt;&lt;/details&gt; or &lt;a /&gt; link). --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 넓게 생각해 보면, 그런 프리미티브들의 전체 모음을 떠올릴 수 있습니다. 일부는 시각(&lt;code&gt;&amp;lt;b&amp;gt;굵게&amp;lt;/b&amp;gt;&lt;/code&gt; 또는 &lt;code&gt;&amp;lt;i&amp;gt;기울임꼴&amp;lt;/i&amp;gt;&lt;/code&gt; 등) 요소이고, 일부는 동작(&lt;code&gt;&amp;lt;details&amp;gt;&amp;lt;/details&amp;gt;&lt;/code&gt; 펼치기 또는 &lt;code&gt;&amp;lt;a /&amp;gt;&lt;/code&gt; 링크 등) 요소입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Now, if a lot of programs used the same Primitives, and everyone was building complex programs out of those, it might make sense to move their internal implementation out of JavaScript into some lower-level language like Rust or C++. Then they could be exposed to JavaScript via some higher-level APIs. Then perform could be rewritten to orchestrate the computation using such APIs: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 많은 프로그램이 동일한 프리미티브를 사용하여 복잡한 프로그램을 구축한다면, 자바스크립트 대신 Rust나 C++와 같은 더 낮은 수준의 언어로 프리미티브를 구현하는 것이 나을 수 있습니다. 이후 프리미티브를 고수준의 API로 감싸 자바스크립트에서 사용할 수 있습니다. &lt;code&gt;perform&lt;/code&gt;은 그 API를 활용해 연산을 적절히 처리하는 방식으로 다시 작성될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;function perform(json) {
  if (json &amp;amp;&amp;amp; json.fn) {
    let tagName = json.fn;
    let children = perform(json.args);
    let node = document.createElement(tagName);
    for (let child of [children].flat().filter(Boolean))) {
      node.appendChild(child);
  }
  return node;
  } else if (typeof json === 'string') {
    return document.createTextNode(json);
  } else if (Array.isArray(json)) {
    return json.map(perform);
  } else {
    return json;
  }
}

const tree = perform(json);
document.body.appendChild(tree);&lt;/code&gt;&lt;/pre&gt;
&lt;!-- You could even design a declarative language just for the purpose of describing trees of such Primitives. It could be designed to be more forgiving than our current setup, since for some use cases it might be nice to write it by hand. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;단지&lt;/i&gt; 프리미티브의 중첩 구조를 표현하기 위한 목적으로 선언형 언어를 따로 설계할 수도 있습니다. 어떤 용도에서는 사람이 직접 작성할 수도 있도록 지금의 구조보다 더 융통성 있게 설계하는 편이 좋을 수도 있습니다.&lt;/p&gt;
&lt;!-- But enough talking about the Primitives. Going forward, we will assume that a fair number of them exist, that they’re written as lowercase tags (such as &lt;p&gt;), and that there exists a perform function that knows what to do with them. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프리미티브에 대한 이야기는 이쯤 해두겠습니다. 이제부터는 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;프리미티브&lt;/a&gt;가 어느 정도 적당히 존재하고, 그것들이 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;처럼 소문자 태그로 작성되며, 어떻게 처리할지 알고 있는 &lt;code&gt;perform&lt;/code&gt; 함수가 존재한다고 가정하고 이야기를 진행하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Time steps aside. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간을 주제로 한 이야기는 여기까지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- You have learned to wield the power of Time—and to respect its laws. Now, should you wish to continue your studies, it is time for you to learn the lessons of Space --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분은 이제 시간의 힘을 다룰 줄 알게 되었고, 그 법칙을 존중할 &lt;i&gt;줄도&lt;/i&gt; 알게 되었습니다. 더 나아가고 싶으신가요? 그렇다면 &quot;공간&quot;을 배울 차례입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제2막&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;The Reader and the Writer&lt;/h3&gt;
&lt;!-- The Reader: That was a long article! --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: 아티클이 정말 기네요!&lt;/p&gt;
&lt;!-- The Writer: You betcha. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: 당연하죠!&lt;/p&gt;
&lt;!-- The Reader: And we’re still just halfway in? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: 설마 아직 반도 안 온 건가요?&lt;/p&gt;
&lt;!-- The Writer: I guess. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: 그런 것 같아요.&lt;/p&gt;
&lt;!-- The Reader: What do you mean you guess? Don’t you know where you’re going? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: 그런 것 같다고요? 어디로 가고 있는지 모른다는 말인가요?&lt;/p&gt;
&lt;!-- The Writer: I have a rough idea, but truthfully, I’m pretty much winging it. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: 대충은 알지만, 솔직히 거의 즉흥으로 쓰고 있긴 해요.&lt;/p&gt;
&lt;!-- The Reader: Well, that’s not very responsible. I’ve invested a lot of time into reading this. What if it doesn’t build up to something satisfying? What if you drop the ball? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: 음, 그건 너무 무책임한 것 같은데요. 전 이 글을 읽느라 많은 시간을 투자했어요. 이 글의 마무리가 만족스럽지 않다면 어떡하죠? 중간에 망쳐버리면요?&lt;/p&gt;
&lt;!-- The Writer: That’s been one of my worries, yes. But there’s no way for me to know that until I finish writing. On your side, I guess you’ll just have to keep on reading. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: 제가 걱정하고 있는 부분이에요. 하지만 글을 끝까지 써봐야 그걸 알 수 있겠네요. 여러분은 그냥 계속 읽어야 합니다. 아마도?&lt;/p&gt;
&lt;!-- The Reader: Well, okay, yes, I guess I’ll just have to do that. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: 음, 일단 알겠습니다. 계속 읽어보는 수밖에 없겠네요.&lt;/p&gt;
&lt;!-- The Writer: Thank you for your understanding. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: 이해해 주셔서 고맙습니다.&lt;/p&gt;
&lt;!-- The Reader: It’s not like I have a choice anyway. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: 어차피 다른 선택지가 없잖아요.&lt;/p&gt;
&lt;!-- The Writer: Why not? You know you can just close the tab and go about your day. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: 왜 없죠? 그냥 탭을 끄고 다른 일 하러 가셔도 되죠.&lt;/p&gt;
&lt;!-- The Reader: You know full well that I cannot do that. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: 아시다시피 저는 그럴 수가 없어요.&lt;/p&gt;
&lt;!-- The Writer: And why is that exactly? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: 도대체 왜요?&lt;/p&gt;
&lt;!-- The Reader: Well, I’m just one of your characters. You’re the one making me say things. I don’t exactly have much, what do you call it… the wiggle room. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: 글쎄요. 사실 저는 그냥 당신이 만든 캐릭터예요. 어떻게 말할지도 당신이 시키고 있는 거예요. 제가 할 수 있는 게 많지 않고... 뭐라 할까요, 여지가 없어요.&lt;/p&gt;
&lt;!-- The Writer: Ah. Right. The wiggle room. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: 아하, 여지가 없군요!&lt;/p&gt;
&lt;!-- The Writer briefly looks at the audience. It’s hard to read his expression. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저자는 잠시 관객을 바라본다. 그의 표정을 읽기가 어렵다.&lt;/p&gt;
&lt;!-- The Reader: … --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: ...&lt;/p&gt;
&lt;!-- The Writer: … --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: ...&lt;/p&gt;
&lt;!-- The Reader: You don’t have many more lines prepared for me, do you? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: 절 위한 대사가 별로 없죠?&lt;/p&gt;
&lt;!-- The Writer: My bad. I think that’s about all I could manage. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: 제 실수입니다. 제가 준비한 건 이게 다예요.&lt;/p&gt;
&lt;!-- The Reader: … --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: ...&lt;/p&gt;
&lt;!-- The Writer: … --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: ...&lt;/p&gt;
&lt;!-- The Reader: Why is this dialog even here? Does it add anything to the story? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: 이 대화는 왜 하는 거예요? 어떤 도움이 되는 건가요?&lt;/p&gt;
&lt;!-- The Writer: I don’t know. You tell me. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: 저도 모르겠어요. 당신 생각은 어때요?&lt;/p&gt;
&lt;!-- The Reader: I thought you’re the one doing the writing. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;독자&lt;/i&gt;: 지금 글을 쓰고 있는 건 당신 아닌가요?&lt;/p&gt;
&lt;!-- The Writer: Sure, but aren’t you the one doing the reading? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;저자&lt;/i&gt;: 그렇긴 하죠. 근데 당신도 읽고 있잖아요?&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드와 데이터&lt;/h3&gt;
&lt;!-- In the first half of this post, we’ve learned how to split a computation in time. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 게시물의 전반부에서는 연산을 시간상으로 나누는 방법에 대해 살펴보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- It turned out that some parts of the computation—the Primitives that are actively doing stuff—don’t like to be split apart and would like to execute together. Other parts of the computation—the Components that are thinking about stuff—can be executed at different times, in a different order, and maybe even in different places. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서, 연산의 일부이자 실제로 작업을 &lt;i&gt;수행하는&lt;/i&gt; 프리미티브는 분리되는 것을 싫어하고 함께 실행되기를 원한다는 사실을 알게 되었습니다. 반면, &lt;i&gt;사고를&lt;/i&gt; 담당하는 컴포넌트는 서로 다른 시간에, 다른 순서로, 심지어는 다른 &lt;i&gt;장소에서&lt;/i&gt; 실행될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- We will now set aside Components and Primitives for a moment. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 잠시 컴포넌트와 프리미티브는 제쳐둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Let us investigate the difference between splitting a function in time and in space. We’ve seen earlier that to split a function across time, it’s enough to add nesting: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수를 &lt;i&gt;시간상으로&lt;/i&gt; 나누는 것과 &lt;i&gt;공간상으로&lt;/i&gt; 나누는 것의 차이를 살펴보겠습니다. 앞서 살펴본 것처럼 함수를 시간상으로 나누기 위해서는 중첩을 추가하는 것만으로 충분합니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return function resume() {
    alert(&quot;Hello, &quot; + name);
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- This lets you run it in steps rather than all at once. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 한 번에 모두 실행하지 않고 단계별로 실행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;const resume = greeting(); // 첫 번째 단계 실행
resume(); // 두 번째 단계 실행&lt;/code&gt;&lt;/pre&gt;
&lt;!-- The return value of the greeting is a function—but that’s not the whole picture. It is crucial that this function is nested inside greeting, for otherwise it would not be able to read the name variable. In other words, greeting returns both a piece of code (the alert call) and a piece of data (the name variable) needed by it. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;greeting&lt;/code&gt; 함수의 반환 값은 함수입니다. 하지만 그것이 전부는 아닙니다. 이 함수가 &lt;code&gt;greeting&lt;/code&gt; &lt;i&gt;내부에&lt;/i&gt; 중첩되어 있다는 점이 중요합니다. 그렇지 않으면 &lt;code&gt;name&lt;/code&gt; 변수에 접근할 수 없기 때문입니다. 다시 말해, &lt;code&gt;greeting&lt;/code&gt; 함수는 코드 조각(&lt;code&gt;alert&lt;/code&gt; 호출) &lt;i&gt;그리고&lt;/i&gt; 코드에 필요한 데이터 조각(&lt;code&gt;name&lt;/code&gt; 변수)을 함께 반환하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- This becomes more apparent if you extract resume into a top-level function. Now it would have to take name as an explicit argument: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점은 &lt;code&gt;resume&lt;/code&gt;을 최상위 함수로 분리했을 때 더 뚜렷해집니다. 이제는 명시적으로 &lt;code&gt;name&lt;/code&gt;을 인수로 받아야 하기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function resume(name) {
  alert(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- How would we adjust the greeting to accommodate that? We could make it return a nested function that would provide name to resume: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;resume&lt;/code&gt;을 최상위 함수로 분리한다면 &lt;code&gt;greeting&lt;/code&gt;을 어떻게 조정할 수 있을까요? &lt;code&gt;resume&lt;/code&gt;에 &lt;code&gt;name&lt;/code&gt;을 제공하는 중첩 함수를 반환하도록 만드는 방법이 있겠네요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return () =&amp;gt; resume(name);
}

function resume(name) {
  alert(&quot;Hello, &quot; + name);
}

const resume = greeting(); // 첫 번째 단계 실행
resume(); // 두 번째 단계 실행&lt;/code&gt;&lt;/pre&gt;
&lt;!-- But we could also go a bit further. Conceptually, () =&gt; resume(name) combines two pieces of information: code (resume) and data (name). We could make this relationship explicit by returning [resume, name]—code paired with data: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 한 걸음 더 나아가보면 어떨까요? 개념적으로, &lt;code&gt;() =&amp;gt; resume(name)&lt;/code&gt;은 두 가지 정보를 결합합니다. 바로 &lt;i&gt;코드&lt;/i&gt;(&lt;code&gt;resume&lt;/code&gt;)와 &lt;i&gt;데이터&lt;/i&gt;(&lt;code&gt;name&lt;/code&gt;)입니다. 이 관계를 더 명시적으로 표현하려면 &lt;code&gt;[resume, name]&lt;/code&gt;, 즉 코드와 데이터를 &lt;i&gt;쌍으로&lt;/i&gt; 반환할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [resume, name];
}

function resume(name) {
  alert(&quot;Hello, &quot; + name);
}

const [code, data] = greeting(); // 첫 번째 단계 실행
code(data); // 두 번째 단계 실행&lt;/code&gt;&lt;/pre&gt;
&lt;!-- In fact, this looks remarkably similar to the object notation that we currently use for tags, except that the fn function is an actual function rather than a string: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 우리가 현재 &lt;i&gt;태그&lt;/i&gt;에 사용하는 객체 표기법과 꽤 비슷해 보입니다. 단지 &lt;code&gt;fn&lt;/code&gt;이 문자열이 아니라 &lt;i&gt;실제&lt;/i&gt; 함수라는 점만 다릅니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return { fn: resume, args: [name] };
}

function resume(name) {
  alert(&quot;Hello, &quot; + name);
}

const { fn, args } = greeting(); // 첫 번째 단계 실행
fn(...args); // 두 번째 단계 실행&lt;/code&gt;&lt;/pre&gt;
&lt;!-- It’s almost like greeting is returning a tag rather than a function call. It expresses the code it wants to run next but it doesn’t actually do that yet. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마치 &lt;code&gt;greeting&lt;/code&gt;이 함수 호출이 아니라 &lt;i&gt;태그&lt;/i&gt;처럼 코드의 구조를 반환하는 것처럼 보입니다. &lt;i&gt;실행하고자 하는 코드&lt;/i&gt;를 나타내긴 하지만, &lt;i&gt;아직&lt;/i&gt; 그 코드를 실행하지는 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- This gives us a new perspective for what tags really are. Yes, a tag is a potential function call. But another way to see it is that a tag is a pairing of code and data. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 태그를 바라보는 새로운 관점을 제공합니다. 물론 태그는 잠재적인 함수 호출인 건 맞습니다. 하지만 또 다른 관점에서 보면, 태그는 &lt;i&gt;코드와 데이터의 쌍&lt;/i&gt;이라고도 할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시간과 공간&lt;/h3&gt;
&lt;!-- Now let us recall how to split a computation across space. We’ve previously discovered one possible pattern for doing so—returning a piece of code as a string: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 연산을 공간에 따라 나누는 방법을 떠올려 봅시다. 이전에 그 방식의 일환으로 코드 조각을 문자열로 반환하는 것을 살펴보았습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return `function resume() {
    alert('Hello, ' + ${JSON.stringify(name)});
  }`;
}

const code = greeting();&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Then you could call greeting(), save the code it returns, and run it as code on another computer. The second computer will think this is the entire program: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 실행하면 &lt;code&gt;greeting()&lt;/code&gt;에서 반환된 &lt;code&gt;code&lt;/code&gt;를 저장한 뒤, 그것을 다른 컴퓨터에서 &lt;i&gt;코드로&lt;/i&gt; 실행할 수 있습니다. 두 번째 컴퓨터는 그것이 전체 프로그램이라고 생각할 것입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function resume() {
  alert(&quot;Hello, &quot; + &quot;Dan&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- But you know that the real program includes both pieces. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;i&gt;여러분은&lt;/i&gt; 실제 프로그램에는 두 조각이 모두 포함되어 있다는 것을 알고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Currently, greeting returns a string of code. However, it would be perfectly appropriate to think of it as returning both code and data. We just happen to be interpolating the data (the name variable) directly into that string of code. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 &lt;code&gt;greeting&lt;/code&gt;은 코드의 문자열을 반환합니다. 그러나 이를 코드&lt;i&gt;와&lt;/i&gt; 데이터 두 가지를 반환하는 것으로 생각해도 전혀 문제가 없습니다. 우리는 단지 데이터를(&lt;code&gt;name&lt;/code&gt; 변수) 코드 문자열에 직접 삽입하고 있을 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- This becomes more apparent if we move the resume code outside of greeting: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점은 &lt;code&gt;resume&lt;/code&gt; 코드를 &lt;code&gt;greeting&lt;/code&gt; 외부로 이동시키면 더 명확해집니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const RESUME_CODE = `
  function resume(name) {
    alert('Hello, ' + name);
  }`;

function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [RESUME_CODE, name];
}

const [code, data] = greeting();
const jsonString = JSON.stringify([code, data]);&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Now that resume takes name as an argument, the greeting needs to return both the code of the resume function and the data it needs (name). Then we could take [code, data], turn it to JSON with JSON.stringify, then JSON.parse it on another computer, and finally call code(data) to finish the program. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;resume&lt;/code&gt;이 &lt;code&gt;name&lt;/code&gt;을 인자로 받으므로, &lt;code&gt;greeting&lt;/code&gt;은 &lt;code&gt;resume&lt;/code&gt; 함수의 코드, 그리고 코드가 필요한 데이터(&lt;code&gt;name&lt;/code&gt;)를 모두 반환해야 합니다. 그러면 우리는 &lt;code&gt;[code, data]&lt;/code&gt;를 받아 &lt;code&gt;JSON.stringify&lt;/code&gt;로 JSON으로 변환한 후, 다른 컴퓨터에서 &lt;code&gt;JSON.parse&lt;/code&gt;로 이를 파싱합니다. 마지막으로 &lt;code&gt;code(data)&lt;/code&gt;를 호출하여 프로그램을 종료할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Of course, when we write our program, we don’t really want to think about the code of resume as a string. We want to think of it as a normal piece of code which is written at the top level, has syntax highlighting, can be typechecked, and so on: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 프로그램을 작성할 때 &lt;code&gt;resume&lt;/code&gt;의 코드를 &lt;i&gt;문자열&lt;/i&gt;로 생각하고 싶지는 않습니다. 그것을 정상적인 코드 조각으로 보고, 최상위 수준에 작성되어 구문 강조가 되며, 타입 체크가 가능하며, 그 외에도 여러 기능을 사용할 할 수 있는 코드로 생각하고 싶습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️
function resume(name) {
  alert(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- But how do we connect this piece of code to the greeting function? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 어떻게 코드 조각과 &lt;code&gt;greeting&lt;/code&gt; 함수를 &lt;i&gt;연결할&lt;/i&gt; 수 있을까요?&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [RESUME_CODE, name];
}
// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️&lt;/code&gt;&lt;/pre&gt;
&lt;!-- It’s like these functions exist in two different worlds—one existing “outside” of the string of code that’s about to be sent, and the other one existing “inside” of it. It’s like greeting is writing a story, and resume is someone inside of that story. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마치 이 함수들이 두 개의 다른 &lt;i&gt;세계에&lt;/i&gt; 존재하는 것 같습니다. 하나는 코드 문자열 밖에 &quot;외부&quot;에 존재하고, 다른 하나는 그 문자열 &quot;내부&quot;에 존재하는 것처럼요. 마치 &lt;code&gt;greeting&lt;/code&gt;이 이야기를 쓰고, &lt;code&gt;resume&lt;/code&gt;은 그 이야기 &lt;i&gt;안에&lt;/i&gt; 있는 사람처럼 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- There is a clear logical continuity between them, but they’re separated by a gap much wider than defined being in different files. When the greeting function runs, resume is merely a string—more like a plan or an idea than an actual function. On the other hand, when resume finally runs, it has no knowledge of greeting having ever existed—all it receives is the name passed down to it. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이들 사이에는 분명한 논리적 연속성이 있지만, 다른 파일에 정의된 것 이상으로 넓은 간격이 존재합니다. &lt;code&gt;greeting&lt;/code&gt; 함수가 실행될 때, &lt;code&gt;resume&lt;/code&gt;은 단지 문자열일 뿐입니다. 실제 함수라기보다는 계획이나 아이디어에 가까운 것입니다. 반면, &lt;code&gt;resume&lt;/code&gt;이 마침내 실행될 때는 &lt;code&gt;greeting&lt;/code&gt;이 존재했다는 사실을 전혀 알지 못합니다. 함수가 받는 것은 오직 &lt;code&gt;name&lt;/code&gt;일 뿐입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [RESUME_CODE, name];
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

function resume(name) {
  alert(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- If you squint at it, you can still make out the “true” shape of the program: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈을 살짝 찡그려 보시면 프로그램의 &quot;진짜&quot; 모습을 찾아낼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return `function resume() {
    alert('Hello, ' + ${JSON.stringify(name)});
  }`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- But this “split” way of looking at it is fairer to both worlds. It doesn’t prioritize one over the other. Both of them are our program, they’re just split by time and space: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 &quot;나눠도&quot; 두 부분 모두 동등하며 특정 부분이 더 중요한 게 아닙니다. 모두 우리의 프로그램이며, 다만 시간과 공간에 의해 나뉘어 있을 뿐입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [RESUME_CODE, name];
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

function resume(name) {
  alert(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- The question is, how do we tie them together? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 질문 하나가 남습니다. 이 둘을 어떻게 묶을까요?&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 세계&lt;/h3&gt;
&lt;!-- The simplest way to tie the two worlds together would be by giving each function in the Late world a unique name that lets us refer to it from the Early world. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 세계를 연결하는 가장 간단한 방법은 뒷 세계의 각 함수에 고유한 이름을 부여하여 앞 세계에서 참조할 수 있게 만드는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- For example, we could assume we’ll only ever need one function called resume: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 한 번만 호출되는 &lt;code&gt;resume&lt;/code&gt;이라는 함수가 필요하다고 가정하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [&quot;resume&quot;, name];
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

window[&quot;resume&quot;] = function resume(name) {
  alert(&quot;Hello, &quot; + name);
};&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Although this is a bit clunky, it does create an explicit (if fragile) connection. If we ever go about renaming resume in the Late world, we might remember to search the codebase for any other code might be referring to it, and we might find the greeting in the Early world. We could even add types for window['resume']. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 이 방식은 다소 번거롭긴 하지만, 명시적인(취약하기도 한) 연결을 &lt;i&gt;만듭니다&lt;/i&gt;. 만약 뒷 세계에서 &lt;code&gt;resume&lt;/code&gt;의 이름을 바꾼다면, 코드베이스에서 함수를 참조하고 있는 다른 코드들을 찾아야 하며, 앞 세계에서 &lt;code&gt;greeting&lt;/code&gt;을 찾을 수도 있습니다. 심지어 &lt;code&gt;window['resume']&lt;/code&gt;에 타입을 추가할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- This solution isn’t that bad. In fact, it’s similar to what’s happening under the hood when you refer to any of the Primitives built into the browser. You’re not directly importing them from anywhere; you just use a global name like p: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 해결책은 &lt;i&gt;그리&lt;/i&gt; 나쁘지 않습니다. 사실 &lt;i&gt;여러분이&lt;/i&gt; 브라우저에 내장된 기능을 참조할 때 일어나는 일과 유사합니다. 기능을 직접 어디서 가져오지 않고, &lt;code&gt;p&lt;/code&gt;와 같은 전역 이름을 이용해 참조합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return &amp;lt;p&amp;gt;Hello, {name}&amp;lt;/p&amp;gt;;
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

document.createElement = function (tagName) {
  switch (tagName) {
    case &quot;p&quot;:
    // ...
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;!-- In that sense, the browser internals are their own sort of a “Late” world. A large part of them is written in a different language than JavaScript and not directly exposed to your program. Much of the logic associated with a primitive like p—including applying styles, laying out text, drawing, compositing, painting, and so on—will run at some point after your document.createElement('p') call. In that sense, &lt;p&gt; really is a tag—a call that still requires some future “carrying out”. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 의미에서 브라우저 내부는 일종의 뒷 세계라고 할 수 있습니다. 내부의 많은 부분은 자바스크립트와 다른 언어로 작성되어 있으며, 프로그램에 직접 노출되지 않습니다. &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;와 같이 스타일 적용, 텍스트 레이아웃, 그리기, 합성, 페인팅 등 프리미티브와 관련된 많은 로직은 &lt;code&gt;document.createElement('p')&lt;/code&gt; 호출 &lt;i&gt;이후에&lt;/i&gt; 실행됩니다. 이런 관점에서 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;는 실제로 &lt;i&gt;태그&lt;/i&gt;입니다. 이 호출은 여전히 미래에 &quot;실행&quot;되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- But let’s not get distracted. Browser Primitives can afford to have global names because there’s a limited list of them, you need to be able to look them up, and they are always the same between the projects. On the other hand, if you define functions yourself, you probably want more explicit connections between them. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 너무 다른 이야기로 빠지지 말도록 하겠습니다. 브라우저의 프리미티브는 전역 이름을 가질 수 있습니다. 프리미티브는 정해져 있고, 찾아볼 수 있어야 하며, 프로젝트 간에 항상 동일하기 때문입니다. 그러나 여러분의 함수를 정의할 때는 더 명시적인 연결이 필요할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Let us come back to the pieces you want to connect: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 연결하고자 하는 부분으로 돌아가 봅시다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [RESUME_CODE, name];
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

function resume(name) {
  alert(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- An obvious first step would be to mark the resume function for export. You want the code in your other files to be able to refer to it. It’s not an implementation detail that can be freely removed. You don’t want it to appear like dead code! --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명한 첫 단계는 &lt;code&gt;resume&lt;/code&gt; 함수를 &lt;code&gt;export&lt;/code&gt;로 표시하는 것입니다. 다른 파일의 코드가 이 함수를 참조해야 하니까요. 아무 이유 없이 뺄 수 있는 사항이 아니며 죽은 코드처럼 보이지 않도록 해야 합니다!&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [RESUME_CODE, name];
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

export function resume(name) {
  alert(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Now that you exported it, the next logical step would be to import it here: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;export&lt;/code&gt; 했으니, 논리적으로 다음 단계는 여기에서 함수를 &lt;code&gt;import&lt;/code&gt; 하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { resume } from &quot;./resume&quot;;

function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [RESUME_CODE, name];
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

export function resume(name) {
  alert(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Except wait. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠시만요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- This doesn’t help you! --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 여러분에게 도움이 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- What you want to obtain is RESUME_CODE, which is this thing from earlier: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분이 얻고 싶은 것은 앞에서 말했던 &lt;code&gt;RESUME_CODE&lt;/code&gt; 였는데 말입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const RESUME_CODE = `
  function resume(name) {
    alert('Hello, ' + name);
  }`;&lt;/code&gt;&lt;/pre&gt;
&lt;!-- But what you got by importing resume is this other thing: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;resume&lt;/code&gt;을 &lt;i&gt;import&lt;/i&gt;하면 다른 것을 얻게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function resume(name) {
  alert(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- You’ve lost the backticks! --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 필요했던 문자열 코드가 없어졌습니다!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;간격(gap)을 주의하기&lt;/h3&gt;
&lt;!-- Let us thoroughly convince ourselves that using an import would not work. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;import&lt;/code&gt;를 사용하면 제대로 작동하지 않는 이유를 제대로 짚어 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Ultimately, what we’re trying to do is to modularize this pattern: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁극적인 목표는 이 패턴을 모듈화하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return `function resume() {
    alert('Hello, ' + ${JSON.stringify(name)});
  }`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- To do that, we’ve split the greeting and the resume functions in two different worlds—but as a result, we’ve lost the syntactic connection between them. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;greeting&lt;/code&gt; 함수와 &lt;code&gt;resume&lt;/code&gt; 함수를 서로 다른 두 세계로 분리했습니다. 하지만 그 결과, 둘 사이의 문법적인 연결이 사라졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Suppose that we try to bridge “the gap” between the worlds with an import: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 두 세계 사이의 간격을 &lt;code&gt;import&lt;/code&gt;로 연결하려 한다고 가정해 봅시다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { resume } from &quot;./resume&quot;;

function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [resume, name];
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

export function resume(name) {
  alert(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Unfortunately, unless we change something about how import works, this would essentially just “bring” the resume function itself into the greeting’s world: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안타깝게도 &lt;code&gt;import&lt;/code&gt;의 동작 방식을 바꾸지 않는 한, 이는 본질적으로 &lt;code&gt;resume&lt;/code&gt; 함수 &lt;i&gt;자체를&lt;/i&gt; &lt;code&gt;greeting&lt;/code&gt;의 세계로 &quot;가져오는&quot; 것에 불과합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function resume(name) {
  alert(&quot;Hello &quot;, +name);
}

function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [resume, name];
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

export function resume(name) {
  alert(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- In other words, the overall shape of the program would look kind of like this: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 프로그램의 전체적인 모양은 다음과 같은 형태입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return function resume() {
    alert(&quot;Hello, &quot; + name);
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리가 필요한 전체적인 모양은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return `function resume() {
    alert('Hello, ' + ${JSON.stringify(name)});
  }`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- It’s all about the backticks! --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 함수 형태를 문자열 그 자체로 가져오고 싶은 거죠!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- When we import something, we bring that code into the importing world. But what we want here is to merely refer to that code without executing any of it. We wanted greeting to return a story about a pumpkin—not an actual pumpkin. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무언가를 &lt;code&gt;import&lt;/code&gt;할 때, 그 코드를 통째로 불러오는 식으로 가져오게 됩니다. 하지만 우리가 원하는 것은 그 코드를 실행하지 않고 단순히 &lt;i&gt;참조만&lt;/i&gt; 하는 것입니다. &lt;code&gt;greeting&lt;/code&gt;이 실제 호박이 아닌 호박의 이야기를 반환하길 원했던 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- The problem with import becomes more apparent if you imagine that resume itself imports some third-party library—for example, to display a toast: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;resume&lt;/code&gt; 함수&lt;i&gt;가&lt;/i&gt; 토스트를 띄우기 위해 어떤 서드파티 라이브러리를 import 한다고 상상해 보면, &lt;code&gt;import&lt;/code&gt;의 문제점은 더 명확해집니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { resume } from &quot;./resume&quot;;

function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return [resume, name];
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

import { showToast } from &quot;toast-library&quot;;

export function resume(name) {
  showToast(&quot;Hello, &quot; + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- With a plain import, our entire program would have a shape equivalent to this: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 &lt;code&gt;import&lt;/code&gt;를 사용하면 전체 프로그램이 이와 같은 모양이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;// From toast-library
function initializeToastLibrary() {
  /_ ... _/;
}
function showToast(message) {
  /_ ... _/;
}
initializeToastLibrary();

function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return function resume() {
    showToast(&quot;Hello, &quot; + name);
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- However, the shape that we want is closer to something like this: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리가 &lt;i&gt;원한&lt;/i&gt; 모양은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return `
    // From toast-library
    function initializeToastLibrary() { /_ ... _/ }
    function showToast(message) { /_ ... _/ }
    initializeToastLibrary();

    function resume() {
      showToast('Hello, ' + name);
    }

  `;
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- The boundaries between the worlds are firm, as they should be. We want each world to behave consistently within itself—at least for any already existing code. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세계 사이의 경계는 견고하며, &lt;i&gt;그래야만&lt;/i&gt; 합니다. 적어도 이미 존재하는 코드에 대해서는 각 세계가 일관되게 동작하길 원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- To ensure that, imports from the Early world should become a part of the Early world. Imports from the Late world should become a part of the Late world. On its own, each world should behave like its own isolated program—no funny stuff. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 보장하기 위해, 앞 세계에서의 import는 뒷 세계의 일부가 &lt;i&gt;되어야&lt;/i&gt; 하고, 뒷 세계에서의 import는 뒷 세계의 일부가 &lt;i&gt;되어야&lt;/i&gt; 합니다. 각 세계는 그 자체로 독립된 프로그램처럼 동작해야 하며, 예외의 일이 일어나서는 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- We don’t want to break that consistency. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 그 일관성을 깨고 싶지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- All we need is a door. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 필요한 건 &lt;i&gt;문&lt;/i&gt; 하나입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;A Door&lt;/h3&gt;
&lt;!-- We need a way to say: “I want to refer to this thing in another file, but I don’t actually want to execute or even load any of its code. Just give me something that will let me programmatically find the code for that thing later.” Luckily, all of this is completely made up, so we can just make up some made-up syntax for that. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 원하는 건 이렇습니다. &amp;ldquo;나는 다른 파일에 있는 코드를 참조하고 싶지만, 실제로 그 코드가 실행되거나 로드되길 원하지는 않아. 단지 나중에 프로그래밍적으로 저 코드에 접근할 수 있는 무언가만 있으면 돼.&amp;rdquo; 다행히도 이 모든 건 전적으로 우리가 만든 개념이므로 이를 위한 가상의 문법도 만들어낼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Tada! --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짜잔!&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import tag { resume } from './resume';

function greeting() {
  const name = prompt('Who are you?');
  return [resume, name];
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

import { showToast } from 'toast-library';

export function resume(name) {
  showToast('Hello, ' + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- What, just like that? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐, &lt;i&gt;그냥 이렇게&lt;/i&gt; 하면 될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Sure, why not. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼요. 왜 안되겠어요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Okay, but what does this syntax do? --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋아요. 그런데 이 문법은 정확히 무슨 일을 하는 거죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Well, for starters, let’s imagine that it just returns the source code of the function. That would let us send that code to the other computer, as we originally intended: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음, 일단 처음에는 이렇게 상상해보는 게 어떨까요? 이 문법이 단순히 함수의 소스 코드를 반환한다고요. 그러면 애초에 의도했던 것처럼, 그 코드를 다른 컴퓨터로 전송할 수 있을 겁니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import tag { resume } from './resume';

function greeting() {
  const name = prompt('Who are you?');
  return [resume, name];
}

const [code, data] = greeting();
// [
// 'function resume(name) { showToast(&quot;Hello, &quot; + name); }',
// 'Dan'
// ]&lt;/code&gt;&lt;/pre&gt;
&lt;!-- However, this actually isn’t terribly useful—notice that showToast is nowhere to be found. We don’t really want the source code of the resume function alone, we want whatever’s necessary for another computer to be able to load and run resume. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사실 이건 그다지 유용하지 않습니다. &lt;code&gt;showToast&lt;/code&gt;가 어디에도 보이지 않는다는 점에 주목해 보세요. 우리는 &lt;i&gt;&lt;code&gt;resume&lt;/code&gt; 함수의&lt;/i&gt; 소스 코드&lt;i&gt;만&lt;/i&gt; 원하는 게 아니라, 다른 컴퓨터가 &lt;i&gt;&lt;code&gt;resume&lt;/code&gt;을 로드하고 실행할 수 있도록 필요한 모든 것&lt;/i&gt;을 원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Here’s a second idea. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 두 번째 아이디어를 생각해 볼까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- Why don’t we have it return some kind of identifier that’s uniquely designed for addressing code. For example, it could combine the filename and the export name: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 코드에 접근할 수 있도록 고유하게 설계된 식별자를 반환하지 않으면 되지 않을까요? 예를 들어, 파일 이름과 &lt;code&gt;export&lt;/code&gt; 이름을 결합하는 것이죠.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import tag { resume } from './resume';

function greeting() {
  const name = prompt('Who are you?');
  return [resume, name];
}

const [code, data] = greeting();
// [
// '/src/stuff/resume.js#resume',
// 'Dan'
// ]&lt;/code&gt;&lt;/pre&gt;
&lt;!-- Now, this means that the format would have to be somewhat aware of how the other computer loads and executes code. For example, if the other computer runs a Node.js process, it could import() that file from the filesystem—provided that it’ll be deployed to the other computer. If the other computer runs a web browser, it could import() that file over HTTP from a server that would have to serve it. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 즉, 형식이 다른 컴퓨터가 코드를 로드하고 실행하는 &lt;i&gt;방식&lt;/i&gt;을 어느 정도 인식해야 한다는 의미입니다. 어떤 컴퓨터가 Node.js 프로세스를 실행한다면, 해당 파일을 파일 시스템에서 &lt;code&gt;import()&lt;/code&gt;할 수 있을 겁니다. 단, 그것이 다른 컴퓨터에 배포되어 있어야 합니다. 만약 다른 컴퓨터가 웹 브라우저를 실행한다면, HTTP를 통해 서버에서 해당 파일을 &lt;code&gt;import()&lt;/code&gt;해야 할 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- In the case of a web browser, it might not be very efficient to import remote files one by one and to rely on the browser’s module system to download each of their dependencies. Instead, it might make sense to use an automated bundler (which would combine such code into chunks) and to use a bundler-specific identifier: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 브라우저의 경우, 원격 파일을 하나씩 가져오고 브라우저의 모듈 시스템에 의존하여 종속성을 다운로드하는 것이 그리 효율적이지 않습니다. 대신, 자동화된 &lt;i&gt;번들러&lt;/i&gt;로 코드를 청크로 만들어 결합하고, 번들러 전용 식별자를 사용하는 것이 더 합리적일 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import tag { resume } from './resume';

function greeting() {
const name = prompt('Who are you?');
return [resume, name];
}

const [code, data] = greeting();
// [
// 'chunk123#module456#resume',
// 'Dan'
// ]&lt;/code&gt;&lt;/pre&gt;
&lt;!-- In the simplest possible case, if all of the code destined for the Late world were ultimately assembled into a giant single file that gets sent to the other computer over the wire, this identifier could just be the referenced function’s global name: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순하게 생각해서 만약 뒷 세계로 갈 &lt;i&gt;모든&lt;/i&gt; 코드가 결국 하나의 거대한 파일로 합쳐져서 다른 컴퓨터로 전송된다면, 이 식별자는 참조된 함수의 전역 이름일 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import tag { resume } from './resume';

function greeting() {
const name = prompt('Who are you?');
return [resume, name];
}

const [code, data] = greeting();
// [
// 'window.resume',
// 'Dan'
// ]&lt;/code&gt;&lt;/pre&gt;
&lt;!-- What matters is that we now have a syntax for some code from the Early world to refer to some code from the Late world. It is a door between the two environments. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 이제 앞 세계의 코드가 뒷 세계의 코드를 참조할 수 있는 문법이 있다는 것입니다. 이는 두 환경 사이의 문이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;!-- It lets us achieve something like this: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 아래와 같은 것도 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  const name = prompt(&quot;Who are you?&quot;);
  return `
import { showToast } from 'toast-library';

    function resume() {
      showToast('Hello, ' + name);
    }

`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- by writing something like this: --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성하면 다음과 같은 결과를 갖게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import tag { resume } from './resume';

function greeting() {
  const name = prompt('Who are you?');
  return [resume, name];
}

// ✂️ ✂️ ✂️ ✂️ ✂️ ✂️

import { showToast } from 'toast-library';

export function resume(name) {
  showToast('Hello, ' + name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- It lets us write a single program spanning two programming environments. --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 두 개의 프로그래밍 환경을 아우르는 하나의 프로그램을 작성할 수 있게 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대청소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 두 세계 사이의 &lt;i&gt;문&lt;/i&gt;을 찾았습니다. 앞 세계와 뒷 세계 사이의 문인 &lt;code&gt;import tag&lt;/code&gt;는 시간과 공간 모두에 걸쳐 연산을 분리할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 문을 사용하기 전에, 우리의 집을 정리해야 합니다. 컴포넌트를 작성하기 위해 태그 문법을 더 좋게 만들어 봅시다. (리액트에 익숙하다면, 이것이 JSX와 더 가까워지는 것임을 알 수 있을 것입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예제를 살펴봅시다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p&amp;gt;
        The time is: &amp;lt;Clock /&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 우리는 이 문법이 아래와 같은 객체 트리를 생성한다고 가정했습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;function App() {
  return {
    fn: &quot;div&quot;,
    args: [
      { fn: &quot;Greeting&quot;, args: [] },
      {
        fn: &quot;p&quot;,
        args: [&quot;The time is: &quot;, { fn: &quot;Clock&quot;, args: [] }],
      },
    ],
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법도 좋지만, &lt;code&gt;&amp;lt;p className=&quot;text-purple-500&quot;&amp;gt;&lt;/code&gt;와 같이 이름이 있는 속성을 전달할 방법이 없습니다. 우리는 규칙을 바꿔서 위치 기반 &lt;code&gt;인수(args)&lt;/code&gt; 대신, 컴포넌트와 프리미티브 모두 &lt;i&gt;이름이 있는&lt;/i&gt; 인수를 가진 단일 객체를 받게 할 것입니다. 이 객체를 &quot;속성(properties)&quot;이라는 뜻의 &lt;code&gt;props&lt;/code&gt;라고 부르겠습니다. 중첩된 태그는 그 객체 안에 &lt;code&gt;children&lt;/code&gt;이라는 속성으로 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;function App() {
  return {
    type: &quot;div&quot;,
    props: {
      children: [
        { type: &quot;Greeting&quot;, props: {} },
        {
          type: &quot;p&quot;,
          props: {
            className: &quot;text-purple-500&quot;,
            children: [&quot;The time is: &quot;, { type: &quot;Clock&quot;, props: {} }],
          },
        },
      ],
    },
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 &lt;code&gt;fn&lt;/code&gt;을 &lt;code&gt;type&lt;/code&gt;으로 바꿨습니다. 이제 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;와 같은 프리미티브가 우리의 &lt;code&gt;p()&lt;/code&gt; 함수가 아닌 &lt;code&gt;document.createElement('p')&lt;/code&gt; (무엇이든)에 의해 처리되므로, &lt;code&gt;p&lt;/code&gt;를 &quot;함수&quot;라고 부르는 것은 오해의 소지가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 사항을 처리하기 위해 &lt;code&gt;interpret&lt;/code&gt;를 조정해야 합니다. 이전 모습이 기억나지 않더라도 걱정하지 마세요. 중요한 부분은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;function interpret(json, knownTags) {
  if (json &amp;amp;&amp;amp; json.type) {
    if (knownTags[json.type]) {
      let Component = knownTags[json.type];
      let props = json.props;
      let result = Component(props);
      return interpret(result, knownTags);
    } else {
      let children = json.props.children?.map((arg) =&amp;gt; interpret(arg, knownTags));
      let props = { ...json.props, children };
      return { type: json.type, props };
    }
  } else if (Array.isArray(json)) {
    return json.map((item) =&amp;gt; interpret(item, knownTags));
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;code&gt;className&lt;/code&gt;과 같은 속성을 적용하기 위한 새로운 로직으로 &lt;code&gt;perform&lt;/code&gt;도 조정해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;function perform(json) {
  if (json &amp;amp;&amp;amp; json.type) {
    let tagName = json.type;
    let node = document.createElement(tagName);
    for (let [propKey, propValue] of Object.entries(json.props)) {
      if (propKey === &quot;children&quot;) {
        let children = perform(propValue);
        for (let child of [children].flat().filter(Boolean)) {
          node.appendChild(child);
        }
      } else {
        node[propKey] = propValue;
      }
    }
    return node;
  } else if (typeof json === &quot;string&quot;) {
    return document.createTextNode(json);
  } else if (Array.isArray(json)) {
    return json.map(perform);
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;&amp;lt;p className=&quot;text-purple-500&quot;&amp;gt;&lt;/code&gt;이 작동합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-forked-sg7dnt?file=%2Fsrc%2Findex.mjs&quot;&gt;코드를 실행해 보세요.&lt;/a&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;더 많은 대청소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 또 다른 편의성 개선을 시도하기에 좋은 시점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 컴포넌트 트리를 프리미티브 트리로 변환하기 위해 알려진 모든 컴포넌트를 사전 형태로 &lt;code&gt;interpret&lt;/code&gt; 함수에 전달해야 한다는 것을 기억하세요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p&amp;gt;
        The time is: &amp;lt;Clock /&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}

function Clock() {
  return new Date().toString();
}

const primitives = interpret(&amp;lt;App /&amp;gt;, {
  App,
  Greeting,
  Clock,
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-forked-sg7dnt?file=%2Fsrc%2Findex.mjs&quot;&gt;코드를 실행해 보세요.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이것은 꽤 어리석게 느껴집니다. &lt;code&gt;&amp;lt;Greeting /&amp;gt;&lt;/code&gt;을 작성할 때, &lt;code&gt;Greeting&lt;/code&gt; 함수는 이미 스코프 내에 있습니다. 그렇지 않더라도, 연결을 명시적으로 하기 위해 스코프로 가져오고 &lt;i&gt;싶을&lt;/i&gt; 것입니다. 그렇다면 &lt;code&gt;Greeting&lt;/code&gt; 함수가 이미 스코프 내에 있다면, &lt;code&gt;&amp;lt;Greeting /&amp;gt;&lt;/code&gt; 구문이 어떤 &lt;code&gt;Greeting&lt;/code&gt;인지 &quot;기억&quot;하지 못할 이유가 없지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 규칙을 채택하여 이 문제를 해결할 수 있습니다. 태그가 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;처럼 소문자라면, 객체 표현에서 그 &lt;code&gt;타입&lt;/code&gt;은 문자열 &lt;code&gt;'div'&lt;/code&gt;로 유지됩니다. 하지만 태그가 &lt;code&gt;&amp;lt;Greeting /&amp;gt;&lt;/code&gt;처럼 대문자로 시작한다면, 그 &lt;code&gt;타입&lt;/code&gt;은 &lt;code&gt;'Greeting'&lt;/code&gt; 문자열이 아니라 &lt;code&gt;_Greeting 함수 자체_&lt;/code&gt;가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;function App() {
  return {
    type: &quot;div&quot;, // 프리미티브 (문자열)
    props: {
      children: [
        { type: Greeting, props: {} }, // 컴포넌트 (함수)
        {
          type: &quot;p&quot;, // 프리미티브 (문자열)
          props: {
            children: [
              &quot;The time is: &quot;,
              { type: Clock, props: {} }, // 컴포넌트 (함수)
            ],
          },
        },
      ],
    },
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편리하게도, 우리는 이미 프리미티브와 구별하기 위해 컴포넌트 이름을 대문자로 시작하고 있었기 때문에 이름을 바꿀 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;code&gt;interpret&lt;/code&gt; 함수를 간단하게 만들 수 있습니다. &lt;code&gt;knownTags&lt;/code&gt; 사전을 가지고 다니는 대신, 단순히 &lt;code&gt;typeof json.type&lt;/code&gt;을 확인합니다. &lt;code&gt;json.type&lt;/code&gt;이 함수라면, 그 &lt;i&gt;함수 자체&lt;/i&gt;가 컴포넌트입니다. 그렇지 않다면, 프리미티브일 것입니다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;function interpret(json) {
  if (json &amp;amp;&amp;amp; json.type) {
    if (typeof json.type === &quot;function&quot;) {
      let Component = json.type;
      let props = json.props;
      let result = Component(props);
      return interpret(result);
    } else {
      let children = json.props.children?.map(interpret);
      let props = { ...json.props, children };
      return { type: json.type, props };
    }
  } else if (Array.isArray(json)) {
    return json.map(interpret);
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 추가 정보 없이 &lt;code&gt;interpret&lt;/code&gt;를 호출할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const primitives = interpret(&amp;lt;App /&amp;gt;);
// {
//   type: 'div',
//   props: {
//     children: [{
//       type: 'p',
//       props: {
//         children: [
//           'Hello, ',
//           { type: 'input', props: { placeholder: 'Who are you?' } }
//         ]
//       }
//     }, {
//       type: 'p',
//       props: {
//         children: ['The time is ', 'Wed Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)']
//       }
//     }]
//   }
// }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;interpret&lt;/code&gt; 함수는 모든 컴포넌트를 바깥에서 안쪽으로 &quot;분해(dissolve)&quot;하여 프리미티브만 남겨둡니다. 그런 다음 &lt;code&gt;perform&lt;/code&gt; 함수가 모든 프리미티브를 안쪽에서 바깥으로 &quot;분해&quot;하여 최종 결과, 즉 브라우저 DOM 트리를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const tree = perform(primitives);
// [HTMLDivElement]
document.body.appendChild(tree);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-forked-n5gvxs?file=%2Fsrc%2Findex.mjs&quot;&gt;코드를 실행해 보세요.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보스 음악이 시작됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;공간의&lt;/i&gt; 도전이 시작됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;앞 세계와 뒷 세계의 컴포넌트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 전체 컴포넌트 트리가 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p&amp;gt;
        The time is: &amp;lt;Clock /&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}

function Clock() {
  return new Date().toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-forked-n5gvxs?file=%2Fsrc%2Findex.mjs&quot;&gt;코드를 실행해 보세요.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공간을 통제하려면, 이 연산을 두 개의 서로 다른 컴퓨터 사이에 분리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;code&gt;App&lt;/code&gt;과 &lt;code&gt;Greeting&lt;/code&gt;은 첫 번째 컴퓨터에서 실행되어야 하고, &lt;code&gt;Clock&lt;/code&gt; 컴포넌트는 두 번째 컴퓨터에서 실행되어야 합니다. 이 두 연산은 잘 결합되어 두 번째 컴퓨터에서 브라우저 DOM 트리로 변환되어야 하고, 컴포넌트 함수 내부의 코드는 수정하지 말아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계별로 해결해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 해야 할 일은 &lt;code&gt;Clock&lt;/code&gt;을 다른 파일로 옮기고 &lt;code&gt;내보내는(export)&lt;/code&gt; 것입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export function Clock() {
  return new Date().toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 메인 파일에서 가져올 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { Clock } from &quot;./Clock&quot;;

export function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p&amp;gt;
        The time is: &amp;lt;Clock /&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠깐만요, 이것은 두 컴퓨터 간에 코드를 분리하지 못합니다. 그러기 위해서는 &lt;code&gt;import&lt;/code&gt;를 &lt;code&gt;import tag&lt;/code&gt;로 변경함으로써 &lt;i&gt;문을 열어야&lt;/i&gt; 합니다. 앞 세계에서 문을 열면 즉시 &lt;i&gt;뒷 세계가 존재하게 됩니다.&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import tag { Clock } from './Clock';

export function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p&amp;gt;The time is: &amp;lt;Clock /&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}

export function Clock() {
  return new Date().toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;App&lt;/code&gt; 컴포넌트가 반환하는 태그를 검사해보면, &lt;code&gt;&amp;lt;Clock /&amp;gt;&lt;/code&gt; 태그가 특이한 것으로 변했음을 알 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;{
  type: 'div', // 프리미티브 (문자열)
  props: {
    children: [{
      type: Greeting, // 컴포넌트 (함수)
      props: {}
    }, {
      type: 'p',
      props: {
        children: [
          'The time is ',
          {
            type: '/src/Clock.js#Clock', // 이게 뭐지?
            props: {}
          }
        ]
      }
    }]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 최신 규칙에 따르면, 대문자로 시작하는 태그는 스코프 내의 해당 값을 &lt;code&gt;타입&lt;/code&gt;으로 사용합니다. 예를 들어, &lt;code&gt;&amp;lt;Greeting /&amp;gt;&lt;/code&gt;은 &lt;code&gt;Greeting&lt;/code&gt;이 함수인 &lt;code&gt;{ type: Greeting, props: {} }&lt;/code&gt;로 변환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;Clock /&amp;gt;&lt;/code&gt;도 마찬가지입니다. &lt;code&gt;Clock&lt;/code&gt;은 대문자로 시작하므로 &lt;code&gt;{ type: Clock, props: {} }&lt;/code&gt;이 됩니다. 그러나 &lt;code&gt;Clock&lt;/code&gt;은 일반 &lt;code&gt;import&lt;/code&gt;가 아니라 &lt;code&gt;import tag&lt;/code&gt;이며, 이는 일반 &lt;code&gt;import&lt;/code&gt;와는 다른 의미를 가집니다. &lt;code&gt;Clock&lt;/code&gt; &lt;i&gt;함수&lt;/i&gt;를 주는 대신, 일종의 &lt;i&gt;참조&lt;/i&gt;, 즉 나중에 다른 컴퓨터에서 &lt;code&gt;Clock&lt;/code&gt; 소스 코드를 로드할 수 있게 해주는 식별자를 줍니다. 그것이 바로 이 &lt;code&gt;'/src/Clock.js#Clock'&lt;/code&gt; 문자열입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 용어를 몇 가지 소개하겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;앞 세계 컴포넌트&lt;/b&gt;는 앞 세계에서 실행되는 컴포넌트입니다. 이 예제에서는 &lt;code&gt;App&lt;/code&gt;과 &lt;code&gt;Greeting&lt;/code&gt;이 앞 세계 컴포넌트입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;뒷 세계 컴포넌트&lt;/b&gt;는 뒷 세계에서 작업을 마치기 위해 보내지는 컴포넌트입니다. 이 예제에서는 &lt;code&gt;Clock&lt;/code&gt;만이 뒷 세계 컴포넌트입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 앞 세계 컴포넌트를 분해해야 합니다. 그러면 뒷 세계용 코드와 그 코드에 필요한 데이터를 얻게 됩니다. 그 코드로 뒷 세계를 구성하고, 거기서 뒷 세계 컴포넌트를 분해합니다. &lt;i&gt;그러면&lt;/i&gt; 프리미티브를 얻게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계획이 세워졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 세계에서 &lt;code&gt;interpret(&amp;lt;App /&amp;gt;)&lt;/code&gt;을 실행하고 결과를 살펴봅시다. 모든 앞 세계 컴포넌트(&lt;code&gt;App&lt;/code&gt;과 &lt;code&gt;Greeting&lt;/code&gt;)가 출력에서 사라진 걸 확인하세요.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;{
  type: 'div',
  props: {
    children: [{
      type: 'p',
      props: {
        children: [
          'Hello, ',
          { type: 'input', props: { placeholder: 'Who are you?' } }
        ]
      }
    }, {
      type: 'p',
      props: {
        children: [
          'The time is ',
          {
            type: '/src/Clock.js#Clock',
            props: {}
          }
        ]
      }
    }]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 것은 프리미티브(&lt;code&gt;'div'&lt;/code&gt;, &lt;code&gt;'p'&lt;/code&gt;, &lt;code&gt;'input'&lt;/code&gt;)와... 뒷 세계 컴포넌트(여기서는 &lt;code&gt;'/src/Clock.js#Clock'&lt;/code&gt;뿐)입니다. 뒷 세계 컴포넌트를 위해 특별히 해야 할 일은 없었습니다. 함수가 아니므로 &lt;code&gt;interpret&lt;/code&gt;는 실행하려고 시도하지 않고, 프리미티브와 유사하게 그대로 둡니다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;function interpret(json) {
  if (json &amp;amp;&amp;amp; json.type) {
    if (typeof json.type === &quot;function&quot;) {
      let Component = json.type;
      let props = json.props;
      let result = Component(props);
      return interpret(result);
    } else {
      let children = json.props.children?.map(interpret);
      let props = { ...json.props, children };
      return { type: json.type, props };
    }
  } else if (Array.isArray(json)) {
    return json.map(interpret);
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;interpret&lt;/code&gt;의 결과에는 함수가 포함되어 있지 않으므로, 네트워크를 통해 전송될 수 있는 문자열로 쉽게 변환할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const lateComponents = intepret(&amp;lt;App /&amp;gt;);
const jsonString = JSON.stringify(lateComponents);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에, 다른 컴퓨터에서, 이 문자열을 다시 객체로 변환할 수 있습니다. 바로 &lt;code&gt;perform&lt;/code&gt;에 전달하여 DOM 트리를 생성하고 싶을 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const lateComponents = JSON.parse(jsonString);
const tree = perform(lateComponents);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이렇게 하면 &lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-forked-t2v9js?file=%2Fsrc%2Findex.mjs&quot;&gt;오류가 발생합니다.&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;function perform(json) {
  if (json &amp;amp;&amp;amp; json.type) {
    let tagName = json.type;
    //   'document.createElement'를 'Document'에서 실행하지 못했습니다.
    // 제공된 태그 이름('/src/Clock.js#Clock')이 유효한 이름이 아닙니다.
    let node = document.createElement(tagName);
    // ...
    return node;
  } else {
    // ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞습니다. &lt;code&gt;perform&lt;/code&gt;은 프리미티브만 처리하지만, &lt;code&gt;Clock&lt;/code&gt;은 뒷 세계 컴포넌트입니다. 앞 세계 컴포넌트(&lt;code&gt;App&lt;/code&gt;, &lt;code&gt;Greeting&lt;/code&gt;)를 앞 세계에서 분해했습니다. 이제 뒷 세계에 있으므로, 뒷 세계 컴포넌트(&lt;code&gt;Clock&lt;/code&gt;)를 분해할 시간입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;interpret&lt;/code&gt;를 호출하여 남은 컴포넌트를 분해하려고 합니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const lateComponents = JSON.parse(jsonString);
const primitives = interpret(lateComponents);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 아무 일도 일어나지 않습니다. &lt;code&gt;'/src/Clock.js#Clock'&lt;/code&gt; 태그는 여전히 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;공간이 비웃습니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아, 그렇죠. &lt;code&gt;interpret&lt;/code&gt;는 함수만 실행하려고 시도합니다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;function interpret(json) {
  if (json &amp;amp;&amp;amp; json.type) {
    if (typeof json.type === &quot;function&quot;) {
      let Component = json.type;
      let props = json.props;
      let result = Component(props);
      return interpret(result);
    } else {
      let children = json.props.children?.map(interpret);
      let props = { ...json.props, children };
      return { type: json.type, props };
    }
  } else if (Array.isArray(json)) {
    return json.map(interpret);
  } else {
    return json;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리가 가진 것은 단지 &lt;i&gt;참조&lt;/i&gt;, 즉 &lt;code&gt;Clock&lt;/code&gt; 함수를 어디서 &lt;i&gt;가져올&lt;/i&gt; 수 있는지 알려주는 주소일 뿐입니다. 여전히 이 컴퓨터에서 실제로 로드해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;공간이 이것을 건네줍니다&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function loadReference(lateReference) {
  // 네트워크를 통해 또는 번들러 캐시에서 로드되었다고 가정합니다.
  await new Promise((resolve) =&amp;gt; setTimeout(resolve, 3000));
  if (lateReference === &quot;/src/Clock.js#Clock&quot;) {
    return Clock;
  } else {
    throw Error(&quot;모듈을 찾을 수 없습니다.&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋습니다. 이 작업을 해주는 함수가 주어졌다고 가정해보겠습니다. 아마도 환경에 의해, 또는 번들러 작업을 하는 친절한 사람들에 의해 제공될 것입니다. &lt;code&gt;'/src/Clock.js#Clock'&lt;/code&gt;을 이 함수에 전달하면, 비동기적으로 &lt;code&gt;Clock&lt;/code&gt;을 로드합니다.&lt;/p&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;await loadReference(&quot;/src/Clock.js#Clock&quot;);
// function Clock(){}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 퍼즐을 완성하는 마지막 조각이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;JSON.parse&lt;/code&gt; 함수가 참조처럼 보이는 것을 발견할 때마다, 그것을 &lt;code&gt;loadReference()&lt;/code&gt;에 전달하고 각각의 Promise를 보관합니다.&lt;/p&gt;
&lt;pre class=&quot;q&quot;&gt;&lt;code&gt;const pendingPromises = [];
const lateComponents = JSON.parse(jsonString, (key, value) =&amp;gt; {
  if (typeof value?.type === &quot;string&quot; &amp;amp;&amp;amp; value.type.includes(&quot;#&quot;)) {
    // `value.type`은 참조이지만, 우리는 함수를 원합니다.
    // 그 함수를 로드하기 시작합니다.
    const promise = loadReference(value.type).then((fn) =&amp;gt; {
      // 함수가 로드되면, 파싱된 JSON에 직접 대체합니다.
      value.type = fn;
    });
    // 언제 완료되는지 추적합니다.
    pendingPromises.push(promise);
  }
  return value;
});

// 모든 참조가 로드될 때까지 기다립니다.
await Promise.all(pendingPromises);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조작 후, &lt;code&gt;lateComponents&lt;/code&gt; 객체는 다음과 같이 보일 것입니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;{
  type: 'div',
  props: {
    children: [{
      type: 'p',
      props: {
        children: [
          'Hello, ',
          { type: 'input', props: { placeholder: 'Who are you?' } }
        ]
      }
    }, {
      type: 'p',
      props: {
        children: [
          'The time is ',
          {
            type: Clock, // 로드된 함수!
            props: {}
          }
        ]
      }
    }]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 뒷 세계 컴포넌트와 프리미티브만 있습니다. 모든 참조가 로드되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 마침내 &lt;code&gt;interpret&lt;/code&gt;에 전달하여 &lt;code&gt;Clock&lt;/code&gt;을 실행할 수 있습니다. 그러면 &lt;code&gt;perform&lt;/code&gt;을 통해 DOM으로 변환할 수 있는 프리미티브 트리가 생성됩니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const primitives = interpret(lateComponents);
const tree = perform(primitives);
document.body.appendChild(tree);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이것으로 완료되었습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-forked-k9jl8g?file=%2Fsrc%2Findex.mjs&quot;&gt;코드를 실행해 보세요.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 그림을 다시 살펴보고 어떻게 작동하는지 요약해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 세계에서는 &lt;code&gt;interpret&lt;/code&gt;으로 모든 앞 세계 컴포넌트를 분해합니다. 이는 뒷 세계에서 연산을 마치는 방법을 나타내는 문자열을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const lateComponents = intepret(&amp;lt;App /&amp;gt;);
const jsonString = JSON.stringify(lateComponents);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒷 세계에서는 그 문자열을 파싱하고, 참조를 로드한 다음, &lt;code&gt;interpret&lt;/code&gt;으로 뒷 세계 컴포넌트를 분해합니다. 그러면 프리미티브 트리만 남게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const pendingPromises = [];
const lateComponents = JSON.parse(jsonString, (key, value) =&amp;gt; {
  if (typeof value?.type === &quot;string&quot; &amp;amp;&amp;amp; value.type.includes(&quot;#&quot;)) {
    const promise = loadReference(value.type).then((fn) =&amp;gt; {
      value.type = fn;
    });
    pendingPromises.push(promise);
  }
  return value;
});

await Promise.all(pendingPromises);
const primitives = interpret(lateComponents);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 이 프리미티브들은 DOM이나 다른 형식으로 변환될 준비가 되어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const tree = perform(json);
document.body.appendChild(tree);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-forked-k9jl8g?file=%2Fsrc%2Findex.mjs&quot;&gt;코드를 실행해 보세요.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;축하합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간과 공간 모두에 걸쳐 연산을 분리했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도넛&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공간이 여러분 앞에서 접히며, 마침내 여러분을 동등한 존재로 인정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&quot;잘 했어요.&quot;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 공간은 비켜주지 않습니다. 대신, 공간은 계속해서 접히고, 자신을 이상한 모양으로 뒤틀며 앞으로, 그리고 안팎으로 뒤집히며, 중앙에 웜홀을 형성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마치 도넛처럼 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 것을 아우르는, 아름답고도, 두려운 도넛.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&quot;하지만 아직 끝나지 않았어요.&quot;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠깐... 이 목소리를 전에 들어본 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&quot;시간인가요?&quot;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 체력 바가 나타납니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;합성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 여러분의 프로그램이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import tag { Clock } from './Clock';

export function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p&amp;gt;
        The time is: &amp;lt;Clock /&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}
export function Clock() {
  return new Date().toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/devbox/r2c-forked-k9jl8g?file=%2Fsrc%2Findex.mjs&quot;&gt;코드를 실행해 보세요.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시공간을 이기려면, &lt;code&gt;Clock&lt;/code&gt;은 앞 세계의 시간을 표시하되, &lt;code&gt;Clock&lt;/code&gt; 주변의 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; 색상은 뒷 세계에서 결정되도록 변경해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 부분은 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 세계의 시간을 &lt;code&gt;Clock&lt;/code&gt;에 표시하기 위해서는 그냥 다시 위로 끌어올리면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p&amp;gt;
        The time is: &amp;lt;Clock /&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}

function Clock() {
  return new Date().toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; 색상을 지정해야 합니다. &lt;code&gt;perform&lt;/code&gt; 함수가 이미 &lt;code&gt;style&lt;/code&gt; 속성을 처리할 줄 안다고 가정하고 다음과 같이 색상을 지정해 봅시다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;p
  style={{
    color: prompt(&quot;Pick a color:&quot;),
  }}
&amp;gt;
  &amp;lt;Clock /&amp;gt;
&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋습니다만, 시공간은 &lt;code&gt;prompt&lt;/code&gt;가 뒷 세계에만 존재한다고 말합니다. 현재 &lt;code&gt;App&lt;/code&gt; 컴포넌트는 &lt;code&gt;prompt&lt;/code&gt;가 존재하지 않는 앞 세계에 정의되어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p
        style={{
          //   ReferenceError: prompt is not defined.
          color: prompt(&quot;Pick a color:&quot;),
        }}
      &amp;gt;
        &amp;lt;Clock /&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}

function Clock() {
  return new Date().toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;App&lt;/code&gt; 컴포넌트 자체를 뒷 세계로 옮기면 어떨까요? 이렇게 하면 &lt;code&gt;prompt&lt;/code&gt; 문제는 해결되지만, &lt;code&gt;Greeting&lt;/code&gt;과 &lt;code&gt;Clock&lt;/code&gt; 모두 뒷 세계에서 사용할 수 없게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}

function Clock() {
  return new Date().toString();
}
export function App() {
  //   ReferenceError: Greeting is not defined
  //   ReferenceError: Clock is not defined
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p
        style={{
          color: prompt(&quot;Pick a color:&quot;),
        }}
      &amp;gt;
        &amp;lt;Clock /&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Greeting&lt;/code&gt;과 &lt;code&gt;Clock&lt;/code&gt;도 아래로 옮기면 어떨까요?&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p
        style={{
          color: prompt(&quot;Pick a color:&quot;),
        }}
      &amp;gt;
        &amp;lt;Clock /&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}

function Clock() {
  return new Date().toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠깐만요, 하지만 우리는 &lt;code&gt;Clock&lt;/code&gt;이 앞 세계의 시간을 표시하기를 원했습니다. 아래로 옮길 수는 없습니다. 이것은 꽤 골치 아픈 문제가 되고 있습니다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;App&lt;/code&gt;은 뒷 세계에 두되, &lt;code&gt;import tag&lt;/code&gt;를 사용해 앞 세계의 &lt;code&gt;Greeting&lt;/code&gt;과 &lt;code&gt;Clock&lt;/code&gt;을 참조해보면 어떨까요? 시도해 봅시다.&lt;/p&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;export function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}

export function Clock() {
  return new Date().toString();
}
//   뒷 세계 모듈에서 앞 세계 태그를 가져올 수 없습니다.
import tag { Clock, Greeting } from './early';

export function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p style={{
        color: prompt('Pick a color:')
      }}&amp;gt;
        &amp;lt;Clock /&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니오, 이것은 말이 되지 않습니다. 백틱 &lt;i&gt;안의&lt;/i&gt; 함수가 백틱 &lt;i&gt;밖의&lt;/i&gt; 함수를 호출할 수 없는 것과 같은 이유입니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function greeting() {
  function showToast() {
    /* ... */
  }

  return `function resume() {
    const name = prompt('Who are you?');
    //   ReferenceError: showToast is not defined
    showToast('Hello, ' + name);
  }`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;import tag&lt;/code&gt; 구문은 &lt;i&gt;위에서 아래로만&lt;/i&gt; 가져올 수 있고, &lt;i&gt;아래에서 위로는&lt;/i&gt; 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시공간 도넛이 여러분 주변을 좁혀오기 시작합니다. 생각할 시간이 많지 않습니다. 반쯤 잊혀진 꿈에서 마지막 아이디어가 떠오릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;import tag&lt;/code&gt; 구문은 &lt;i&gt;아래 세계&lt;/i&gt;에서만 가져올 수 있습니다. 하지만 &lt;i&gt;네트워크 경계를 넘어&lt;/i&gt; 함수를 가져올 수 있는 &lt;code&gt;import rpc&lt;/code&gt; 구문도 만들지 않았나요? 앞 세계가 아직 어딘가에 있다면, 아마도 여러분의 요청에 응답하여 &lt;code&gt;Greeting&lt;/code&gt;과 &lt;code&gt;Clock&lt;/code&gt;의 결과를 반환할 수 있지 않을까요?&lt;/p&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;export function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}

export function Clock() {
  return new Date().toString();
}
import rpc { Clock, Greeting } from './early';

export function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;p style={{
        color: prompt('Pick a color:')
      }}&amp;gt;
        &amp;lt;Clock /&amp;gt;
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도넛이 흔들리며 잠시 소용돌이가 멈춥니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 해결책이었나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동하는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&quot;추가 네트워크 호출은 안됩니다. 한 번에 모든 것을 해야 합니다.&quot;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도넛이 다시 소용돌이치며 여러분을 감싸기 시작합니다. 웜홀이 점점 더 가까워집니다. 더 이상 두렵지 않고, 오히려 반기게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문득 생각이 떠오릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각이라기보다는 그림이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형태.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;import tag { Donut } from './Donut';

export function App() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Greeting /&amp;gt;
      &amp;lt;Donut&amp;gt;
        The time is: &amp;lt;Clock /&amp;gt;
      &amp;lt;/Donut&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

function Greeting() {
  return (
    &amp;lt;p&amp;gt;
      Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}

function Clock() {
  return new Date().toString();
}
export function Donut({ children }) {
  return (
    &amp;lt;p style={{
      color: prompt('Pick a color:')
    }}&amp;gt;
      {children}
    &amp;lt;/p&amp;gt;
  );
}

export function Donut({ children }) {
  return (
    &amp;lt;p style={{
      color: prompt('Pick a color:')
    }}&amp;gt;
      {children}
    &amp;lt;/p&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에서 미래를 &lt;i&gt;부를&lt;/i&gt; 수는 없지만, 과거를 미래로 &lt;i&gt;감쌀&lt;/i&gt; 수는 있습니다. 이것이 무슨 의미인지는 모르지만, 이제 어떤 규칙도 어기지 않고 있다는 것을 알고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 이것은 작동해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;눈을 감습니다.&lt;/i&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;꿈의 연속&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태초에 태그가 있었고, 태그는 앞 세계에 있었으며, 태그는 &lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt;이었습니다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;&amp;lt;App /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;App&lt;/code&gt;이란 무엇인가요? 그것은 &lt;code&gt;&amp;lt;Greeting&amp;gt;&lt;/code&gt;이 있는 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;이고, &lt;code&gt;&amp;lt;Clock /&amp;gt;&lt;/code&gt;이 있는 &lt;code&gt;&amp;lt;Donut&amp;gt;&lt;/code&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;Greeting /&amp;gt;
  &amp;lt;Donut&amp;gt;
    The time is: &amp;lt;Clock /&amp;gt;
  &amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;란 무엇인가요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;Greeting /&amp;gt;
  &amp;lt;Donut&amp;gt;
    The time is: &amp;lt;Clock /&amp;gt;
  &amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 알 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;Greeting /&amp;gt;&lt;/code&gt;은 무엇인가요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;Greeting /&amp;gt;
  &amp;lt;Donut&amp;gt;
    The time is: &amp;lt;Clock /&amp;gt;
  &amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것은 &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;이 있는 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;Donut&amp;gt;
    The time is: &amp;lt;Clock /&amp;gt;
  &amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;란 무엇인가요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;Donut&amp;gt;
    The time is: &amp;lt;Clock /&amp;gt;
  &amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 알 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;이란 무엇인가요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;Donut&amp;gt;
    The time is: &amp;lt;Clock /&amp;gt;
  &amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 알 수 없습니다. &lt;code&gt;Donut&lt;/code&gt;이란 무엇인가요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;Donut&amp;gt;
    The time is: &amp;lt;Clock /&amp;gt;
  &amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 알 수 없습니다. &lt;code&gt;Clock&lt;/code&gt;이란 무엇인가요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;Donut&amp;gt;
    The time is: &amp;lt;Clock /&amp;gt;
  &amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것은 이 세계의 시간입니다, 이 세계는 앞 세계이며, 그 시간이 끝날 때가 왔습니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;Donut&amp;gt;The time is: Wed Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)&amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 가 &lt;code&gt;App&lt;/code&gt;, 잘 가 &lt;code&gt;Greeting&lt;/code&gt;, 잘 가 &lt;code&gt;Clock&lt;/code&gt;.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(모뎀 소리)&lt;/i&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;란 무엇인가요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;Donut&amp;gt;The time is: Wed Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)&amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 신경쓰지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;란 무엇인가요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;Donut&amp;gt;The time is: Wed Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)&amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 신경쓰지 않습니다. &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;이란 무엇인가요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;Donut&amp;gt;The time is: Wed Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)&amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 신경쓰지 않습니다. &lt;code&gt;&amp;lt;Donut&amp;gt;&lt;/code&gt;이란 무엇인가요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;Donut&amp;gt;The time is: Wed Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)&amp;lt;/Donut&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;로드해 봅시다.&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;chunk123.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아, &lt;code&gt;Donut&lt;/code&gt;은 사용자가 선택한 색상의 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;입니다. 선택하세요!&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;p style={{ color: &quot;purple&quot; }}&amp;gt;The time is: Wed Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;선택하셨습니다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;란 무엇인가요?&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;p style={{ color: &quot;purple&quot; }}&amp;gt;The time is: Wed Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 신경쓰지 않습니다. 그것은 우리의 일이 아닙니다. 잘 가 &lt;code&gt;Donut&lt;/code&gt;; 이것을 C++의 한 조각에 넘겨줍시다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
  &amp;lt;p&amp;gt;
    Hello, &amp;lt;input placeholder=&quot;Who are you?&quot; /&amp;gt;
  &amp;lt;/p&amp;gt;
  &amp;lt;p style={{ color: &quot;purple&quot; }}&amp;gt;The time is: Wed Apr 09 2025 15:13:04 GMT+0900 (Japan Standard Time)&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/sandbox/8dgdz8?file=%2Fsrc%2Findex.mjs&quot;&gt;코드를 실행해 보세요.&lt;/a&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에필로그&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다루지 못한 내용이 더 있지만 안타깝게도 지면이 부족해지고 있습니다. 여기 의욕 있는 독자가 이 생각의 흐름을 계속 따라간다면 발견할 수 있는 몇 가지 사항들이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;포이즌 필(Poison Pills)&lt;/b&gt;: 코드베이스가 커질수록, 특정 순간에 어떤 세계에 있는지 고민하기보다는 의존하고 있는 기능만 명시하고 싶어질 것입니다. 예를 들어, 데이터베이스에서 읽기를 수행하는데 전체 데이터베이스가 앞 세계에만 존재한다면, 뒷 세계에서 데이터베이스 모듈을 가져오려고 할 때 즉시 빌드 오류가 발생하도록(예: 데이터베이스 코드를 번들링하려고 시도하는 대신) &quot;포이즌 필&quot;을 넣는 방법이 필요할 것입니다. Node.js에서는 &lt;a href=&quot;https://nodejs.org/api/packages.html#resolving-user-conditions&quot;&gt;사용자 정의 조건&lt;/a&gt;이 이를 강제하는 편리한 방법을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지시어(Directives)&lt;/b&gt;: &lt;code&gt;import tag&lt;/code&gt;와 &lt;code&gt;import rpc&lt;/code&gt;는 이론적으로는 우아하지만, 실제 사용하기에는 그리 좋지 않습니다. 세계 간의 기술적 분리는 확고히 유지되어야 합니다. 그러나 정신적으로는 점차 어떤 세계에 있는지 상관없이 코드를 작성하는 방향으로 바뀔 것입니다. 포이즌 필을 이용해 &lt;i&gt;잘못된&lt;/i&gt; 세계에서 코드가 실행되지 않도록 강제함으로써, 빌드 오류에 대응하여 코드를 이동하고 새로운 &quot;문&quot;을 만들면서 대부분 자동으로 경계를 이동할 수 있습니다. &quot;문&quot;을 만들고 싶을 때, &lt;i&gt;가져오는(&lt;code&gt;import&lt;/code&gt;)&lt;/i&gt; 위치보다 &lt;i&gt;내보내는(&lt;code&gt;export&lt;/code&gt;)&lt;/i&gt; 위치 옆에 표시하는 것이 더 자연스럽다는 것을 알게 될 것입니다. 이렇게 하면 경계를 빠르게 존재하거나 존재하지 않게 &quot;이동&quot;할 수 있습니다. 가져온 모듈이 어떤 세계에 있는지는 구현 세부사항이 됩니다. 내보내기를 주석으로 표시하는 한 가지 방법은 &lt;a href=&quot;https://stackoverflow.com/a/37535869&quot;&gt;지시어 구문&lt;/a&gt;을 (남용)하는 것입니다. 또한 앞 세계와 뒷 세계를 더 의미를 잘 드러내는 이름으로 바꾼다면(예: &quot;앞 세계&quot;는 &quot;서버&quot;가 되고 &quot;뒷 세계&quot;는 &quot;클라이언트&quot;가 됨), &lt;code&gt;import tag&lt;/code&gt;는 &lt;code&gt;export&lt;/code&gt; 옆에 &lt;a href=&quot;https://react.dev/reference/rsc/use-client&quot;&gt;&lt;code&gt;'use client'&lt;/code&gt;&lt;/a&gt;로 대체될 수 있고, &lt;code&gt;import rpc&lt;/code&gt;는 &lt;a href=&quot;https://react.dev/reference/rsc/use-server&quot;&gt;&lt;code&gt;'use server'&lt;/code&gt;&lt;/a&gt;로 바뀔 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 가져오기(Data Fetching)&lt;/b&gt;: 앞 세계(또는 선호에 따라 서버 세계)는 낮은 지연 환경에 코드를 배포할 기회가 있기 때문에 데이터 가져오기에 완벽한 장소입니다. &quot;생각하기&quot; 단계가 비동기적으로 진행되도록 코드를 조정하는 것은 어렵지 않을 것입니다. 연습 삼아 이를 처리할 수 있는지 확인해 보세요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스트리밍 실행(Streaming Execution)&lt;/b&gt;: 우리의 예제에서는 계산의 모든 단계가 순차적으로 일어납니다. 이전 단계가 완전히 끝날 때까지 시작하지 않습니다. 그러나 실제로는 컴포넌트가 바깥에서 안쪽으로 실행될 수 있기 때문에, &lt;i&gt;모든 단계&lt;/i&gt;를 혼합하여 차단 없이 실행할 수 있습니다. 특히, 뒷 세계 컴포넌트(또는 클라이언트 컴포넌트라고 부를까요?)의 전체 JSON 트리를 기다리는 대신, 미완료된 계산 자리에 &quot;구멍&quot;을 남기고 나중에 그 구멍을 더 많은 JSON으로 채울 수 있는 특별한 와이어 형식을 개발할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 있는 뒷 세계(Stateful Late World)&lt;/b&gt;: 상태 개념을 도입하면 뒷 세계 컴포넌트가 훨씬 더 유용해집니다. 이는 다시 한번 태그가 &lt;i&gt;잠재적인&lt;/i&gt; 함수 호출이라는 점을 강조합니다. 호출될 수도 있고, 호출되지 않을 수도 있으며, 또는 &lt;i&gt;여러 번 호출될 수도 있습니다.&lt;/i&gt; 뒷 세계 컴포넌트의 상태를 변경할 때마다, 앞 세계 컴포넌트에 영향을 주지 않고 다시 실행할 수 있습니다. 이는 상태 변경이 예측 가능하게 즉시 이루어지도록 보장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;앞 세계와 뒷 세계의 용도 변경&lt;/b&gt;: 앞 세계와 뒷 세계가 반드시 &quot;서버&quot;와 &quot;클라이언트&quot;라는 기존 개념과 1:1로 매핑될 필요는 없다는 점을 명심하세요. 예를 들어, 뒷 세계 컴포넌트가 상태를 가지고 있고, 서버가 있다면, 서버에서 앞 세계와 뒷 세계를 &lt;i&gt;모두&lt;/i&gt; 실행하는 것이 유용할 수 있습니다. 서버에서는 &lt;i&gt;초기&lt;/i&gt; 상태로 뒷 세계를 호출하여 초기 프리미티브 트리를 생성하고, 이를 HTML과 같은 형식으로 변환할 수 있습니다. 이를 통해 뒷 세계 컴포넌트가 사용자의 기기에 로드되기 전에 매우 빠르게 콘텐츠를 표시하기 시작할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐싱&lt;/b&gt;: 앞 세계가 요청 시에 실행될 필요는 없습니다. 실제로, 미리 실행하고 계산의 중간 결과를 저장할 수 있습니다(이는 종종 정적 사이트 생성이라고 알려져 있습니다). 욕심을 내본다면, 요청 간에 계산 부분을 재사용하기 위한 또 다른 세계&amp;mdash;&lt;a href=&quot;https://nextjs.org/docs/app/api-reference/directives/use-cache&quot;&gt;캐시 세계&lt;/a&gt;&amp;mdash;를 추가할 수도 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 예제를 직접 실행해 보고 싶다면 여기 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://codesandbox.io/p/sandbox/8dgdz8?file=%2Fsrc%2Findex.mjs&quot;&gt;최종 코드를 실행해 보세요.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 코드를 사용해 보고 싶지만 프레임워크를 사용하고 싶지 않다면, &lt;a href=&quot;https://parceljs.org/recipes/rsc/&quot;&gt;Parcel이 최근에 리액트 서버 컴포넌트 지원&lt;/a&gt;을 출시했으니 확인해 보세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어주셔서 감사합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt; &amp;nbsp;한국어로&amp;nbsp;된&amp;nbsp;프런트엔드&amp;nbsp;아티클을&amp;nbsp;빠르게&amp;nbsp;받아보고&amp;nbsp;싶다면&amp;nbsp;&lt;a href=&quot;https://kofearticle.substack.com/&quot;&gt;Korean&amp;nbsp;FE&amp;nbsp;Article&lt;/a&gt;을&amp;nbsp;구독해주세요!&lt;/blockquote&gt;</description>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/363</guid>
      <comments>https://imnotadevleoper.tistory.com/363#entry363comment</comments>
      <pubDate>Wed, 25 Jun 2025 02:42:32 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 클린 코드의 심리학: 우리가 지저분한 리액트 컴포넌트를 작성하는 이유</title>
      <link>https://imnotadevleoper.tistory.com/362</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://cekrem.github.io/posts/psychology-of-clean-code/&quot;&gt;The Psychology of Clean Code: Why We Write Messy React Components&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 모두 클린 코드를 작성해야 한다는 것을 알고 있습니다. 관련한 책을 읽기도하고, 세미나도 참석하며 여러 원칙을 이해하고 공감했습니다. 그런데도 여전히 지저분한 리액트 컴포넌트를 작성하고 있는 자신을 발견하게 됩니다. 왜 그럴까요? 그 이유는 기술력 부족이 아니라, 우리의 심리적인 요인에 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인지 부하의 함정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 일반적인 상황을 생각해 보세요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const UserDashboard = () =&amp;gt; {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [filter, setFilter] = useState(&quot;&quot;);
  const [sortBy, setSortBy] = useState(&quot;name&quot;);
  const [page, setPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1);

  useEffect(() =&amp;gt; {
    fetchUsers();
  }, [filter, sortBy, page]);

  const fetchUsers = async () =&amp;gt; {
    try {
      setLoading(true);
      const response = await fetch(`/api/users?filter=${filter}&amp;amp;sort=${sortBy}&amp;amp;page=${page}`);
      const data = await response.json();
      setUsers(data.users);
      setTotalPages(data.totalPages);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleFilterChange = (e) =&amp;gt; setFilter(e.target.value);
  const handleSortChange = (e) =&amp;gt; setSortBy(e.target.value);
  const handlePageChange = (newPage) =&amp;gt; setPage(newPage);

  if (loading) return &amp;lt;LoadingSpinner /&amp;gt;;
  if (error) return &amp;lt;ErrorMessage error={error} /&amp;gt;;

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;FilterBar filter={filter} onFilterChange={handleFilterChange} sortBy={sortBy} onSortChange={handleSortChange} /&amp;gt;
      &amp;lt;UserList users={users} /&amp;gt;
      &amp;lt;Pagination currentPage={page} totalPages={totalPages} onPageChange={handlePageChange} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컴포넌트는 나쁘지는 않지만, 그렇다고 좋다고 할 수도 없습니다. 너무 많은 일을 담당하고 있고, 다양한 관심사를 한곳에서 다루고 있어, 유지보수가 어려운 구조입니다. 그럼에도 불구하고, 이런 코드는 우리가 시간에 쫓기거나 빠르게 작업하려 할 때 자주 작성하게 되는 컴포넌트의 전형적인 모습입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;우리가 지저분한 코드를 작성하는 이유&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 계획 오류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 항상 작업에 걸리는 시간을 과소평가합니다. 이는 다음과 같은 결과를 초래합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마감일을 맞추기 위해 서두름&lt;/li&gt;
&lt;li&gt;지름길을 선택함&lt;/li&gt;
&lt;li&gt;리팩터링을 건너뜀&lt;/li&gt;
&lt;li&gt;모범 사례를 무시함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 매몰 비용 오류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 코드를 작성하고 나면 우리는 그것을 쉽게 바꾸려 하지 않습니다. 그 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미 많은 시간을 투자했기 때문에&lt;/li&gt;
&lt;li&gt;자신의 해결책에 감정적으로 집착하게 되기 때문에&lt;/li&gt;
&lt;li&gt;기존 기능이 동작하지 않을까봐 두렵기 때문에&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 복잡성 편향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 종종 다음과 같은 행동을 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순한 해결책을 지나치게 복잡하게 만듦&lt;/li&gt;
&lt;li&gt;나중에 필요할지도 모를 기능을 미리 추가함&lt;/li&gt;
&lt;li&gt;너무 이른 시점에 추상화를 시도함&lt;/li&gt;
&lt;li&gt;절대 발생하지 않을지도 모를 예외 상황을 위해 코드를 작성함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 결정 피로와 인지 부하&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경과학 연구에 따르면, 우리의 뇌는 제한된 의사결정 능력을 가지고 있습니다. Diederich와 Trueblood(2018)*의 연구 결과는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자는 2시간 연속 코딩 후 오류율이 30% 증가함&lt;/li&gt;
&lt;li&gt;컴포넌트에 상태 변수(state variable)가 하나 추가될 때마다 인지 부하가 37% 증가함&lt;/li&gt;
&lt;li&gt;복잡한 컴포넌트는 멀티태스킹과 유사한 &quot;신경 전환 비용(neural switching cost)&quot;을 유발함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유로 우리는 종종 다음과 같은 행동을 하게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;올바른 추상화보다는 빠른 해결책을 선택함&lt;/li&gt;
&lt;li&gt;기존 로직을 리팩터링 하기보다는 코드를 중복해서 작성함&lt;/li&gt;
&lt;li&gt;문제를 즉시 해결하기보다는 TODO 주석만 남겨둠&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sweller(1988)의 인지 부하 이론(Cognitive Load Theory)*에 따르면, 작업 기억(working memory)은 한 번에 4&amp;plusmn;1개의 정보 덩어리만 처리할 수 있습니다. 컴포넌트가 여러 관심사(데이터 페칭, 상태 관리, UI 렌더링 등)를 동시에 처리하게 되면 이 한계를 초과하여 코드 품질이 저하됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;악순환 끊어내기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 작게 시작하고 점진적으로 발전시키기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 본 거대한 컴포넌트 대신, 다음과 같이 단순하게 시작할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;const UserDashboard = () =&amp;gt; {
  const { users, loading, error } = useUsers();

  if (loading) return &amp;lt;LoadingSpinner /&amp;gt;;
  if (error) return &amp;lt;ErrorMessage error={error} /&amp;gt;;

  return &amp;lt;UserList users={users} /&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 다음, 필요에 따라 점진적으로 기능을 추가합니다:&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;const UserDashboard = () =&amp;gt; {
  const { users, loading, error } = useUsers();
  const { filter, setFilter } = useFilter();
  const { sortBy, setSortBy } = useSort();

  if (loading) return &amp;lt;LoadingSpinner /&amp;gt;;
  if (error) return &amp;lt;ErrorMessage error={error} /&amp;gt;;

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;FilterBar filter={filter} onFilterChange={setFilter} sortBy={sortBy} onSortChange={setSortBy} /&amp;gt;
      &amp;lt;UserList users={users} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 심리적 안전감 조성하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리팩토링을 위한 시간을 따로 마련하기&lt;/li&gt;
&lt;li&gt;실수를 인정하는 것이 괜찮다는 분위기 만들기&lt;/li&gt;
&lt;li&gt;코드 리뷰를 장려하기&lt;/li&gt;
&lt;li&gt;클린 코드의 좋은 예시를 칭찬하고 공유하기&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. &quot;보이스카웃 규칙&quot; 활용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 발견했을 때보다 더 깨끗하게 만들어두세요. 이는 다음을 의미합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작은 문제들을 발견하는 즉시 수정하기&lt;/li&gt;
&lt;li&gt;점진적으로 리팩터링하기&lt;/li&gt;
&lt;li&gt;작업하면서 문서화하기&lt;/li&gt;
&lt;li&gt;팀원들과 지식을 공유하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실용적인 전략들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 5분 규칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 작성하기 전에 스스로에게 물어보세요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이것이 작동할 수 있는 가장 단순한 방법은 무엇일까?&lt;/li&gt;
&lt;li&gt;5분 안에 이 문제를 해결할 수 있을까?&lt;/li&gt;
&lt;li&gt;내가 해야 할 최소한의 작업은 무엇일까?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &quot;코드 리뷰&quot; 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 커밋하기 전에 스스로에게 물어보세요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 코드를 코드 리뷰에서 보여도 자부심을 느낄 수 있을까?&lt;/li&gt;
&lt;li&gt;이것이 이 문제를 해결하는 가장 깔끔한 방법일까?&lt;/li&gt;
&lt;li&gt;이 코드를 더 좋게 만들 수 있는 방법은 무엇일까?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. &quot;미래의 나&quot; 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음을 고려해 보세요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미래의 내가 이 코드를 이해할 수 있을까?&lt;/li&gt;
&lt;li&gt;미래의 내가 이 코드를 쉽게 수정할 수 있을까?&lt;/li&gt;
&lt;li&gt;미래의 내가 과거의 나에게 이 코드를 작성해 줘서 고마워할까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이것도 같이 고려해 보세요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 컴포넌트의 책임을 한 문장으로 설명할 수 있을까?&lt;/li&gt;
&lt;li&gt;새로운 기능을 추가하려면 3개 이상의 파일을 수정해야 할까?&lt;/li&gt;
&lt;li&gt;'내가 여기서 뭘 생각했던 거지?'라는 코드 패턴이 있나?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 코드 작성은 단순히 기술적 역량의 문제가 아닙니다. 우리의 심리적 편향을 이해하고 이를 극복하려는 노력이 필요합니다. 이러한 패턴들을 인식하고 제시된 전략들을 실행함으로써, 우리는 더 나은 코드를 작성하고 더 유지보수하기 쉬운 애플리케이션을 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기억하세요. 클린 코드는 완벽함을 추구하는 것이 아닙니다. 작고 꾸준한 개선을 이루어내고, 지름길을 택하려는 우리의 자연스러운 경향에 주의를 기울이는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 읽을거리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;책과 개요&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/B09Y9XKBZR&quot;&gt;Clean Code&lt;/a&gt; by Robert C. Martin&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.amazon.com/Psychology-Computer-Programming-Gerald-Weinberg/dp/0442292643&quot;&gt;The Psychology of Computer Programming&lt;/a&gt; by Gerald M. Weinberg&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374275637&quot;&gt;Thinking, Fast and Slow&lt;/a&gt; by Daniel Kahneman&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Cognitive_load&quot;&gt;Cognitive Load&lt;/a&gt; (Wikipedia overview)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://queue.acm.org/detail.cfm?id=3595878&quot;&gt;The Science of Developer Productivity&lt;/a&gt; (ACM Queue)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;학술 연구&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://psycnet.apa.org/doiLanding?doi=10.1037%2F0022-3514.74.5.1252&quot;&gt;Ego Depletion: Is the Active Self a Limited Resource?&lt;/a&gt; by Baumeister et al. (1998). Journal of Personality and Social Psychology.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sciencedirect.com/science/article/abs/pii/S1364661303000287&quot;&gt;Task Switching&lt;/a&gt; by Monsell (2003). Trends in Cognitive Sciences.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.springer.com/article/10.1023/A:1022193728205&quot;&gt;Cognitive Architecture and Instructional Design&lt;/a&gt; by Sweller et al. (1998). Educational Psychology Review.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://onlinelibrary.wiley.com/doi/10.1207/s15516709cog1202_4&quot;&gt;Cognitive Load Theory&lt;/a&gt; by Sweller (1988). Educational Psychology Review.&lt;/li&gt;
&lt;li&gt;Multi-attribute Choice Experiments by Diederich and Trueblood (2018). Journal of Mathematical Psychology. (이 논문의 온라인 링크는 찾지 못했습니다, 죄송합니다!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt; &amp;nbsp;한국어로&amp;nbsp;된&amp;nbsp;프런트엔드&amp;nbsp;아티클을&amp;nbsp;빠르게&amp;nbsp;받아보고&amp;nbsp;싶다면&amp;nbsp;&lt;a href=&quot;https://kofearticle.substack.com/&quot;&gt;Korean&amp;nbsp;FE&amp;nbsp;Article&lt;/a&gt;을&amp;nbsp;구독해주세요!&lt;/blockquote&gt;</description>
      <category>개발/번역</category>
      <category>리액트</category>
      <category>컴포넌트</category>
      <category>클린코드</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/362</guid>
      <comments>https://imnotadevleoper.tistory.com/362#entry362comment</comments>
      <pubDate>Sat, 21 Jun 2025 16:29:12 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 솔로프리너의 시대</title>
      <link>https://imnotadevleoper.tistory.com/361</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;solo.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VEIi6/btsOvYlUnTG/XCdMbXK0gM5KR1YYl8jqcK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VEIi6/btsOvYlUnTG/XCdMbXK0gM5KR1YYl8jqcK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VEIi6/btsOvYlUnTG/XCdMbXK0gM5KR1YYl8jqcK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVEIi6%2FbtsOvYlUnTG%2FXCdMbXK0gM5KR1YYl8jqcK%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;solo.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 책을 읽기 전에는 단순히 AI가 상당히 발전했기 때문에 그로 인해 개인이 다양한 일을 할 기회가 많아졌다고만 생각했습니다. 그런데 책을 읽으면서 정말로 솔로프리너의 시대가 오고 있구나, 어쩌면 이미 왔구나 하는 생각이 들었습니다. 앞으로 이 시대를 어떻게 살아가야 할지 고민하는 사람이라면 누구든 한 번쯤 읽어보시길 추천드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책을 집필하신 고승원 님께서는 이미 솔로프리너로서의 삶을 살아가고 계시며, 그 경험들이 책에 잘 녹아 있습니다. 처음에는 &amp;lsquo;솔로프리너&amp;rsquo;가 무엇인지부터 시작해, 개인이 어떻게 기업만큼의 성과를 낼 수 있는지를 설명하고 있는데, 공감이 되어서 이후의 내용을 더욱 몰입해서 읽을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 롤 플레이어로 살아가며 겪는 어려움이나, 어떻게 하면 솔로프리너의 삶을 실현할 수 있는지를 하나하나 설명해 주시는데, 그동안의 경험이 책 곳곳에 잘 담겨 있어 매우 인상 깊었습니다. &amp;lsquo;나&amp;rsquo;라는 사람을 어떻게 알리고, 가치를 어떻게 키워나갈 것인지에 대해서도 자세히 설명되어 있습니다. 결국 자기 PR이 정말 중요하다는 것을 느꼈고, 기술적인 역량을 쌓는 것만큼이나 나라는 콘텐츠를 잘 포장해 외부에 전달하는 것도 매우 중요하다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책이 중반을 지나면서는 꼭 필요한 생성형 AI를 어떻게 제대로 활용할 수 있는지, 그리고 이를 바탕으로 비즈니스 모델을 어떻게 만들 수 있는지를 소개합니다. 이어서 앞으로 솔로프리너로서 어떻게 살아가야 할지에 대한 내용으로 책은 마무리됩니다. 책의 마지막에는 저자가 독자에게 던지는 질문이 있는데, 저 역시 그 질문을 통해 앞으로 어떤 삶을 살아가야 할지 깊이 고민해 볼 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책을 다 읽고 나서 느낀 점은, 저자의 삶을 들여다본 듯한 느낌이었고, 그 과정에서 얻은 시행착오를 후배에게 친절하게 들려주는 듯한 인상이었습니다. 솔로프리너가 무엇인지 궁금하거나 그 삶에 대해 호기심이 생겼다면, 한 번쯤 읽어보시길 추천드립니다.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <category>1인기업</category>
      <category>솔로프리너</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/361</guid>
      <comments>https://imnotadevleoper.tistory.com/361#entry361comment</comments>
      <pubDate>Wed, 11 Jun 2025 02:53:47 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식</title>
      <link>https://imnotadevleoper.tistory.com/360</link>
      <description>&lt;h2&gt;&amp;quot;한빛미디어 서평단 &amp;lt;나는리뷰어다&amp;gt; 활동을 위해서 책을 협찬 받아 작성된 서평입니다.&amp;quot;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-05-30-23-48-08.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhAKQw/btsOmrgnvtj/SMULt2BZWpWnEJXqNqy4u1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhAKQw/btsOmrgnvtj/SMULt2BZWpWnEJXqNqy4u1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhAKQw/btsOmrgnvtj/SMULt2BZWpWnEJXqNqy4u1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhAKQw%2FbtsOmrgnvtj%2FSMULt2BZWpWnEJXqNqy4u1%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-05-30-23-48-08.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;요즘은 백엔드 개발자가 단순히 클라이언트를 위한 API를 만드는 것을 넘어, 인프라 환경과 성능 최적화, 보안, 운영 등 다양한 영역의 지식까지 요구되는 시대인 것 같습니다. 그러나 방대한 영역에 어떤 것부터 접근해야 할지 막막하게 느껴질 것 같은데요. &amp;lt;주니어 백엔드 개발자가 반드시 알아야 할 실무 지식&amp;gt;은 막막함을 메워줄 수 있는 훌륭한 안내서라고 생각합니다. 저는 프론트엔드 개발자로 일하고 있지만 그렇게 어렵지 않게 읽어나갈 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-05-30-23-48-14.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ni4XA/btsOkmuIz70/jS032Ru3ypxPiQrCEKlX31/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ni4XA/btsOkmuIz70/jS032Ru3ypxPiQrCEKlX31/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ni4XA/btsOkmuIz70/jS032Ru3ypxPiQrCEKlX31/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fni4XA%2FbtsOkmuIz70%2FjS032Ru3ypxPiQrCEKlX31%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-05-30-23-48-14.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;책 소개&lt;/h2&gt;
&lt;p&gt;이 책은 단순한 개념 설명이나 코드 예시를 넘어, 실무 현장에서 마주치는 다양한 문제 상황과 그 해결 방안을 중심으로 구성되어 있습니다. 성능 문제, 외부 연동, 동시성 제어, 보안, 네트워크, 아키텍처 등 백엔드 개발자가 반드시 알아야 할 주제들을 실질적인 시나리오를 통해 설명하고 있어, 실전 감각을 익히기에 매우 적합합니다.  &lt;/p&gt;
&lt;p&gt;특히 특정 기술 스택에 국한되지 않고 백엔드 전반에 걸친 개념을 폭넓게 다루고 있어, 새로운 환경이나 시스템에 적응해야 하는 상황에서도 도움이 됩니다. 책에서는 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식이라고 얘기하고 있지만 개발자라면 꼭 읽어야 할 책이라고도 얘기할 수 있을 것 같은데요. 그만큼 실무에서 겪는 문제에 대한 효과적인 해결책을 얘기하고 있기 때문입니다.  &lt;/p&gt;
&lt;h2&gt;목차 구성&lt;/h2&gt;
&lt;p&gt;책은 총 11장과 3개의 부록으로 구성되어 있으며, 실무에서 마주칠 수 있는 주제들을 다음과 같이 정리해 다룹니다:  &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;백엔드 개발자로서의 자세 – 코딩만 잘해서는 안 된다는 기본 인식.  &lt;/li&gt;
&lt;li&gt;성능 진단의 출발점 – 처리량, 응답시간 등 지표 중심의 접근.  &lt;/li&gt;
&lt;li&gt;DB 설계와 쿼리 최적화 – 인덱스, 조회 최적화, 트랜잭션까지 폭넓은 DB 활용 지식.  &lt;/li&gt;
&lt;li&gt;외부 시스템과의 연동 이슈 – 타임아웃, 재시도, 서킷 브레이커 등 안정적인 연동을 위한 방법론.  &lt;/li&gt;
&lt;li&gt;비동기 시스템 설계 – 메시징 시스템, CDC, 트랜잭션 아웃박스 등 현대 시스템에서 꼭 필요한 설계 패턴.  &lt;/li&gt;
&lt;li&gt;동시성 제어 – 데이터 무결성과 레이스 컨디션 방지를 위한 전략.  &lt;/li&gt;
&lt;li&gt;IO 병목 해결 – 가상 스레드, 논블로킹 IO 등 자원 효율 향상 방안.  &lt;/li&gt;
&lt;li&gt;보안 지식 – 인증/인가, 암호화, 방화벽 설정 등 실무에서 필요한 보안 전략.  &lt;/li&gt;
&lt;li&gt;운영 서버 지식 – 프로세스, 디스크, 크론, 네트워크 정보 등 서버 운영 기본기.  &lt;/li&gt;
&lt;li&gt;네트워크 기초 – IP, TCP/UDP, NAT, VPN 등 이해해야 할 기본 개념.  &lt;/li&gt;
&lt;li&gt;서버 구조 및 아키텍처 패턴 – MVC부터 CQRS까지 다양한 설계 전략 소개.  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;부록으로는 성능 테스트, NoSQL 이해, 분산 잠금 구현과 같은 추가 주제도 다뤄 실무 대응력을 높여줍니다.  &lt;/p&gt;
&lt;h2&gt;서평&lt;/h2&gt;
&lt;p&gt;&amp;lt;주니어 백엔드 개발자가 반드시 알아야 할 실무 지식&amp;gt;은 그야말로 실무에 바로 적용할 수 있는 기술 교양서입니다. 단순히 &amp;quot;이론적으로 이런 게 있다&amp;quot; 수준을 넘어서서, &amp;quot;실제로 문제가 생겼을 때 어떻게 접근할 것인가&amp;quot;에 대한 방향성을 제시해주는 책입니다.  &lt;/p&gt;
&lt;p&gt;개발 중 성능이 느려졌을 때 어디서부터 봐야 하는지, 외부 API 연동에서 생길 수 있는 장애에 어떻게 대응해야 할지, 데이터 정합성을 어떻게 지켜야 하는지 등 개발자라면 언젠가는 반드시 마주칠 문제들을 현실적으로 풀어냅니다. 각 장의 설명도 친절하고, 예시 중심의 구성이라 이해가 쉽습니다.  &lt;/p&gt;
&lt;p&gt;특히 개인적으로는 5장과 6장의 비동기 연동 및 동시성 처리 부분이 인상 깊었습니다. 단순히 메시지 큐를 사용하는 것을 넘어서, 왜 그 방법이 필요한지에 대한 맥락까지 설명해 주는 방식이 실무 감각을 높이는 데 큰 도움이 되었습니다.  &lt;/p&gt;
&lt;h2&gt;추천 대상&lt;/h2&gt;
&lt;p&gt;특정 대상을 제한하기 보다는 모든 개발자 분들에게 추천드리고 싶은 책입니다.  &lt;/p&gt;
&lt;p&gt;이 책은 단순한 백엔드 개발 지침서라기 보다는 실제 문제를 해결할 수 있는 사고의 틀을 제공한다고 생각하는데요. 더 &amp;quot;잘&amp;quot;하고 싶은 모든 개발자에게 추천드립니다.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/360</guid>
      <comments>https://imnotadevleoper.tistory.com/360#entry360comment</comments>
      <pubDate>Fri, 30 May 2025 23:52:27 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 리액트에서의 의존성 역전: 테스트하기 쉬운 컴포넌트 만들기</title>
      <link>https://imnotadevleoper.tistory.com/359</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문: &lt;a href=&quot;https://cekrem.github.io/posts/dependency-inversion-in-react/&quot;&gt;Dependency Inversion in React: Building Truly Testable Components&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;리액트 개발을 하다 보면 컴포넌트가 외부 의존성과 강하게 결합되는 경우가 많습니다. 이는 테스트를 어렵게 만들고, 유지보수를 힘들게 하며, 사실상 변경을 불가능하게 만듭니다. 의존성 역전 원칙(DIP)은 이러한 문제를 해결할 수 있는 방법을 제시하지만, &lt;/span&gt;&lt;b&gt;리액트에서&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 이 원칙을 효과적으로 적용하려면 어떻게 해야 할까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;: 백엔드 관점에서 본 의존성 역전에 대해 알고 싶다면, 제가 이전에 작성한 &lt;a href=&quot;https://cekrem.github.io/posts/clean-architecture-and-plugins-in-go/&quot;&gt;Go에서 플러그인을 활용한 의존성 역전 글&lt;/a&gt;을 참고해보세요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제: 리액트에서의 강한 결합&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 일반적인 상황을 생각해보세요.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const UserProfile = () =&amp;gt; {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() =&amp;gt; {
    fetch(&quot;/api/user&quot;)
      .then((res) =&amp;gt; res.json())
      .then((data) =&amp;gt; {
        setUser(data);
        setLoading(false);
      });
  }, []);

  if (loading) return &amp;lt;LoadingSpinner /&amp;gt;;
  return &amp;lt;UserDetails user={user} /&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컴포넌트에는 아래와 같은 여러 가지 문제가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fetch API에 강하게 결합되어 있습니다.&lt;/li&gt;
&lt;li&gt;API 호출이 직접적으로 이루어지기 때문에 테스트하기 어렵습니다.&lt;/li&gt;
&lt;li&gt;데이터 소스를 변경하기가 어렵습니다.&lt;/li&gt;
&lt;li&gt;로딩 상태를 쉽게 테스트하는 것이 사실상 불가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExejNkMWJuengyMnJ4MGw2eHkwNTJyNjhrZXJndTZxcjExNXBuMDltciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZHnKJsXLI6ZClYFwzH/giphy.gif&quot; alt=&quot;img.gif&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책: 의존성 역전&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 역전 원칙은 핵심 로직(고수준 모듈)이 세부 구현(저수준 모듈)에 직접 의존해서는 안 되며, 둘 다 공통된 인터페이스와 같은 추상화된 구조에 의존해야 한다는 원칙입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이를 어떻게 리팩터링할 수 있는지 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;interface UserRepository {
  getUser: () =&amp;gt; Promise&amp;lt;User&amp;gt;;
}

const UserProfile = ({ userRepository }: { userRepository: UserRepository }) =&amp;gt; {
  const [user, setUser] = useState&amp;lt;User | null&amp;gt;(null);
  const [loading, setLoading] = useState(true);

  useEffect(() =&amp;gt; {
    userRepository.getUser().then((data) =&amp;gt; {
      setUser(data);
      setLoading(false);
    });
  }, [userRepository]);

  if (loading) return &amp;lt;LoadingSpinner /&amp;gt;;
  return &amp;lt;UserDetails user={user} /&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(물론 이 모든 상태와 useEffect 로직은 커스텀 훅으로 분리할 수도 있지만, 여기서의 핵심 논점은 아닙니다.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리포지토리 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 리포지토리의 구체적인 구현체를 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;class ApiUserRepository implements UserRepository {
  async getUser(): Promise&amp;lt;User&amp;gt; {
    const response = await fetch(&quot;/api/user&quot;);
    return response.json();
  }
}

class MockUserRepository implements UserRepository {
  private resolveUser: (user: User) =&amp;gt; void = () =&amp;gt; {};
  private rejectUserPromise: (error: Error) =&amp;gt; void = () =&amp;gt; {};

  getUser(): Promise&amp;lt;User&amp;gt; {
    return new Promise((resolve, reject) =&amp;gt; {
      this.resolveUser = resolve;
      this.rejectUserPromise = reject;
    });
  }

  // Promise를 resolve 하기 위한 헬퍼 메서드
  resolveWithUser(user: User) {
    this.resolveUser(user);
  }

  // Promise를 reject 하기 위한 헬퍼 메서드
  rejectUser(error: Error) {
    this.rejectUserPromise(error);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쉬워진 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 사용하면 테스트가 훨씬 간단해집니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;describe(&quot;UserProfile&quot;, () =&amp;gt; {
  it(&quot;shows loading state initially&quot;, () =&amp;gt; {
    const mockRepo = new MockUserRepository();
    render(&amp;lt;UserProfile userRepository={mockRepo} /&amp;gt;);
    expect(screen.getByTestId(&quot;loading-spinner&quot;)).toBeInTheDocument();
  });

  it(&quot;displays user data when loaded&quot;, async () =&amp;gt; {
    const mockRepo = new MockUserRepository();
    render(&amp;lt;UserProfile userRepository={mockRepo} /&amp;gt;);

    // 데이터 페칭을 흉내냅니다.
    mockRepo.resolveWithUser({
      id: 1,
      name: &quot;Test User&quot;,
      email: &quot;test@example.com&quot;,
    });

    const userData = await screen.findByText(&quot;Test User&quot;);
    expect(userData).toBeInTheDocument();
  });

  // 예외 테스트도 마찬가지로 간단하게 할 수 있지만, 간결함을 위해 제외했습니다.
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모범 사례&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;명확한 인터페이스 정의하기&lt;/b&gt;: 의존성을 나타내는 인터페이스를 생성하세요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의존성 주입하기&lt;/b&gt;: 의존성을 프로퍼티나 컨텍스트를 통해 전달하거나, 더 나은 방법으로는 &lt;a href=&quot;https://cekrem.github.io/posts/dependency-inversion-in-react/#note-on-dependency-injection&quot;&gt;TSyringe&lt;/a&gt;를 사용하세요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;독립적인 테스트&lt;/b&gt;: 각 컴포넌트는 의존성 없이도 독립적으로 테스트할 수 있어야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트에서 의존성 역전 원칙을 적용하면 다음과 같은 이점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 테스트하기 쉬운 컴포넌트&lt;/li&gt;
&lt;li&gt;더 쉬워지는 유지보수&lt;/li&gt;
&lt;li&gt;더 나은 관심사 분리&lt;/li&gt;
&lt;li&gt;더 유연하고 재사용 가능한 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기억하세요. 목적은 복잡성을 더하는 것이 아니라, 코드를 더 유지보수하기 쉽고 테스트 가능한 구조로 만드는 데 있습니다. 작게 시작하되, 이 원칙들을 가장 효과적인 지점에 적용해보세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExejNkMWJuengyMnJ4MGw2eHkwNTJyNjhrZXJndTZxcjExNXBuMDltciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZHnKJsXLI6ZClYFwzH/giphy.gif&quot; alt=&quot;img.gif&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 읽을거리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164&quot;&gt;Clean Architecture&lt;/a&gt; by Robert C. Martin&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://testing-library.com/docs/react-testing-library/intro/&quot;&gt;React Testing Library&lt;/a&gt; (공식 문서)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cekrem.github.io/posts/single-responsibility-principle-in-react/&quot;&gt;Single Responsibility Principle in React&lt;/a&gt; (이전 글)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;의존성 주입에 대한 참고 사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가이드는 리액트에서 의존성 역전 원칙을 적용하는 데 초점을 맞추고 있어서, 의존성 주입을 깔끔하고 확장 가능한 방식으로 구현하는 구체적인 방법에 대해서는 깊이 다루지 않습니다. 하지만 이 주제에 대해 더 자세히 알아보고 싶다면, &lt;a href=&quot;https://github.com/microsoft/tsyringe&quot;&gt;TSyringe&lt;/a&gt;와 같은 라이브러리가 리액트 애플리케이션에서 의존성을 효과적으로 관리하는 좋은 시작점이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &amp;nbsp;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;한국어로&amp;nbsp;된&amp;nbsp;프런트엔드&amp;nbsp;아티클을&amp;nbsp;빠르게&amp;nbsp;받아보고&amp;nbsp;싶다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;a style=&quot;color: #ff5544;&quot; href=&quot;https://kofearticle.substack.com/&quot;&gt;&lt;u&gt;Korean&amp;nbsp;FE&amp;nbsp;Article&lt;/u&gt;&lt;/a&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;을&amp;nbsp;구독해주세요!&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발/번역</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/359</guid>
      <comments>https://imnotadevleoper.tistory.com/359#entry359comment</comments>
      <pubDate>Sun, 25 May 2025 22:40:17 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 그림으로 배우는 도커</title>
      <link>https://imnotadevleoper.tistory.com/358</link>
      <description>&lt;h3 id=&quot;%22%ED%95%9C%EB%B9%9B%EB%AF%B8%EB%94%94%EC%96%B4%20%EC%84%9C%ED%8F%89%EB%8B%A8%20%3C%EB%82%98%EB%8A%94%EB%A6%AC%EB%B7%B0%EC%96%B4%EB%8B%A4%3E%20%ED%99%9C%EB%8F%99%EC%9D%84%20%EC%9C%84%ED%95%B4%EC%84%9C%20%EC%B1%85%EC%9D%84%20%ED%98%91%EC%B0%AC%C2%A0%EB%B0%9B%EC%95%84%20%EC%9E%91%EC%84%B1%EB%90%9C%20%EC%84%9C%ED%8F%89%EC%9E%85%EB%8B%88%EB%8B%A4.%22-1&quot; style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a style=&quot;color: #353638;&quot; href=&quot;https://imnotadevleoper.tistory.com/355#%22%ED%95%9C%EB%B9%9B%EB%AF%B8%EB%94%94%EC%96%B4%20%EC%84%9C%ED%8F%89%EB%8B%A8%20%3C%EB%82%98%EB%8A%94%EB%A6%AC%EB%B7%B0%EC%96%B4%EB%8B%A4%3E%20%ED%99%9C%EB%8F%99%EC%9D%84%20%EC%9C%84%ED%95%B4%EC%84%9C%20%EC%B1%85%EC%9D%84%20%ED%98%91%EC%B0%AC%C2%A0%EB%B0%9B%EC%95%84%20%EC%9E%91%EC%84%B1%EB%90%9C%20%EC%84%9C%ED%8F%89%EC%9E%85%EB%8B%88%EB%8B%A4.%22-1&quot;&gt;&quot;한빛미디어 서평단 &amp;lt;나는리뷰어다&amp;gt; 활동을 위해서 책을 협찬&amp;nbsp;받아 작성된 서평입니다.&quot;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-04-25-23-43-35 002.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nYla2/btsNATMrzYV/c3aGco3yr4l2DpzWVWEZhk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nYla2/btsNATMrzYV/c3aGco3yr4l2DpzWVWEZhk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nYla2/btsNATMrzYV/c3aGco3yr4l2DpzWVWEZhk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnYla2%2FbtsNATMrzYV%2Fc3aGco3yr4l2DpzWVWEZhk%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2025-04-25-23-43-35 002.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘은 프론트엔드 개발자라고 하더라도 Frontend Ops Developer와 같은 직군도 있고 인프라를 구축할 수 있는 역량이 꽤나 필요합니다. 평소에도 도커를 배워야겠다는 생각은 많이 했었는데 강의를 보거나 막상 책을 사려고 해도 어려운 개념이 있다보니 망설여졌는데요. 그런데 『그림으로 배우는 도커』는 이름에서 알 수 있듯이 부담을 적게 도커를 학습할 수 있는 책이라고 생각합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;책 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 도커의 기본 개념부터 시작하여, 컨테이너, 이미지, 네트워크, 볼륨 등 필수 요소들을 그림과 함께 설명합니다. 또한, 도커 컴포즈를 활용한 개발 환경 구성 방법과 운영 시 주의할 점, 디버깅 노하우 등 실무에서 필요한 내용도 다루고 있습니다. 명령어와 개념을 시각적으로 설명하여 독자의 이해를 돕고, 다양한 예제를 통해 실습할 수 있도록 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;목차 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책은 총 5부로 구성되어 있습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;1.&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;b&gt;가상화와 도커 기본 지식&lt;/b&gt;&lt;/span&gt;: 가상화의 개념과 도커의 구성 요소를 소개합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;2.&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;b&gt;도커 컨테이너 활용법&lt;/b&gt;&lt;/span&gt;: 컨테이너의 기본 조작 방법과 다양한 실습 예제를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;3.&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;b&gt;도커 이미지 활용법&lt;/b&gt;&lt;/span&gt;: 이미지의 구조와 조작 방법을 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;4.&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;b&gt;도커파일 활용법&lt;/b&gt;&lt;/span&gt;: 도커파일을 이용한 이미지 빌드 방법을 다룹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;5.&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;b&gt;고급 도커 컨테이너 활용법&lt;/b&gt;&lt;/span&gt;: 볼륨과 네트워크 설정 등 고급 기능을 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 장은 실제 예제를 통해 도커의 개념을 이해하고 실습할 수 있도록 구성되어 있어, 독자가 단계적으로 학습할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서평&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;『그림으로 배우는 도커』는 도커를 처음 접하는 개발자부터 실무에서 도커를 활용하고자 하는 분들까지 모두에게 유용한 입문서입니다. 복잡하게 느껴질 수 있는 도커의 개념을 그림과 함께 설명하여 이해를 돕고, 단계별로 실습할 수 있는 예제를 통해 학습 효과를 높였습니다. 특히, 도커 컴포즈를 활용한 개발 환경 구성 방법과 운영 시 주의할 점, 디버깅 노하우 등 실무에서 필요한 내용도 다루고 있어 실용적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 도커를 처음 배우는 분들에게는 친절한 가이드가 되어주고, 이미 도커를 사용하고 있는 분들에게는 개념을 정리하고 실무에 적용할 수 있는 팁을 제공해줍니다. 도커에 대한 이해를 깊이 있게 하고자 하는 모든 분들께 추천합니다.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/358</guid>
      <comments>https://imnotadevleoper.tistory.com/358#entry358comment</comments>
      <pubDate>Fri, 25 Apr 2025 23:53:09 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 일반적인 리액트 라이브러리 아키텍처</title>
      <link>https://imnotadevleoper.tistory.com/357</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;common-react-libraries.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cS3lFv/btsNnZzzNOf/bgR6ZV821kxEshguwEcHt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cS3lFv/btsNnZzzNOf/bgR6ZV821kxEshguwEcHt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cS3lFv/btsNnZzzNOf/bgR6ZV821kxEshguwEcHt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcS3lFv%2FbtsNnZzzNOf%2FbgR6ZV821kxEshguwEcHt1%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;common-react-libraries.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;원문: &lt;a href=&quot;https://www.felgus.dev/blog/common-react-lib-architecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Common&amp;nbsp;React&amp;nbsp;libraries&amp;nbsp;architecture&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;리액트는&amp;nbsp;풍부한&amp;nbsp;생태계로&amp;nbsp;유명합니다.&amp;nbsp;리액트&amp;nbsp;생태계에는&amp;nbsp;상태&amp;nbsp;관리,&amp;nbsp;폼,&amp;nbsp;라우팅,&amp;nbsp;스타일&amp;nbsp;등의&amp;nbsp;다양한&amp;nbsp;도구들이&amp;nbsp;있는데&amp;nbsp;이들은&amp;nbsp;내부적으로&amp;nbsp;어떻게&amp;nbsp;동작할까요?&lt;br /&gt;&lt;br /&gt;대부분의&amp;nbsp;도구는&amp;nbsp;일부&amp;nbsp;아키텍처&amp;nbsp;선택에서&amp;nbsp;유사하며,&amp;nbsp;리액트의&amp;nbsp;작동&amp;nbsp;방식에&amp;nbsp;더&amp;nbsp;부합합니다.&amp;nbsp;이러한&amp;nbsp;일반적인&amp;nbsp;구조를&amp;nbsp;살펴보겠습니다.&lt;u&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;바인딩&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대부분의&amp;nbsp;라이브러리는&amp;nbsp;코어와&amp;nbsp;바인딩이라는&amp;nbsp;두&amp;nbsp;가지&amp;nbsp;주요&amp;nbsp;부분으로&amp;nbsp;구성된&amp;nbsp;아키텍처에&amp;nbsp;의존합니다.&amp;nbsp;코어는&amp;nbsp;로직과&amp;nbsp;기능이&amp;nbsp;있는&amp;nbsp;곳이고&amp;nbsp;바인딩은&amp;nbsp;코어와&amp;nbsp;프런트엔드&amp;nbsp;도구&amp;nbsp;사이의&amp;nbsp;연결입니다.&amp;nbsp;리액트의&amp;nbsp;경우,&amp;nbsp;프런트엔드&amp;nbsp;도구는&amp;nbsp;컴포넌트와&amp;nbsp;커스텀&amp;nbsp;훅을&amp;nbsp;의미합니다.&lt;br /&gt;&lt;br /&gt;대부분의&amp;nbsp;경우&amp;nbsp;코어&amp;nbsp;객체는&amp;nbsp;외부에서&amp;nbsp;생성됩니다.&amp;nbsp;이렇게&amp;nbsp;하면&amp;nbsp;리렌더링&amp;nbsp;사이클에서&amp;nbsp;벗어나고,&amp;nbsp;리액트&amp;nbsp;메모이제이션&amp;nbsp;과정에&amp;nbsp;신경&amp;nbsp;쓸&amp;nbsp;필요가&amp;nbsp;없는&amp;nbsp;등의&amp;nbsp;여러&amp;nbsp;이점이&amp;nbsp;있습니다.&amp;nbsp;이를&amp;nbsp;리액트와&amp;nbsp;연결하기&amp;nbsp;위해&amp;nbsp;일반적으로&amp;nbsp;Context&amp;nbsp;API에&amp;nbsp;의존합니다.&lt;br /&gt;&lt;br /&gt;그렇기&amp;nbsp;때문에&amp;nbsp;많은&amp;nbsp;도구에&amp;nbsp;Provider가&amp;nbsp;있습니다.&amp;nbsp;이는&amp;nbsp;라이브러리가&amp;nbsp;모든&amp;nbsp;컴포넌트&amp;nbsp;트리에&amp;nbsp;외부&amp;nbsp;코어&amp;nbsp;데이터를&amp;nbsp;주입하는&amp;nbsp;방식입니다.&amp;nbsp;Context&amp;nbsp;API는&amp;nbsp;동적&amp;nbsp;데이터에&amp;nbsp;문제가&amp;nbsp;있어&amp;nbsp;성능&amp;nbsp;문제가&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있지만&amp;nbsp;코어의&amp;nbsp;참조가&amp;nbsp;안정적이기&amp;nbsp;때문에&amp;nbsp;문제가&amp;nbsp;되지&amp;nbsp;않습니다.&lt;br /&gt;&lt;br /&gt;Context API를 적용하면 외부 데이터를 직접 연결하지 않더라도, 리액트 트리의 Provider 아래에 다른 컴포넌트와 훅을 추가하는 것만으로 외부 데이터에 접근할 수 있습니다.&amp;nbsp;&lt;a href=&quot;https://github.com/TanStack/query/blob/main/packages/react-query/src/QueryClientProvider.tsx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tanstack Query&lt;/a&gt;의&amp;nbsp;경우를&amp;nbsp;예로&amp;nbsp;들어보겠습니다.&amp;nbsp;코어&amp;nbsp;클라이언트와&amp;nbsp;Provider를&amp;nbsp;사용하면&amp;nbsp;서로&amp;nbsp;다른&amp;nbsp;페이지에서&amp;nbsp;동일한&amp;nbsp;쿼리&amp;nbsp;캐시를&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있으며,&amp;nbsp;모두&amp;nbsp;동일한&amp;nbsp;키를&amp;nbsp;사용하기만&amp;nbsp;하면&amp;nbsp;됩니다.&amp;nbsp;라이브러리는&amp;nbsp;코어&amp;nbsp;및&amp;nbsp;Context&amp;nbsp;Provider&amp;nbsp;전체에서&amp;nbsp;모든&amp;nbsp;것을&amp;nbsp;연결하므로&amp;nbsp;이&amp;nbsp;데이터를&amp;nbsp;공유하는&amp;nbsp;데&amp;nbsp;신경&amp;nbsp;쓸&amp;nbsp;필요가&amp;nbsp;없습니다.&lt;br /&gt;&lt;br /&gt;이같은 방식은 React-router와 같은 라이브러리에서도 볼 수 있습니다. 당신이 &lt;b&gt;useParams&lt;/b&gt;, &lt;b&gt;useNavigate&lt;/b&gt;를 호출하여 데이터나 메서드를 가져오고자 하면, React-router는 &lt;a href=&quot;https://github.com/remix-run/react-router/blob/9d0020a8fa77a54f0e026fd7710a856d6172b7a8/packages/react-router/lib/hooks.tsx#L106&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;커스텀 훅에서 내부적으로 사용하는 컨텍스트&lt;/a&gt;를 통해&amp;nbsp;이를&amp;nbsp;제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;외부&amp;nbsp;연결&lt;br /&gt;&lt;/u&gt;&lt;u&gt;&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만&amp;nbsp;또&amp;nbsp;다른&amp;nbsp;문제가&amp;nbsp;있습니다.&amp;nbsp;클라이언트는&amp;nbsp;리액트&amp;nbsp;렌더링&amp;nbsp;모델과&amp;nbsp;연결하기&amp;nbsp;위해&amp;nbsp;반드시&amp;nbsp;어떤&amp;nbsp;패턴을&amp;nbsp;사용해야&amp;nbsp;합니다.&amp;nbsp;클라이언트에서&amp;nbsp;무언가가&amp;nbsp;변경되었을&amp;nbsp;때,&amp;nbsp;리액트가&amp;nbsp;최신&amp;nbsp;값을&amp;nbsp;사용하기&amp;nbsp;위해&amp;nbsp;다시&amp;nbsp;렌더링&amp;nbsp;해야&amp;nbsp;한다는&amp;nbsp;것을&amp;nbsp;어떻게&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있을까요?&lt;br /&gt;&lt;br /&gt;대부분은&amp;nbsp;옵서버&amp;nbsp;패턴을&amp;nbsp;사용합니다.&amp;nbsp;여기에는&amp;nbsp;데이터&amp;nbsp;변경&amp;nbsp;사항을&amp;nbsp;알리기&amp;nbsp;위해&amp;nbsp;구독하고&amp;nbsp;콜백&amp;nbsp;함수를&amp;nbsp;실행하는&amp;nbsp;메서드가&amp;nbsp;있습니다.&amp;nbsp;그리고&amp;nbsp;현재&amp;nbsp;원시(raw)&amp;nbsp;데이터를&amp;nbsp;가져오는&amp;nbsp;getState&amp;nbsp;메서드와,&amp;nbsp;상태&amp;nbsp;업데이트를&amp;nbsp;트리거&amp;nbsp;하는&amp;nbsp;메서드(상태&amp;nbsp;설정자,&amp;nbsp;디스패치,&amp;nbsp;리페치&amp;nbsp;등&amp;nbsp;무엇이든)가&amp;nbsp;있습니다.&amp;nbsp;물론&amp;nbsp;라이브러리의&amp;nbsp;코어에는&amp;nbsp;더&amp;nbsp;많은&amp;nbsp;메서드가&amp;nbsp;포함될&amp;nbsp;수&amp;nbsp;있지만,&amp;nbsp;이&amp;nbsp;세&amp;nbsp;가지(구독&amp;nbsp;및&amp;nbsp;알림&amp;nbsp;메서드,&amp;nbsp;getState&amp;nbsp;메서드,&amp;nbsp;상태&amp;nbsp;업데이트&amp;nbsp;트리거&amp;nbsp;메서드)는&amp;nbsp;필수적입니다.&lt;br /&gt;&lt;br /&gt;리액트와&amp;nbsp;연결하기&amp;nbsp;위해서는&amp;nbsp;2가지&amp;nbsp;방식이&amp;nbsp;있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react/useSyncExternalStore&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useSyncExternalStore&lt;/a&gt;: 외부 데이터를 다루기 위한 리액트 기본 훅입니다. 이 훅에 getState 및 subscribe 함수(추가로 서버 측 렌더링이 필요한 경우에는 이를 위한 getState 포함)를 전달하면, 훅은 변경 사항을 구독하고, 데이터 변경 시 다시 렌더링 하여 getState 함수를 호출함으로써 변경된 데이터를 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;커스텀 훅:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://github.com/pmndrs/jotai/blob/ec352d5db13255ebfa711bc7d29f8e251ad3f02c/src/react/useAtomValue.ts#L114&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useEffect와 useReducer를 이용하여 자신만의 버전&lt;/a&gt;을&amp;nbsp;만들&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;&lt;b&gt;useSyncExternalStore&lt;/b&gt; 훅이 존재하기 이전에는 모든 사람이 이 방법을 사용했고, 이는 나름의 이점이 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;useSyncExternalStore&lt;/b&gt;는 최적화가 되어있지 않습니다. 그래서 우선순위가 높은 업데이트로 취급되고, 애플리케이션에 동시성 기능이 있는 경우 그에 대한 최적화를 무효화합니다. 이렇게 하면 테어링(또는 사용 중인 상태의 업데이트되지 않은 버전)을 방지할 수 있으므로, 바람직한 동작일 수 있습니다.&lt;br /&gt;&lt;br /&gt;동시성&amp;nbsp;기능은&amp;nbsp;기본적으로&amp;nbsp;다른&amp;nbsp;상태와의&amp;nbsp;우선&amp;nbsp;순위를&amp;nbsp;정하기&amp;nbsp;위해&amp;nbsp;일부&amp;nbsp;상태의&amp;nbsp;테어링을&amp;nbsp;허용하는&amp;nbsp;것은&amp;nbsp;사실입니다.&amp;nbsp;하지만&amp;nbsp;리액트는&amp;nbsp;외부&amp;nbsp;상태의&amp;nbsp;&quot;테어링&quot;을&amp;nbsp;나쁜&amp;nbsp;패턴으로&amp;nbsp;간주하기&amp;nbsp;때문에,&amp;nbsp;클라이언트가&amp;nbsp;알림을&amp;nbsp;트리거하면&amp;nbsp;리액트는&amp;nbsp;이를&amp;nbsp;동기화하고,&amp;nbsp;이에&amp;nbsp;따라&amp;nbsp;최신&amp;nbsp;버전의&amp;nbsp;데이터로&amp;nbsp;리렌더링이&amp;nbsp;발생합니다.&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://blog.axlight.com/posts/why-use-sync-external-store-is-not-used-in-jotai/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;자체 연결을 생성하여&lt;/a&gt; 외부 상태를 우선순위가 가장 낮은 상태로 취급하고, 다른 상호작용을 처리하기 위해 마지막 업데이트의 재렌더링을 일시 중지한다면, 이 시나리오를 피할 수 있습니다.&lt;br /&gt;&lt;br /&gt;이는 장단점을 따져봐야 하는 문제이지만 대부분의 라이브러리는&amp;nbsp;&lt;b&gt;useSyncExternalStore&lt;/b&gt;을&amp;nbsp;사용하는&amp;nbsp;것을&amp;nbsp;고수하고&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;리액트 19&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;리액트 19에는 라이브러리 구현에 영향을 미칠 업데이트가 있습니다. 대부분 서스펜스와 프로미스를 처리하기 위해 &lt;a href=&quot;https://react.dev/reference/react/use&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;use&lt;/a&gt; 훅을&amp;nbsp;사용하겠지만,&amp;nbsp;새로운&amp;nbsp;훅이&amp;nbsp;더&amp;nbsp;필요한&amp;nbsp;다른&amp;nbsp;사례도&amp;nbsp;있을&amp;nbsp;것입니다.&lt;br /&gt;&lt;br /&gt;예를 들어, 폼 라이브러리는 특히 Next.js, Waku 및 Tanstack Start와 같은 리액트 서버 컴포넌트에서 &lt;a href=&quot;https://react.dev/reference/react/useOptimistic&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useOptimistic&lt;/a&gt;,&amp;nbsp; &lt;a href=&quot;https://react.dev/reference/react-dom/hooks/useFormStatus&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useFormStatus &lt;/a&gt;및 &lt;a href=&quot;https://react.dev/reference/react/useActionState&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useActionState&lt;/a&gt; 를 매우&amp;nbsp;유용하게&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;이러한&amp;nbsp;아키텍처들은&amp;nbsp;특정한&amp;nbsp;사례에&amp;nbsp;맞춰&amp;nbsp;발전하고&amp;nbsp;변화할&amp;nbsp;수&amp;nbsp;있지만,&amp;nbsp;일반적으로는&amp;nbsp;이&amp;nbsp;글에서&amp;nbsp;그린&amp;nbsp;정도가&amp;nbsp;현재의&amp;nbsp;리액트&amp;nbsp;생태계&amp;nbsp;구조에&amp;nbsp;대한&amp;nbsp;전체적인&amp;nbsp;그림입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; &amp;nbsp;&lt;/b&gt;한국어로&amp;nbsp;된&amp;nbsp;프런트엔드&amp;nbsp;아티클을&amp;nbsp;빠르게&amp;nbsp;받아보고&amp;nbsp;싶다면 &lt;b&gt;&lt;a href=&quot;https://kofearticle.substack.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Korean&amp;nbsp;FE&amp;nbsp;Article&lt;/u&gt;&lt;/a&gt;&lt;/b&gt;을&amp;nbsp;구독해주세요!&lt;/p&gt;</description>
      <category>개발/번역</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/357</guid>
      <comments>https://imnotadevleoper.tistory.com/357#entry357comment</comments>
      <pubDate>Thu, 17 Apr 2025 01:44:50 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 패턴으로 익히고 설계로 완성하는 리액트</title>
      <link>https://imnotadevleoper.tistory.com/355</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&quot;한빛미디어 서평단 &amp;lt;나는리뷰어다&amp;gt; 활동을 위해서 책을 협찬&amp;nbsp;받아 작성된 서평입니다.&quot;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-03-28-23-36-59 002.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G9d2N/btsM1ozsvJ5/FoxHrRtLBNnUE7eKtbAnpK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G9d2N/btsM1ozsvJ5/FoxHrRtLBNnUE7eKtbAnpK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G9d2N/btsM1ozsvJ5/FoxHrRtLBNnUE7eKtbAnpK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG9d2N%2FbtsM1ozsvJ5%2FFoxHrRtLBNnUE7eKtbAnpK%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;533&quot; data-filename=&quot;KakaoTalk_Photo_2025-03-28-23-36-59 002.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 저자분은 아틀라시안에서 근무를 하고 계시는 분이고 이전에도 리액트와 관련된 책을 많이 쓰셨던 분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 리액트를 단순히 배우는 것에서 그치는 것이 아니라, 어떻게 하면 더 유지보수하기 쉽고 확장 가능한 구조로 개발할 수 있을지에 대해서 설명하고 있습니다. 확실히 저자의 경험을 통해서 많은 내용이 설명되어 있다는 느낌을 받을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지 정도로 책의 포인트를 찝어봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 패턴을 통해 리액트를 체계적으로 익히다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 단순히 리액트에서 제공하는 API를 개념적으로만 나열하는 방식이 아니라, 실무에서 자주 사용되는 패턴을 중심으로 리액트를 학습할 수 있도록 구성되어 있습니다. 컴포넌트의 재사용성을 높이는 방법, 상태 관리를 효율적으로 하는 패턴, 성능 최적화 전략 등 실무에서 고민해야 하는 내용에 대해서 잘 다루고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 설계 원칙을 고민하게 하는 책&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴을 익히는 것에서 끝나는 것이 아니라, 어떻게 하면 더 구조적으로 설계할 수 있을지에 대한 고민도 함께 할 수 있습니다. 디자인 패턴과 SOLID 원칙을 리액트 환경에서 어떻게 적용할 수 있는지 설명하는 부분이 좋았는데 보통 일반적인 책에서는 이에 대한 내용은 잘 다루지 않기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-03-28-23-36-59 001.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZC1HX/btsM114SA0V/ZyaT64MNSDnWPsKDRlMkYk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZC1HX/btsM114SA0V/ZyaT64MNSDnWPsKDRlMkYk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZC1HX/btsM114SA0V/ZyaT64MNSDnWPsKDRlMkYk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZC1HX%2FbtsM114SA0V%2FZyaT64MNSDnWPsKDRlMkYk%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;533&quot; data-filename=&quot;KakaoTalk_Photo_2025-03-28-23-36-59 001.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 실무에서 바로 적용할 수 있는 내용이 많다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서 다루는 예제들은 실제로 개발을 하면서 접하게 될 문제들에 대한 해결책도 제시하고 있어서 실무에 바로 적용할 수 있다는 장점이 있습니다. 대부분의 사람들이 리액트로 개발을 하면서 고민했던 내용들이 많이 포함되어 있어서 공부한 내용을 실제로 회사의 코드에 녹이는 싸이클을 만들어 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;총평&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;패턴으로 익히고 설계로 완성하는 리액트&amp;gt;는 단순히 리액트를 배우는 것이 아니라, 더 좋은 리액트 애플리케이션을 만들기 위해 어떻게 설계해야 하는지를 고민할 수 있게 도와주는 책입니다. 실무에서 자주 고민하는 문제들을 깊이 있게 다루고 있어, 리액트를 더 깊이 있게 공부하고 싶은 개발자에게 적극 추천합니다.&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <category>리액트</category>
      <category>패턴으로 익히고 설계로 완성하는 리액트</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/355</guid>
      <comments>https://imnotadevleoper.tistory.com/355#entry355comment</comments>
      <pubDate>Fri, 28 Mar 2025 23:44:24 +0900</pubDate>
    </item>
    <item>
      <title>[리뷰] 그로킹 알고리즘</title>
      <link>https://imnotadevleoper.tistory.com/354</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&quot;한빛미디어 서평단 &amp;lt;나는리뷰어다&amp;gt; 활동을 위해서 책을 협찬&amp;nbsp;받아 작성된 서평입니다.&quot;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-02-28-23-16-57 001.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cF3MYo/btsMyDYGOgz/1bMg61qY4sTusiyJisZqhK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cF3MYo/btsMyDYGOgz/1bMg61qY4sTusiyJisZqhK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cF3MYo/btsMyDYGOgz/1bMg61qY4sTusiyJisZqhK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcF3MYo%2FbtsMyDYGOgz%2F1bMg61qY4sTusiyJisZqhK%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;533&quot; data-filename=&quot;KakaoTalk_Photo_2025-02-28-23-16-57 001.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에 제가 선택했던 책은 그로킹 알고리즘이라는 책인데요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;제가 해당 책을 읽고 싶다는 생각을 하게 된 계기는 코딩 테스트를 위해서가 아닌 알고리즘 자체를 학습해보고 다양한 상황에서 겪게 되는 문제를 해결할 때 좋은 해결 방안을 도출하는 경험을 해보고 싶었기 때문이에요. 그리고 해당 책은 제 니즈에 맞게 내용이 구성되어 있는 것 같아 읽어보게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;책은 초반부에는 알고리즘이 무엇인지에 대해서 설명하고 빅오 표기법을 설명하는 시간을 갖습니다. 빅오 표기법은 대부분의 사람들에게 익숙하겠지만 왜 중요한지에 대해서 조금 더 자세하게 설명해주고 있는 느낌이었어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이후에는 배열, 연결리스트, 재귀와 같은 알고리즘의 기초가 되는 내용에 대해서 학습을 시작하고 나머지 부분에서는 다양한 알고리즘들을 소개하고 있어요. 이 알고리즘들은 코테에서 많이 언급되는 내용에 덧붙여서 더 공부하면 좋을만한 내용들도 많이 포함하고 있어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가장 인상적이라고 느꼈던 부분은 책 표지에 있는 &quot;그림으로 개념을 이해하는&quot;이라는 표현에 맞게 알고리즘에 대한 모든 설명이 그림과 같이 실생활의 예시를 들어서 설명하고 있다는 점이에요. 알고리즘을 공부할 때 어렵게 느껴지는 부분중에 하나는 &quot;실제로 이런 알고리즘을 어디에 사용하지?&quot;라는 부분도 빠질 수 없다고 생각하는데 해당 고민에 대한 내용을 해소시켜주는 것 같아서 좋았어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2025-02-28-23-16-58 002.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIap48/btsMyITabzR/LSKNmZD0AqKNlbwOQZoKD0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIap48/btsMyITabzR/LSKNmZD0AqKNlbwOQZoKD0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIap48/btsMyITabzR/LSKNmZD0AqKNlbwOQZoKD0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIap48%2FbtsMyITabzR%2FLSKNmZD0AqKNlbwOQZoKD0%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;533&quot; data-filename=&quot;KakaoTalk_Photo_2025-02-28-23-16-58 002.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;먼저 알고리즘에 대한 개념 이해를 돕고, 이후에 해당 알고리즘이 적용되는 문제를 어떻게 해결할 수 있을지 차근차근 설명하는 방식으로 진행되는 것이 어려운 개념을 공부할 때도 도움이 많이 되었던 것 같아요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 각 챕터의 마지막에는 제대로 학습했는지를 확인할 수 있는 연습문제가 있어요. 공부를 할 때 가장 중요한 것은 제대로 공부를 했는지 내가 판단할 수 있어야 한다고 생각하는데요. 확인할 수 있는 방법으로는 인출을 하는 방법이 제일 좋다고 생각해요. 저도 이론적인 내용을 공부할 때는 책을 보지 않고 스스로 학습한 지식을 모두 꺼내보는 연습을 하는데요. 자연스럽게 챕터의 마지막에 연습문제를 풀면서 비슷한 과정을 경험할 수 있어서 이 부분도 좋았던 것 같아요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 책은 코딩테스트를 위한 알고리즘을 공부하는 목적으로도 읽을 수 있지만 저처럼 알고리즘 자체를 평소에 어렵게 느끼시거나, 혹은 개념을 위주로 공부하고 싶은 니즈가 있으신 분들에게 꼭 추천드리고 싶은 책중에 하나입니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발 서적/리뷰</category>
      <category>그로킹 알고리즘</category>
      <category>알고리즘</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/354</guid>
      <comments>https://imnotadevleoper.tistory.com/354#entry354comment</comments>
      <pubDate>Fri, 28 Feb 2025 23:29:57 +0900</pubDate>
    </item>
    <item>
      <title>개발을 공부하는 것에 대한 개인적인 생각</title>
      <link>https://imnotadevleoper.tistory.com/353</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 개발을 공부해오면서 들었던 생각을 간단하게 정리해봤습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공부에는 왕도가 있을까?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 사람들은 효율적인 길을 좋아하고 찾으려고 한다. 예를 들어서 게임을 할 때도 어떤 직업을 키우는게 좋은지, 레벨에 따라서 어떤 사냥터가 좋은지, 스킬 트리는 어떻게 찍는게 좋은지와 같이 최적화를 하고 싶어 하는데 공부에 있어서도 이런 길을 찾으려고 한다. 공부도 당연히 그런 길이 있을 것이라고 생각하기 때문이다.&lt;/li&gt;
&lt;li&gt;이에 대한 내 생각은 공부에도 그런 길은 있다고 생각한다. 다만 효율적인 길을 선택하지 않더라도 그 속에서 배움이 분명히 있다고도 생각한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;효율적인 길을 선택하는 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1~2년차에 좋은 회사로 점프&lt;/li&gt;
&lt;li&gt;그리고 이후의 커리어&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비효율적인 길을 선택하는 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3~4년차에 좋은 회사로 점프&lt;/li&gt;
&lt;li&gt;그리고 이후의 커리어&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비효율적인 길을 걸어가는 동안에 시행착오를 겪으면서 정말로 나에게 &lt;b&gt;최적화된 길&lt;/b&gt;을 찾는다면, 그 이후의 성장 속도는 &lt;b&gt;효율적인 길을 선택한 것보다 더 앞설 수 있다&lt;/b&gt;고 생각한다.&lt;/li&gt;
&lt;li&gt;그래서 결론은 &lt;b&gt;둘 다 무엇을 선택해도 괜찮다고 생각이 든다.&lt;/b&gt; 가장 &lt;b&gt;주의해야 할 점&lt;/b&gt;은 &lt;b&gt;완벽한 길을 찾는 데 너무 많은 시간을 쓰는 것&lt;/b&gt;이다. &lt;b&gt;그냥 하는 것. 이게 제일 중요하다고 생각한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 공부는 무엇을 위함인가?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현실적으로는 &lt;b&gt;개발 공부가 취업 또는 이직을 위한 행동임에 동의&lt;/b&gt;하지만 &lt;b&gt;마인드는 달라야 한다&lt;/b&gt;고 생각한다. 그리고 사람은 정말 신기하게도 같은 행동을 하더라도 어떻게 생각하고 하냐에 따라서 분명히 다른 결과가 나오곤 한다. 그러면 &lt;b&gt;어떤 마인드를 가져야 하는 것일까?&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;근본적으로 생각했을 때 &lt;b&gt;개발 공부는 왜 하는 것일까&lt;/b&gt;?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개발을 하기 위함이다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그러면 &lt;b&gt;개발을 왜 하는 것인가?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;돈을 벌기 위함&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제품, 서비스를 만들기 위함&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;즉, &lt;b&gt;개발 공부는 제품, 서비스를 만들기 위함&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;그런데 &lt;b&gt;제품이나 서비스를 만드는 것이 꼭 회사를 다녀야만 할 수 있는 행위인가?&lt;/b&gt; 라고 생각했을 때 그렇지 않다고 생각한다. &lt;b&gt;개발은 혼자서도 할 수 있다&lt;/b&gt;. 그리고 &lt;b&gt;코드 퀄리티와 제품의 성공은 관련이 없진 않지만 다른 결의 이야기다.&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그래서 개발자로 취업 준비를 하는 것이 오로지 &lt;b&gt;회사 면접을 위한 공부가 되는 방향&lt;/b&gt;은 아니어야 한다고 생각하는데 여기서 &lt;b&gt;개발자라는 직업을 왜 선택했는지에 따라 사람들의 행동이 갈린다고 생각한다&lt;/b&gt;.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단순히 직업으로 선택한 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;돈을 많이 주니까&lt;/li&gt;
&lt;li&gt;앞으로 미래에 괜찮아 보이니까&lt;/li&gt;
&lt;li&gt;몸은 편할 것 같으니까&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정말 개발 자체가 재밌는 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제품을 만들어 내는 게 재밌어서&lt;/li&gt;
&lt;li&gt;내가 만든 서비스를 사용자들이 쓰는 게 좋아서&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단순히 직업으로 선택을 한 사람의 경우 가장 큰 목표가 취업
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;취업을 위해서 &lt;b&gt;코테를 공부하고, 기술 면접 질문을 달달 외우고&lt;/b&gt; 할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그러나 개발 자체가 재밌는 사람들은 다양한 제품들을 만든다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;블로그 자동화&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;나만의 &lt;b&gt;오픈 소스&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;다양한 &lt;b&gt;사이드 프로젝트&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;개발자라는 직업을 선택한 것이 개발 자체가 재밌고 제품을 만드는 게 재밌어서 선택했다고 하면 취업이 당장 되지 않더라도 큰 부담이 있지는 않는 것 같으나 우선 직업으로서 선택한 경우에는 지치기 쉽다고 생각한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개발자는 후자와 같은 사람이 되어야 한다고 생각한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지금의 내가 어떻든 상관 없다&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞에서 얘기한 뉘앙스로 보면 개발자를 단순히 직업으로만 생각한 경우에 대해 부정적으로 표현하기는 했지만 사실 전혀 문제가 없다고 생각한다. 적어도 지금의 상태로만 봤을 때는 그렇다.&lt;/li&gt;
&lt;li&gt;가장 중요한 것은 &lt;b&gt;앞으로 어떤 관점으로 개발을 바라보고 공부할 것인지에 달려있다고 생각한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;저 같은 경우에는&amp;hellip;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고3때 대학은 가야겠는데 하고 싶은 일은 없었다. 게임은 좋아해서 그러면 컴퓨터? 관련된 걸로 갈까? &amp;rarr; 컴퓨터공학과&lt;/li&gt;
&lt;li&gt;대학생 1학년 ~ 4학년까지 학점은 땄지만 웹, 앱 이런 기술을 공부하지는 않았다.&lt;/li&gt;
&lt;li&gt;졸업하고 나서 큰일 났다고 생각하고 부트 캠프에서 개발 공부해서 취업 했다.&lt;/li&gt;
&lt;li&gt;취업하고 1년 정도는 제품에 관심은 없었다. 관심 있던 건 &lt;b&gt;개발을 잘하고 싶다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발을 잘하면 연봉, 근무 환경등으로 인한 내가 얻을 수 있는 것들이 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일을 하다보니까, 여러 회사를 다니다 보니까 제품에 관심이 생겼다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지금은 제품 만드는 게 재밌는 것 같고 어떤 사업을 해야 돈을 벌 수 있을까? 자면서도 어떻게 해야 돈 벌 수 있을 까? &amp;rarr; 일을 시작하고 1년 반은 지나서&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프론트엔드 개발자로서 역량 쌓기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내가 통제할 수 없는 영역&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;역량을 쌓는 것이 &lt;b&gt;취업이나 이직&lt;/b&gt;과 연관된다면 사실 &lt;b&gt;굉장히 어려운 이야기&lt;/b&gt;라고 생각한다. &lt;b&gt;사람(회사의 인사팀, 면접관)이 사람(지원자)를 평가하는 방식&lt;/b&gt;이기 때문에 사람마다 너무나도 취향이 다르다. 그러나 &lt;b&gt;어느 정도 일관된 취향은 존재&lt;/b&gt;한다. 사람을 예로 들면 &lt;b&gt;나이, 외모, 학벌, 직업, 집안, 성격&lt;/b&gt;등이 될 수 있듯이 개발자에게도 이런 요소들이 있다고 생각한다. &lt;b&gt;FE에게 이런 요소&lt;/b&gt;들은 어떤 것들이 있을까?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사실 JD(Job Description)를 보면 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://toss.im/career/job-detail?job_id=4071101003&amp;amp;company=토스&amp;amp;detailedPosition=Mobile&quot;&gt;https://toss.im/career/job-detail?job_id=4071101003&amp;amp;company=토스&amp;amp;detailedPosition=Mobile&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wanted.co.kr/wd/251582&quot;&gt;https://www.wanted.co.kr/wd/251582&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;React&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TypeScript&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Browser, Network&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내가 통제할 수 있는 영역&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무엇을 공부할 지 결정할 수 있다.&lt;/li&gt;
&lt;li&gt;특정 회사에 100% 합격할 수 있는 공부 방법은 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다만 우리는 &lt;b&gt;최대한 확률을 높이는 것&lt;/b&gt;에 집중해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공부를 어떤식으로 할까&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이론&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;이론적인 지식은 매일 꾸준히 채워 넣어야 하는 지식&lt;/b&gt;이라고 생각한다. 이론 공부가 어렵게 느껴지거나 재미없게 느껴지는 점중에 하나는 &lt;b&gt;공부를 한 것을 바로 코드에 적용하는 것이 쉽지 않기 때문이다&lt;/b&gt;. 결과적으로 이론 공부가 코드에 녹아들고 제품에 녹아드는 방향으로 가야하지만 이 지점에 도달하기 까지는 &lt;b&gt;수련이 필요하다고 생각한다&lt;/b&gt;. 그리고 &lt;b&gt;이 수련의 시간을 견디냐 못견디냐 싸움&lt;/b&gt;이라고 생각한다.&lt;/li&gt;
&lt;li&gt;개인적으로 생각하는 이론 공부 할 것들
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JavaScript
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바스크립트 딥다이브
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코어 자바스크립트&lt;/li&gt;
&lt;li&gt;자바스크립트 완벽가이드&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ko.javascript.info/&quot;&gt;https://ko.javascript.info/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TypeScript
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타입으로 견고하게 다형성으로 유연하게
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;러닝 타입스크립트&lt;/li&gt;
&lt;li&gt;이펙티브 타입스크립트&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/intro.html&quot;&gt;https://www.typescriptlang.org/docs/handbook/intro.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;React
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;http://react.dev/&quot;&gt;react.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;모던 리액트 Deep Dive&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Browser
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저 렌더링
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/59361&quot;&gt;https://d2.naver.com/helloworld/59361&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;크롬 개발자 도구 활용하기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;element, console, network, performance, react devtools등등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Network
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/15894097&quot;&gt;그림으로 배우는 HTTP &amp;amp; Network&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;http 완벽 가이드&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;코드, 설계에 관련된 책
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;좋은 코드, 나쁜 코드&lt;/li&gt;
&lt;li&gt;리팩토링, 클린코드, 클린 아키텍처와 같은 책&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;기타
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함께 자라기&lt;/li&gt;
&lt;li&gt;프로그래머의 길, 멘토에게 묻다&lt;/li&gt;
&lt;li&gt;실용주의 프로그래머&lt;/li&gt;
&lt;li&gt;프로그래머의 뇌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실습&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드를 작성한다 &amp;rarr; 제품을 만든다 &amp;rarr; 출시하고 운영한다&lt;/li&gt;
&lt;li&gt;위 싸이클은 &lt;b&gt;실제로 회사에서 일을 하는 방식과 다를 게 없다&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;이 방식대로 개발을 하면 사실 혼자 공부를 하면서도 &lt;b&gt;회사에서 일을 하는 것과 비슷한 경험&lt;/b&gt;을 할 수 있다고 생각한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 코드 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무언가를 할 때 그게 진짜로 어려운 게 맞나?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;진짜로 어려운 게 맞다, 착각이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떻게 잘 이해할 수 있을까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큰 그림 그려보기(아키텍처)&lt;/li&gt;
&lt;li&gt;동료를 괴롭혀서 질문 많이 하기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;신입 입장에서는 눈치 보인다고&lt;/b&gt; 생각한다.&lt;/li&gt;
&lt;li&gt;(결론) 질문 무조건 하는 게 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PR을 통해서 피드백 받기&lt;/li&gt;
&lt;li&gt;코드 많이 읽기&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/353</guid>
      <comments>https://imnotadevleoper.tistory.com/353#entry353comment</comments>
      <pubDate>Sun, 2 Feb 2025 23:52:11 +0900</pubDate>
    </item>
    <item>
      <title>웹 Storage를 조금만 더 잘 다뤄보기</title>
      <link>https://imnotadevleoper.tistory.com/352</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 프론트엔드 개발을 하면서 때때로 사용해야 하는 저장소 중에 하나인 &lt;code&gt;Browser의 Storage를 어떻게 하면 잘 활용할 수 있을까?&lt;/code&gt;에 대한 글을 작성해보려고 하는데요. 저희가 많이 사용하는 Storage에는 LocalStorage, SessionStorage등이 있을 것 같은데요. 이 중에서 LocalStorage에 대한 예시를 같이 보면서 얘기하려고 해요. 기존에 Stroage Interface가 제공하는 API들을 사용하는 것은 개발을 할 때 어떤 아쉬움이 있는지 확인하고 어떤 방향으로 개선하려고 하는지 고민을 하고 구현체를 만들어 보려고 해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Storage Interface&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Storage&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/Storage&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Storage Interface가 제공하고 있는 메소드 중에서 주로 사용하는 것에는 아래와 같은 것들이 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Storage.getItem(): key값을 넘겨주면 해당 key값에 해당하는 value값을 반환한다.&lt;/li&gt;
&lt;li&gt;Storage.setItem(): key, value값을 넘겨주는 storage에 해당 key에 value값을 저장한다, 만약 이미 해당 key에 값이 존재하는 경우에는 update 처리를 한다.&lt;/li&gt;
&lt;li&gt;Storage.removeItem(): key값을 넘겨주면 해당 key를 제거한다.&lt;/li&gt;
&lt;li&gt;Storage.clear(): storage의 모든 key가 제거된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이들 익숙하실 것이라고 생각이 들지만 만약 잘 모르시더라도 조회, 추가, 수정, 삭제등의 CRUD 기능을 할 수 있는 메소드를 제공한다고 생각하시면 이해하시는 데 문제가 없을 것 같아요.&lt;br /&gt;인터페이스를 보면 사실 크게 문제는 없는 것 같아요. 메소드 이름도 적절하다고 느껴지고 input, output도 적절하다고 느껴지기 때문이에요. 그러면 해당 인터페이스의 구현체인 localStorage를 사용하는 코드를 같이 보면서 어디서 아쉬움을 느꼈는지 확인해보려고 해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 코드를 작성하기 보다는 실제로 로컬 스토리지 코드를 사용하는 부분만 보면서 예상되는 문제점을 얘기해보려고 해요. 아래는 투두리스트에서 사용할법한 코드 조각들이에요. 로컬스토리지와 관련된 코드가 어떻게 사용되는지 위주로 확인해주시면 좋을 것 같아요. (불변성에 대한 얘기는 하지 않으려고 해요)&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 할 일 목록을 조회해요.
function getTodos() {
    const todos = JSON.stringify(localstoage.getItem('todos'));

    return todos;
}

// 할 일을 추가해요.
function addTodo(todo) {
    const todos = JSON.stringify(localstoage.getItem('todos'));

    todos.push(todo)

    localStorage.setItem('todos', JSON.stringify(newTodos));
}

// 할 일을 변경해요.
function updateTodo(id, newTodo) {
    const todos = JSON.stringify(localstoage.getItem('todos'));
    const index = todos.findIndex(todo =&amp;gt; todo.id === id);

    todos[index] = newTodo;

    localStorage.setItem('todos', JSON.stringify(todos)); 
}

// 할 일을 삭제해요.
function deleteTodo(id) {
    const todos = JSON.stringify(localstoage.getItem('todos'));
    const newTodos = todos.filter(todo =&amp;gt; todo.id !== id);

    localStorage.setItem('todos', JSON.stringify(newTodos)); 
}

// 할 일을 초기화해요.
function clearTodos() {
    localStorage.clear();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 작성된 코드들을 봤을 때 제가 생각하는 아쉬운 점들은 아래와 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스토리지에 접근할 때 key값에 해당하는 문자열을 넘겨줘야 한다.&lt;/li&gt;
&lt;li&gt;스토리지에서 데이터를 조회하거나 추가할 때 직렬화 과정이 필요하다.&lt;/li&gt;
&lt;li&gt;여 러 곳에서 스토리지 코드를 사용하는 경우 key값을 변경해야 할 때 비용이 크다.&lt;/li&gt;
&lt;li&gt;에러 케이스에 대응해야 한다.&lt;/li&gt;
&lt;li&gt;어떤 스토리지에 접근하는지 직관적이지 않다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4, 5번에 대해서는 조금 더 얘기를 드려보려고 해요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬 스토리지의 에러 케이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 로컬 스토리지를 활용할 때 에러 처리를 거의 해보지 않으셨을 것 같아요. 그러나 로컬 스토리지를 사용할 때 발생할 수 있는 에러들은 분명히 있기 때문에 간단하게 설명드리려고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브라우저에서 로컬 스토리지를 사용하지 못하게 설정되어 있는 경우&lt;/li&gt;
&lt;li&gt;로컬 스토리지에 저장되어 있는 데이터가 깨져서 파싱에 실패하는 경우&lt;/li&gt;
&lt;li&gt;로컬 스토리지의 약 5MB 저장 공간을 초과하는 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 위와 같은 에러가 발생할 경우가 드물긴 하지만, 에러가 발생할 가능성이 있다라고 하면 그에 대한 대응을 하는게 맞다고 생각을 해요. 따라서 에러 대응이 필요한데, 위의 예제 코드와 같이 로컬 스토리지를 사용한다면 사용하는 곳에서 반복적으로 에러 처리를 해야하는 불편함이 있을 것 같아요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스토리지 접근의 직관성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 내용은 개인적인 취향이라고 볼 수 있을 것 같아요. 코드를 같이 보면서 저는 코드를 어떤식으로 읽어나가는지 말씀드리려고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;localStorage.getItem('todos'): 로컬스토리지에서 값을 가져올 건데, 그 값은 투두 목록에 해당하는 값이야.&lt;/li&gt;
&lt;li&gt;localStorage.setItem('todos', todos): 로컬스토리지에 값을 저장할건데, todos라는 key에 투두 목록을 저장할거야.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 아래와 같이 코드를 읽을 수 있다면 어떨까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스토리지에서 할일 목록을 가져올거야.&lt;/li&gt;
&lt;li&gt;스토리지에 할일 목록을 저장할거야.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 정도의 간단한 코드에서는 어떤 방식으로 코드를 작성하더라도 전혀 상관이 없지만, 코드의 양이 많아지고 복잡도가 높아지는 경우에는 최대한 간결하게 읽을 수 있도록 표현하는게 좋다고 생각해요. 그래서 조금 더 명확하게 코드를 읽을 수 있도록 하는 것도 고려해보려고 해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이러면 어떨까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 위에서 얘기했던 아쉬운 점들을 개선해보려고 해요.&lt;br /&gt;로컬 스토리지를 사용하는 쪽에서 어떻게 사용할 수 있으면 좋을까요?&lt;br /&gt;저는 아래와 같이 사용할 수 있으면 좋을 것 같아요.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// todo 목록을 조회해요.
function getTodos() {
    const todos = todoLocalStorage.getItem();

    return todos;
}

// todo를 추가해요.
function addTodo(todo) {
    const todos = todoLocalStorage.getItem();

    todos.push(todo)

    todoLocalStorage.setItem(newTodos)
}

// todo를 변경해요.
function updateTodo(id, newTodo) {
    const todos = todoLocalStorage.getItem();
    const index = todos.findIndex(todo =&amp;gt; todo.id === id);

    todos[index] = newTodo;

    todoLocalStorage.setItem(todos) 
}

// todo를 삭제해요.
function deleteTodo(id) {
    const todos = todoLocalStorage.getItem();
    const newTodos = todos.filter(todo =&amp;gt; todo.id !== id);

    todoLocalStorage.setItem(todos) 
}

function clearTodos() {
    todoStorage.clear();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요하게 반복되는 코드들을 제거하고 무엇(What)을 하고 싶은지에 대해 집중할 수 있는 코드로 표현이 된 것 같아요. 이와 같이 사용하기 위해서는 todoLocalStorage는 어떻게 구성이 되어야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제는 로컬 스토리지와 할일 목록에 대한 데이터를 다루고 있지만, 실제로는 세션 스토리지를 사용할 수도 있고 여러 데이터를 저장할 수 있기 때문에 저는 원형이 되는 하나의 클래스를 만들고, 특정 데이터를 저장하고 싶을 때는 클래스를 통해 객체를 생성하여 데이터 저장소를 만들어 보려고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 원형이 되는 클래스는 아래와 같이 만들 수 있을 것 같아요.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;export class BaseStorage&amp;lt;T&amp;gt; {
  private storage: Storage;

  private key: string;

  private errorFallbackValue?: T;

  constructor({
    storage = localStorage,
    key,
    initialValue,
    errorFallbackValue,
  }: {
    storage?: Storage;
    key: string;
    initialValue?: T;
    errorFallbackValue?: T;
  }) {
    this.storage = storage;
    this.key = key;
    this.errorFallbackValue = errorFallbackValue;

    if (initialValue) {
      this.setItem(initialValue);
    }
  }

  getItem() {
    try {
      const item = JSON.parse(this.storage.getItem(this.key) ?? JSON.stringify(this.errorFallbackValue)) as T;
      return item;
    } catch {
      // TODO: 스토리지를 사용하는 상황에 맞게 별도의 처리를 할 수 있을 것 같은데요.
      // 만약 스토리지에 데이터가 꺠져서 에러가 발생하는 경우에 해당 데이터를 아예 제거하거나
      // errorFallbackValue로 교환할 수 있을 것 같아요.
      return this.errorFallbackValue;
    }
  }

  setItem(value: T) {
    try {
      this.storage.setItem(this.key, JSON.stringify(value));
    } catch (error) {
      // 여기서의 에러 처리는 어떤 데이터를 저장하는지에 따라 달라질 것 같아요.
      if (error instanceof Error) {
        console.warn(&quot;스토리지에 저장하는 동작이 실패했어요.&quot;, error.message);
        return;
      }
    }
  }

  removeItem() {
    try {
      this.storage.removeItem(this.key);
    } catch (error) {
      // 여기서의 에러 처리는 어떤 데이터를 저장하는지에 따라 달라질 것 같아요.
      if (error instanceof Error) {
        console.warn(&quot;스토리지의 아이템 제거에 실패했어요.&quot;, error.message);
        return;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 클래스는 생성자로 storage, key, initialValue, errorFallbackValue등의 값을 주입 받아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;storage: 어떤 스토리지를 사용할 것인지&lt;br /&gt;key: 스토리지에 어떤 key 값으로 접근할 것인지&lt;br /&gt;initialValue: 스토리지를 사용할 때 초기값을 지정하고 싶은 경우&lt;br /&gt;errorFallbackValue: 만약 에러가 발생했을 경우 대신해서 어떤 값을 반환할 것인지&lt;br /&gt;(initialValue와 errorFallbackValue는 많이 사용 될 옵션은 아닐 것 같지만 글을 작성하면서 추가해봤어요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 위 클래스를 이용해서 특정 데이터를 저장하게 될 스토리지는 아래와 같이 만들 수 있을 것 같아요.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export type Todo = {
  id: string;
  title: string;
  description: string;
  done: boolean;
  dueDate?: Date;
};


export const todoStorage = new BaseStorage&amp;lt;Todo[]&amp;gt;({ key: &quot;todos&quot;, storage: localStorage }); &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 스토리지를 다루는 코드를 만들어보면 스토지리를 사용하는 입장에서 알 필요가 없는 How에 대한 것들은 클래스 내부에 숨기고 어떤 데이터를 조회하고 추가할 것인지에 대한 관심사만 명확하게 드러낼 수 있다고 생각해요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 제공하는 스토리지를 사용할 때 주어진 Storage API를 그대로 사용하는 것도 물론 괜찮을 것 같아요. 그러나 한번 래핑을 했고 한 단계 상위 수준의 추상화를 통해서 불필요한 정보를 내부로 숨기고 꼭 알아야 되는 정보만 외부로 노출시킬 수 있었던 것 같아요. 이와 같이 평소에 당연하게 사용하던 인터페이스도 불편함을 느끼거나 더 좋은 방식으로 사용할 수 있는 방법이 있다면 추상화를 시도해봐도 좋을 것 같아요.&lt;/p&gt;</description>
      <category>개발</category>
      <category>로컬스토리지</category>
      <category>스토리지</category>
      <category>인터페이스</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/352</guid>
      <comments>https://imnotadevleoper.tistory.com/352#entry352comment</comments>
      <pubDate>Sun, 5 Jan 2025 20:31:58 +0900</pubDate>
    </item>
    <item>
      <title>2024년 한 해를 돌아보며</title>
      <link>https://imnotadevleoper.tistory.com/351</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년은 되게 많은 일이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 해에 이직을 2번이나 하기도 했고 멘토로서 첫 활동을 시작했고 그 외에 하고 싶었던 것들을 했던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;있었던 일들을 간단하게 기록하며 어떤 일들이 있었고 무엇을 배웠는지 정리해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자유인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 다니고 있던 회사가 결국에는  그만 운영하게 되어서 23년 12월부터 자유인이 됐다. 적어도 2~3년은 다니면서 좋은 동료분들과 함께 하고 싶었는데 갑작스럽게 나오게 되어서 아쉬움이 컸고 다음 이직에 대한 걱정이 있었다. 이직 자체는 23년 9월 부터 꾸준히 했었으나 잘 안됐기 때문이다. 그러나 계속 두드리다보니 3~4개 정도 회사와 동시에 면접을 진행하게 되었고 2개 회사에 최종 합격을 하게 되었다. 공백기가 너무 길지 않아서 다행이라고 느껴졌다. 한편으로는 항상 준비되어 있는 사람이어야 겠구나 하는 생각이 들었다. 인생에는 내가 제어할 수 없는 영역이 있고 하루 아침에 나에게 찾아올 수 있다. 인생에 우연히 기회가 찾아왔을 때 잡을 수 있는 사람이 되자.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;24년 첫 번째 이직&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;새로운 회사인 식스티헤르츠로 출근을 시작했다. 첫 출근은 항상 설레임과 함께 걱정이 존재하는 것 같다. 연차에 비해서 회사를 여러 곳 다녔기 때문에 이번이 3번째 경험이었지만 익숙해지기는 어려운 것 같다. 더욱이 새로운 조직에서 조금 더 긴장하는 사람이기 떄문에 시간이 조금 필요했던 것 같다. 시간이 조금 지나고 동료분들과 얘기를 해보니 모두 좋은 분들이라는 것을 느낄 수 있었다. 커리어를 이어나가는 동안 좋은 동료들과 함께할 수 있다는 점이 참 감사한 것 같다. 이전에 경험해보지 못한 것들을 제품 관점에서, 개발 관점에서 경험해볼 수 있었고 좋은 시간이었다. 아쉬운 점들도 있었지만  불완전한 인간이 만들어 나가고 있는 세상에 완벽한 것이 어디있을까? 단지 상황이 좋지 않았던 것이라고 생각한다, 물론 이것을 어떻게 받아들일지는 구성원들의 몫이지만. 퇴사를 결심하고 나서 팀 리더에게 이야기를 꺼내는 순간은 항상 어려운 것 같다. 언젠가는 다시 만나 일을 할 수 있게 된다면 좋겠다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;24년 두 번째 이직&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 해에 또 이직을 하게 됐다. 토스 플레이스로 이직을 하게 되었고 다음달부터 출근을 할 예정이다. 토스라는 회사는 일을 정말 잘 하는 회사라고 생각하고 있었는데 지금 토스에 계신 예전 동료분께서 너무 만족하면서 다니고 있다고 말씀해주셔서 항상 관심을 갖고 있었다. NEXT 전형을 통해서 합격을 하게 되었는데 코테, 과제, 기술 인터뷰를 경험해보니 내가 해왔던 공부 방향이 적절했던 것 같다는 생각이 들어서 좋았다. 특히 기술 인터뷰에서 면접관분들과 대화를 나누는 시간은 되게 재밌었다. 기술적으로 더 좋은 방향을 위해 토론하는 과정이 즐거웠다. 이전에 경험해보지 못했던 회사의 규모와 도메인이라서 설레임이 있고 가슴이 뛰는 것 같다. 좋은 동료가 되는 것이 목표다. 그러기 위해서 제품 개발에 몰입하는 시기를 보내려고 한다. 이번 이직을 진행하면서 꾸준히 무언가를 해오다보면 관련된 기회를 잡을 수 있다는 생각이 들었고 앞으로도 이 꾸준함으로 더 많은 것들을 이뤄내고자 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;멘토&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드잇 프론트엔드 부트캠프에서 첫 멘토를 시작했다. 개발 부트캠프에서 자기소개를 할 때 스스로 멘토라고 얘기하는 게 굉장히 어색했다. 그래서 실제로 멘티분들에게는 멘토라는 명칭보다는 내 이름을 불러주시면 더 좋을 것 같다는 얘기도 드렸었는데 나 스스로는 누군가의 멘토라고 할 만큼 역량이 있거나 뛰어난 사람이 아니라고 생각했던 것 같다 단지 같은 업계 동료일 뿐이고 조금 일찍 경험해본 사람에 불과하다고 생각했기 때문이다. 그러다보니 되게 진심으로 많은 시간을 들여서 멘토링을 했던 것 같다. 많은 분들에게 도움을 드릴 수 있었고 좋은 멘토라는 얘기를 들었지만 나를 돌보는 데 있어서는 조금 소홀했다는 생각이 들었다. 좋은 멘토가 되기 위해서는 여러가지 준비물이 필요하지만 가장 중요한 것을 놓치고 있었던 게 아닐까 싶었다. 나는 성장했지만 성장하지 않았던 것 같다. 나를 돌보는 것, 나라는 사람을 가장 중요하게 여기는 것 또한 정말 중요하다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;교육&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다양한 교육들을 들었다. NEXTSTEP, 프로덕트 엔지니어 캠프, 패스트 캠퍼스의 이너써클, 각종 멘토링 교육들이었는데 전문가의 도움을 받아서 새로운 지식을 습득하는 과정은 언제나 의미가 있는 것 같다. 개발에 대한 지식을 채워가면서 개발자로도 성장했지만 개발 외적인 교육도 있었고 다양한 동료분들을 만나면서 한 사람으로서도 성장할 수 있었던 것 같다. 교육을 통해서 세상을 더욱 깊고 다양하게 바라볼 수 있게 되는 것 같은데 아는 만큼 보인다라는 말이 정말 많이 공감이 됐다. 내년에는 개발과 관련된 교육도 물론 좋지만 이 외에 다양한 분야의 것들을 배워보고 싶다는 생각이 들었다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외부 활동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년에 세웠던 계획중에 24년에는 네트워킹도 하고 다양한 활동들을 해보자는 것이 있었는데 어느 정도는 실행에 옮겼던 것 같다. 큼지막하게는 대략 아래 활동들을 했던 것 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FEConf 2024 Lightning Speaker 발표&lt;/li&gt;
&lt;li&gt;프로그래머스 데브코스 특강&lt;/li&gt;
&lt;li&gt;패스트캠퍼스 이너써클 특강&lt;/li&gt;
&lt;li&gt;SIPE 두 번째 내친소 행사 참여&lt;/li&gt;
&lt;li&gt;왓에버 챌린지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 4~5명 앞에서만 가볍게 이야기를 하다가 오프라인에서 약 100명에 가까운 사람들 앞에서 발표를 해보는 경험도 해봤는데 재밌었다. 시작하기 전에는 긴장이 많이 됐지만 막상 마이크를 잡으니 마음이 조금 편해졌는데 나에게 긴장이 풀리는 트리거는 발표하기 직전의 무언가라는 생각이 들었다. 동아리 활동에 참여하면서 다양한 직군의 동료분들을 만날 수 있었는데 정말로 훌륭한 분들이 너무 많았고 얘기를 듣고만 있어도 재밌었다. 일과 관련한 얘기도 많이 하고 나중에는 가벼운 듯 가볍지 않은 토론도 했는데 그냥 이 상황 자체가 되게 웃겼다. 새로운 사람을 만나는 활동이 되게 즐겁다 라는 것을 직접 느낄 수 있었던 시간이었던 것 같다. 내년에 어느 정도 회사에 적응을 하고 나서는 새로운 개발 동아리도 좋을 것 같고 스터디도 좋고 올해보다 다양한 활동들을 해보려고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발은 매일 해오고 있지만 매일 새롭고 재밌다. 그 이유는 여전히 어렵고 공부해야 할 것이 많기 때문이라고 생각한다. 내년에는 Computer Science에 대해서 착실하게 공부를 하면서 쌓아 나갈 생각이고 내가 가장 메인으로 다루는 FE 개발에 있어서도 꾸준히 지식을 쌓으려고 한다. 이 외에는 새로운 언어를 하나 공부하고 인프라 환경에 대해서도 공부하려고 한다. 그리고 코드 레벨에서 더 좋은 설계를 하기 위한 치열한 고민도 재밌지만 요즘에는 제품에 대한 생각을 많이 하게 된 것 같다. 제품이 사용자에게 가치를 전달하지 않으면 어떤 의미가 있을까. 나는 의미가 없다고 생각한다. 주변 지인분중에서 제품에 정말 관심이 많고 직접 다양한 개발을 하시는 분이 있는데 그 분과 매일 제품에 대해서 얘기하면서 많은 생각을 할 수 있게 된다. 인공지능이 점점 발전함에 따라서 기술적인 허들을 조금씩 낮아지는 것 같다는 생각이 들고 개발자들은 코드를 작성하는 것도 중요하지만 제품 자체에 대한 배움도 필요한 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 사람을 많이 만났던 것 같다. 다양한 활동을 하면서 이전보다 알고 지내는 사람들이 훨씬 많아졌는데 신기했다. 예전의 나라면 외부 활동을 거의 하지 않았을탠데 이런 쪽으로는 사람은 충분히 변하는 것 같다. 꾸준히 연락하고 지내는 분들도 있는데 각자의 자리에서 최선을 다하면서 서로의 안부를 묻는 느낌이라서 뭔가 멋있고 재밌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해도 여전히 뚜렷한 취미는 딱히 만들지 않았다. 그래도 조금 재밌어 하는 것들은 찾았는데 보드게임이랑 다트가 되게 재밌었다. 내년에는 내 마음의 소리를 들어보고 하고 싶은 것들은 적극적으로 찾아서 하고 경험해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/351</guid>
      <comments>https://imnotadevleoper.tistory.com/351#entry351comment</comments>
      <pubDate>Thu, 19 Dec 2024 22:17:22 +0900</pubDate>
    </item>
    <item>
      <title>토스 2024 NEXT 개발자 챌린지 Frontend Developer 최종 합격 후기</title>
      <link>https://imnotadevleoper.tistory.com/350</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이번에 토스 2024 NEXT 전형을 통해서 토스플레이스에 합격하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터뷰를 진행하면서 경험이 너무 좋았고 경험해보지 못한 도전 과제들이 많을 것 같아 선택하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합격 후기는 약 6주간의 과정을 되돌아보며 기록하고자 작성하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전형은 아래와 같이 진행되었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코딩 테스트 &amp;gt; 사전 과제 &amp;gt; 직무 인터뷰 &amp;gt; 문화적합성 인터뷰 &amp;gt; 레퍼런스 체크 &amp;gt; 최종 합격&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;unnamed.png&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R4oyo/btsKTzw7H70/XcKKRYnB8k9KaF0D9dOndK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R4oyo/btsKTzw7H70/XcKKRYnB8k9KaF0D9dOndK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R4oyo/btsKTzw7H70/XcKKRYnB8k9KaF0D9dOndK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR4oyo%2FbtsKTzw7H70%2FXcKKRYnB8k9KaF0D9dOndK%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;865&quot; height=&quot;496&quot; data-filename=&quot;unnamed.png&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;415&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지원 준비&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매년 토스에서는 NEXT 전형이 진행된다는 것을 알고 있었는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;붙을 수 있겠다는 생각보다는 코딩 테스트에서는 어떤 문제가 나오는지 한번 풀어보자 라는 생각을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주말 중에 이력서를 다시 작성했고 가벼운 마음으로 지원을 하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계열사는 토스, 토스뱅크, 토스증권, 토스플레이스중에서 두 곳을 선택할 수 있었는데 저는 토스와 토스플레이스를 선택했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코딩 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩테스트는 JavaScript 언어로 2시간 30분 동안 진행되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 생각하는 기업들의 코딩테스트 유형보다는 당장 실무에 투입된다고 했을 때 필요한 역량을 검증하기 위한 문제로 구성되어 있다는 생각이 들었습니다. 전반적으로는 잘 풀었다고 생각했지만 특정 영역은 제대로 풀지 못했기 때문에 크게 기대는 하지 않고 결과를 기다리고 있었습니다. 결과 발표날에는 합격 메일을 받게 되어서 놀랍기도 했고 사전 과제는 어느 정도 자신이 있었기 때문에 열심히 해봐야겠다는 생각을 하게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BCOHd/btsKTp840Ph/rZWZ6kLBsorfPgGkHQkn5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BCOHd/btsKTp840Ph/rZWZ6kLBsorfPgGkHQkn5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BCOHd/btsKTp840Ph/rZWZ6kLBsorfPgGkHQkn5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBCOHd%2FbtsKTp840Ph%2FrZWZ6kLBsorfPgGkHQkn5k%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;1642&quot; height=&quot;712&quot; data-origin-width=&quot;1642&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사전 과제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 과제는 토요일날 6시간 동안 진행되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 타임어택처럼 진행하는 과제라서 긴장되기도 했지만 구현하면서는 재밌다는 감정이 더 크기는 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항들을 만족시켜 나가면서도 어느 정도의 코드 퀄리티를 신경 쓰고 시간 배분을 잘하는 것에 집중했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전 과제를 보고나서 느낀 점은 정말 실무에 필요한 역량을 검증할 수 있는 문제들이 잘 구성되어 있다는 생각이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 관련한 글이 올라와서 확인해 보시면 재밌을 것 같습니다. (&lt;a href=&quot;https://toss.im/career/article/2024_NEXTDEVELOPER_6&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://toss.im/career/article/2024_NEXTDEVELOPER_6)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전 과제도 감사하게도 합격 메일을 받을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJQbIz/btsKVwZS2sf/hfX8AaYncxc3j5QOXQQIG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJQbIz/btsKVwZS2sf/hfX8AaYncxc3j5QOXQQIG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJQbIz/btsKVwZS2sf/hfX8AaYncxc3j5QOXQQIG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJQbIz%2FbtsKVwZS2sf%2FhfX8AaYncxc3j5QOXQQIG1%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;1636&quot; height=&quot;672&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;직무 인터뷰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직무 인터뷰는 과제가 종료되고 나서 약 2주 뒤에 일정을 잡게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 여유가 있는 만큼 과제와 기술, 이력서등에 대해서 개인적으로 준비하는 시간을 가졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 작성한 과제에 대해서 스스로 코드리뷰를 타이트하게 하면서 리뷰를 50개 정도 달았고 해당 코드를 개선하기 위한 솔루션까지 고민해 봤습니다. 기술에 대해서는 React, TypeScript 위주로 신경 써서 중요한 개념들을 정리했던 것 같습니다. 이력서는 지난 시간 동안 풀어왔던 기술적인 문제들을 다시 한번 떠올려보는 식으로 정리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직무 인터뷰는 약 1시간 30분 정도 진행하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무 역량을 확인할 수 있는 질문들을 매우 깊게 많이 해주셨는데요. 관련해서 이야기를 나누는 것도 재밌었고 면접관분 께서도 제 의견에 동의해주시기도 하시고 다른 의견도 제시해주셔서 기술적인 토론을 하는 시간처럼 느껴져서 시간 가는 줄 모르고 진행했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에 기술에 대한 명확한 기준을 세우는 방식으로 공부를 했던게 도움이 많이 된 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막에 시간이 많이 지나서 질문을 많이 드리고 싶었는데 일부만 드리게 되어서 쪼끔 아쉬웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터뷰가 종료되고 나서 1시간 안에 바로 합격 전화를 받게 되었는데 굉장히 놀라운 경험이었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문화적합성 인터뷰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문화적합성 인터뷰를 준비하면서 많은 생각을 해보게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;회사는 왜 이런 인터뷰를 보는 것인가?&quot;에서부터 시작해서 &quot;문화적합성 인터뷰를 잘 본다라는 정의는 무엇일까?&quot; 등의 생각이었는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 '나'라는 사람이 회사가 원하는 모습에 적합한지가 중요하다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 토스 커뮤니티의 핵심 가치, 문화들을 블로그나 유튜브 영상, 책등을 통해서 접할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 내가 이러한 환경속에서 어떠한 기여를 할 수 있을지에 대해서 고민해 보는 것으로 준비는 마쳤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문화적합성 인터뷰도 약 1시간 30분 정도 진행하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부드러운 분위기 속에서 '나'라는 사람에 대해서 같이 이야기를 나누는 과정이었다고 생각하고 저 스스로도 되게 의미 있는 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 1시간 안에 합격 전화를 받게 되어서 정말 기쁘면서도 실감이 나지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 붙을 것이라고 생각하지 못했는데 평소에 꾸준히 학습하면서 기회가 왔을 때 두드려서 그 기회를 잡게 되었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 꾸준히 학습하며 좋은 제품을 만들고 사용자에게 최고의 경험을 주는 것에 집중하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업과 이직이 모두 힘든 시기이지만 묵묵히 하나씩 해나가면 꼭 좋은 소식이 올 것이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 분들이 축하해주셔서 정말 감사했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend Developer</category>
      <category>토스</category>
      <category>토스 넥스트</category>
      <category>토스NEXT</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/350</guid>
      <comments>https://imnotadevleoper.tistory.com/350#entry350comment</comments>
      <pubDate>Sun, 24 Nov 2024 17:50:03 +0900</pubDate>
    </item>
    <item>
      <title>우아콘 2024를 다녀오고 느낀점</title>
      <link>https://imnotadevleoper.tistory.com/349</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 많은 회사들에서 기술 컨퍼런스를 적극적으로 진행했던 것 같은데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중에서 우아한 형제들에서 진행했던 기술 컨퍼런스에 다녀오면서 현장에서 느낀 점을 기록해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(우아콘 추첨에는 떨어졌지만 감사하게도 지인분께 표를 받아서 우아콘에 다녀올 수 있었습니다)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현장 분위기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이전에 인프콘, FEConf 2024를 다녀온 경험이 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 우아콘 2024는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;10월 30일,&lt;span&gt;&amp;nbsp;그랜드 인터컨티넨탈 서울 파르나스 5층에서 진행했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;입구에서 입장권을 보여드리면 시간표, 굿즈, 물 등을 받을 수 있었고 활동을 참여할 수 있는 부스들도 있어서 발표를 기다리는 동안에는 참여를 하거나 네트워킹을 할 수도 있었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 컨퍼런스를 참여할 때마다 느끼는 점은 열정으로 가득한 업계분들을 만날 수 있다는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 동일하게 많은 분들의 열정을 느낄 수 있고 개인적으로 많은 동기 부여가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 회사일을 하다가 오후에 참여하게 되어서 모든 세션을 다 듣지는 못했는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 들었던 세션에 대해 공유드려보고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; 통합테스트로 보다 안전한 웹프런트엔드 서비스 제공하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간부터 듣게 되었던 세션이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드에서 테스트를 작성하는 데 있어서 어떻게 접근하면 좋을지에 대한 생각을 공유해 주셨고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트의 범위를 어떻게 가져갈지에 대해서 많은 의견을 주셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을 들으면서 들었던 생각은 의미 있는 단위로 관심사가 잘 분리된 모듈들로 구성되는 게 필요할 것 같고 테스트하기 용이한 코드로 작성이 될 필요가 있다는 생각이 들었습니다. 또한 현실적으로 모든 부분에 테스트를 작성할 수는 없는데 애플리케이션의 어떤 위치에 테스트를 작성할 것인지, 테스트 종류별로 우선순위는 어떻게 되는지에 대한 얘기도 공유해 주셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모킹을 이용해서 외부 의존성을 다루고 어느 지점을 테스트하려고 하는지 구분하는 게 중요하다고 말씀해 주셨던 부분이 많은 공감이 되었습니다. 저도 회사 제품을 개발하는 데 있어서 꼭 필요한 테스트를 하루에 적어도 1개 이상은 작성해 보겠다는 액션 아이템을 만들게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디자인 시스템 문서를 생동감 있게 만들기 위한 '우아한플레이그라운드' 제작기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 개발자들이 디자인 시스템을 사용하는 과정에서 겪을 수 있는 어려움에 대해서 먼저 설명을 해주셨는데 이 부분이 많은 공감이 되었습니다. 막상 컴포넌트들을 많이 만들어놨지만 해당 컴포넌트들에 대한 맥락을 이해하고 있지 않은 사람이라면 제대로 다지인 시스템을 사용하는 게 어렵기 때문입니다. 이를 위해서 디자인 시스템 문서를 보면서 바로 코드를 수정하고 화면에 어떻게 반영되는지 볼 수 있는 플레이그라운드를 제작하셨던 과정을 설명해 주셨는데요. 약 반년 정도의 기간을 통해서 제작이 되었고 사용했던 에디터나 패키지들, 그리고 이 과정에서 겪은 에러에 대한 트러블슈팅 과정을 공유해 주셔서 재밌게 들을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 작업을 해보고 싶다는 생각이 많이 들었으나 이런 것들을 도입하는 과정에서는 회사의 현재 상황을 고려하는 게 중요할 것 같습니다. 조금 불편함을 겪더라도 이런 인터널 제품을 만드는 것보다 제품을 만들어서 시장의 핏을 찾는 게 더 중요한 단계에 있는 회사도 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트가 시작되게 된 계기는 Today I Learned 같은 슬랙 채널을 통해서 의견을 공유하면서 시작이 되었는데 상당히 인상 깊었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네이티브 모듈? 그게 뭔데요?: React Native로 만드는 배민커넥트 앱 두 번째 이야기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배민에서 리액트 네이티브로 배민커넥트 앱을 개발하셨던 경험에 대해서 소개해주셨습니다. 리액트 네이티브를 통해서 iOS, Android를 개발할 수 있다는 장점이 있지만 이로 인해서 겪게 되는 어려움들도 많은데요. 이 어려움들에는 어떤 것들이 있는지 소개를 해주셨고 각각 어떻게 풀어나가셨는지를 설명해 주셔서 마치 그 상황에 같이 있는 것 같은 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히나 네이티브 모듈을 사용하는 과정에서 지원하는 패키지가 충분하지 않아 실제로 개발을 하셨던 부분이나 버그가 발생했을 때 그 버그를 해결하기 위해 문제를 접근했던 방식들도 소개해주셨는데 제품의 퀄리티를 위해서 양보하지 않겠다는 느낌을 받아서 열정을 느낄 수 있었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 꾸준히 리액트 네이티브를 통해 앱을 개발하시게 될 것 같다고 말씀해 주셔서 앞으로의 기술적인 도전도 기대가 되는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Feconf 2024 이후로 오랜만에 개발 컨퍼런스에 참여하게 되었는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 우연히 만나게 된 지인분도 계셔서 반가웠고 다양한 기술적인 경험들을 들어볼 수 있어서 저 스스로도 생각을 많이 하고 배울 수 있었던 시간이 된 것 같습니다. 앞으로도 많은 회사들이 적극적으로 이야기를 나눠준다면 더 좋은 개발 생태계를 만들어갈 수 있겠다는 생각이 들었고 저도 다음에는 기술에 대한 발표를 꼭 해보고 싶다는 계획을 세워보게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL/일기</category>
      <category>우아콘</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/349</guid>
      <comments>https://imnotadevleoper.tistory.com/349#entry349comment</comments>
      <pubDate>Sun, 10 Nov 2024 23:56:07 +0900</pubDate>
    </item>
    <item>
      <title>글또 10기를 시작하는 나의 다짐</title>
      <link>https://imnotadevleoper.tistory.com/348</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요, 프론트엔드 개발자로 일하고 있는 유승완이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 9기에 이어서 글또 10기에도 참여하게 되었는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 글또 10기 활동을 어떻게 해볼지에 대해서 계획을 작성하고 공유해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 글또 10기 계획을 만들어보고 활동하는 데 있어서도 스스로 도움을 받으려고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계획은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 글을 작성하는 시간을 정해두기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 글을 작성할 때 AI 활용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 프론트 반상회 참여하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 커피챗 5회 이상 진행해 보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글을 작성하는 시간을 정해두기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 글이라는 것은 시간이 남을 때 써야겠다고 생각하면 시작하기가 되게 어렵다고 생각하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 생각을 논리적으로 정리해야 하기 때문에 글을 쓰는 것 자체가 되게 에너지가 많이 들기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운동과 마찬가지로 특정 시간을 만들어서 하는 게 아니라면 꾸준히 하기도 되게 어렵다고 생각하기 때문에 글을 쓰는 시간을 만들어서 작성하려고 하고 글의 종류마다 전략을 다르게 가져가려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 작성하는 글과 같이 저의 생각을 정리하는 글의 경우에는 매일 정기적인 시간을 만들어서 다루고 기술과 관련된 글은 먼저 기술에 대한 학습을 정기적인 시간에 진행하고 이후에 글을 작성하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글을 작성할 때 AI 활용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 NextStep에서 글쓰기 워크숍 수업을 들을 때 챗지피티를 활용해서 글을 쓰는 방식을 배웠던 적이 있습니다. 실제로 실습을 해보면서 글을 작성했을 때 효과적으로 글을 작성하는 경험을 했었는데요. 글의 방향성에 대한 아이디어를 얻고 퇴고를 하는 과정에서도 ChatGPT와 같은 도구는 도움이 정말 많이 되기 때문에 적극적으로 활용해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프론트 반상회 참여하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 9기 활동을 할 때 프론트 반상회가 있었는데요. 참석해서 다양한 동료분들과 많은 얘기를 나누고 싶었는데 회사 일정이 매우 바빠져서 참석을 못했습니다. 이번 10기 활동에서는 반상회 일정이 정해지면 미리 시간을 잘 관리해서 꼭 참석해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;커피챗 5회 이상 진행해 보기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;글또는 다양한 직군의 동료분들이 계시기 때문에 커피챗을 하기에 되게 좋은 환경인 것 같습니다. 내가 평소에 궁금했던 분야에 대해서 알아볼 수도 있고 일을 하고 있는 프론트엔드 분야에 있어서도 궁금한 점이 있다면 다양한 도메인에서 일하시는 동료분들과 이야기를 나눠볼 수도 있습니다. 혹은 제가 관심을 가지고 있는 회사의 동료분들과도 이야기를 나눠볼 수 있습니다. 이런 과정에서 새로운 경험을 할 수 있고 다른 사람의 경험을 듣는 것 자체가 저에게는 생각을 확장하는 데 있어 많은 도움이 되었던 것 같습니다. 따라서 적극적으로, 최소 5회 이상의 커피챗을 진행해보려고 합니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 글을 작성하는 것으로 시작해서 다양한 활동까지 알차게 챙겨볼 수 있는 글또 10기 활동을 해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 10기를 마무리하는 시점에서는 더욱 풍성한 이야기를 작성할 수 있게 되면 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;</description>
      <category>TIL/일기</category>
      <category>글또</category>
      <category>글또10기</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/348</guid>
      <comments>https://imnotadevleoper.tistory.com/348#entry348comment</comments>
      <pubDate>Sun, 13 Oct 2024 19:56:39 +0900</pubDate>
    </item>
    <item>
      <title>2024년 2년 차로서 2번째 이직을 경험하며</title>
      <link>https://imnotadevleoper.tistory.com/347</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요, 프론트엔드 개발자로 일을 하고 있는 유승완이라고합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 24년에 2월에 이직을 해서 현재는 &amp;lt;식스티헤르츠&amp;gt;라고 하는 회사에서 EMS(Energy Market Service) 스쿼드에서 일을 하고 있는데요! 제가 이직을 어떤식으로 진행했었는지, 그리고 그 속에서 배웠던 것들은 어떤것들이 있는지 스스로 회고하며 공유하면 좋을 것 같아 이렇게 글로 정리를 해보게 되었습니다. 이 글을 보시게 된다면 처음부터 이야기를 읽어보시는 것도 좋고 나에게 필요한 부분만 찾아서 보시는것도 좋은 접근이 될 것 같습니다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2024-08-04-11-48-51 001.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFyDYo/btsIUrGyTlO/sMkxnRZ2bvTQkk7iZTkPy0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFyDYo/btsIUrGyTlO/sMkxnRZ2bvTQkk7iZTkPy0/img.jpg&quot; data-alt=&quot;뜬금 회식짤&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFyDYo/btsIUrGyTlO/sMkxnRZ2bvTQkk7iZTkPy0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFyDYo%2FbtsIUrGyTlO%2FsMkxnRZ2bvTQkk7iZTkPy0%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2024-08-04-11-48-51 001.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;뜬금 회식짤&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;이직을 시작한 계기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 게임 회사에서 일을 하다가 23년 6월에 피크페이라는 회사에 합류해서 채용과 관련된 제품을 개발하고 있었는데요. B2B 제품을 메인으로 개발하고 있었고 출시를 앞두고 있었지만 현실적으로 제품으로 만들어낼 수 있는 수익을 생각했을 때 제품을 출시 한다고 해도 팀을 계속 운영하기에는 어렵다는 판단이 내부적으로 있어서 23년 12월에 출시만 하고 팀을 해체하는 것으로 합의를 하게 되었어요. 대표님께서도 일찍이 이직을 진행하는 것을 추천해주셨는데요. 그래서 저는 23년 9월부터 이직을 준비하게 되었어요. 마침 이 당시에는 왓에버라고 하는 교육 기관에서 멘토링을 받고 있었는데요. 기존에는 멘토님께 이직 생각은 없고 개발자로서 역량 향상을 목적으로 멘토링을 받고 싶다고 말씀드렸기 때문에 그와 관련된 방향으로 진행하고 있었는데 급하게 이직을 해야겠다고 말씀을 드렸고 멘토님께서도 저의 상황에 맞게 멘토링 방식을 이직에 맞게 바꿔서 도와주시게 되었어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUI0LU/btsIS7hpnhY/oWC8XLUeaKpd3PyOXFKxw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUI0LU/btsIS7hpnhY/oWC8XLUeaKpd3PyOXFKxw0/img.png&quot; data-alt=&quot;앞광고&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUI0LU/btsIS7hpnhY/oWC8XLUeaKpd3PyOXFKxw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUI0LU%2FbtsIS7hpnhY%2FoWC8XLUeaKpd3PyOXFKxw0%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;280&quot; height=&quot;280&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;앞광고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;첫 시작은 좋았지만&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직을 해야겠다고 결심하고 서류를 넣기 시작했는데요. 저는 당시에 있던 회사에 적어도 2년은 다니려는 생각을 하고 있었기 때문에 너무 갑작스러운 이직을 하게 되었던 것 같아요. 그래서 급하게 이력서를 다시 작성하는 것부터 시작하게 되었어요. 그럼에도 불구하고 첫 시작은 되게 좋았다고 생각이 들었는데요. 10곳 정도를 넣었고 금융 스타트업에서 연락이 와서 바로 과제 전형을 진행하게 되었어요. 실제로 꽤 좋은 회사였기 때문에 의욕 넘치게 할 수 있었던 것 같아요. 과제 전형에 합격했고 기술 면접에서도 합격해서 이직을 하자마자 바로 성공할 것 같은 느낌에 되게 설레였는데요. 아쉽게도 최종 면접에서 떨어지고 말았어요. 결과 발표가 2~3일 정도 미뤄졌었는데 이렇게 탈락을 하게 되니까 생각보다 더 허무하더라구요. 결과를 보기 전에는 &quot;떨어져도 아무렇지 않게 계속 이직을 준비하자!&quot; 라고 스스로에게 되뇌었지만 막상 최종에서 떨어지니 조금 아쉽긴 했던 것 같아요. 그래도 금방 회복하고서 꾸준히 서류 전형을 진행했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;서류 전형&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류 전형을 진행하다 보니 이력서에 대해서 많은 생각을 하게 되었던 것 같아요. 저는 제가 이력서를 잘 쓰는 사람이라고 생각하지 않는데요. 그리고 냉정하게 지난 경력속에서 임팩트 있는 일을 해왔다는 생각이 들지는 않았어요. 그 당시의 회사의 상황에서는 꼭 필요한 일이었지만 그 일이 외부에서 봤을 때 매력적으로 보이느냐는 다를 수 있기 때문이에요. 그러다보니 서류 합격률이 그렇게 높지는 않다고 생각했었는데요. 지금 생각해보면 어느 정도는 변명이 아니었을까? 싶기도 해요. 천천히 생각해보면 분명히 경력속에서 임팩트 있고 제가 치열하게 고민했던 부분이 있었는데 이러한 내용들을 녹이지 않고 &quot;무엇(What)을 했다&quot; 라고만 저를 표현했던 것 같더라구요. 같은 행동을 하더라도 그 행동속에 숨겨져 있는 이야기를 드러낸다면 서류 전형의 합격률이 훨씬 높았을 것 같다는 생각이 들어요.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;mcdonald.jpg&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;765&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pcLm6/btsIPsfOtrX/eLmMzzYqJz0pFvr1f1vYe1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pcLm6/btsIPsfOtrX/eLmMzzYqJz0pFvr1f1vYe1/img.jpg&quot; data-alt=&quot;이력서는 이렇게?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pcLm6/btsIPsfOtrX/eLmMzzYqJz0pFvr1f1vYe1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpcLm6%2FbtsIPsfOtrX%2FeLmMzzYqJz0pFvr1f1vYe1%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;397&quot; data-filename=&quot;mcdonald.jpg&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;765&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이력서는 이렇게?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;내가 지금 하는 일은 어떻게 생각하냐에 따라서 실제로 위의 짤과 같이 표현할 수 있다고 생각하는데요. What보다는 Why를 생각하면서 내가 어떠한 이유로 그런 일을 했었는지, 그 과정속에서 배우고 실패했던 경험들을 잘 녹여낸다면 나를 잘 드러낼 수 있는 이력서를 만들 수 있을 것 같아요.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;과제 전형&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이직 과정에서 과제 전형도 꽤 진행해봤는데요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3~5시간안에 구현해야 했던 과제도 있었고 3일에서 7일 정도의 시간을 주는 과제도 있었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 과제에서 원하는 것들은 아래와 같았던 것 같아요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인에 대한 이해&lt;/li&gt;
&lt;li&gt;컴포넌트 설계&lt;/li&gt;
&lt;li&gt;문제 해결 능력&lt;/li&gt;
&lt;li&gt;빠른 개발 속도(생산성)&lt;/li&gt;
&lt;li&gt;클린 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간을 길게 주는 과제의 경우에는 충분히 여유롭게 생각하면서 개발을 할 수 있었지만 짧은 시간안에 개발해야 하는 상황에서는 평소에 코드를 작성하는 습관이 얼마나 잘 형성되어 있느냐에 따라서 크게 갈리게 되는 것 같아요. 그래서 평소에 코드를 작성하는 매 순간, 그리고 모든 라인에 왜 그렇게 작성했는지 명확한 이유를 가지고 설명할 수 있어야 한다는 생각이 많이 들었어요. 연습은 실전처럼, 실전은 연습처럼이라는 말과 같이 평소 내가 코드를 작성하는 실력이 과제 전형에서 그대로 드러나는 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 과제 전형을 진행하면서도 내가 어떤 역량을 꾸준히 늘려가야 하는지 알 수 있어서 좋았던 것 같아요. 막연하게 생각하던 나의 단점을 명확하게 알 수 있었고 내가 어떤 부분을 채워야 하는지에 대해서 힌트를 얻었던 것 같아요. 저는 개인적으로 하나를 뚜렷하게 잘하지만 단점도 있는 사람보다 단점이 없이 적당히 다 잘하는 사람이 매력적이라고 생각하는데요. 단점을 먼저 채워나가는 게 좋은 전략이 될 수 있을 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술 면접&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 면접은 대부분의 회사에서 이력서를 기반으로 진행이 되었던 것 같아요. 아무래도 경력직이다보니 프론트엔드의 특정 기술에 대해서 딱딱하게 물어보기 보다는 경력에서 어떤 일들을 했는지, 그리고 문제들을 어떤 방식으로 해결했는지, 정말 깊이 알아보고 적절한 방법으로 해결한 것이 맞는지와 같이 경력을 엄격하게 검증을 하는 느낌이 들었어요. 대부분의 회사에서의 기술 면접은 크게 어렵지 않았지만 일부 회사에서는 프레임워크 레벨에서 동작하는 기능에 대해서 실제로 이 기능이 내부적으로 어떻게 구현이 되어 있는지에 대한 질문을 받았었고 비슷하게는 대답을 했지만 답변하는 제 모습이 자신있는 모습은 아니었다는 게 기억에 남아요. 그래서 좋은 회사의 기준을 맞추기 위해서는 기술에 대해서 정말 깊이 파고들어야 한다는 생각이 많이 들었는데요. 평소에 개발을 하면서 특정 문제를 해결할 때 명확한 이유를 가지고 문제를 해결하고 기술에 대해 일반적인 기준보다 훨씬 더 깊이 공부하는 습관을 들이면 기술 면접 도 당연히 통과할 수 있다는 생각이 들었어요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최종 면접&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 면접은 대부분 그 동안의 경험을 조금 더 편하게 나누고 개발자로서, 그리고 동료로서 회사에 합류하게 되었을때 어떤 모습을 보여줄 수 있는 사람인지에 대해서 질문한다는 느낌을 많이 받았던 것 같아요. 다양한 질문들을 받았지만 속뜻은 당신이 일에 대해서 얼마나 진심이고 일을 잘 할 수 있는 사람인지에 대해서 묻는게 대부분이었던 것 같아요. 사실은 이 부분도 내가 평소에 얼마나 일에 진심이고 몰입하는지에 따라서 특별히 준비하지 않더라도 나의 이야기를 하는 것 만으로도 최종 면접을 충분히 통과할 수 있다는 생각이 들었는데요. 결국에 인터뷰라는 긴 과정은 내가 평소에 어떤 삶을 살아왔는지를 그대로 반영한다고 생각했는데요. 오늘 내가 어떻게 하루를 보냈냐에 따라서 1년, 2년뒤에 내가 속하게 될 조직이 달라지는 것은 분명한 사실인 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 여러 회사에 지원하면서 수십번의 인터뷰를 진행했고 최종적으로 2곳의 회사에 합격할 수 있었는데요. 이직을 하는 시간동안 함께 해주시며 좋은 말씀을 많이 해주셨던 멘토님이 많은 힘이 되었던 것 같아요. 결과를 두고 고민을 했고 대부분의 사람들이 추천했던 회사가 아닌 다른 곳으로 합류하게 되었어요. 두 회사 모두 장/단점이 있었지만 저는 제가 하고 싶은대로 하는게 좋을 것 같다는 생각이 들었고 제 마음이 이끄는 곳을 선택했었어요. 그리고 약 6개월이 지난 지금 좋은 선택이었다는 생각이 들어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 4달이라는 시간동안 이직을 하면서 채용 시장이 되게 힘들었지만 좋은 결과를 얻을 수 있어서 다행이라는 생각이 들었는데요. 한편으로는 내가 열심히 하면 시장이 힘들긴 해도 충분히 잘 할 수 있다고 생각했기 때문에 그에 맞는 결과가 찾아온 것 같아서 뿌듯했던 것 같아요. 그리고 앞으로도 사람의로서의 유승완, 개발자로서의 유승완도 더욱 많이 성장하려고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재의 나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금의 저는 다양한 활동들을 해보고 있어요. 지금보다 더 좋은 개발자가 되고 싶고 더 큰 회사들에서 일을 해보고 싶다는 생각을 항상 하기 때문인 것 같아요. 패스트캠퍼스에서 운영하는 재직자 교육인 이너 써클에 참여하고 있고 제품에도 관심이 많아서 Product Engineer Camp에도 참여하고 있어요. 또한 제가 개발자가 되겠다고 공부를 시작하면서부터 어느 정도 경험을 쌓고 나면 꼭 멘토 활동을 하겠다고 생각을 했었는데요. 감사하게도 기회가 닿아서 코드잇 부트캠프 프론트엔드 멘토로 활동을 했었고 현재는 프로그래머스 데브코스의 멘토로 활동을 하고 있어요. 일을 시작하면서 부터 다양한 교육 프로그램들을 들으면서 업계 선배분들께 배웠던 내용들이 저에게 정말 많은 도움이 되었고 저도 이러한 내용들을 다시 후배들한테 나눔으로써 개발 생태계가 더욱 긍정적으로 변화할 수 있다고 생각하고 있어요. 또한 세상의 문제를 해결하는 좋은 제품들이 많아지면서 더 좋은 세상이 될 것이라고도 확신을 하고 있구요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 또 마음에 새기는 것이 있는데요. What보다는 Why를 바라보면서 살자는 것이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Why는 개발을 공부할때도 중요하다고 생각하는데요. &quot;그냥 이렇게 하는거야&quot; 처럼 단순하게 생각하고 마는 것이 아니라 왜 그렇게 해야하는지, 특정 기술은 문제를 해결하기 위해서 왜 그러한 방식을 채택했는지와 같이 Why를 생각하면 한없이 깊이 파고들 수 있고 기술적으로 정말 많이 성장할 수 있는 것 같아요. 또 인생에 있어 여러가지 선택을 해야하는 시기가 올 때도 Why는 선택을 할 수 있는 좋은 기준이 된다고 생각해요. 대부분의 사람들은 내가 무엇을 해야겠다. 즉, What을 기준으로 선택을 하지만 내가 왜 그 일을 해야하는지, 그 일이 세상에 어떤 영향을 주는지, 나에게는 어떤 영향을 주는지와 같은 기준으로 생각을 하게 되면 정말 의미있는 일을 내가 선택해서 할 수 있다고 생각해요. 그리고 저는 이러한 기준으로 현재 재생에너지 스타트업에 합류하게 되었던 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;끝으로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 개발자들이 개발을 재밌게, 그리고 힘들지 않게 했으면 좋겠다는 바램이 있는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업이나 이직 자체가 물론 중요한 일이지만 너무 스트레스 받지 않고 개발에 집중하면서 개발자로서 성장해나간다면 좋은 커리어는 알아서 따라오는 존재라고 생각해보면 좋을 것 같아요. 저 또한 이렇게 앞으로의 개발자 커리어를 쌓아보려고 해요. 모두 잘 되면 좋을 것 같습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL/개발</category>
      <category>이직</category>
      <category>회고</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/347</guid>
      <comments>https://imnotadevleoper.tistory.com/347#entry347comment</comments>
      <pubDate>Thu, 1 Aug 2024 22:53:20 +0900</pubDate>
    </item>
    <item>
      <title>[PEC] 4주차 세션 돌아보기</title>
      <link>https://imnotadevleoper.tistory.com/346</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포넌트 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트를 왜 설계해야 하는 것일까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재사용성&lt;/li&gt;
&lt;li&gt;유연함&lt;/li&gt;
&lt;li&gt;생산성&lt;/li&gt;
&lt;li&gt;기타 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 설계의 궁극적인 목적은 Performance(생산성)이라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 얘기하는 Performance는 시스템의 성능이 아닌 개발자가 단위 시간당 수행할 수 있는 작업의 양을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생산성이 높아지면 작업의 양이 늘어나고 제품의 가치가 더욱 빠르게 사용자에게 전달될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 일을 하다보면 일을 많이 하기 어려워지는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 소프트웨어의 요구사항이 변경되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항이 변경되면 기획을 다시 해야하고, 디자인을 다시 해야하고, 개발을 다시 해야하고, QA를 다시 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 소프트웨어의 변경은 당연한 것이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경이라는 것 자체는 우리가 제어할 수 없는 영역이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 변경을 어떻게 잘 다룰지에 대한 고민을 해야하고 이에 대한 답은 '설계'라고 얘기할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퍼포먼스를 높이기 위해서는 변화에 대응할 때 최소한만 수정할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변화에 대응하기 쉬운 유연한 구조를 가진 설계를 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁극적으로 퍼포먼스를 높이게 되면 많은 이점들을 얻을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;돈을 많이 벌 수 있다.&lt;/li&gt;
&lt;li&gt;문제 해결을 쉽고 빠르게 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FSD(Feature Sliced Design)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트를 잘 설계하기 위해서 FSD 방법론을 도입합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1355&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg1iXn/btsIPUpahkd/6iWABPkcwRm53E2SCTjz41/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg1iXn/btsIPUpahkd/6iWABPkcwRm53E2SCTjz41/img.jpg&quot; data-alt=&quot;출처: https://feature-sliced.design/docs/get-started/overview&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg1iXn/btsIPUpahkd/6iWABPkcwRm53E2SCTjz41/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg1iXn%2FbtsIPUpahkd%2F6iWABPkcwRm53E2SCTjz41%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;239&quot; data-origin-width=&quot;1355&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://feature-sliced.design/docs/get-started/overview&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FSD에서는 기능 단위로 애플리케이션의 전체적인 구조를 작성하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 또한 각각의 기능(feature)단위로 분류가 되면서 코드를 쉽게 찾을 수 있게 하고 효율적으로 협업을 할 수 있도록 돕습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 기능 단위로 분류가 되면 왜 이러한 이점을 가지게 되는지에 대해서 개인적으로 생각을 해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이해하기 쉽다&quot;라는 것은 직관적이라는 것을 의미한다고 볼 수 있을 것 같고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;직관적이다&quot;라는 것은 생각할 필요가 없이 바로 알아볼 수 있는 것이라고 볼 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 어떤 서비스를 생각할때는 개발자든, 사용자든 기능을 중심으로 생각을 시작하는 것 같은데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예약을 하거나, 송금을 하거나, 제품을 구매하는 것과 같이 기능에서 생각이 시작을 하기 때문에 소스코드도 기능을 단위로 분리가 되면 생각할 필요 없이 바로 해당 기능의 폴더를 직관적으로 찾아볼 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 비즈니스의 요구사항에 대응하는 관점에서도 항상 특정 기능에서부터 시작을 한다고 생각하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에도 소스코드를 추가하거나 수정할 때 기능에서 부터 시작할 수 있어서 생각의 흐름이 일치하는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FSD의 폴더 위계는 Layers, Slices, Segments와 같이 이루어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;layers의 각 폴더의 종류를 간략하게 설명하면 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;app: React의 Provider, Router, global style, global type등&lt;/li&gt;
&lt;li&gt;pages: 페이지 컴포넌트&lt;/li&gt;
&lt;li&gt;widgets: 페이지에 사용되는 UI 컴포넌트, features로 구성&lt;/li&gt;
&lt;li&gt;features: 비즈니스 로직을 담당하는 컴포넌트&lt;/li&gt;
&lt;li&gt;entities: 데이터, api layer&lt;/li&gt;
&lt;li&gt;shared: 도메인에 종속적이지 않는 공통 모듈(component, hooks, helper등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;layer가 참조하는 방향은 무조건 단방향이어야 하며 또한 위에서 아래로 참조해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래쪽에 위치한 layer일수록 더 많은 곳에서 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slice는 특정 비즈니스 엔티티를 기준으로 나누게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;segment는 목적에 맞게 slice의 코드를 나누게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;api&lt;/li&gt;
&lt;li&gt;ui&lt;/li&gt;
&lt;li&gt;model&lt;/li&gt;
&lt;li&gt;lib&lt;/li&gt;
&lt;li&gt;config&lt;/li&gt;
&lt;li&gt;consts&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FSD는 Public API를 가지며 index.ts 파일을 두어서 외부에 export할 것들을 표현해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지는 private하게 격리하여 내부에서만 사용하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4주차에는 FSD 방법론을 통해 실제로 컴포넌트 설계를 진행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PEC/돌아보기</category>
      <category>PEC</category>
      <category>product engineer camp</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/346</guid>
      <comments>https://imnotadevleoper.tistory.com/346#entry346comment</comments>
      <pubDate>Sat, 27 Jul 2024 23:19:06 +0900</pubDate>
    </item>
    <item>
      <title>[패스트캠퍼스 베타러너 후기] React Native 앱 개발의 모든 것 : 4가지 프로젝트로 마스터하는 웹뷰부터 앱 개발까지</title>
      <link>https://imnotadevleoper.tistory.com/345</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;1288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdxcdm/btsIKWHVPha/AoKxFE7A63zWIXyCBZkKOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdxcdm/btsIKWHVPha/AoKxFE7A63zWIXyCBZkKOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdxcdm/btsIKWHVPha/AoKxFE7A63zWIXyCBZkKOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcdxcdm%2FbtsIKWHVPha%2FAoKxFE7A63zWIXyCBZkKOk%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;286&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;1288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이번에 패스트캠퍼스 베타러너를 신청해서 감사하게도 당첨이 되었는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 React Native 강의를 미리 수강해볼 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의명은 &amp;lt;React Native 앱 개발의 모든 것 : 4가지 프로젝트로 마스터하는 웹뷰부터 앱 개발까지&amp;gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의는 현업에서 React Native로 회사에서 직접 앱을 출시해서 많은 다운로드 수를 만들어낸 김대훈 강사님과 안다희 강사님이 진행하시게 되는데요. 확실히 실무에서 사용해보신 경험을 바탕으로 React Native를 어떻게 활용하면 좋을지에 대한 노하우를 많이 담고 있는 강의였습니다. 강의 구성은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의는 전체적으로 기초 - 실습 - 배포와 같은 순서로 진행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 디테일하게는 Expo를 이용해서, React Native CLI를 이용해서 각각 웹뷰로 개발을 하고 웹뷰에 대한 기초적인 이론을 배우게 됩니다. 이후에 웹뷰와 RN 앱간의 통신을 하는 방식에 대해서 프로젝트를 통해서 학습하게 되고 웹을 웹뷰용, 일반 웹으로 분기해 개발하는 방식도 배우게 됩니다. 이후에는 앱에서 빼놓을 수 없는 배포에 대해서 배우게 됩니다. Fastlane 배포를 하게 되고 코드 푸쉬도 함께 학습하게 됩니다. 마지막 파트에서는 웹뷰로 개발하는 것이 아닌 오직 React Native로 개발하는 프로젝트를 진행하면서 마무리하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의를 들으면서 느끼는 점은 React Native를 처음 사용하는 개발자들도 큰 무리가 없이 작은 것 하나부터 설명을 진행해주신다는 느낌을 받을 수 있었고 최신 강의다보니 버전에 대한 큰 문제없이 따라가면서 학습할 수 있었습니다. 저는 이전에 React Native를 이용해서 웹뷰로 앱을 출시해본 경험이 있는데요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnX7sT/btsIKBYyi1t/tFKdzf2lh3riXndaX8yXwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnX7sT/btsIKBYyi1t/tFKdzf2lh3riXndaX8yXwk/img.png&quot; data-alt=&quot;출시했던 앱&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnX7sT/btsIKBYyi1t/tFKdzf2lh3riXndaX8yXwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnX7sT%2FbtsIKBYyi1t%2FtFKdzf2lh3riXndaX8yXwk%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;259&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출시했던 앱&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 당시에는 React Native에 대한 이해보다는 웹으로만 개발을 해서 어떻게든 앱에 띄우고 배포를 진행했던 기억이 있는데요. 이번에 개념부터 차근차근 공부하면서 이전에 모르고 넘어갔던 내용들도 새로 알게되면서 학습할 수 있었고 어려웠던 개념들도 쉽게 이해할 수 있었습니다. 또한 겸사겸사 강의를 통해 공부하면서 이전에 한번 봤었던 React Native 공식문서도 한번 더 보게 되었다는 뿌듯함도 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;1714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3VVcu/btsINfTvT7e/lo5HxKkxCcm2xZyMkEkc3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3VVcu/btsINfTvT7e/lo5HxKkxCcm2xZyMkEkc3K/img.png&quot; data-alt=&quot;React Native Docs&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3VVcu/btsINfTvT7e/lo5HxKkxCcm2xZyMkEkc3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3VVcu%2FbtsINfTvT7e%2Flo5HxKkxCcm2xZyMkEkc3K%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;294&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;1714&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;React Native Docs&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 당장은 회사 업무가 바빠서 새로운 앱을 출시하려는 생각은 없지만, 지속적으로 React Native로 토이 프로젝트를 진행하면서 다양한 기능들을 만들어보고 상황이 되면 소소하게 다양한 앱들을 출시해보는 경험을 하려고 계획해보고 있습니다. 앱 출시에 대한 니즈가 있으시거나 React Native라는 기술을 어떻게 공부해야 할지 막막하게 느껴지시는 분들이 계시다면 이 강의를 통해서 첫 시작을 해보시는 걸 추천드리고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이번에 베타러너에 참여하면서 강의를 먼저 확인하고 학습하는 경험을 할 수 있었는데요. 강의를 들으면서 개선되면 좋을 것 같은 점들도 따로 기록을 해두고 이 과정속에서 조금 더 집중하면서 강의를 학습할 수 있었던 게 좋은 경험이 되었던 것 같습니다. 그래서 지속적으로 베타러너와 같은 기회들이 학습하는 사람들에게 많이 돌아갔으면 하는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어주셔서 감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>reactnative</category>
      <category>베타러너</category>
      <category>패스트캠퍼스</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/345</guid>
      <comments>https://imnotadevleoper.tistory.com/345#entry345comment</comments>
      <pubDate>Wed, 24 Jul 2024 23:06:16 +0900</pubDate>
    </item>
    <item>
      <title>[PEC] 3주차 세션 돌아보기</title>
      <link>https://imnotadevleoper.tistory.com/344</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 만들려는 유기동물 서비스는 Entertaiment적인 요소를 가진 서비스는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 재미보다는 정보를 전달하기 위한 목적을 가지고 있다. (비타민 vs Pain Killer)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정보가 지닌 강점은 디자인이 그다지 중요하지 않다는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스는 아주 뾰족해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 뭉뚝하면 임팩트를 줄 수가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 구체화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3주차 세션의 핵심은 구체화다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ideation은 발산과 수렴 과정을 거친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 우리는 과제를 진행하면서 해당 과정을 거쳐왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제정의 - Information Flow - 서비스 구체화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Golden Circle&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Why에서 부터 시작해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 문제 정의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 왜?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- How는 추상화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 우리는 서비스를 만드는 사람들이기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 현실 세계의 문제에 대한 해결책을 서비스로 추상화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; - 앱에서 버튼만 누르면 내가 겪오있는 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 구체화는 What이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;What은 무엇을 만들어야 하느냐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체화란 쪼개서 만들 수 있도록 하는 과정, 머리속에 무언가가 정확히 무엇인지 알게 되는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선명하게 만드는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;What을 만들 때 고려해야할 것은 어떤걸 만들지 말지 결정하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤걸 만들까는 결정할 필요가 없다, 다 아는 것이기 때문.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤걸 만들지 말까가 중요하다. 만들지 말것들은 다 걸러낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 무엇을 해야할지 생각할 필요 없이 학습할 필요 없이 알아야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들지 말아야 할 것을 걸러내기 위한 필터가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기준이 필요하다. 그렇다면 기준이 뭘까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;직관&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직관이란 사유 과정을 거치지 않고, 어떻게 지식이 취득되는가를 이해하지 않고 대상을 직접적으로 파악하는 것을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게는 생각을 안하고 그냥 아는 것이 직관이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아크 브라우저는 웹사이트에 마우스를 올렸을 때 요약본을 보여준다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;식수 필터는 정말 직관적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람들은 빨대와 같은 모양을 보면 입으로 빨아 먹는 것을 생각하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oryFW/btsIF1v3iDe/QyaEvTVMI4hNKHLgEkrL1k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oryFW/btsIF1v3iDe/QyaEvTVMI4hNKHLgEkrL1k/img.jpg&quot; data-alt=&quot;출처: https://www.ceopartners.co.kr/news/articleView.html?idxno=1065&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oryFW/btsIF1v3iDe/QyaEvTVMI4hNKHLgEkrL1k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2ForyFW%2FbtsIF1v3iDe%2FQyaEvTVMI4hNKHLgEkrL1k%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;322&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://www.ceopartners.co.kr/news/articleView.html?idxno=1065&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스내의 기능은 직관적이어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직관적이지 않은 기능은 빼야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능을 열심히 구현해도 사용자가 쓰지 않으면, 헤매면 너무나 안타까운 결과가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각할 필요가 없다는 것을 어떻게 알 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 사용자가 무엇을 알고 무엇을 모르는지 알고 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러기 위해서는 사용자와 눈높이를 맞춰야하고 공감을 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 공감에서부터 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PEC/돌아보기</category>
      <category>PEC</category>
      <category>product engineer camp</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/344</guid>
      <comments>https://imnotadevleoper.tistory.com/344#entry344comment</comments>
      <pubDate>Sat, 20 Jul 2024 01:05:11 +0900</pubDate>
    </item>
    <item>
      <title>[PEC] 2주차 세션 돌아보기</title>
      <link>https://imnotadevleoper.tistory.com/343</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Information Flow&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data와 Information의 차이와 추상화에 대해서 다뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 어떤 개념을 표현하고 있는 것일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 이해하기 쉬운 예시가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벽돌은 낱개로 있으면 말 그대로 벽돌일 뿐이지만,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSmNeQ/btsIwNKfu3c/V0DzRV8m03cu3tWmNEHCF0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSmNeQ/btsIwNKfu3c/V0DzRV8m03cu3tWmNEHCF0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSmNeQ/btsIwNKfu3c/V0DzRV8m03cu3tWmNEHCF0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSmNeQ%2FbtsIwNKfu3c%2FV0DzRV8m03cu3tWmNEHCF0%2Fimg.webp&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;430&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벽돌을 가지고 만약 집을 짓게되면 벽돌은 집을 의미하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBKwQf/btsIu6xI6V1/s1QfrfxHT5eqC2o8KLxBl1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBKwQf/btsIu6xI6V1/s1QfrfxHT5eqC2o8KLxBl1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBKwQf/btsIu6xI6V1/s1QfrfxHT5eqC2o8KLxBl1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBKwQf%2FbtsIu6xI6V1%2Fs1QfrfxHT5eqC2o8KLxBl1%2Fimg.webp&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;430&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벽돌 자체는 Data라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data가 잘 조직화가 되어서 집이라는 의미를 가지게 되었는데, 이는 관계가 맺어졌다는 것을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data가 관계가 맺어지는 순간 Information이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data와 Information의 차이는 구체화된 것으로 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 관계를 맺어주는 일을 추상화라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상화를 통해서 복잡한 것을 쉽게 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 그렇게 관계를 맺어줬기 때문에, 조직화해줬기 때문에, 쉽게 만들어 줬기 때문에.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계를 맺어주는 것은 누군가의 추가적인 작업이 필요하고 우리가, 엔지니어가 어떻게 관계를 맺어주냐(추상화)에 따라 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노출할 것을 노출하고 노출하지 않을 것을 노출하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제정의 세션에서 우리는 공감으로부터 시작했는데 공감은 사용자와 눈높이를 맞추는 것이고 Data를 Information으로 관계를 맺어주는 것이다. 프로그래밍 또한 현실 세계의 여러 데이터들을 필요한 사람들이 쓸 수 있도록 관계를 맺어주는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 무엇을 알고 무엇을 모르는지를 알아야 적절한 추상화를 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉬운걸 노출하고 어려운걸 감추는 방향이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 눈높이에 맞게 해주는 것이 중요한 이유는 의미가 없을 수 있기 때문인데 만약 수학과 교수에게 계산기 앱을 만들어 주는 것은 어떠한 가치도 창출할 수 없기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Layered Archirecture&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계층을 UI Layer, Business Logic Layer, Data Layer로 나눈다고 가정해보면 Data Layer는 가장 많이 추상화가 되어 있다. 실제로 내부적으로 어떻게 동작하는지 사용자는 몰라도 된다. 사용자는 해당 서비스를 이용하는 방법, UI Layer에 대해서만 알고 있으면 된다. Data Layer의 Data들이 UI Layer에서는 Information이 된다. 여기서도 추상화를 통해서 관계를 맺어줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Information &lt;b&gt;Flow&lt;/b&gt;라고 하는 이유는 Data가 Information으로 가는 방향은 정해져 있기 때문이고 계속 흐르기 때문이다. 마치 애자일 같다는 생각이 들었다. UI -&amp;gt; Business Logic -&amp;gt; Data -&amp;gt; Business Logic -&amp;gt; UI -&amp;gt; ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI를 건들면, 버튼을 클릭하면 Data가 바뀌는데 이러한 동작을 위해서 중간에 Business Logic이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 코드로 작성해주는게 엔지니어가 하는 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 추상화를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data Layer는 복잡하고 이를 UI에서 단순하게 표현할 수 있도록 한다.&lt;/p&gt;</description>
      <category>PEC/돌아보기</category>
      <category>PEC</category>
      <category>product engineer camp</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/343</guid>
      <comments>https://imnotadevleoper.tistory.com/343#entry343comment</comments>
      <pubDate>Fri, 12 Jul 2024 00:45:49 +0900</pubDate>
    </item>
    <item>
      <title>[PEC] 1주차 세션 돌아보기</title>
      <link>https://imnotadevleoper.tistory.com/342</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발자로 일을 하면서 개발자는 무엇을 해야하는 사람인지에 대한 고민을 꾸준히 해왔는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이에 대한 답변으로는 사람마다 개발을 어떻게 생각하냐에 따라서 다양한 얘기를 해볼 수 있을 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누군가에게 개발자는 단순히 회사에서 시키는 일을 하면서 돈을 버는 행위, 수동적인 주체로 받아들일 수도 있지만 저에게 개발자라는 직업은 세상의 문제를 적극적으로 해결해야 하는 사람이 되어야 한다는 생각이 들었어요. 우리와 함께 살아가는 사람들이 겪고 있는 문제를 해결하는게 개발자가 만들어낼 수 있는 가장 큰 가치라는 생각이 들었고 저또한 코드외에도 사용자 중심의 비즈니스에 집중하는 역량을 쌓아야겠다는 생각이 들었어요. 이러한 생각의 결과가 Product Engineer Camp에 참여하게된 이유라고 말씀드릴 수 있을 것 같아요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1주차 세션의 part1 에서는 먼저 캠프를 함께하게 되는 동료분들과 간단히 자기소개를 하고 각자가 생각해온 해결해보고 싶은 문제에 대해서 얘기를 나눌 수 있었어요. 서로 어떠한 기대를 가지고 캠프에 참여하게 됐고 어떤 문제를 해결하고 싶은지 들어보면서 제품에 대한 마음이 진심이라는 걸 알 수 있었고 바라보고 있는 문제들이 너무 흥미로워서 마음이 따뜻해지는 시간이었어요. 해결하고 싶은 문제는 아래와 같은 것들이 있었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 출/퇴근만 찍는 현재의 근태 관리 서비스는 직원들의 감정, 상태를 파악하기 어렵다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 개발을 처음 공부하는 사람들은 길을 잃을 확률이 높고 제대로 공부하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 반려동물들이 너무나도 쉽게 버려지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 문제들중에 저희는 반려동물의 문제를 해결하기로 결정했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 본격적으로 1주차 세션의 키워드인 문제 정의에 대해서 다뤄볼 수 있었는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 들어보는 용어들과 방법론들이 많았는데 내가 관심을 가지지 않아서 모르고 있었다는 것이 아쉬웠고 새롭게 공부할 수 있다는 생각에 매우 들뜬 기분이었던 것 같아요. (서비스를 표현하는 비유중에 비타민, 페인킬러라는 표현을 소개해주셨는데 비타민같은 제품은 사용하지 않더라도 사는데 지장은 없지만, 고통을 줄여주는 페인킬러같은 제품은 꼭 사용할 수 밖에 없고 이런 제품을 만들어내는 것은 정말 재밌을 것 같다는 생각이 들었네요.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Simon Sinek의 The Golden Circle을 함께 보면서 문제 정의에 대한 이야기를 나눴는데요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byzcTL/btsIooX6JmW/cHN2yZgZ6Gk6NNuPqXFFt0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byzcTL/btsIooX6JmW/cHN2yZgZ6Gk6NNuPqXFFt0/img.jpg&quot; data-alt=&quot;출처:https://simonsinek.com/golden-circle/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byzcTL/btsIooX6JmW/cHN2yZgZ6Gk6NNuPqXFFt0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyzcTL%2FbtsIooX6JmW%2FcHN2yZgZ6Gk6NNuPqXFFt0%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;540&quot; height=&quot;540&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:https://simonsinek.com/golden-circle/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분은 What, 무엇을 할지에 집중하지만, 핵심은 Why라는 것이었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반려동물을 예시로든다면 아래와 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 왜 유기동물이 생길까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 왜 사람들은 가족같던 반려동물을 버리게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 왜 버려지고 나서 발견되는게 느릴까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 Why에 집중하고 찾기위한 노력들을 해야해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Why를 찾기 위해서는 What에서 바로 갈 수 없기 때문에 How, 어떻게를 찾아야해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 우리는 사용자 인터뷰를 통해서 해결하려고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 인터뷰는 우리의 목적을 달성할 수 있도록 도와주는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 정의를 위한 사용자 인터뷰를 하려고 하고 이는 Why에 해당돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 공감이에요. 공감을 달성하려고 사용자 인터뷰를 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Design Thinking이라고 하는 프로세스에서도 첫 시작에서는 공감을 얘기하고 있어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;795&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxgSbn/btsIn281cq5/NSGF7KGz6VghmBGp6NC4KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxgSbn/btsIn281cq5/NSGF7KGz6VghmBGp6NC4KK/img.png&quot; data-alt=&quot;출처: https://www.nngroup.com/articles/design-thinking/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxgSbn/btsIn281cq5/NSGF7KGz6VghmBGp6NC4KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxgSbn%2FbtsIn281cq5%2FNSGF7KGz6VghmBGp6NC4KK%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;795&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;795&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://www.nngroup.com/articles/design-thinking/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 각자가 사용자 인터뷰를 진행하고 이 결과물들을 통합(Combine)하려고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MECE Method를 통해서 중복없이, 그리고 빠짐없이 통합을 하면서 페르소나(Persona)를 만들거에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페르소나는 우리가 정의한 문제를 겪고 있는 타겟유저를 의미하고 이를 정의함으로써 Why를 더욱 명확하게 할 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 사용자 인터뷰를 어떻게 할지가 중요할 것 같은데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 반구조화 인터뷰를 하려고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 많이 하는 인터뷰는 구조화 인터뷰라고 볼 수 있을 것 같은데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 말하면 객관식의 질문들로만 사용자의 의견을 받아보는 방법이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식의 문제점은 답변이 딱딱할 수 밖에 없고 저희가 가장 중요하게 생각하는 진짜 공감을 할 수가 없어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 보기를 먼저 제공한다는 것은, 출발이 나에게서 시작하는 것이기 때문에 제대로 공감하기가 어려워요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 공감을 위해서는 객관식 질문과 더불어 주관식 질문이 필요해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이러한 인터뷰 방식을 반구조화 인터뷰라고 할 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주관식 질문이 공감을 위해서 좋다고 하지만, 처음부터 너무 어려운 질문을 하면 사용자 입장에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불편하게 느낄 수 있기 때문에 첫 시작은 객관식 질문으로 시작하는게 좋아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이런 객관식 질문들은 본질적인 질문에 다가가기 위한 오프너의 역할을 하게 될 거에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 인터뷰는 가능하면 대면으로 하는게 좋은데요. 왜냐하면 사람은 실제로 언어적 행동보다 비언어적 행동으로 많은 생각을 전달하기 때문이에요. 특히 인터뷰는 속마음, 무의식을 캐치할 수 있어야 하는데 이러한 것들은 바디랭귀지에서 많이 표현돼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 대면이 가장 좋고, 그 다음으로는 줌, 전화순으로 권장할 수 있을 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 내용으로 수업이 진행이 되었고 캠프에서는 매주 과제를 진행하는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과제에 대한 내용은 다음 글에서 나눠보면 좋을 것 같아요.&lt;/p&gt;</description>
      <category>PEC/돌아보기</category>
      <category>PEC</category>
      <category>프로덕트</category>
      <category>프로덕트엔지니어캠프</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/342</guid>
      <comments>https://imnotadevleoper.tistory.com/342#entry342comment</comments>
      <pubDate>Fri, 5 Jul 2024 07:05:49 +0900</pubDate>
    </item>
    <item>
      <title>[일기] 24-05-28</title>
      <link>https://imnotadevleoper.tistory.com/341</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt; 일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 최근에는 어떻게 하면 팀이 겪고 있는 문제를 효과적으로 해결할 수 있을까에 대해서 고민을 많이 하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이런 고민은 출/퇴근길에, 산책을 할 때, 화장실에 갈 때, 자기 전과 같이 자투리 시간에 틈틈이 하는 것 같다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이런 고민을 하게된 이유는 나를 포함해서 우리 팀원들이 불안하지 않게 더욱 안정적인 환경에서 제품을 개발해나갔으면 하는 바람이 생겼기 때문이다. 이런 바람이 생기게 된 이유는 2가지가 있는데,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;첫 번째는 켄트 벡의 &amp;lt;Tidy First?&amp;gt;라는 책을 읽으면서 시작되었다. 여기서 켄트 벡은 본인의 사명을 이렇게 소개하고 있다. &quot;Helping geeks feel safe in the world(괴짜들이 세상에서 안전하다고 느끼도록 돕는다) 이 문장이 개인적으로 너무 마음에 들었고 나도 이러한 환경을 만드는 것에 기여하는 사람이 되어야겠다는 생각을 했는데, 작은 단위에서는 내가 속한 조직에 이러한 영향력을 끼치고 더 나아가서는 개발자 커뮤니티에 영향력을 전달하려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;두 번째는 팀 리더분과 산책을 하면서 대화를 나눴는데 일을 잘 하는 것에 대한 얘기를 많이 해주셨다. 나도 비슷한 생각을 했었는데 개발 실력이라는 것은 시간이 지나면 자연스럽게 향상되는 것이기도 하고 개발을 잘한다는 것 자체가 회사의 문제를 해결해 주고 돈을 벌어다 주는 것과 비례하지는 않기 때문이다. 그리고 개발을 잘하는 것과 일을 잘하는 것은 어느 정도 다른 부분이 있다고 생각하는데 일을 잘하는 사람이 회사에 돈을 벌어다 줄 확률이 더 높다고 생각한다. 일을 잘하는 것이 1순위가 되어야 하고 일을 잘하기 위해서 개발도 잘해야 하는 것이라고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 개발을 공부하고 회사에서 일을 시작했을 때는 내가 어떻게 하면 잘 할 수 있을지에 대한 고민을 많이 했었는데 시간이 지나면서 개인에서 팀으로 생각의 전환이 자연스럽게 이루어지는 것 같다. 그리고 나의 성장이 아닌 팀의 성장을 더욱 바라게 되는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 부트캠프의 멘토로 참여를 했었는데 다양한 활동들을 했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온라인 멘토링, 코드리뷰, 실시간 Q&amp;amp;A, 모의 기술 면접, 강의 자료 제작등을 하였고 나와 함께했던 멘티분들과는 많은 교류를 하기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 바라는 기준에 현재의 나는 한없이 부족하지만 멘티분들께서 좋게 봐주셔서 정말 감사했다. 내가 좋아서 열심히 하려고 했기 때문에 부족하지만 진심이 닿았던 것이라는 생각이 들었고 더 의미 있는 시간을 만들어 드렸으면 좋았을 것 같다는 아쉬움도 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 분들과 함께할 수 있어서 좋았고 나보다 더 좋은 사람, 좋은 개발자가 되실 것이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 성장에 대한 생각을 많이 했다. 그리고 지식을 공유하는 방향으로 시작해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 지식을 먼저 나에게 공유하는 것이고, 이후에는 팀원들에게 공유를 하고, 나아가서 스터디를 통해서 공유를 해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자 하면 어렵지만, 같이 하면 어렵지 않다. 그리고 사실 개발은 꼭 어렵지만은 않다. 나는 이렇게 생각하는 사람이기 때문에 작은 부분부터 시작해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;잡&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;핸드폰 대신에 몰입할 수 있는 무언가를 만들어낼 수 있을까?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출/퇴근길에 버스나 지하철에서 주위를 둘러보면서 하는 생각이다. 거의 대부분의 사람들은 핸드폰을 보는 편이다. 그 외에는 책을 읽거나 피곤함에 눈을 감고 있거나. 만약 핸드폰을 대신에 사람들이 사용할 수밖에 없는 무언가를 만들어 낸다면 대박일 것 같다고 생각을 했는데 가능할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;선반에 사람이 탈 수 없을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출/퇴근길 피크 타임에 지하철을 타면서 느끼는 감정인데 만약에 좌석위의 선반에 사람이 타면 조금 더 많은 사람이 탈 수 있지 않을까 생각을 해본 적이 있다. (물론 어떻게 선반에 올라갈 것이며 내려올 땐 어떻게 할 것인지까지는 생각을 안 했다) 혹은 이건 아니더라도 대부분 사람들이 안쪽으로 들어가는 것을 싫어하다 보니 실제로 빈틈을 차곡차곡 채우면 더 많은 사람들이 탈 수 있음에도 불구하고 밖에 있는 사람들이 타지 못하고 다음 지하철을 기다리게 되는 것들을 보면서 사람들이 자발적으로 빈틈을 채울 수 있게 하는 방법이 없을까?라는 생각도 해본 적이 있었다. (물론 그 방법은 자세하게 생각해보지는 않았다)&lt;/p&gt;</description>
      <category>TIL/일기</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/341</guid>
      <comments>https://imnotadevleoper.tistory.com/341#entry341comment</comments>
      <pubDate>Wed, 29 May 2024 02:03:09 +0900</pubDate>
    </item>
    <item>
      <title>코드 리뷰를 잘 받기 위해 리뷰어의 경험 설계하기</title>
      <link>https://imnotadevleoper.tistory.com/332</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 프로그램을 개발하면서 코드를 작성하고 작성된 코드를 리뷰를 받고 기존 코드 베이스에 통합하는 과정을 거치게 되는데요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드 리뷰를 통해서 작성된 코드에서 버그를 찾거나, 더 좋은 설계에 대한 고민을 나눌 수 있기 때문에 코드 리뷰는 개발 프로세스에 있어서 중요한 요소로 여겨지고 있어요. 하지만 코드 리뷰라는 과정에서 PR의 크기가 크거나, 남들이 잘 모르는 도메인에 대해서라면 좋은 리뷰를 받기가 어려운데요. 이런 상황에서 코드 리뷰어의 경험을 설계하여 좋은 코드 리뷰를 만들 수 있는 방법에 대해 예시와 함께 설명해보려고 해요.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;코드 리뷰에 대해서 생각해 봐요&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;남이 작성한 코드를 읽고 리뷰하는 것은 많은 비용이 들어요.&lt;/li&gt;
&lt;li&gt;그렇기 때문에 리뷰어의 코드 리뷰를 돕기 위한 무언가가 필요해요.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 지식&lt;/li&gt;
&lt;li&gt;문제를 해결한 방식과 그 이유&lt;/li&gt;
&lt;li&gt;작업의 핵심이 되는 부분&lt;/li&gt;
&lt;li&gt;의미 없는 diff 제거&lt;/li&gt;
&lt;li&gt;기타 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이런 경험을 설계하는 것은 코드 작성자는 물론이고 리뷰어를 위해서도 필요해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;먼저 리뷰어의 경험을 설계하는 단계를 이렇게 정의했어요&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;단계 세분화 (가장 작은 단위)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 지식 찾기&lt;/li&gt;
&lt;li&gt;해결하려는 문제 찾기&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단계별로 실행 가능한 액션&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문제를 이해하기 위해 필요한 도메인 지식을 간단하게 작성한다.&lt;/li&gt;
&lt;li&gt;작성한 코드가 어떤 문제를 해결하려는 것인지 글로 작성한다.&lt;/li&gt;
&lt;li&gt;문제를 기술적으로 어떻게 해결했는지 글로 작성한다.&lt;/li&gt;
&lt;li&gt;내가 작업하면서 가장 많이 시간을 쏟았거나 고민했던 부분을 글로 작성한다.&lt;/li&gt;
&lt;li&gt;해당 작업을 통해서 다른 부분에도 영향이 갈 수 있는지 확인하고 글로 작성한다.&lt;/li&gt;
&lt;li&gt;이제 위에서 작성한 내용들을 각 단계별로 간단하게 1~2줄로 축약하고 그중에서 리뷰어가 가장 집중해서 봐야 할 부분에 대해서는 따로 언급한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;리뷰어의 경험을 설계하는 단계와, 각 단계별로 어떤 액션들을 해보면 좋을지를 작성했어요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 예시를 통해서 실제로 어떤 식으로 접근하면 좋을지 알아보려고 해요.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;로또 프로그램을 예시로 경험을 설계해요&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존에 웹을 기반으로 만들어져 있던 로또 프로그램이 있다고 가정할게요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 연금 복권을 판매할 수 있는 기능도 추가하고자 하는 상황이에요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저는 프론트엔드 개발자로서 해당 요구 사항을 전달받아서 해당 기능을 추가했고&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드 리뷰를 받기 위해서 PR을 올리려고 해요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;1. 해당 PR을 이해하기 위한 도메인 지식에는 어떤 것들이 있는지 작성해 봐요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(여기서는 연금 복권이라는 상품에 대해 가상의 규칙을 만들었어요.)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연금 복권은 구매자의 나이에 따라 가격이 다르다.&lt;/li&gt;
&lt;li&gt;연금 복권은 구매자의 나이에 따라 구매할 수 있는 수량이 다르다.&lt;/li&gt;
&lt;li&gt;연금 복권은 한 사람이 1달에 50장을 넘게 구매할 수 없다.&lt;/li&gt;
&lt;li&gt;연금 복권 당첨금 수령은 판매처에서는 신경 쓰지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 작성한 코드가 어떤 문제를 해결하려고 하는 것인지 작성해 봐요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;(우리가 겪고 있는 문제에 대한 설명을 작성해요.)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구매자의 나이에 따라서 가격과 구매할 수 있는 수량이 달라져서 이런 예외처리를 효과적으로 할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;한 사람당 1달에 50장을 넘게 구매할 수 없기 때문에 총 몇 장을 구매했었는지에 대한 기록을 잘 기록할 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 두 규칙들은 PR보다 어떤 툴을 사용하냐에 따라서 Github, Jira 등의 이슈에 같이 작성해 주면 좋을 것 같아요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;3. 문제를 해결하기 위해 기술적으로 어떻게 접근했는지를 작성해 봐요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일관된 예외처리를 하기 위해 validation을 다루는 하나의 모듈을 만들었다.&lt;/li&gt;
&lt;li&gt;리액트로 웹 프로그램을 만들고 있었기 때문에 선언적인 에러 처리를 하고자 에러바운더리를 사용했다.&lt;/li&gt;
&lt;li&gt;나이와 수량에 대해서는 어디서든 동일하게 사용될 수 있도록 매직넘버를 없애고 상수화하여 관리했다.&lt;/li&gt;
&lt;li&gt;프론트에서는 UX를 위한 에러처리를 주로 하고, 서버에서 조건을 더욱 엄격하게 검증하도록 백엔드 개발자와 협의를 했다.&lt;/li&gt;
&lt;li&gt;당첨금 수령을 우리가 만든 프로그램에서 하는 줄 알고 찾아오는 사람을 위해 별도의 안내 팝업을 만들었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. 작업하면서 가장 많이 시간을 쏟았던 부분을 작성해 봐요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(이 부분이 사실상 코드에서 가장 중요한 부분이 될 거예요)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;validation을 위한 모듈을 만드는데 시간을 많이 쏟았다.&lt;/li&gt;
&lt;li&gt;에러 처리를 어떻게 하는 게 사용자 경험에 좋을지 고민이 많이 되었고 Alert를 띄워서 사용자에게 인지시켜줬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5. 작업한 코드에서 다른 도메인에 영향을 줄 수 있는 사이드 이펙트가 있는지 작성해 봐요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금 상황에서 연금 복권 기능이 추가되었다고 해도 기존 로또 기능에는 영향이 없을 것으로 보인다.&lt;/li&gt;
&lt;li&gt;그래도 혹시 모르니 걱정되는 면이 조금 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;6. 위에서 작성한 내용들로 작업을 요약하고 중요한 포인트를 작성해 봐요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결국 예외 처리를 선언적이고 명확하게, 일관되게 가져갈지에 대한 고민이 가장 컸다.&lt;/li&gt;
&lt;li&gt;또한 혹시나 이 기능이 다른 도메인에 영향을 줄지 다시 한번 확인은 해봐야 할 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 PR을 올리기 전에 작업을 하면서 가장 많은 시간을 쏟았던 부분, 중요한 포인트들을 작성하고 리뷰어가 어떤 부분에 신경을 써줬으면 좋겠는지 리뷰어에게 전달해요. 이런 과정을 거치고 PR을 올리게 되면 코드를 작성한 나 스스로도 다시 한번 작업 내용을 정리할 수 있고, 코드를 보는 리뷰어도 내가 무엇에 집중해야 하는지 알 수 있어서 더욱 효과적인 리뷰를 할 수 있어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람은 어떤 일을 하더라도 내가 무엇에 집중해야 하는지를 알고 모르냐에 따라서 일의 효율이 매우 달라지게 되는데요. 코드 리뷰를 하는 과정 속에서도 리뷰어는 내가 무엇에 집중해야 하는지 알고 있어야 더욱 효율적인 리뷰를 할 수 있어요. 하지만 개발자의 특성상 코드 리뷰에 많은 에너지를 쏟을 수는 없는데요. 따라서 코드를 작성한 사람이 코드 리뷰어의 경험을 어떻게 설계하냐에 따라서 코드 리뷰어의 시간을 아껴주는 것은 물론이고 의미 있는 코드 리뷰를 만들 수 있다고 생각하고 있어요. 따라서 이런 관점으로 코드 리뷰어의 경험을 설계해 보시는 것을 추천드리고 싶어요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL/개발</category>
      <category>경험설계</category>
      <category>리뷰어</category>
      <category>코드리뷰</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/332</guid>
      <comments>https://imnotadevleoper.tistory.com/332#entry332comment</comments>
      <pubDate>Sun, 14 Apr 2024 00:39:51 +0900</pubDate>
    </item>
    <item>
      <title>유데미(Udemy) 옆집 개발자와 같이 진짜 이해하며 만들어보는 첫 Spring Boot 프로젝트</title>
      <link>https://imnotadevleoper.tistory.com/331</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕헤사요, 저는 글또 9기에 참여하고 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;글또와 Udemy가 협업을 하게 되었고 무료로 강의를 제공해주셔서 들어볼 수 있는 기회가 생겼는데요.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;저는 그 중에서 김송아 강사님께서 강의를 하시는 스프링 부트 강의를 들어보게 되었습니다!&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;프론트엔드 개발자로 일을 하고 있지만, 회사에서 어드민을 풀스택으로 구축하기도 했었고 평소에 스프링에 관심이 있어서 선택하게 되었습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 좋은 기회를 주신 글또와 Udemy 관계자분들에게 감사드립니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;1220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nl9I4/btsFR0dYyZ8/TdxGHUkPKvpFEU7KYu5z60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nl9I4/btsFR0dYyZ8/TdxGHUkPKvpFEU7KYu5z60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nl9I4/btsFR0dYyZ8/TdxGHUkPKvpFEU7KYu5z60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnl9I4%2FbtsFR0dYyZ8%2FTdxGHUkPKvpFEU7KYu5z60%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;2552&quot; height=&quot;1220&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;1220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;강의는 어떻게 진행되나요?&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;강의의 제목은 &amp;lt;옆집 개발자와 같이 진짜 이해하며 만들어보는 첫 Spring Boot 프로젝트&amp;gt;입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.udemy.com/course/spring-boot-kimsonga/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.udemy.com/course/spring-boot-kimsonga/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;제목에서부터 흥미로웠는데요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;강의가 전반적으로 아주 기초부터, 그리고 쉬운 설명과 함께 진행되는데 이러한 강의의 내용이 그대로 드러나는 제목이라는 생각이 들었습니다. 정말 기초부터 접근하기 때문에 Spring을 간단하게 접근해보기에 큰 어려움은 없었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 공부라는 것은 제대로 하기 위해서는 강의만 듣고 끝나지 않고 혼자서 공부하는 시간을 가져야하는데, 이런 흐름을 가져가기에 적절한 강의라는 생각도 들었습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;강의에서 좋았던 점은 무엇인가요?&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 기술적인 개념을 강사님만의 단어로 누구나 이해하기 쉽게 설명한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 기술보다 원리에 대해 집중하고, 그 이후에 기술을 설명하고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 스프링에 대해서 가볍고 빠르게 배우기 좋았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 혼자 공부하는 방법을 얻어갈 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;이 강의를 들으면 어떤걸 얻을 수 있나요?&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-  백엔드에 대한 전반적인 흐름을 이해할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 스프링을 어떤식으로 접근해야 하는지 기초부터 배울 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 앞으로의 학습 방향성에 대해 생각해볼 수 있다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;개인적인 후기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 저는  평소에 스프링에 대해서 관심이 있어서 가볍게 보려고 했는데 딱 좋았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 강의력이 좋아서 학습을 지속하기 어려운 동영상 강의임에도 불구하고 완강할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 내용이 간단하기 때문에 스프링에 대해 깊은 학습을 위해서는 적절하진 않은 것 같습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;어떤 사람에게 추천하고 싶나요?&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 백엔드 개발을 공부하는 사람&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Spring을 처음 공부하려는 사람&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Spring에 대해서 궁금한사람&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 강의가 그렇지만 이 강의도 Spring에 대한 기본적인 내용을 다루고 있다고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 개발자가 되려고 공부를 하시거나 Spring이라는 기술에 관심이 있으신 분들에게 권해드리고 싶습니다!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: left;&quot;&gt;&quot;해당 콘텐츠는 유데미로부터 강의 쿠폰을 제공받아 작성되었습니다.&quot;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>TIL/개발</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <category>유데미</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/331</guid>
      <comments>https://imnotadevleoper.tistory.com/331#entry331comment</comments>
      <pubDate>Sun, 17 Mar 2024 23:51:42 +0900</pubDate>
    </item>
    <item>
      <title>유데미(Udemy) 프로젝트로 배우는 React.js &amp;amp; Next.js 마스터리 클래스 수강 후기</title>
      <link>https://imnotadevleoper.tistory.com/330</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕헤사요, 저는 글또 9기에 참여하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또와 Udemy가 협업을 하게 되었고 무료로 강의를 제공해주셔서 들어볼 수 있는 기회가 생기게 되어서 다양한 강의중에서 제가 현재 일하고 있는 프론트엔드 직무와 관련이 있는 React, Next 강의를 선택하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 좋은 기회를 주신 글또와 Udemy 관계자분들에게 감사드립니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2246&quot; data-origin-height=&quot;1216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oAkSp/btsE2AmxWeS/5nttzr6KZSZJFFE9k0r0xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oAkSp/btsE2AmxWeS/5nttzr6KZSZJFFE9k0r0xk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oAkSp/btsE2AmxWeS/5nttzr6KZSZJFFE9k0r0xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoAkSp%2FbtsE2AmxWeS%2F5nttzr6KZSZJFFE9k0r0xk%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;325&quot; data-origin-width=&quot;2246&quot; data-origin-height=&quot;1216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;강의는 어떻게 진행되나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의의 제목은 &amp;lt;프로젝트로 배우는 React.js &amp;amp; Next.js 마스터리 클래스&amp;gt;입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.udemy.com/course/react-next-master/&quot;&gt;https://www.udemy.com/course/react-next-master/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목 그대로 프로젝트를 기반으로 React.js와 Next.js(pages router)를 학습하게 되고 프로젝트를 하기 위한 필수 지식을 빠르게 획득할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 시작하기 전에는 각 섹션마다 필요한 이론에 대한 설명이 먼저 진행되고 이후에 프로젝트를 진행하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의를 진행하시는 분은 이정환 강사님이신데요. 2023 인프콘 현장에서 발표도 듣고 끝나고 간단하게 얘기했던 기억이 있어서 저에게는 더욱 친근감이 느껴졌던 것 같습니다. 특히 이정환 강사님은 제가 생각했을 때 강의력이 정말 좋다고 생각하기 때문에 다른 분들에게 적극적으로 추천드리고 싶습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;강의에서 좋았던 점은 무엇인가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기술적인 개념을 강사님만의 스타일로 쉽게 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 강의력과 강의 커리큘럼의 흐름이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 강의의 구성(이론 + 실습)이 잘 짜여져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프로젝트를 하는 데 필요한 개념을 빠르게 학습할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 강의에 대한 QnA가 빠르다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이 강의를 들으면 어떤걸 얻을 수 있나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- React, Next를 이용하여 기본적인 프로젝트를 개발할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프론트엔드 개발에 필요한 다양한 이론들을 학습할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 논리적인 사고를 경험할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개인적인 후기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 저는 리마인드의 목적으로 해당 강의를 수강했는데 만족스러웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 강의력이 워낙 좋아서 배속으로 봐도 이해하기가 좋았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 강의의 흐름이 좋고 꼭 필요한 내용을 쉽게 설명하고 있어서 개념을 다시 정리하기에 좋았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 사람에게 추천하고 싶나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프론트엔드 개발을 공부하는 사람&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- React, Next로 당장 프로젝트를 해야 하는 사람&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 개념에 대한 이해가 부족한 사람&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 강의가 그렇지만 이 강의또한 기본적인 내용을 다루고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 어느정도 개발 경험이 있는 사람에게는 리마인드를 하는 목적으로 추천할 수 있을 것 같고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에 프론트엔드 개발자가 되려고 공부를 하시거나 이제 막 일을 시작하신 분들에게는 학습하는 첫 걸음으로 좋을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: left;&quot;&gt;&quot;해당 콘텐츠는 유데미로부터 강의 쿠폰을 제공받아 작성되었습니다.&quot;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>TIL/개발</category>
      <category>온라인강의</category>
      <category>유데미</category>
      <category>프론트엔드</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/330</guid>
      <comments>https://imnotadevleoper.tistory.com/330#entry330comment</comments>
      <pubDate>Sun, 18 Feb 2024 03:16:40 +0900</pubDate>
    </item>
    <item>
      <title>투두앱을 Cypress로 접근하기</title>
      <link>https://imnotadevleoper.tistory.com/329</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 면접을 많이 보면서 느꼈던 건 프론트엔드에서도 테스팅에 대한 중요성이 점점 늘어나고 있다는 것이었는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 그 중에서 E2E테스트를 투두앱을 만들면서 적용해보려고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드에서 테스트를 나타내는 트로피 그림이 하나 있는데요.&lt;br /&gt;이 그림에서 보면 e2e(End to End)테스트는 비용이 크지만 그만큼 확실하게 기능이 동작하는 것을 검증할 수 있다는 장점이 있어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;3278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqvo4l/btsEkCtEgIx/M934FbZsTpta1rvPepJ0ck/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqvo4l/btsEkCtEgIx/M934FbZsTpta1rvPepJ0ck/img.jpg&quot; data-alt=&quot;출처:&amp;amp;amp;nbsp;https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqvo4l/btsEkCtEgIx/M934FbZsTpta1rvPepJ0ck/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbqvo4l%2FbtsEkCtEgIx%2FM934FbZsTpta1rvPepJ0ck%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;440&quot; data-origin-width=&quot;3200&quot; data-origin-height=&quot;3278&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;amp;nbsp;https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 프로젝트를 구축할게요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite를 이용해서 React + TypeScript 프로젝트를 세팅해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 패키지 매니저로 pnpm을 사용할게요.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;&amp;gt; pnpm create vite&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 Cypress를 설치해요.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;&amp;gt; pnpm install -D cypress&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cypress의 Launchpad를 편하게 실행시키기 위해 package.json 파일에 아래 스크립트를 추가해요.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;&quot;scripts&quot; : {
  &quot;cypress:open&quot;: &quot;cypress open&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어로 Launchpad를 실행시켜줘요.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;&amp;gt; npx cypress open&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 아래와 같은 화면을 볼 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 여기서 E2E Testing을 할 예정이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 왼쪽에 있는 E2E Testing을 클릭해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2404&quot; data-origin-height=&quot;1600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vqtfl/btsEmEj0EIz/lWuTBdp28bKKv7O1cknTmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vqtfl/btsEmEj0EIz/lWuTBdp28bKKv7O1cknTmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vqtfl/btsEmEj0EIz/lWuTBdp28bKKv7O1cknTmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvqtfl%2FbtsEmEj0EIz%2FlWuTBdp28bKKv7O1cknTmK%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;286&quot; data-origin-width=&quot;2404&quot; data-origin-height=&quot;1600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 브라우저를 선택할 수 있는 화면이 나오는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chrome 브라우저에서 테스트를 할 예정이라 Chrome이 선택된 상태로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Start E2E Testing in Chrome 버튼을 클릭해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2408&quot; data-origin-height=&quot;1610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nc0GW/btsEkDeGXPw/6lrNkDGiIKt6UGGY26VCc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nc0GW/btsEkDeGXPw/6lrNkDGiIKt6UGGY26VCc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nc0GW/btsEkDeGXPw/6lrNkDGiIKt6UGGY26VCc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnc0GW%2FbtsEkDeGXPw%2F6lrNkDGiIKt6UGGY26VCc0%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;288&quot; data-origin-width=&quot;2408&quot; data-origin-height=&quot;1610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 아래와 같은 화면을 확인할 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Create new spec을 클릭해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2256&quot; data-origin-height=&quot;1338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rFyjw/btsEkAikwgw/cJxkilpZxjVfQhVIlfkVqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rFyjw/btsEkAikwgw/cJxkilpZxjVfQhVIlfkVqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rFyjw/btsEkAikwgw/cJxkilpZxjVfQhVIlfkVqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrFyjw%2FbtsEkAikwgw%2FcJxkilpZxjVfQhVIlfkVqk%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;255&quot; data-origin-width=&quot;2256&quot; data-origin-height=&quot;1338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일명을 todo로 변경해주고 Create spec 버튼을 눌러줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dd7i4o/btsEmSicxkE/2uj2sHpO1sLL8A6hETK171/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dd7i4o/btsEmSicxkE/2uj2sHpO1sLL8A6hETK171/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dd7i4o/btsEmSicxkE/2uj2sHpO1sLL8A6hETK171/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdd7i4o%2FbtsEmSicxkE%2F2uj2sHpO1sLL8A6hETK171%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;203&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌측의 Spec 탭에서 방금 생성한 todo.cy.ts 파일을 확인할 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 해당 파일을 클릭하면 바로 e2e 테스트가 실행이 되요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2758&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yRvdH/btsEnWxSTKZ/0nudfb4zuxSU1RKeY06Uuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yRvdH/btsEnWxSTKZ/0nudfb4zuxSU1RKeY06Uuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yRvdH/btsEnWxSTKZ/0nudfb4zuxSU1RKeY06Uuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyRvdH%2FbtsEnWxSTKZ%2F0nudfb4zuxSU1RKeY06Uuk%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;141&quot; data-origin-width=&quot;2758&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cypress 자체에서 작성된 테스트 코드가 실행되고 통과하는 것을 확인할 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 테스트 코드를 작성할 수 있는 환경은 구축이 된 것 같아요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;1194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ApSmn/btsEqw6eGYb/T3JP2Y2tlXqrGGV1bTHKg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ApSmn/btsEqw6eGYb/T3JP2Y2tlXqrGGV1bTHKg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ApSmn/btsEqw6eGYb/T3JP2Y2tlXqrGGV1bTHKg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FApSmn%2FbtsEqw6eGYb%2FT3JP2Y2tlXqrGGV1bTHKg1%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;201&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;1194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적인 기능을 배제하고 단순하게 생각하면 아래와 같은 테스트 코드를 작성할 수 있을 것 같아요.&lt;br /&gt;(아래에 작성되는 코드는 편의상 간단하게 작성하게 되었는데요. 흐름을 위주로 가볍게 봐주시면 좋을 것 같아요!)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 사용자는
  1-1. 할일을 추가할 수 있다.
  1-2. 할일을 수정할 수 있다.
  1-3. 할일을 삭제할 수 있다.
  1-4. 할일 목록을 확인할 수 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 1-1을 만족하기 위해 아래와 같이 작성해봤어요.&lt;br /&gt;추후에 cy.visit에서 사용하는 baseUrl이나 data-attribute로 dom을 가져오는 부분은 별도로 모듈화를 하면 좋을 것 같아요.&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;describe('todo page', () =&amp;gt; {
  context('사용자는', () =&amp;gt; {
    beforeEach(() =&amp;gt; {
      cy.visit('http://localhost:5173');
    });

    it('할일을 추가할 수 있다.', () =&amp;gt; {
      cy.get('[data-cy=&quot;todo-input&quot;]').type('리액트 공부하기');

      cy.get('[data-cy=&quot;todo-form&quot;]').submit();

      cy.get('[data-cy=&quot;todo-list&quot;]').should('contain', '리액트 공부하기');

    });
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 &lt;code&gt;cy.get('\[data-cy=&quot;todo-input&quot;\]').type('리액트 공부하기')&lt;/code&gt; 부분이 터지게 되는데요.&lt;br /&gt;테스트를 통과시키기 위해서 먼저 input 태그를 만들어볼 수 있을 것 같아요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function Todo() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input type=&quot;text&quot; data-cy=&quot;todo-input&quot; /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;cy.get('\[data-cy=&quot;todo-input&quot;\]').type('리액트 공부하기')&lt;/code&gt;코드는 통과하게 되었는데요.&lt;br /&gt;리액트에서 input을 핸들링하려면 state를 만들고 이벤트 핸들러 함수를 작성해야 하기 때문에 이 처리를 해줄게요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { ChangeEvent, useState } from 'react';

export default function Todo() {
  const [todo, setTodo] = useState('');

  const handleChange = (e: ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
    setTodo(e.target.value);
  };
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input
        type=&quot;text&quot;
        value={todo}
        onChange={handleChange}
        data-cy=&quot;todo-input&quot;
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;cy.get('[data-cy=&quot;todo-form&quot;]').submit();&lt;/code&gt; 코드가 터지는 것을 확인할 수 있는데요.&lt;br /&gt;이 테스트를 통과시켜볼게요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;form 태그만 추가하면 우선 테스트는 통과할 수 있어요.&lt;br /&gt;form을 제출했을 때 처리를 해주기 위해서 이벤트 핸들러를 만들고 등록해줄게요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { ChangeEvent, FormEvent, useState } from 'react';

export default function Todo() {
  const [todo, setTodo] = useState('');

  const handleChange = (e: ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
    setTodo(e.target.value);
  };

  const handleSubmit = (e: FormEvent&amp;lt;HTMLFormElement&amp;gt;) =&amp;gt; {
    e.preventDefault();
  };

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;form onSubmit={handleSubmit} data-cy=&quot;todo-form&quot;&amp;gt;
        &amp;lt;input
          type=&quot;text&quot;
          value={todo}
          onChange={handleChange}
          data-cy=&quot;todo-input&quot;
        /&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아래 테스트코드를 통과시켜보려고 해요.&lt;br /&gt;그러면 form을 제출했을 때 리스트에 입력한 할일이 추가되어야 할 것 같아요.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;cy.get('[data-cy=&quot;todo-list&quot;]').should('contain', '리액트 공부하기');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 테스트만 통과시키려면 아래와 같이 통과시킬 수 있어요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { ChangeEvent, FormEvent, useState } from 'react';

export default function Todo() {
  const [todo, setTodo] = useState('');

  const handleChange = (e: ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
    setTodo(e.target.value);
  };

  const handleSubmit = (e: FormEvent&amp;lt;HTMLFormElement&amp;gt;) =&amp;gt; {
    e.preventDefault();
  };

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;form onSubmit={handleSubmit} data-cy=&quot;todo-form&quot;&amp;gt;
        &amp;lt;input
          type=&quot;text&quot;
          value={todo}
          onChange={handleChange}
          data-cy=&quot;todo-input&quot;
        /&amp;gt;
      &amp;lt;/form&amp;gt;

      &amp;lt;ul data-cy=&quot;todo-list&quot;&amp;gt;
        &amp;lt;li&amp;gt;리액트 공부하기&amp;lt;/li&amp;gt;
      &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 우리는 리팩토링을 해야하는데요, 실제로 입력한 할일이 리스트에 추가되도록 해야할 것 같아요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { ChangeEvent, FormEvent, useState } from 'react';

export default function Todo() {
  const [todo, setTodo] = useState('');
  const [todos, setTodos] = useState&amp;lt;string[]&amp;gt;([]);

  const handleChange = (e: ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
    setTodo(e.target.value);
  };

  const handleSubmit = (e: FormEvent&amp;lt;HTMLFormElement&amp;gt;) =&amp;gt; {
    e.preventDefault();

    setTodos((prev) =&amp;gt; [...prev, todo]);
    setTodo('');
  };

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Todo App&amp;lt;/h1&amp;gt;

      &amp;lt;form data-cy=&quot;todo-form&quot; onSubmit={handleSubmit}&amp;gt;
        &amp;lt;input
          type=&quot;text&quot;
          value={todo}
          onChange={handleChange}
          data-cy=&quot;todo-input&quot;
        /&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;추가하기&amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;

      &amp;lt;ul data-cy=&quot;todo-list&quot;&amp;gt;
        {todos.map((todo) =&amp;gt; (
          &amp;lt;li key={todo}&amp;gt;{todo}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 테스트 코드를 통과하게 되었어요.&lt;br /&gt;여기서 구현 코드의 리팩토링을 진행하면 좋을 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관심사에 따라서 컴포넌트를 분리해볼게요.&lt;br /&gt;아래와 같은 느낌으로 리팩토링을 진행했어요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { TodoForm, TodoList } from '@/components';

import useTodo from './todo.hooks';

export default function Todo() {
  const { todos, addTodo } = useTodo();

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Todo App&amp;lt;/h1&amp;gt;
      &amp;lt;TodoForm addTodo={addTodo} /&amp;gt;
      &amp;lt;TodoList todos={todos} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 사용자 시나리오에 맞춰서 삭제하는 부분을 확인할 수 있는 테스트 코드를 작성하려고 해요.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;    it('할일을 삭제할 수 있다.', () =&amp;gt; {
      cy.get('[data-cy=&quot;todo-input&quot;]').type('리액트 공부하기');

      cy.get('[data-cy=&quot;todo-form&quot;]').submit();

      cy.get('[data-cy=&quot;todo-list&quot;]').should('have.length', 1);

      cy.contains('삭제').click();

      cy.get('[data-cy=&quot;todo-list&quot;]').should('not.contain', '리액트 공부하기');
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 &lt;code&gt;cy.contains('삭제').click();&lt;/code&gt; 이 부분이 터지게 되어요.&lt;br /&gt;당연히 삭제 버튼이 없기 때문인데요, 이 테스트를 통과시켜볼게요.&lt;br /&gt;TodoItem 컴포넌트에 삭제 버튼을 추가해주면 될 것 같아요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;interface TodoItemProps {
  todo: string;
}

export default function TodoItem({ todo }: TodoItemProps) {
  return (
    &amp;lt;li&amp;gt;
      &amp;lt;span&amp;gt;{todo}&amp;lt;/span&amp;gt;
      &amp;lt;button type=&quot;button&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
    &amp;lt;/li&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;cy.get('[data-cy=&quot;todo-list&quot;]').should('not.contain', '리액트 공부하기');&lt;/code&gt; 이 부분이 터지게 되는데요.&lt;br /&gt;테스트를 통과시켜주기 위해서 삭제 기능을 구현해볼게요.&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;// todo.hooks.ts
import { useState } from 'react';

export default function useTodo() {
  const [todos, setTodos] = useState&amp;lt;string[]&amp;gt;([]);

  const addTodo = (todo: string) =&amp;gt; {
    setTodos((prev) =&amp;gt; [...prev, todo]);
  };

  const deleteTodo = (text: string) =&amp;gt; {
    setTodos((prev) =&amp;gt; prev.filter((todo) =&amp;gt; todo !== text));
  };

  return { todos, addTodo, deleteTodo };
}

// todo.tsx
import { TodoForm, TodoList } from '@/components';

import useTodo from './todo.hooks';

export default function Todo() {
  const { todos, addTodo, deleteTodo } = useTodo();

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Todo App&amp;lt;/h1&amp;gt;
      &amp;lt;TodoForm addTodo={addTodo} /&amp;gt;
      &amp;lt;TodoList todos={todos} deleteTodo={deleteTodo} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

// todo-item.tsx
interface TodoItemProps {
  todo: string;
  deleteTodo: (text: string) =&amp;gt; void;
}

export default function TodoItem({ todo, deleteTodo }: TodoItemProps) {
  return (
    &amp;lt;li&amp;gt;
      &amp;lt;span&amp;gt;{todo}&amp;lt;/span&amp;gt;
      &amp;lt;button type=&quot;button&quot; onClick={() =&amp;gt; deleteTodo(todo)}&amp;gt;
        삭제
      &amp;lt;/button&amp;gt;
    &amp;lt;/li&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 기능을 구현하게 되면 이제 테스트 코드가 모두 통과하게 되어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2130&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjkx8L/btsEoo8KONh/ji3m40tXqayZJcrj8U9hfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjkx8L/btsEoo8KONh/ji3m40tXqayZJcrj8U9hfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjkx8L/btsEoo8KONh/ji3m40tXqayZJcrj8U9hfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjkx8L%2FbtsEoo8KONh%2Fji3m40tXqayZJcrj8U9hfK%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;126&quot; data-origin-width=&quot;2130&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 남은 테스트 케이스는 할일 목록을 수정하는 경우와, 이미 추가했던 할일 목록을 저장하고 해당 페이지에 방문했을 때 이전에 작성한 할일 목록을 확인할 수 있도록 하는 2가지 기능인데요. 이 기능들을 테스트하면서 위에서 작성했던 로직의 문제점도 찾게 되고 더욱 견고한 테스트를 작성하는 데 도움이 될 것 같아요. 이 기능을 구현하는 것은 다음 스텝으로 남겨놓겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications&quot;&gt;https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cypress.io/&quot;&gt;https://www.cypress.io/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL/개발</category>
      <category>Cypress</category>
      <category>e2e</category>
      <category>TDD</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/329</guid>
      <comments>https://imnotadevleoper.tistory.com/329#entry329comment</comments>
      <pubDate>Sun, 4 Feb 2024 23:16:06 +0900</pubDate>
    </item>
    <item>
      <title>[글쓰기 워크숍 2기] 전자책을 출시했어요!</title>
      <link>https://imnotadevleoper.tistory.com/328</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2024-01-09-17-45-03 001.jpeg&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx2XZR/btsDd3emK25/g1yNxw2mV7GxEHQIXmannK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx2XZR/btsDd3emK25/g1yNxw2mV7GxEHQIXmannK/img.jpg&quot; data-alt=&quot;종이책!!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx2XZR/btsDd3emK25/g1yNxw2mV7GxEHQIXmannK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx2XZR%2FbtsDd3emK25%2Fg1yNxw2mV7GxEHQIXmannK%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;574&quot; data-filename=&quot;KakaoTalk_Photo_2024-01-09-17-45-03 001.jpeg&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;종이책!!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 2023년 말에 NEXTSTEP의 &lt;a href=&quot;https://edu.nextstep.camp/c/81psVr9U/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;글쓰기 워크숍 2기 &lt;/a&gt;에 참여했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정속에서 같이 글을 점진적으로 개선해나가는 과정을 거치면서 전차책을 출시하게 되었는데요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전자책은 &lt;a href=&quot;https://bookk.co.kr/bookStore/658febd6e571aee94d9b89e6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;에서 무료로 확인해보실 수 있습니다. 사진의 종이책은 저자들에게만 기념으로 제공되었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과정속에서 어떤 주제로 책을 작성할지 결정하고 결정한 후에는 저자 워크숍을 통해서 글을 점진적으로 개선해나가는 과정을 진행했었는데요. 덕분에 글을 작성하면서 재미도 생기고 용기도 얻으면서 마무리할 수 있었던 것 같습니다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_Photo_2024-01-09-17-45-03 002.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rTJ9p/btsDayyG621/t7cI3mxkL5RkdVRaylW920/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rTJ9p/btsDayyG621/t7cI3mxkL5RkdVRaylW920/img.jpg&quot; data-alt=&quot;2장&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rTJ9p/btsDayyG621/t7cI3mxkL5RkdVRaylW920/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrTJ9p%2FbtsDayyG621%2Ft7cI3mxkL5RkdVRaylW920%2Fimg.jpg&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;573&quot; data-filename=&quot;KakaoTalk_Photo_2024-01-09-17-45-03 002.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2장&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 글은 2장에서 확인하실 수 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;창의적인 저자 소개를 생각하다가 &amp;lt;키보드워리어&amp;gt;라는 표현을 생각해냈는데 오묘한 것 같습니다 ㅎㅎ&lt;/p&gt;</description>
      <category>NEXTSTEP/글쓰기 워크숍 2기</category>
      <category>글쓰기 워크숍</category>
      <category>어른이들의 세발자전거</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/328</guid>
      <comments>https://imnotadevleoper.tistory.com/328#entry328comment</comments>
      <pubDate>Tue, 9 Jan 2024 18:05:49 +0900</pubDate>
    </item>
    <item>
      <title>그림으로 배우는 Http &amp;amp; Network Basic - 11장</title>
      <link>https://imnotadevleoper.tistory.com/327</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;보안과 관련된 웹을 공격하는 기술에 대해서 알아봅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11.1 웹 공격 기술&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷에서 벌어지는 공격 대부분은 웹 사이트를 노리는 게 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 웹 애플리케이션을 대상으로 많은 공격이 발생합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.1.1 HTTP에는 보안 기능이 없다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP에는 보안과 관련된 기능이 없습니다. 또한 인증도 개발자가 웹 애플리케이션에서 스스로 설계하고 구현해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.1.2 리퀘스트는 클라이언트에서 변조 가능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 리퀘스트의 내용은 모든 클라이언트에서 자유롭게 변경하고 변조할 수 있다. 따라서 의도치 않은 값이 서버로 보내질 수 있음. 서버에서는 이에 대응할 수 있어야 한다. 유효성 검증을 각 레이어별로 해야한다. (다른 레이어에 의존성 없이)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리, 폼, HTTP Header, 쿠키 등으로 공격 코드를 보냅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.1.3 웹 애플리케이션에 대한 공격 패턴&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;능동적 공격
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직접 웹 애플리케이션에 액세스해서 공격 코드를 보내는 타입의 공격.&lt;/li&gt;
&lt;li&gt;SQL 인젝션과 OS 커맨드 인젝션 등이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;수동적 공격
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함정을 이용해서 유저에게 공격 코드를 실행시키는 공격.&lt;/li&gt;
&lt;li&gt;공격자가 설치한 함정에 유저를 유도한다.&lt;/li&gt;
&lt;li&gt;함정에 걸리면 유저의 브라우저나 메일에서 함정을 엵 된다.&lt;/li&gt;
&lt;li&gt;함정에 걸리면 공격 코드를 HTTP 리퀘스트에 포함하여 서버에 보낸다.&lt;/li&gt;
&lt;li&gt;공격 코드를 실행하면 쿠키 등의 기밀 정보를 도둑맞거나 유저의 권한이 악용된다.&lt;/li&gt;
&lt;li&gt;XSS, CSRF 등이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;유저가 처한 환경을 이용한 인트라넷 등에 대한 공격
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부망의 경우에도 함정을 이용해서 공격할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11.2 출력 값의 이스케이프 미비로 인한 취약성&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트에서 체크
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대부분 JavaScript를 이용하는데 이는 변조될 가능성이 있다.&lt;/li&gt;
&lt;li&gt;따라서 UI를 위한 체크정도로 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;웹 애플리케이션(서버 측)에서 체크
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력값 체크
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유효성 검증을 주로 한다.&lt;/li&gt;
&lt;li&gt;이스케이프 처리가 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;출력값 체크&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2.1 크로스 사이트 스크립팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XSS는 취약성이 있는 웹 사이트를 방문한 사용자의 브라우저에서 부정한 HTML 태그나 JS등을 동작시키는 공격이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XSS 공격의 피해&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유저의 개인 정보를 도둑 맞는다.&lt;/li&gt;
&lt;li&gt;쿠키 값이 도둑맞거나 의도치 않은 리퀘스트가 전송된다.&lt;/li&gt;
&lt;li&gt;가짜 문장이나 이미지등이 표시된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XSS 공격 사례&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공격자가 함정을 준비하는 수동적 공격
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Form을 입력하는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URL에 script를 넣는다.&lt;/li&gt;
&lt;li&gt;설치한 스크립트가 동작하게 되어 유저의 ID와 Password가 공격자의 사이트에 전송된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;유저의 쿠키를 뺴앗는 공격
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;script에 쿠키를 탈취하여 공격자의 사이트에 전송하는 코드를 작성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2.2 SQL 인젝션&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부정한 SQL을 실행하는 SQL 인젝션&lt;/li&gt;
&lt;li&gt;웹 애플리케이션에 SQL을 부정하게 실행하는 공격이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 인젝션의 피해&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 부정 열람이나 변조&lt;/li&gt;
&lt;li&gt;인증 회피&lt;/li&gt;
&lt;li&gt;데이터베이스 서버를 경유한 프로그램 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL이란?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 베이스를 조작하는 언어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 인젝션의 공격 사례&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;where 조건에 SQL의 주석인 &amp;mdash;를 넣어서 원하는 조건에 맞는 데이터를 가져올 수 있도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 인젝션은 SQL 문장의 구문을 파괴하는 공격이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2.3 OS 커맨드 인젝션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션을 경유하여 OS 명령을 부정하게 실행하는 공격이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS 커맨드 인젝션 공격 사례&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이메일을 보내는 폼
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메일 주소에 커맨드 명령어를 입력하여 내부적으로 리눅스의 계정 정보가 포함된 파일을 공격자의 메일로 정송하게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2.4 HTTP 헤더 인젝션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자라 리스폰스 헤더 필드에 개행 문자 등을 삽입함으로써 임의의 리스폰스 헤더 필드나 바디를 추가하는 공격이다 특히 바디를 추가하는 공격은 HTTP 리스폰스 분할 공격이라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 헤더 인섹션의 영향&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임의의 쿠키 세트&lt;/li&gt;
&lt;li&gt;임의의 URL에 리다이렉트&lt;/li&gt;
&lt;li&gt;임의의 바디 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 헤더 인젝션의 공격 사례&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Set-Cookie 헤더를 이용해서 세션 ID를 강제적으로 세트한다.&lt;/li&gt;
&lt;li&gt;HTTP 리스폰스 분할 공격
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가짜 바디를 표시하는 공격이다.&lt;/li&gt;
&lt;li&gt;가짜 웹사이트를 표시해서 개인 정보를 입력하게 하거나 XSS와 같은 효과를 얻을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2.5 메일 헤더 인젝션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션의 메일 송신 기능에 공격자가 임의의 To 및 Subject 등의 메일 헤더를 부정하게 추가하는 공격이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메일 헤더 인젝션의 공격 사례&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이메일을 보내는 폼
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;%0D%0A등의 개행 문자를 이용해서 메일 주소와 내용을 변조할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2.6 디렉토리 접근 공격&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비공개 디렉토리의 파일에 대해서 부정하게 디렉토리 패스를 가로질러 액세스하는 공격이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상대 경로나 절대 경로를 지정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2.7 리모트 파일 인클루션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트의 일부를 다른 파일에서 읽어올 때 공격자가 지정한 외부 서버의 URL을 파일에서 읽게 함으로써 임의의 스크립트를 동작시키는 공격이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리모트 파일 인클루션의 공격 사례&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리에 파일 이름을 지정해서 include를 이용해서 파일을 읽는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11.3 웹 서버의 설정이나 설계 미비로 인한 취약성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.3.1 강제 브라우징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강제 브라우징은 웹 서버의 공개 디렉토리에 있는 파일중에서 공개 의도가 없는 파일이 열람되는 취약성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강제 브라우징의 영향&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고객 정보 등 중요 정보 누설&lt;/li&gt;
&lt;li&gt;본래 액세스 권한이 있는 사용자에게만 표시하지 않는 정보 누설&lt;/li&gt;
&lt;li&gt;어디에서도 링크되지 않는 파일 누설&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL이 외부에 노출되었을 경우 피해를 입을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 후에만 표시되어야 하는 파일들은 액세스 권한을 확인해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.3.2 부적절한 에러 메시지 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 메시지에 너무 정확한 정보를 제공하면 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 DB 에러 로그를 그대로 출력하면 안된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.3.3 오픈 리다이렉트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임의의 URL로 리다이렉트 하는 기능이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11.4 세션 관리 미비로 인한 취약성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저의 인증 상태가 뺴앗겨 버리는 피해가 발생된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.4.1 세션 하이잭&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자가 유저의 세션 ID를 취득해서 악용하는 것을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입수 방법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부적절한 생성 방법에 의한 세션 ID 추측&lt;/li&gt;
&lt;li&gt;도청이나 XSS 등에 의한 세션 ID 도용&lt;/li&gt;
&lt;li&gt;세션 고정 공격에 의한 세션 ID 강제&lt;/li&gt;
&lt;li&gt;XSS로 쿠키 탈취&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.4.2 세션 픽세이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저에게 세션 ID를 강제로 부여한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 ID를 미리 입수하고 유저가 그 세션 ID를 이용해 인증하도록 기다린다. 이후에 이 세션 ID로 유저 정보에 액세스할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.4.3 크로스 사이트 리퀘스트 포저리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증된 유저가 의도하지 않은 개인 정보나 설정 정보 등을 공격자가 설치해 둔 함정에 의해 어떤 상태를 갱신하는 처리를 강제로 실행시키는 공격이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증된 유저의 권한으로 설정 정보 등을 갱신&lt;/li&gt;
&lt;li&gt;인증된 유저의 권한으로 상품을 구입&lt;/li&gt;
&lt;li&gt;인증된 유저의 권한으로 게시판에 글 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 의도치않게 본인의 세션 ID로 임의의 글이나 코멘트를 추가하게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11.5 기타&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.5.1 패스워드 크래킹&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패스워드를 논리적으로 이끌어내서 인증을 돌파하는 공격&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 경유로 패스워드 시행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무차별 대입 공격&lt;/li&gt;
&lt;li&gt;사전 공격&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;암호화된 패스워드를 해독
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무차별 대입 공격/사전 공격에 의한 유추&lt;/li&gt;
&lt;li&gt;레인보우 테이블&lt;/li&gt;
&lt;li&gt;열쇠 입수&lt;/li&gt;
&lt;li&gt;암호 알고리즘의 취약성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.5.2 클릭 재킹&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;투명한 버튼이나 링크를 함정으로 사용할 웹 페이지에 심어 두고 유저가 링크를 클릭하게 함으로써 의도치 않은 콘텐츠에 액세스 시키는 공격이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.5.3 DoS 공격&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 제공을 정지 상태로 만드는 공격이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;액세스를 집중시킴으로써 부하를 걸어 리소스를 다 소비하기 해 서비스를 정지 상태로 만든다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정상적인 액세스와 구별이 힘들다.&lt;/li&gt;
&lt;li&gt;여러 대의 컴퓨터에서 실행하는 DoS 공격은 DDoS 공격이라고 불린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;취약성을 공격한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.5.4 백도어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제한된 기능을 정규 절차를 밟지 않고 이용하는 것을 말한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발 단계에 디버그용으로 추가한 백도어&lt;/li&gt;
&lt;li&gt;개발자가 본인의 이익을 위해 추가한 백도어&lt;/li&gt;
&lt;li&gt;공격자가 어떠한 방법을 써서 설치한 백도어&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL/개발</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/327</guid>
      <comments>https://imnotadevleoper.tistory.com/327#entry327comment</comments>
      <pubDate>Mon, 8 Jan 2024 14:57:52 +0900</pubDate>
    </item>
    <item>
      <title>누군가 나를 멘토라고 부르기 시작했다</title>
      <link>https://imnotadevleoper.tistory.com/326</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요, 개발자로 일하고 있는 유승완입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 2023년 12월 28일부터 &amp;lt;코드잇&amp;gt;에서 진행하는 부트캠프 4기 프론트엔드 과정의 멘토로 참여하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 되어야 겠다고 마음을 먹었을 때부터 지금까지 저는 Udemy, SeSAC, NEXTSTEP, Programmers, Inflearn, Megaptera, Whatever등 다양한 곳에서 교육을 많이 들어왔는데요. 교육을 들으면서 개발자로서도 성장했지만 사람으로서도 성장할 수 있었고 스스로 교육을 통해서 정말 많이 성장했다고 느끼는 사람입니다. 그래서 저도 언젠가는 내가 경험하고 배운 것들을 다른 누군가에게 전달하고 싶다는 생각을 항상 하고 있었습니다. 그러던 중에 지인분을 통해서 &amp;lt;코드잇&amp;gt;에서 부트캠프의 멘토를 모집하고 있다는 것을 알게 되었는데요. 저는 아직 2년 차의 주니어 개발자이기 때문에 걱정이 되기도 했는데요. 누군가에게 도움을 주고 가르쳐주는 것에는 자신이 있고 즐거운 일로 생각하고 있었기 때문에 지원하게 되었고 이런 마음을 좋게 봐주셔서 멘토로 합류할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 시작을 함께한다는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 멘토는 스프린터분들과 주 2회 멘토링을 진행하게 되는데요. 첫번째 멘토링에서 먼저 저에 대한 소개를 드렸고 이후에 스프린터분들의 소개를 들어보게 되었습니다. 저는 왜 개발자가 되려고 하셨는지, 부트캠프를 진행하면서 어떤 점들이 걱정되는지와 같은 질문을 드렸는데요. 말씀해 주신 내용을 들어보았고 대부분은 특별한 계기로 개발에 흥미를 느끼셨고 해당 부트캠프를 통해서 개발자로서 커리어를 시작하시려는 분들이 많았습니다. 제가 들었던 생각은 스프린터분들께서 개발자로서의 첫 시작을 저와 함께한다는 것인데요. 누군가의 첫 시작을 함께한다는 것이 저에게도 정말 소중하고 감사한 일이라는 생각이 들었습니다. 또한 많은 책임감이 느껴지기도 했는데요. 제가 스프린터분들께 어떤 것들을 전달해 드리냐에 따라서 개발에 대한 마음가짐이 달라질 것이라는 생각이 들었습니다. 부담이 되기보다는 내가 더 열심히 해서 많은 도움을 드리고 싶다는 긍정적인 방향으로 생각할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발을 처음 시작하는 당신에게&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부트캠프에서는 처음에는 HTML과 CSS를 학습하고 3주차부터 코드 리뷰를 진행하기 때문에 초반에는 코드에 대해서 얘기하기에는 아직 이른 감이 있었는데요. 그래서 개발자를 시작하면서 도움이 될만한 내용을 전달드리면 좋을 것 같다는 생각이 들었고 아래와 같은 토픽들을 준비했었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마음가짐&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2502&quot; data-origin-height=&quot;1404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZGRXK/btsC36Qflew/2PO3cbOKAQyhgaFdx7wIV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZGRXK/btsC36Qflew/2PO3cbOKAQyhgaFdx7wIV1/img.png&quot; data-alt=&quot;개발자로서의 마음가짐&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZGRXK/btsC36Qflew/2PO3cbOKAQyhgaFdx7wIV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZGRXK%2FbtsC36Qflew%2F2PO3cbOKAQyhgaFdx7wIV1%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;1404&quot; data-origin-width=&quot;2502&quot; data-origin-height=&quot;1404&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개발자로서의 마음가짐&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;팀, 그리고 동료&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2496&quot; data-origin-height=&quot;1398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9oj4R/btsC35wX8Vz/1eXdJ70JtDTguOfUmzHJ4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9oj4R/btsC35wX8Vz/1eXdJ70JtDTguOfUmzHJ4K/img.png&quot; data-alt=&quot;팀, 그리고 동료&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9oj4R/btsC35wX8Vz/1eXdJ70JtDTguOfUmzHJ4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9oj4R%2FbtsC35wX8Vz%2F1eXdJ70JtDTguOfUmzHJ4K%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;241&quot; data-origin-width=&quot;2496&quot; data-origin-height=&quot;1398&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;팀, 그리고 동료&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기술과 공부&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2498&quot; data-origin-height=&quot;1394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cT1gNE/btsC4KMTN60/fohKX6QWQHWqlT5qEkOgpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cT1gNE/btsC4KMTN60/fohKX6QWQHWqlT5qEkOgpk/img.png&quot; data-alt=&quot;기술과 공부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cT1gNE/btsC4KMTN60/fohKX6QWQHWqlT5qEkOgpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcT1gNE%2FbtsC4KMTN60%2FfohKX6QWQHWqlT5qEkOgpk%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;240&quot; data-origin-width=&quot;2498&quot; data-origin-height=&quot;1394&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기술과 공부&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Code&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2494&quot; data-origin-height=&quot;1394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q13jU/btsC9oPmsPi/MqW6M1ym0XT9oXxxASa951/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q13jU/btsC9oPmsPi/MqW6M1ym0XT9oXxxASa951/img.png&quot; data-alt=&quot;Code&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q13jU/btsC9oPmsPi/MqW6M1ym0XT9oXxxASa951/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq13jU%2FbtsC9oPmsPi%2FMqW6M1ym0XT9oXxxASa951%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;1394&quot; data-origin-width=&quot;2494&quot; data-origin-height=&quot;1394&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Code&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 관리&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2496&quot; data-origin-height=&quot;1400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFRgJ2/btsC871lb6Z/IADDWdhfOMSpMkrg22L9d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFRgJ2/btsC871lb6Z/IADDWdhfOMSpMkrg22L9d0/img.png&quot; data-alt=&quot;시간 관리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFRgJ2/btsC871lb6Z/IADDWdhfOMSpMkrg22L9d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFRgJ2%2FbtsC871lb6Z%2FIADDWdhfOMSpMkrg22L9d0%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;241&quot; data-origin-width=&quot;2496&quot; data-origin-height=&quot;1400&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시간 관리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프린터분들에게 각 부분에 대해서 저의 생각들을 공유드렸고 잘 경청해주셔서 감사했습니다. 다음 멘토링 시간에는 아래와 같은 주제들로 얘기를 나눠보려고 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;건강한 코드 리뷰 주고 받기&lt;/li&gt;
&lt;li&gt;개발 스터디는 어떻게 하는 게 좋을까?&lt;/li&gt;
&lt;li&gt;개발자와 야근, 워라밸&lt;/li&gt;
&lt;li&gt;잡답(현업/실무, 동료, 영어 이름, -님 칭호, 회사에서의 3일, 네트워킹등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발을 처음 접하는 입장에서의 어려움&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 대학교에서 컴퓨터 공학을 전공했었기 때문에 개발을 처음 접해봤던 건 거의 9년 전인데요. 그래서 개발을 처음 공부하는 상황에서의 어려움이 잘 기억이 나지 않았습니다. 멘토링 시간에 스프린터 분들에게 부트캠프 과정에서 우려되는 점에 대해서 여쭤보았는데요. 많은 분들이 입을 모아 얘기해 주신 부분은 '내가 잘할 수 있을까?', '뒤처지지 않고 잘 따라갈 수 있을까?'와 같은 학습에 관련된 부분이었습니다. 그리고 제가 개발을 처음 배웠을 때를 생각하니 똑같은 감정을 느꼈던 것 같습니다. 저는 대학교 1학년때 Java라는 프로그래밍 언어를 처음 배웠는데요. 사실 너무 어려웠습니다. 'public static void main(String[] args)' 구문도 이해하기 어려웠고 왜 사용하는지 이해하기보다는 그냥 예제에 있으니까 써야 되는구나 하고 생각했던 것 같습니다. (지금 생각하면 개념이 어렵다기보다는 처음 보는 거라서 그렇게 느껴졌던 것 같아요) 그리고 지금의 스프린터분들도 개발을 처음 접하시는 분들이 많다 보니 낯섦이 가장 큰 적이 아닐까라는 생각이 들었습니다. 그래서 이 부분에 있어서 어떻게 도움을 드리면 좋을까 생각을 해봤는데요. 3가지 정도 얘기를 드렸던 것 같습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다른 사람을 최대한 이용하기, 지식 훔치기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무언가를 배우는 데 있어서 빠르게 배우려면 저는 해당 분야의 전문가의 지식을 습득하는 것이라고 생각하는데요. 만약 혼자 공부했다면 여러 가지 시행착오를 겪으면서 긴 시간이 지난 뒤에야 정답을 찾을 수 있는 것들을 이미 이런 시행착오를 겪고 정답을 찾은 사람의 도움을 받아서 바로 정답에 접근한다면 매우 빠르게 배울 수 있기 때문입니다. 교육 프로그램, 인터넷 강의, 커피챗, 개발 컨퍼런스, 기술 블로그등을 이용하고 부트캠프와 같은 환경 속에 있다면 강사, 멘토, 매니저분을 괴롭히면서 최대한 많은 것을 빼먹고 도움을 받아야 한다고 말씀드렸습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;한 번에 모두 이해하기보다는 흐름을 이해하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무언가를 처음 배운다면 어려운 게 당연한 것이라고 생각합니다. 그리고 생각보다 다른 개념들을 공부하면서 이전에 배웠던 어려운 개념들을 이해하게 되는 경우도 생기는데요. 이와 같은 이유로 저는 완벽하게 이해하고 넘어가기보다는 처음 방대한 지식을 공부할 때는 흐름을 이해하고 넘어가는 게 중요하다고 말씀을 드렸습니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기술의 우선순위 판단하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프린터분들께서 저한테 요청을 주셨던 것 중에 하나는 매주 커리큘럼에서 어떤 내용이 상대적으로 더 중요하고 실무에서 많이 쓰이는지 알면 좋겠다는 말씀을 해주셨습니다. 저도 많이 공감이 되었는데요. 개발을 처음 공부하는 입장에서는 모든 문법들을 외워야 하는 것인지, 실제로 실무에서 자주 쓰이는 문법은 무엇인지 이러한 정보를 알기가 어렵기 때문이에요. 따라서 매 주차의 커리큘럼에서 공부하시는 내용들에 대해 사진과 같이 가이드를 드리고 있어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;1530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cP2cH4/btsC38UL3TN/cAMEIklLWC5eeHZZ7dIrdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cP2cH4/btsC38UL3TN/cAMEIklLWC5eeHZZ7dIrdK/img.png&quot; data-alt=&quot; 커리큘럼 정리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cP2cH4/btsC38UL3TN/cAMEIklLWC5eeHZZ7dIrdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcP2cH4%2FbtsC38UL3TN%2FcAMEIklLWC5eeHZZ7dIrdK%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;430&quot; height=&quot;687&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;1530&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 커리큘럼 정리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 리뷰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커리큘럼상 3주 차(01/08 ~ 01/14)부터 스프린터분들과 코드 리뷰를 진행하게 되는데요. 아직 본인만의 코드 스타일이 잡혀있지 않은 상황이기 때문에 많은 정보를 드릴 수 있을 것 같아서 기대하고 있습니다. 코드 속에 존재하는 비즈니스 로직에서의 버그나 더 좋은 구조를 제안드리는 것도 중요하지만 저는 코드 스타일에 대한 리뷰를 디테일하게 드리는 것도 중요하다고 생각하고 있습니다. 코드에 정답은 없지만 대체로 좋은 코드는 있는 것처럼 이런 부분들에 대해서도 좋은 코드를 작성하는 습관을 가지실 수 있도록 디테일하게 봐드리려고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;우리는 동료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 형식적으로 멘토-멘티 관계로 있지만 저는 사실 개발자로 같이 일을 하게 될 미래의 업계의 동료분들이라고 생각하고 있어요. 그래서 많이 친해지면 좋을 것 같다는 생각을 하고 부트캠프가 끝나고 나서, 그리고 취업을 하고 나서도 종종 근황을 공유하며 이야기를 나눌 수 있는 사이가 되면 좋을 것 같아요. 이를 위해서 제가 먼저 다가가고 고민도 나누고 같이 공부도 하면서 어렵고 외로운 부트캠프가 아니라 재밌고 내일이 기대되는 그런 부트캠프 과정을 보내실 수 있도록 노력하려고 합니다. 이런 과정 속에서 저 스스로도 정말 많이 성장할 수 있을 것 같아서 많이 기대를 하고 있어요. 그리고 제가 배운 것들을 다시 나눌 수 있는 이런 선순환의 사이클을 가져가려고 해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리하며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 개발 업계는 국내와 해외 모두 힘든 상황이라고 생각해요. 하지만 이런 상황에서도 일부 사람들은 그동안 본인들이 노력했던 것들을 바탕으로 좋은 기회들을 잡고 있어요. 결국 힘든 상황 속에서도 내가 어떻게 하냐에 따라서 충분히 헤쳐나갈 수 있다는 것인데요. 지금 부트캠프를 수강하고 계시는 분들께서도 이러한 기회를 잡을 수 있도록 많은 도움을 드리려고 하고 실제로도 좋은 결과가 있으면 너무 기쁠 것 같아요. 멘토링을 진행하면서 근황은 꾸준히 블로그에 작성해 보겠습니다.&lt;/p&gt;</description>
      <category>TIL/개발</category>
      <category>부트캠프</category>
      <category>코드잇</category>
      <category>코드잇 스프린트</category>
      <category>프론트엔드</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/326</guid>
      <comments>https://imnotadevleoper.tistory.com/326#entry326comment</comments>
      <pubDate>Sun, 7 Jan 2024 07:07:29 +0900</pubDate>
    </item>
    <item>
      <title>그림으로 배우는 Http &amp;amp; Network Basic - 10장</title>
      <link>https://imnotadevleoper.tistory.com/325</link>
      <description>&lt;h1&gt;10장 웹 콘텐츠에서 사용하는 기술&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10.1 HTML&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.1.1 웹 페이지의 대부분은 HTML로 되어 있다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML은 웹 상에서 하이퍼텍스트를 보내기 위해 개발된 언어다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하이퍼텍스트는 문서 시스템의 하나로서, 문서 중에 임의의 장소의 정보가 다른 정보에 관련된 즉 링크되어 있는 문서를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML로 쓰여진 문서를 브라우저가 해석해서 렌더링 된 결과를 우리가 보게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.1.2 HTML 버전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML1.0: 1993년에 일리노이 대학의 NCSA에서 모자이크라는 브라우저가 발표되었는데 이 브라우저가 해석할 수 있는 HTML의 사양이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML4.0: 1999년 12월에 W3C라는 조직에 의해 출시되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML5: 2014년에 정식으로 권고안이 발표되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.1.3 디자인을 적용하는 CSS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 각 요소를 어떻게 표시할지를 지시한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10.2 다이나믹 HTML&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.2.1 웹 페이지를 동적으로 변경하는 다이나믹 HTML&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적인 HTML을 스크립트를 사용해서 동적으로 변경하는 기술을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript등의 스크립트로 조작하여 변화시킨다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.2.2 HTML을 조작하기 쉽게 해주는 DOM&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM은 HTML 문서와 XML 문서를 위한 API다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10.3 웹 애플리케이션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.3.1 웹을 사용해서 기능을 제공하는 웹 애플리케이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션은 웹 기능을 사용해서 제공되는 프로그램을 말한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.3.2 웹 서버와 프로그램을 연계하는 CGI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CGI(Common Gateway Interface)는 웹 서버가 클라이언트에서 받은 리퀘스트를 프로그램에 전달하기 위한 구조다. Perl, PHP, Ruby등의 언어로 CGI 프로그램을 만들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.3.3 Java에서 보급된 서블릿&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿은 서버 상에 HTML 등의 동적 콘텐츠를 생성하기 위한 프로그램을 가리킨다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10.4 데이터 송신에 이용되는 포맷이나 언어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.4.1 범용적으로 사용할 수 있는 마크업 언어 XML&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML은 목적에 맞게 확장 가능한 범용적으로 사용할 수 있는 마크업 언어다. XML을 사용함으로써 인터넷을 통해 데이터 공유를 용이하게 하는 것을 목적으로 한다. 트리 구조로 되어있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.4.2 갱신 정보를 송신하는 RSS/Atom&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RSS와 Atom은 뉴스나 블로그의 기사 등의 갱신 정볼르 송신하기 위한 문서 포맷을 말하고 XML을 이용하고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RSS 0.9(RDF Site Summary) : 최초의 RSS로 넷스케이프 커뮤니케이션사가 자사의 포털 사이트를 위해서 1999년 3월에 개발, RDF 구문 사용&lt;/li&gt;
&lt;li&gt;RSS 0.91(Rich Site Summary): RSS 0.9에 요소를 확장할 목적으로 1999년 7월에 개발, XML 사용&lt;/li&gt;
&lt;li&gt;RSS 1.0(RDF Site Summary): 2000년 12월에 RSS-DEV 워킹 그룹에 의해 릴리스&lt;/li&gt;
&lt;li&gt;RSS 2.0(Really Simple Syndication): RSS 1.0 노선과는 별도로 RSS 0.91과 호환성을 유지하기 위해 2000년 12월에 유저랜드 소프트웨어사가 개발&lt;/li&gt;
&lt;li&gt;Atom 전송 포맷(Atom Syndication Format): 콘텐츠를 전송하기 위한 피드의 포맷&lt;/li&gt;
&lt;li&gt;Atom 출판 프로토콜(Atom Publishing Protocol): 웹 상의 콘텐츠를 편집하기 위한 프로토콜&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그 갱신 정보 등을 구독하기 위해서 RSS 리더 애플리케이션에서는 대부분 RSS의 각종 버전과 Atom을 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.4.3 JavaScript에서 이용하기 쉽고 가벼운 JSON&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON은 경량 데이터 기술 언어로서 JS의 오브젝트 표기법을 바탕으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ajax에서 JSON을 많이 사용한다.&lt;/p&gt;</description>
      <category>TIL/개발</category>
      <author>유승완</author>
      <guid isPermaLink="true">https://imnotadevleoper.tistory.com/325</guid>
      <comments>https://imnotadevleoper.tistory.com/325#entry325comment</comments>
      <pubDate>Sat, 6 Jan 2024 03:35:05 +0900</pubDate>
    </item>
  </channel>
</rss>