<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>OIL</title>
    <link>https://rocketengine.tistory.com/</link>
    <description>가끔 독서도 하고, 가끔 운동도 하고, 가끔 여행을 떠나요. 어제보다 더 나은 사람이 되기위해 노력중입니다. </description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 20:41:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>_OIL</managingEditor>
    <item>
      <title>[프레임워크 없는 프론트엔드 개발] - 3장. DOM 이벤트 관리</title>
      <link>https://rocketengine.tistory.com/entry/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%97%86%EB%8A%94-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C-3%EC%9E%A5-DOM-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EA%B4%80%EB%A6%AC</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;YAGNI 원칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YAGNI는 You Aren&amp;rsquo;t Gonna Need It의 약자로 &amp;ldquo;정말 필요하다고 간주할 때까지 기능을 추가하지 마라&amp;rdquo; 라는 원칙이다. 자신만의 아키텍처를 작성할 때 반드시 YAGNI 원칙을 적용해 당시에 직면한 문제만을 해결해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DOM 이벤트 API&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트는 웹 애플리케이션에서 발생하는 동작이다. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Events&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;li&gt;키보드 이벤트 (키다운, 키업 등)&lt;/li&gt;
&lt;li&gt;뷰 이벤트 (뷰 리사이징, 스크롤 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 포함한 사용자가 트리거한 이벤트에 반응 할 수 있다. 또한 네트워크의 상태나 DOM 콘텐츠의 변화에 따라 이벤트를 발생시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWl2AW/btsIpM6tgV2/Wmt7J16SEudXNBLk0eBpZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWl2AW/btsIpM6tgV2/Wmt7J16SEudXNBLk0eBpZ1/img.png&quot; data-alt=&quot;기본 클릭 이벤트 라이프사이클&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWl2AW/btsIpM6tgV2/Wmt7J16SEudXNBLk0eBpZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWl2AW%2FbtsIpM6tgV2%2FWmt7J16SEudXNBLk0eBpZ1%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;2000&quot; height=&quot;316&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기본 클릭 이벤트 라이프사이클&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;속성에 핸들러 연결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;on속성 이용 : onclick, ondblclick, onmouseover, onblur, onfocus 등&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;이 방법을 사용하면 한번에 하나의 핸들러만 연결할 수 있다. 즉 다른 코드가 onclick핸들러를 덮어 쓰면 원래 핸들러는 사라진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;let button = document.querySelector(&quot;#button&quot;);
button.onclick = () =&amp;gt; {
  console.log(&quot;Click 1&quot;);
};
button.onclick = () =&amp;gt; {
  console.log(&quot;Click 2&quot;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;button = document.querySelector(&quot;#eventListener&quot;);
const firstHandler = () =&amp;gt; {
  console.log(&quot;First handler&quot;);
};

const secondHandler = () =&amp;gt; {
  console.log(&quot;Second handler&quot;);
};

button.addEventListener(&quot;click&quot;, firstHandler);
button.addEventListener(&quot;click&quot;, secondHandler);

window.setTimeout(() =&amp;gt; {
  button.removeEventListener(&quot;click&quot;, firstHandler);
  button.removeEventListener(&quot;click&quot;, secondHandler);
  console.log(&quot;Removed Event Handlers&quot;);
}, 1000);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EventTarget가 모든 DOM 노드의 베이스에 있기때문에 모든 DOM 노드에서 '이벤트&amp;rsquo;를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;addEventListener의 첫 번째 매개변수는 이벤트 타입이다.&lt;/li&gt;
&lt;li&gt;두 번째 매개 변수는 콜백이며 이벤트가 트리거될 때 호출된다.&lt;/li&gt;
&lt;li&gt;property메서드와 달리 addEventListener는 중복 이벤트 등록이 가능하다.&lt;/li&gt;
&lt;li&gt;DOM에 요소가 더 이상 존재하지 않으면 메모리 누수를 방지하고자 이벤트 리스너도 삭제해야 한다. 이를 위해 removeEventListerner 메서드를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이벤트 객체&lt;/h4&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;button = document.querySelector(&quot;#event&quot;);
button.addEventListener(&quot;click&quot;, (e) =&amp;gt; {
  console.log(&quot;event&quot;, e);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;addEventListener의 콜백에는 이벤트를 나타내는 매개변수를 포함할 수 있다.&lt;/li&gt;
&lt;li&gt;이벤트 객체에는 포인터 좌표, 이벤트 타입, 이벤트를 트리거한 요소 같은 유용한 정보가 많이 들어있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DOM 이벤트 라이프사이클&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;addEventListener(type, listener, useCapture);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세 번째 매개변수는 useCapture라고 불리며 기본값은 false이다.&lt;/li&gt;
&lt;li&gt;이 매개변수는 선택 사항이지만 브라우저 호환성을 얻으려면 포함시켜야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
	&amp;lt;div&amp;gt;
		this is a container
		&amp;lt;button&amp;gt;Click here&amp;lt;/button&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;

const button = document.querySelector('button')
const div = document.querySelector('div')

div.addEventListener('click', () =&amp;gt; console.log('Div Clicked'), false)
button.addEventListener('click', () =&amp;gt; console.log('Button Clicked', false)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버튼을 클릭하면 button이 div안에 있으므로 button부터 시작해 두개의 핸들러가 모두 호출된다. 즉, 이벤트 객체는 트리거한 DOM 노드(예제의 경우 button)에서 시작해 모든 조상 노드로 올라간다. 이러한 매커니즘을 이벤트 버블링 이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const button = document.querySelector(&quot;button&quot;);
const div = document.querySelector(&quot;div&quot;);

div.addEventListener(&quot;click&quot;, () =&amp;gt; console.log(&quot;Div Clicked&quot;), false);
button.addEventListener(
  &quot;click&quot;,
  (e) =&amp;gt; {
    e.stopPropagation();
    console.log(&quot;Button Clicked&quot;);
  },
  false
);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 예제에서는 div 핸들러는 동작하지 않는다.&lt;/li&gt;
&lt;li&gt;Event 인터페이스의 stopPropagation메서드를 사용해 버블 체인을 중지 할 수 있다.&lt;/li&gt;
&lt;li&gt;이 기술은 복잡한 레이아웃에서 유용할 수 있지만 핸들러의 순서에 의존하는 경우 코드를 유지하기 어려울 수 있다. 이런 경우 3장 끝에서 나오는 이벤트 위임 패턴이 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const button = document.querySelector('button')
const div = document.querySelector('div')

div.addEventListener('click', () =&amp;gt; console.log('Div Clicked'), true)
button.addEventListener('click', () =&amp;gt; console.log('Button Clicked', true)

// output
Div Clicked
Button Clicked
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useCapture 매개 변수를 사용해 핸들러의 실행 순서를 반대로 할 수 있다.&lt;/li&gt;
&lt;li&gt;즉,&amp;nbsp;addEventListener&amp;nbsp;를 호출할때&amp;nbsp;useCapture&amp;nbsp;파라미터에&amp;nbsp;true&amp;nbsp;값을 주게되면 버블 단계 대신에 캡쳐 단계에 이벤트 핸들러를 추가한다는 것을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해보면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;캡쳐 단계: 이벤트가 html에서 목표 요소로 이동한다.&lt;/span&gt; &lt;br /&gt;(하향식) 목표 단계: 이벤트가 목표 요소에 도달한다. &lt;br /&gt;(상향식) 버블 단계: 이벤트가 목표 요소에서 html로 이동한다.&lt;/p&gt;
&lt;/blockquote&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 정의 이벤트 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM 이벤트 API에서는 사용자 정의 이벤트 타입을 정의하고 다른 이벤트처럼 처리 할 수 있다. 사용자 정의 이벤트를 생성하려면 CustomEvent생성자 함수를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;verilog&quot;&gt;&lt;code&gt;const EVENT_NAME = &quot;FiveCharInputValue&quot;;
const input = document.querySelector(&quot;input&quot;);

input.addEventListener(&quot;input&quot;, () =&amp;gt; {
  const { length } = input.value;
  console.log(&quot;input length&quot;, length);
  if (length === 5) {
    const time = new Date().getTime();
    const event = new CustomEvent(EVENT_NAME, {
      detail: {
        time,
      },
    });

    input.dispatchEvent(event);
  }
});

input.addEventListener(EVENT_NAME, (e) =&amp;gt; {
  console.log(&quot;handling custom event...&quot;, e.detail);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;input이벤트를 관리할 때 값 자체의 길이를 확인한다.&lt;/li&gt;
&lt;li&gt;값의 길이가 5라면 FiveCharInputValue라는 이벤트를 발생시킨다.&lt;/li&gt;
&lt;li&gt;사용자 정의 이벤트를 처리 하려면 addEventListener 메서드로 표준 이벤트 리스너를 추가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TodoMVC에 이벤트 추가&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;렌더링 엔진 리뷰&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2장에서 작성한 마지막 구현의 문제점은 todos 구성 요소가 문자열로 동작한다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;const TodoElement = (todo: Todo) =&amp;gt; {
  const { text, completed, id } = todo;
  return `
  &amp;lt;li class=&quot;todo-item ${completed ? &quot;completed&quot; : &quot;&quot;}&quot; data-id=&quot;${id}&quot;&amp;gt;
        &amp;lt;div class=&quot;display-todo&quot;&amp;gt;
          &amp;lt;label for=&quot;toggle-todo&quot; class=&quot;toggle-todo-label visually-hidden&quot;&amp;gt;Toggle Todo&amp;lt;/label&amp;gt;
          &amp;lt;input id=&quot;toggle-todo&quot; class=&quot;toggle-todo-input&quot; type=&quot;checkbox&quot; ${completed ? &quot;checked&quot; : &quot;&quot;} /&amp;gt;
          &amp;lt;span class=&quot;todo-item-text truncate-singleline&quot; tabindex=&quot;0&quot;&amp;gt;${text}&amp;lt;/span&amp;gt;
          &amp;lt;button class=&quot;remove-todo-button&quot; title=&quot;Remove Todo&quot;&amp;gt;&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;edit-todo-container&quot;&amp;gt;
          &amp;lt;label for=&quot;edit-todo&quot; class=&quot;edit-todo-label visually-hidden&quot;&amp;gt;Edit todo&amp;lt;/label&amp;gt;
          &amp;lt;input id=&quot;edit-todo&quot; class=&quot;edit-todo-input&quot; /&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/li&amp;gt;
      `;
      

const Todos = (targetElement: Element, { todos }: State) =&amp;gt; {
  const newTodoList = targetElement.cloneNode(true) as Element;
  const todosElements = todos.map(TodoElement).join(&quot;&quot;);
  newTodoList.innerHTML = todosElements;
  return newTodoList;
};

export default Todos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;기존&amp;nbsp;코드:&amp;nbsp;문자열로&amp;nbsp;만드는&amp;nbsp;todo-item&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리스트의 모든 todo 요소는 문자열로 생성되고 하나로 합쳐진 다음 &lt;b&gt;innerHTML&lt;/b&gt;로 부모 리스트에 추가된다. 그러나 문자열에는 이벤트 핸들러를 추가 할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;템플릿 요소&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;document.createElement API를&lt;/b&gt; 사용해 비어있는 새 DOM 노드를 생성하는 방법도있지만 코드를 읽고 유지하기 어렵다. 더 나은 방법으로는 index.html 파일의 &lt;b&gt;template&lt;/b&gt; 태그 안에 todo 요소의 마크업을 유지하는 것이다. &lt;b&gt;template&lt;/b&gt;태그는 이름에서 알 수 있듯이 렌더링 엔진의 &amp;lsquo;스탬프&amp;rsquo;로 사용할 수 있는 보이지 않는 태그이다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;  &amp;lt;template id=&quot;todo-item&quot;&amp;gt;
    &amp;lt;li class=&quot;todo-item&quot;&amp;gt;
      &amp;lt;div class=&quot;display-todo&quot;&amp;gt;
        &amp;lt;label for=&quot;toggle-todo&quot; class=&quot;toggle-todo-label visually-hidden&quot;
          &amp;gt;Toggle Todo&amp;lt;/label
        &amp;gt;
        &amp;lt;input id=&quot;toggle-todo&quot; class=&quot;toggle-todo-input&quot; type=&quot;checkbox&quot; /&amp;gt;
        &amp;lt;span class=&quot;todo-item-text truncate-singleline&quot; tabindex=&quot;0&quot;
          &amp;gt;${text}&amp;lt;/span
        &amp;gt;
        &amp;lt;button class=&quot;remove-todo-button&quot; title=&quot;Remove Todo&quot;&amp;gt;&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;edit-todo-container&quot;&amp;gt;
        &amp;lt;label for=&quot;edit-todo&quot; class=&quot;edit-todo-label visually-hidden&quot;
          &amp;gt;Edit todo&amp;lt;/label
        &amp;gt;
        &amp;lt;input id=&quot;edit-todo&quot; class=&quot;edit-todo-input&quot; /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/li&amp;gt;
  &amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot; data-token-index=&quot;0&quot;&gt;index.html 에 todo-item template요소 생성&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let template: HTMLTemplateElement;

const createTodoNode = () =&amp;gt; {
  if (!template) {
    template = document.getElementById(&quot;todo-item&quot;) as HTMLTemplateElement;
  }

  const firstChild = template?.content?.firstElementChild;
  return firstChild?.cloneNode(true) as HTMLElement;
};

const TodoElement = (todo: Todo, index: number) =&amp;gt; {
  const { text, completed } = todo;
  const element = createTodoNode();
  if (element) {
    const inputEdit = element.querySelector(&quot;input.edit-todo-input&quot;) as HTMLInputElement;
    const label = element.querySelector(&quot;toggle-todo-label&quot;);
    const inputToggle = element.querySelector(&quot;input.toggle-todo-input&quot;) as HTMLInputElement;
    const buttonDestroy = element.querySelector(&quot;button.remove-todo-button&quot;) as HTMLButtonElement;
    const spanText = element.querySelector(&quot;span.todo-item-text &quot;) as HTMLButtonElement;
    spanText.textContent = text;
    if (inputEdit) inputEdit.value = text;
    if (label) label.textContent = text;

    if (completed) {
      element.classList.add(&quot;completed&quot;);
      if (inputToggle) inputToggle.checked = true;
    }

    if (buttonDestroy) buttonDestroy.dataset.index = index.toString();

    return element;
  }
};

const Todos = (targetElement: Element, { todos }: State, { deleteItem }: Events) =&amp;gt; {
  const newTodoList = targetElement.cloneNode(true) as HTMLUListElement;
  newTodoList.innerHTML = &quot;&quot;;

  todos.map(TodoElement)!.forEach((element) =&amp;gt; {
    if (element) newTodoList.appendChild(element);
  });

  newTodoList.addEventListener(&quot;click&quot;, (e: MouseEvent) =&amp;gt; {
    const target = e.target as HTMLElement;

    if (target.matches(&quot;button.remove-todo-button&quot;)) {
      deleteItem(Number(target.dataset.index));
    }
  });
  return newTodoList;
};

export default Todos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot; data-token-index=&quot;0&quot;&gt;템플릿을 사용해 todo-item 생성&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
	&amp;lt;template id=&quot;todo-item&quot;&amp;gt;
		&amp;lt;!-- todo 항목 내용을 여기에 놓는다 --&amp;gt;
	&amp;lt;/template&amp;gt;
	&amp;lt;template id=&quot;todo-app&quot;&amp;gt;
		&amp;lt;section class=&quot;todoapp&quot;&amp;gt;
			&amp;lt;!-- 앱 내용을 여기에 놓는다 --&amp;gt;
		&amp;lt;/section&amp;gt;
	&amp;lt;/template&amp;gt;
	&amp;lt;div id=&quot;root&quot;&amp;gt;
		&amp;lt;div data-component=&quot;app&quot;&amp;gt;&amp;lt;/div&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d; text-align: start;&quot;&gt;전체 앱에 템플릿 사용&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let template: HTMLTemplateElement;

const createAppElement = (): HTMLElement =&amp;gt; {
  if (!template) {
    template = document.getElementById(&quot;todo-app&quot;) as HTMLTemplateElement;
  }

  const firstChild = template?.content?.firstElementChild;
  return firstChild?.cloneNode(true) as HTMLElement;
};

const TodoApp = (targetElement: Element, state: State, events: Events) =&amp;gt; {
  const newApp = targetElement.cloneNode(true) as Element;

  newApp.innerHTML = &quot;&quot;;
  newApp.appendChild(createAppElement());
  addEvents(newApp, events);

  return newApp;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { TodoApp } from &quot;@pages/todo-app&quot;;
import { registry, applyDiff } from &quot;@shared/libs&quot;;
import { State } from &quot;@shared/types/index.js&quot;;
import { getTodos } from &quot;@shared/utils&quot;;
import { Counter } from &quot;@widgets/counter&quot;;
import { Filters } from &quot;@widgets/filters&quot;;
import { Todos } from &quot;@widgets/todos&quot;;

registry.add(&quot;app&quot;, TodoApp);
registry.add(&quot;todos&quot;, Todos);
registry.add(&quot;counter&quot;, Counter);
registry.add(&quot;filters&quot;, Filters);

const state: State = {
  // todos: getTodos(),
  todos: [],
  currentFilter: &quot;All&quot;,
};

const render = () =&amp;gt; {
  window.requestAnimationFrame(() =&amp;gt; {
    const main = document.querySelector(&quot;#root&quot;) as Element;
    const newMain = registry.renderRoot(main, state);
    applyDiff(document.body, main, newMain);
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;app 이라는 data-component가 새로 생겼다. app 컴포넌트는 새로 작성된 템플릿을 사용해 콘텐츠를 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 이벤트 처리 아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 대신 DOM 요소로 동작하는 새로운 렌더링 엔진을 작성했다. 2장에서 작성한 렌더링 엔진은 상태를 가져오고 DOM 트리를 생성하는 순수 함수를 기반으로 한다. 이 시나리오에서는 &amp;lsquo;루프&amp;rsquo; 사이에서 이벤트 핸들러를 쉽게 연결할 수 있다. 모든 이벤트 다음에 상태를 조작한 후 새로운 상태로 렌더링 함수를 호출하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1881&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRWeTt/btsIpLmbpld/JbrzM9Dz8Ggt7BuKzOjxx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRWeTt/btsIpLmbpld/JbrzM9Dz8Ggt7BuKzOjxx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRWeTt/btsIpLmbpld/JbrzM9Dz8Ggt7BuKzOjxx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRWeTt%2FbtsIpLmbpld%2FJbrzM9Dz8Ggt7BuKzOjxx0%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;2000&quot; height=&quot;1881&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1881&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { TodoApp } from &quot;@pages/todo-app&quot;;
import { registry, applyDiff } from &quot;@shared/libs&quot;;
import { Events, State } from &quot;@shared/types/index.js&quot;;
import { getTodos } from &quot;@shared/utils&quot;;
import { Counter } from &quot;@widgets/counter&quot;;
import { Filters } from &quot;@widgets/filters&quot;;
import { Todos } from &quot;@widgets/todos&quot;;

registry.add(&quot;app&quot;, TodoApp);
registry.add(&quot;todos&quot;, Todos);
registry.add(&quot;counter&quot;, Counter);
registry.add(&quot;filters&quot;, Filters);

const state: State = {
  // todos: getTodos(),
  todos: [],
  currentFilter: &quot;All&quot;,
};

const events: Events = {
  deleteItem: (index: number) =&amp;gt; {
    state.todos.splice(index, 1);
    render();
  },
  addItem: (text: string) =&amp;gt; {
    state.todos.push({
      text,
      completed: false,
      id: state.todos.length + 1,
    });
    render();
  },
};

const render = () =&amp;gt; {
  window.requestAnimationFrame(() =&amp;gt; {
    const main = document.querySelector(&quot;#root&quot;) as Element;
    const newMain = registry.renderRoot(main, state, events);
    applyDiff(document.body, main, newMain);
  });
};

render();

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot; data-token-index=&quot;0&quot;&gt;index.js에 이벤트를 가진 컨트롤러 작성&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;events 객체는 상태를 수정하고 렌더링 함수를 수동으로 호출 하는 간단한 함수이다.&lt;/li&gt;
&lt;li&gt;렌더링 엔진의 진입점인 renderRoot 함수는 이벤트를 포함하는 events 를 세 번째 매개 변수를 받는다.&lt;/li&gt;
&lt;li&gt;events 는 모든 data-component에 접근할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt; import { Events, State } from &quot;@shared/types&quot;;
 
 const addEvents = (targetElement: Element, events: Events): void =&amp;gt; {
  const inputElement = targetElement.querySelector(&quot;.new-todo-input&quot;) as HTMLInputElement;
  if (inputElement) {
    inputElement.addEventListener(&quot;keypress&quot;, (e: KeyboardEvent) =&amp;gt; {
      if (e.key === &quot;Enter&quot;) {
        console.log(&quot;inputElement.value&quot;, inputElement.value);

        events.addItem(inputElement.value);
        inputElement.value = &quot;&quot;;
      }
    });
  }
};

export const TodoApp = (targetElement: Element, state: State, events: Events) =&amp;gt; {
  const newApp = targetElement.cloneNode(true) as Element;

  newApp.innerHTML = &quot;&quot;;
  newApp.appendChild(createAppElement());
  addEvents(newApp, events);

  return newApp;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot; data-token-index=&quot;0&quot;&gt;addItem 이벤트를 가진 앱 구성 요소&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 렌더링 주기에 대해 새 DOM 요소를 생성하고 새 todo-item의 값을 삽입하는 데 사용되는 input 핸들러에 이벤트 핸들러를 연결한다.&lt;/li&gt;
&lt;li&gt;사용자가 Enter를 누르면 addItem함수가 호출된 후 input 핸들러가 지워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&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;reasonml&quot;&gt;&lt;code&gt;import { Todo, State, Events } from &quot;@shared/types&quot;;
import { createTodoNode } from &quot;../model&quot;;

const TodoElement = (todo: Todo, index: number) =&amp;gt; {
  const { text, completed } = todo;
  const element = createTodoNode();
  if (element) {
    const inputEdit = element.querySelector(&quot;input.edit-todo-input&quot;) as HTMLInputElement;
    const label = element.querySelector(&quot;toggle-todo-label&quot;);
    const inputToggle = element.querySelector(&quot;input.toggle-todo-input&quot;) as HTMLInputElement;
    const buttonDestroy = element.querySelector(&quot;button.remove-todo-button&quot;) as HTMLButtonElement;
    const spanText = element.querySelector(&quot;span.todo-item-text &quot;) as HTMLButtonElement;
    spanText.textContent = text;
    if (inputEdit) inputEdit.value = text;
    if (label) label.textContent = text;

    if (completed) {
      element.classList.add(&quot;completed&quot;);
      if (inputToggle) inputToggle.checked = true;
    }

    if (buttonDestroy) buttonDestroy.dataset.index = index.toString();

    return element;
  }
};

const Todos = (targetElement: Element, { todos }: State, { deleteItem }: Events) =&amp;gt; {
  const newTodoList = targetElement.cloneNode(true) as HTMLUListElement;
  newTodoList.innerHTML = &quot;&quot;;

  todos.map(TodoElement)!.forEach((element) =&amp;gt; {
    if (element) newTodoList.appendChild(element);
  });

  newTodoList.addEventListener(&quot;click&quot;, (e: MouseEvent) =&amp;gt; {
    const target = e.target as HTMLElement;

    if (target.matches(&quot;button.remove-todo-button&quot;)) {
      deleteItem(Number(target.dataset.index));
    }
  });
  return newTodoList;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot; data-token-index=&quot;0&quot;&gt;이벤트 위임 기반의 todo-item&lt;/span&gt;&lt;span style=&quot;color: #9d9d9d;&quot; data-token-index=&quot;0&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;todo-item 마다 별도의 이벤트 핸들러를 갖지 않고 있으며 리스트 자체에 하나의 이벤트 핸들러만 연결돼 있다&lt;/li&gt;
&lt;li&gt;리스트가 아주 길다면 이 접근 방식으로 성능과 메모리 사용성을 개선 시킬 수 있다.&lt;/li&gt;
&lt;li&gt;matches API는 요소가 실제 이벤트 대상인지 확인하는 데 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;3장에서는 DOM 이벤트 API의 몇 가지 기본 개념을 설명했다.&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;참고&lt;/h3&gt;
&lt;figure id=&quot;og_1720269660121&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;&amp;lt;template&amp;gt;: 콘텐츠 템플릿 요소 - HTML: Hypertext Markup Language | MDN&quot; data-og-description=&quot;HTML &amp;lt;template&amp;gt; 요소는 페이지를 불러온 순간 즉시 그려지지는 않지만, 이후 JavaScript를 사용해 인스턴스를 생성할 수 있는 HTML 코드를 담을 방법을 제공합니다.&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/HTML/Element/template&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/HTML/Element/template&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dbcFo2/hyWvOWcp7K/pWX8lUR35XAeJk20a4bYf0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTML/Element/template&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/HTML/Element/template&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dbcFo2/hyWvOWcp7K/pWX8lUR35XAeJk20a4bYf0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;template&amp;gt;: 콘텐츠 템플릿 요소 - HTML: Hypertext Markup Language | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;HTML &amp;lt;template&amp;gt; 요소는 페이지를 불러온 순간 즉시 그려지지는 않지만, 이후 JavaScript를 사용해 인스턴스를 생성할 수 있는 HTML 코드를 담을 방법을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1720269658334&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[JavaScript]  이벤트 생명주기 (Event Life Cycle)&quot; data-og-description=&quot;자바스크립트에서 작업할 때 이벤트는 간단한 호버나 클릭과 같이 흔한 현상이다.각 이벤트마다 동작은 문서 객체 모델(DOM)을 통해 전파된다. DOM은 형제, 자식 및 부모 요소를 포함한 트리 구조&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@sonaki0811/JavaScript-이벤트-생명주기-Event-Life-Cycle&quot; data-og-url=&quot;https://velog.io/@sonaki0811/JavaScript-이벤트-생명주기-Event-Life-Cycle&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b9jQP4/hyWvK7mpnm/BNjshQc8r2ol43IBE5kdcK/img.png?width=850&amp;amp;height=548&amp;amp;face=0_0_850_548,https://scrap.kakaocdn.net/dn/OrvM0/hyWvMjMtgY/7HJ0ZFEaEmHngtoh8tuKSk/img.png?width=850&amp;amp;height=548&amp;amp;face=0_0_850_548,https://scrap.kakaocdn.net/dn/cVUops/hyWvKfd0gA/lAVLYbb5FnVbV6kYT4dFK1/img.png?width=850&amp;amp;height=548&amp;amp;face=0_0_850_548&quot;&gt;&lt;a href=&quot;https://velog.io/@sonaki0811/JavaScript-이벤트-생명주기-Event-Life-Cycle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@sonaki0811/JavaScript-이벤트-생명주기-Event-Life-Cycle&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b9jQP4/hyWvK7mpnm/BNjshQc8r2ol43IBE5kdcK/img.png?width=850&amp;amp;height=548&amp;amp;face=0_0_850_548,https://scrap.kakaocdn.net/dn/OrvM0/hyWvMjMtgY/7HJ0ZFEaEmHngtoh8tuKSk/img.png?width=850&amp;amp;height=548&amp;amp;face=0_0_850_548,https://scrap.kakaocdn.net/dn/cVUops/hyWvKfd0gA/lAVLYbb5FnVbV6kYT4dFK1/img.png?width=850&amp;amp;height=548&amp;amp;face=0_0_850_548');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[JavaScript] 이벤트 생명주기 (Event Life Cycle)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트에서 작업할 때 이벤트는 간단한 호버나 클릭과 같이 흔한 현상이다.각 이벤트마다 동작은 문서 객체 모델(DOM)을 통해 전파된다. DOM은 형제, 자식 및 부모 요소를 포함한 트리 구조&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1720269658610&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Event reference | MDN&quot; data-og-description=&quot;Events are fired to notify code of &amp;quot;interesting changes&amp;quot; that may affect code execution. These can arise from user interactions such as using a mouse or resizing a window, changes in the state of the underlying environment (e.g. low battery or media events&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/Events&quot; data-og-url=&quot;https://developer.mozilla.org/en-US/docs/Web/Events&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gBNER/hyWvVA4msk/buEOoFWM6mayeP6onqwgq0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Events&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/Events&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gBNER/hyWvVA4msk/buEOoFWM6mayeP6onqwgq0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Event reference | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Events are fired to notify code of &quot;interesting changes&quot; that may affect code execution. These can arise from user interactions such as using a mouse or resizing a window, changes in the state of the underlying environment (e.g. low battery or media events&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>독서/2024</category>
      <author>_OIL</author>
      <guid isPermaLink="true">https://rocketengine.tistory.com/195</guid>
      <comments>https://rocketengine.tistory.com/entry/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%97%86%EB%8A%94-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C-3%EC%9E%A5-DOM-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EA%B4%80%EB%A6%AC#entry195comment</comments>
      <pubDate>Sat, 6 Jul 2024 21:41:52 +0900</pubDate>
    </item>
    <item>
      <title>[프레임워크 없는 프론트엔드 개발] - 2장. 렌더링</title>
      <link>https://rocketengine.tistory.com/entry/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%97%86%EB%8A%94-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C-2%EC%9E%A5-%EB%A0%8C%EB%8D%94%EB%A7%81</link>
      <description>&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;W3C(world Wide Web Consortium)는 프로그래밍 방식으로 요소를 렌더링 하는 방식을 DOM(Document Object Model)로 정의했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2장의 목적은 프레임워크 없이 DOM을 효과적으로 조작하는 방법을 배우는 데 있습니다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://www.w3.org/TR/WD-DOM/introduction.html&quot;&gt;DOM&lt;/a&gt;은 웹 애플리케이션을 구성하는 요소를 조작할 수 있는 API입니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;table&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;th&amp;gt;Framework&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;Github stars&amp;lt;/th&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;th&amp;gt;Vue&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;118917&amp;lt;/th&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;th&amp;gt;React&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;115392&amp;lt;/th&amp;gt;
            &amp;lt;/tr&amp;gt;
        &amp;lt;/table&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;2-1.&amp;nbsp;간단한&amp;nbsp;HTML&amp;nbsp;테이블&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;const SELECTOR = 'tr:nth-child(3) &amp;gt; td'
const cell = document.querySelector(SELECTOR)
cell.style.background = 'red'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;React 셀의 색상 변경&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QmtT6/btsIkdcsU86/KKPorJPDcEmjbQNnZ7C4Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QmtT6/btsIkdcsU86/KKPorJPDcEmjbQNnZ7C4Z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QmtT6/btsIkdcsU86/KKPorJPDcEmjbQNnZ7C4Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQmtT6%2FbtsIkdcsU86%2FKKPorJPDcEmjbQNnZ7C4Z0%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;852&quot; height=&quot;375&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;375&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적 관점에서 보면 모든 HTML페이지(또는 그 일부)는 트리로 구성되고 그림 2-2처럼 DOM으로 표현됩니다. 그리고 HTML 요소로 정의된 트리를 관리하는 방법은 아래와 같은 형식을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 간단하게 표준 CSS 선택자를 사용해 올바른 셀을 선택한 다음 셀 노드의 style 속성을 변경합니다. querySelector 메서드는 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Node&quot;&gt;Node&lt;/a&gt; 메서드입니다. Node는 HTML 트리에서 노드를 나타내는 기본 인터페이스입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;렌더링 성능 모니터링&lt;/b&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;/li&gt;
&lt;li&gt;렌더링 엔진을 처음부터 새로 작성하기로 결정했다면 이해하고 발전시키기 쉽게 설계해야합니다.&lt;/li&gt;
&lt;/ul&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;FPS(Frames Per Second) : 렌더링 성능 모니터링에서 사용할 수 있는 기능 중 하나로 초당 프레임 수를 추적할 수 있습니다. 크롬 개발자 도구 &amp;rarr; cmd/ctrl + Shift + P : FPS 및 GPU 사용량 확인 가능&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-03 오후 12.50.06.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1dkin/btsIlRyW2Uz/oYHMP90Myeqp23AIKmgx91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1dkin/btsIlRyW2Uz/oYHMP90Myeqp23AIKmgx91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1dkin/btsIlRyW2Uz/oYHMP90Myeqp23AIKmgx91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1dkin%2FbtsIlRyW2Uz%2FoYHMP90Myeqp23AIKmgx91%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;1284&quot; height=&quot;552&quot; data-filename=&quot;스크린샷 2024-07-03 오후 12.50.06.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  웹 개발에서 권장되는 FPS(Frames Per Second)는 일반적으로 60 FPS입니다. 일반적으로 대부분의 웹 브라우저에서는 W3C 권장사항에 따라 디스플레이 주사율과 일치됩니다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a href=&quot;https://github.com/mrdoob/stats.js/&quot;&gt;stats.js&lt;/a&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션의 FPS를 모니터링할 수 있는 라이브러리중 하나입니다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프레임 &amp;amp; 할당된 메가바이트의 메모리 렌더링에 필요한 밀리초 표시 가능합니다&lt;/li&gt;
&lt;li&gt;어떤 웹 사이트에도 위젯을 연결할 수 있는 간단한 북마크렛(bookmarklet)기능 제공합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;  Bookmarklet은 북마크를 통해 실행할 수 있는 작은 자바스크립트 프로그램입니다. 북마크를 클릭하면, 해당 자바스크립트 코드가 현재 브라우저 탭에서 실행됩니다.&amp;nbsp;&lt;a href=&quot;https://nuknukhan.tistory.com/54&quot;&gt;북마크렛 만드는 방법&lt;/a&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사용자 정의 성능 위젯&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function fps() {
  let panel: HTMLDivElement | null = null;
  let start: number | null = null;
  let frames: number = 0;

  function create(): HTMLDivElement {
    const div: HTMLDivElement = document.createElement(&quot;div&quot;);

    div.style.position = &quot;fixed&quot;;
    div.style.left = &quot;0px&quot;;
    div.style.top = &quot;0px&quot;;
    div.style.width = &quot;50px&quot;;
    div.style.height = &quot;50px&quot;;
    div.style.backgroundColor = &quot;black&quot;;
    div.style.color = &quot;white&quot;;

    return div;
  }

  function resetFramesAndStart(now: number): void {
    if (panel !== null) {
      panel.innerText = frames.toString();
    }
    frames = 0;
    start = now;
  }

  function tick(): void {
    frames++;
    const now: number = window.performance.now();
    if (start !== null &amp;amp;&amp;amp; now &amp;gt;= start + 1000) {
      resetFramesAndStart(now);
    }
    window.requestAnimationFrame(tick);
  }

  function initializePanel(parent: HTMLElement): void {
    panel = create();
    if (panel !== null) {
      parent.appendChild(panel);
    }
  }

  function init(parent: HTMLElement = document.body): void {
    initializePanel(parent);

    window.requestAnimationFrame(() =&amp;gt; {
      start = window.performance.now();
      tick();
    });
  }

  return init();
}

export default fps;

&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;fps 함수는 panel, start, frames라는 세 가지 변수를 선언합니다. panel은 FPS를 표시하는 HTML 요소를 참조하고, start는 FPS 계산을 시작하는 시간을 저장하며, frames는 계산된 프레임 수를 저장합니다.&lt;/li&gt;
&lt;li&gt;create 함수는 FPS를 표시하는 HTML 요소를 생성하고 반환합니다.&lt;/li&gt;
&lt;li&gt;resetFramesAndStart 함수는 프레임 수를 0으로 재설정하고, 시작 시간을 현재 시간으로 설정합니다. 또한, panel이 null이 아닌 경우, panel의 텍스트를 현재 프레임 수로 설정합니다.&lt;/li&gt;
&lt;li&gt;tick 함수는 프레임 수를 증가시키고, 현재 시간이 시작 시간보다 1초 이상 지났는지 확인합니다. 만약 1초 이상 지났다면, resetFramesAndStart 함수를 호출하여 프레임 수와 시작 시간을 재설정합니다. 그리고 window.requestAnimationFrame을 사용하여 tick 함수를 다시 호출합니다.&lt;/li&gt;
&lt;li&gt;initializePanel 함수는 create 함수를 호출하여 FPS를 표시하는 HTML 요소를 생성하고, 이 요소를 부모 요소에 추가합니다.&lt;/li&gt;
&lt;li&gt;init 함수는 initializePanel 함수를 호출하여 FPS를 표시하는 HTML 요소를 초기화하고, window.requestAnimationFrame을 사용하여 tick 함수를 호출합니다. 이 때, 시작 시간을 현재 시간으로 설정합니다.&lt;/li&gt;
&lt;li&gt;마지막으로, fps 함수는 init 함수를 호출하여 FPS 계산을 시작합니다. 이 함수는 init 함수의 반환값을 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;이번 장은 순수하게 함수를 사용해 요소를 DOM에 렌더링하는 다양한 방법을 분석해봅니다. 순수 함수로 요소를 렌더링한다는 것은 DOM 요소가 애플리케이션의 상태에만 의존한다는 것을 의미합니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;view = f(state)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;순수 함수 렌더링의 수학적 표현&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Todo MVC&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2장의 예제를 위해 TodoMVC(&lt;a href=&quot;https://todomvc.com/&quot;&gt;https://todomvc.com/&lt;/a&gt;) 템플릿을 사용합니다.&lt;/li&gt;
&lt;li&gt;책에서 사용된 코드는 &lt;a href=&quot;https://github.com/Apress/frameworkless-front-end-development/tree/747d119cd515f74d54cea67e92a31f45e0a803ea/Chapter02&quot;&gt;https://github.com/Apress/frameworkless-front-end-development/tree/747d119cd515f74d54cea67e92a31f45e0a803ea/Chapter02&lt;/a&gt; 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/easpxP/btsIla0npY7/hNyZUVK0AHMvXJpyatnGSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/easpxP/btsIla0npY7/hNyZUVK0AHMvXJpyatnGSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/easpxP/btsIla0npY7/hNyZUVK0AHMvXJpyatnGSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeaspxP%2FbtsIla0npY7%2FhNyZUVK0AHMvXJpyatnGSK%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;1230&quot; height=&quot;734&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;734&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;h4 data-ke-size=&quot;size20&quot;&gt;순수 함수 렌더링&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index.html은 TodoMVC 애플리케이션의 골격을 나타냅니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;link rel=&quot;shortcut icon&quot; href=&quot;../favicon.ico&quot; /&amp;gt;
    &amp;lt;link
      rel=&quot;stylesheet&quot;
      href=&quot;https://cdn.jsdelivr.net/npm/todomvc-common@1.0.5/base.css&quot;
    /&amp;gt;
    &amp;lt;link
      rel=&quot;stylesheet&quot;
      href=&quot;https://cdn.jsdelivr.net/npm/todomvc-app-css@2.1.2/index.css&quot;
    /&amp;gt;
    &amp;lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/faker.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;title&amp;gt;Frameworkless Frontend Development: Rendering&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;section class=&quot;todoapp&quot;&amp;gt;
      &amp;lt;header class=&quot;header&quot;&amp;gt;
        &amp;lt;h1&amp;gt;todos&amp;lt;/h1&amp;gt;
        &amp;lt;input
          class=&quot;new-todo&quot;
          placeholder=&quot;What needs to be done?&quot;
          autofocus
        /&amp;gt;
      &amp;lt;/header&amp;gt;
      &amp;lt;section class=&quot;main&quot;&amp;gt;
        &amp;lt;input id=&quot;toggle-all&quot; class=&quot;toggle-all&quot; type=&quot;checkbox&quot; /&amp;gt;
        &amp;lt;label for=&quot;toggle-all&quot;&amp;gt;Mark all as complete&amp;lt;/label&amp;gt;
        &amp;lt;ul class=&quot;todo-list&quot;&amp;gt;&amp;lt;/ul&amp;gt;
      &amp;lt;/section&amp;gt;
      &amp;lt;footer class=&quot;footer&quot;&amp;gt;
        &amp;lt;span class=&quot;todo-count&quot;&amp;gt;1 Item Left&amp;lt;/span&amp;gt;
        &amp;lt;ul class=&quot;filters&quot;&amp;gt;
          &amp;lt;li&amp;gt;
            &amp;lt;a href=&quot;#/&quot;&amp;gt;All&amp;lt;/a&amp;gt;
          &amp;lt;/li&amp;gt;
          &amp;lt;li&amp;gt;
            &amp;lt;a href=&quot;#/active&quot;&amp;gt;Active&amp;lt;/a&amp;gt;
          &amp;lt;/li&amp;gt;
          &amp;lt;li&amp;gt;
            &amp;lt;a href=&quot;#/completed&quot;&amp;gt;Completed&amp;lt;/a&amp;gt;
          &amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
        &amp;lt;button class=&quot;clear-completed&quot;&amp;gt;Clear completed&amp;lt;/button&amp;gt;
      &amp;lt;/footer&amp;gt;
    &amp;lt;/section&amp;gt;
    &amp;lt;footer class=&quot;info&quot;&amp;gt;
      &amp;lt;p&amp;gt;Double-click to edit a todo&amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;
        Created by
        &amp;lt;a href=&quot;http://twitter.com/thestrazz86&quot;&amp;gt;Francesco Strazzullo&amp;lt;/a&amp;gt;
      &amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;Thanks to &amp;lt;a href=&quot;http://todomvc.com&quot;&amp;gt;TodoMVC&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/footer&amp;gt;
    &amp;lt;script type=&quot;module&quot; src=&quot;index.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;section class=&quot;todoapp&quot;&gt;&lt;header class=&quot;header&quot;&gt;&lt;/header&gt;&lt;/section&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;index.html&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이 어플리케이션을 동적으로 만들려면 to-do 리스트 데이터를 가져와 다음을 업데이트 해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필터링된 todo 리스트를 가진 ul&lt;/li&gt;
&lt;li&gt;완료되지 않은 todo 수를 가진 span&lt;/li&gt;
&lt;li&gt;선택된 filter를 강조 표시하기 위해 filters하위의 li &amp;gt; a class에 selected 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;const getTodoElement = (todo) =&amp;gt; {
  const { text, completed } = todo;

  return `
  &amp;lt;li ${completed ? 'class=&quot;completed&quot;' : &quot;&quot;}&amp;gt;
    &amp;lt;div class=&quot;view&quot;&amp;gt;
      &amp;lt;input 
        ${completed ? &quot;checked&quot; : &quot;&quot;}
        class=&quot;toggle&quot; 
        type=&quot;checkbox&quot;&amp;gt;
      &amp;lt;label&amp;gt;${text}&amp;lt;/label&amp;gt;
      &amp;lt;button class=&quot;destroy&quot;&amp;gt;&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;input class=&quot;edit&quot; value=&quot;${text}&quot;&amp;gt;
  &amp;lt;/li&amp;gt;`;
};

const getTodoCount = (todos) =&amp;gt; {
  const notCompleted = todos.filter((todo) =&amp;gt; !todo.completed);

  const { length } = notCompleted;
  if (length === 1) {
    return &quot;1 Item left&quot;;
  }

  return `${length} Items left`;
};

export default (targetElement, state) =&amp;gt; {
  const { currentFilter, todos } = state;

  const element = targetElement.cloneNode(true); //노드 복사

  const list = element.querySelector(&quot;.todo-list&quot;);
  const counter = element.querySelector(&quot;.todo-count&quot;);
  const filters = element.querySelector(&quot;.filters&quot;);

  list.innerHTML = todos.map(getTodoElement).join(&quot;&quot;);
  counter.textContent = getTodoCount(todos);

  Array.from(filters.querySelectorAll(&quot;li a&quot;)).forEach((a) =&amp;gt; {
    if (a.textContent === currentFilter) {
      a.classList.add(&quot;selected&quot;);
    } else {
      a.classList.remove(&quot;selected&quot;);
    }
  });

  return element;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;view.js&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;getTodoElement(todo): 이 함수는 하나의 todo 항목을 받아서 HTML 요소를 문자열로 반환합니다. 이 문자열은 각 todo 항목의 상태와 텍스트를 반영합니다.&lt;/li&gt;
&lt;li&gt;getTodoCount(todos): 이 함수는 완료되지 않은 todo 항목의 수를 반환합니다. 이 수는 &quot;1 Item left&quot; 또는 &quot;{length} Items left&quot; 형식의 문자열로 반환됩니다.&lt;/li&gt;
&lt;li&gt;export default (targetElement, state): 이 함수는 대상 HTML 요소와 상태를 인자로 받아서, 상태에 따라 대상 요소를 업데이트하고 새로운 요소를 반환합니다. 이 함수는 todo 항목의 목록, 완료되지 않은 항목의 수, 그리고 현재 필터 상태를 업데이트합니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기본으로 사용되는&amp;nbsp;targetElement를 받습니다.&lt;/li&gt;
&lt;li&gt;원래 노드를 복제하고&amp;nbsp;state&amp;nbsp;매개 변수 사용을 통해 업데이트 합니다.&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;본 코드에서의 DOM 수정은 가상(virtual)이며,&amp;nbsp;분리된(detached) **요소&amp;nbsp;로 작업하고 있습니다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분리된 요소 생성을 위해&amp;nbsp;cloneNode메소드로 기존 노드 복제 합니다.&lt;/li&gt;
&lt;li&gt;새로 생성된 DOM element는 targetElement 와 동일한 복제본이지만 targetElement 는 전혀 관련이 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아직 DOM의 실제 수정 사항이 커밋되지 않은 상태입니다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분리된 DOM요소의 수정이 이루어질 경우 성능 향상&lt;/li&gt;
&lt;li&gt;view.js 를 실제 DOM에 연결 하기 위해 컨트롤러인 index.js를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;import getTodos from &quot;./getTodos.js&quot;;
import view from &quot;./view.js&quot;;

const state = {
  todos: getTodos(),
  currentFilter: &quot;All&quot;,
};

const main = document.querySelector(&quot;.todoapp&quot;);

window.requestAnimationFrame(() =&amp;gt; {
  const newMain = view(main, state);
  main.replaceWith(newMain);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;control 역할을 하는 index.js&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;control 역할을 하는 index.js처럼 간단한 &amp;lsquo;렌더링 엔진은&amp;rsquo; &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame&quot;&gt;requestAnimationFrame&lt;/a&gt;을 기반으로 합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;requestAnimationFrame&lt;/b&gt; 콜백 내에서 DOM작업을 수행하면 더 휴율적이게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;requestAnimationFrame&lt;/b&gt;는 메인 스레드를 차단하지 않으며 실행될 다시 그리기(repaint) 가 이벤트 루프에서 스케줄링되기 직전에 실행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceWith&quot;&gt;replaceWith&lt;/a&gt;: Element.replaceWith()메서드는 부모의 하위 목록에서 이 Element 노드 또는 문자열 객체 집합으로 대체합니다. 문자열 객체는 동등한 텍스트 노드로 삽입됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2530&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pyFEf/btsIlcXWxxJ/QGFq9eu94doqi9szw6Ub2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pyFEf/btsIlcXWxxJ/QGFq9eu94doqi9szw6Ub2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pyFEf/btsIlcXWxxJ/QGFq9eu94doqi9szw6Ub2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpyFEf%2FbtsIlcXWxxJ%2FQGFq9eu94doqi9szw6Ub2k%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;796&quot; height=&quot;150&quot; data-origin-width=&quot;2530&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;const { faker } = window;

const createElement = () =&amp;gt; ({
  text: faker.random.words(2),
  completed: faker.random.boolean(),
});

const repeat = (elementFactory, number) =&amp;gt; {
  const array = [];
  for (let index = 0; index &amp;lt; number; index++) {
    array.push(elementFactory());
  }
  return array;
};

export default () =&amp;gt; {
  const howMany = faker.random.number(10);
  return repeat(createElement, howMany);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;model역할을 하는 getTodos.js&lt;/span&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://fakerjs.dev/&quot; data-token-index=&quot;1&quot;&gt;Faker.js로&lt;/a&gt; 생성된 랜덤 배열입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코드 리뷰&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서의 렌더링 방식은&amp;nbsp;requestAnimationFrame과 가상 노드 조작을 사용해 충분한 성능을 보여줍니다. 하지만 뷰 함수는 읽기 쉽지 않습니다. 코드는 두 가지 중요한 문제를 갖고 있습니다.&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;&amp;nbsp;여러 DOM 요소를 조작하는 함수가 단 하나 뿐입니다. 이는 상황을 아주 쉽게 복잡하게 만들 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동일한 작업을 수행하는 여러 방법:&lt;/b&gt;&amp;nbsp; DOM조작을 위해 필요한 요소를 각각의 query selector로 접근 하지만 DOM을 조작하는 방법은 모두 다름
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;list: 문자열로 리스트 항목 생성 후 innerHTML 에 값을 넣어줍니다.&lt;/li&gt;
&lt;li&gt;count&amp;nbsp;: 완료되지 않은 todo의 개수를 textContent 에 넣어줍니다.&lt;/li&gt;
&lt;li&gt;filter:&amp;nbsp; &amp;lsquo;selected&amp;rsquo; class를 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/classList&quot;&gt;classList&lt;/a&gt;&amp;nbsp;로 관리 하고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;  위 문제를 해결 하기 위해 view.js 를 조금더 작은 함수로 분리 했을 때의 예제 코드입니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;view폴더를 만들고 기능에 맞게 counter.js, filter.js, todos.js 파일로 코드를 분리하고 app.js에서 만들어직 각각의 새 노드들을 replaceWith 메서드로 대체 해줍니다. 위 예제 코드는 구성 요소 라이브러의 첫 번째 초안이 됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719979200842&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;frameworkless-front-end-development/Chapter02/02 at 747d119cd515f74d54cea67e92a31f45e0a803ea &amp;middot; Apress/frameworkless-front-end-d&quot; data-og-description=&quot;Source code for 'Frameworkless Front-End Development' by Francesco Strazzullo - Apress/frameworkless-front-end-development&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Apress/frameworkless-front-end-development/tree/747d119cd515f74d54cea67e92a31f45e0a803ea/Chapter02/02&quot; data-og-url=&quot;https://github.com/Apress/frameworkless-front-end-development/tree/747d119cd515f74d54cea67e92a31f45e0a803ea/Chapter02/02&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2c2xy/hyWvV73Mag/vwTBpI7mK7MQb3pHURxEIk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Apress/frameworkless-front-end-development/tree/747d119cd515f74d54cea67e92a31f45e0a803ea/Chapter02/02&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Apress/frameworkless-front-end-development/tree/747d119cd515f74d54cea67e92a31f45e0a803ea/Chapter02/02&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2c2xy/hyWvV73Mag/vwTBpI7mK7MQb3pHURxEIk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;frameworkless-front-end-development/Chapter02/02 at 747d119cd515f74d54cea67e92a31f45e0a803ea &amp;middot; Apress/frameworkless-front-end-d&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Source code for 'Frameworkless Front-End Development' by Francesco Strazzullo - Apress/frameworkless-front-end-development&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Apress/frameworkless-front-end-development/tree/master/Chapter02/03는&quot;&gt;https://github.com/Apress/frameworkless-front-end-development/tree/master/Chapter02/03는&lt;/a&gt; 구성 요소 레지스트리를 갖는 렌더링 엔진의 예입니다. Todo 앱에서는 todos, counters, filters의 세 가지 구성 요소를 가집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719979267199&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;

&amp;lt;head&amp;gt;
    &amp;lt;link rel=&quot;shortcut icon&quot; href=&quot;../favicon.ico&quot; /&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/todomvc-common@1.0.5/base.css&quot;&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/todomvc-app-css@2.1.2/index.css&quot;&amp;gt;
    &amp;lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/faker.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;title&amp;gt;
        Frameworkless Frontend Development: Rendering
    &amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
    &amp;lt;section class=&quot;todoapp&quot;&amp;gt;
        &amp;lt;header class=&quot;header&quot;&amp;gt;
            &amp;lt;h1&amp;gt;todos&amp;lt;/h1&amp;gt;
            &amp;lt;input 
                class=&quot;new-todo&quot; 
                placeholder=&quot;What needs to be done?&quot; 
                autofocus&amp;gt;
        &amp;lt;/header&amp;gt;
        &amp;lt;section class=&quot;main&quot;&amp;gt;
            &amp;lt;input 
                id=&quot;toggle-all&quot; 
                class=&quot;toggle-all&quot; 
                type=&quot;checkbox&quot;&amp;gt;
            &amp;lt;label for=&quot;toggle-all&quot;&amp;gt;
                Mark all as complete
            &amp;lt;/label&amp;gt;
            &amp;lt;ul class=&quot;todo-list&quot; data-component=&quot;todos&quot;&amp;gt;
            &amp;lt;/ul&amp;gt;
        &amp;lt;/section&amp;gt;
        &amp;lt;footer class=&quot;footer&quot;&amp;gt;
            &amp;lt;span 
                class=&quot;todo-count&quot; 
                data-component=&quot;counter&quot;&amp;gt;
                    1 Item Left
            &amp;lt;/span&amp;gt;
            &amp;lt;ul class=&quot;filters&quot; data-component=&quot;filters&quot;&amp;gt;
                &amp;lt;li&amp;gt;
                    &amp;lt;a href=&quot;#/&quot;&amp;gt;All&amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;
                    &amp;lt;a href=&quot;#/active&quot;&amp;gt;Active&amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;
                    &amp;lt;a href=&quot;#/completed&quot;&amp;gt;Completed&amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;
            &amp;lt;button class=&quot;clear-completed&quot;&amp;gt;
                Clear completed
            &amp;lt;/button&amp;gt;
        &amp;lt;/footer&amp;gt;
    &amp;lt;/section&amp;gt;
    &amp;lt;footer class=&quot;info&quot;&amp;gt;
        &amp;lt;p&amp;gt;Double-click to edit a todo&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;Created by &amp;lt;a href=&quot;http://twitter.com/thestrazz86&quot;&amp;gt;Francesco Strazzullo&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;Thanks to &amp;lt;a href=&quot;http://todomvc.com&quot;&amp;gt;TodoMVC&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/footer&amp;gt;
    &amp;lt;script type=&quot;module&quot; src=&quot;index.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;

&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;2-11. 구성 요소를 사용하고자 데이터 속성을 사용하는 앱&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2-11. 구성 요소를 사용하고자 데이터 속성을 사용하는 앱에서는 todos, counters, filters의 세 가지 구성 요소를 &lt;b&gt;data-component&lt;/b&gt; 속성에 넣었습니다.&lt;/li&gt;
&lt;li&gt;이 속성은 뷰 함수에서의 호출에서 사용됩니다.&lt;/li&gt;
&lt;li&gt;컴포넌트 라이브러리를 생성하기 위한 또 다른 필수 조건은 registry로 입니다. 레지스트리는 애플리케이션에서 사용할 수 있는 모든 구성 요소의 인덱스 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;const registry = {
  todos: todosView,
  counter: counterView,
  filters: filtersView,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;간단한 구성 요소 레지스트리&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레지스트리의 키는 data-component 속성 값과 일치 합니다.(이것이 구성 요소 기반 렌더링 엔진의 핵심 메커니즘입니다.)&lt;/li&gt;
&lt;li&gt;이 메커니즘은 루트 컨테이너 뿐만 아니라 생성할 모든 컴포넌트에 적용돼야 합니다. 이렇게 하면 모든 컴포넌트가 다른 컴포넌트 안에서 불러와 사용될 수 있습니다. 이런 재사용성은 컴포넌트 기반 애플리케이션에서 필수적입니다.&lt;/li&gt;
&lt;li&gt;모든 컴포넌트가 data-component 속성의 값을 읽고 올바른 함수를 자동으로 호출하는 기본 구성 요소에 상속돼야합니다. 하지만 순수 함수로 작성하고 있기 때문에 실제로는 이 기본 객체에서 상속 받을 수 없어서 구성 요소를 래핑하는 고차 함수를 생성해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;const renderWrapper = (component) =&amp;gt; {
  return (targetElement, state) =&amp;gt; {
    const element = component(targetElement, state);

    const childComponents = element.querySelectorAll(&quot;[data-component]&quot;);

    Array.from(childComponents).forEach((target) =&amp;gt; {
      const name = target.dataset.component;

      const child = registry[name];
      if (!child) {
        return;
      }

      target.replaceWith(child(target, state));
    });

    return element;
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;고차 함수 렌더링&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;renderWrapper(component): 이 함수는 컴포넌트를 인자로 받아서, 새로운 함수를 반환합니다. 반환된 함수는&amp;nbsp;targetElement와&amp;nbsp;state를 인자로 받아서 컴포넌트를 렌더링합니다.&lt;/li&gt;
&lt;li&gt;반환된 함수 내부에서는 먼저&amp;nbsp;component(targetElement, state)를 호출하여 컴포넌트를 렌더링하고, 결과로 나온 DOM 요소를&amp;nbsp;element에 저장합니다.&lt;/li&gt;
&lt;li&gt;element.querySelectorAll('[data-component]'): 이 코드는&amp;nbsp;element&amp;nbsp;내부에서&amp;nbsp;data-component&amp;nbsp;속성을 가진 모든 자식 컴포넌트를 찾습니다. 이 속성은 자식 컴포넌트의 이름을 나타냅니다.&lt;/li&gt;
&lt;li&gt;Array.from(childComponents).forEach(target =&amp;gt; {...}): 찾아낸 자식 컴포넌트들을 배열로 변환한 후, 각각에 대해 반복 처리를 합니다. 반복 처리 내부에서는&amp;nbsp;registry에서 해당 컴포넌트의 렌더링 함수를 찾아 호출하고, 결과로 나온 DOM 요소로 기존의 자식 컴포넌트를 대체합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const add = (name, component) =&amp;gt; {
  registry[name] = renderWrapper(component);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;2-14 레지스트리 접근자 메서드&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레지스트리에 구성 요소를 추가하려면 2-14와 같이 이전 함수로 구성 요소를 래핑하는 간단한 함수가 필요합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;const renderRoot = (root, state) =&amp;gt; {
  const cloneComponent = (root) =&amp;gt; {
    return root.cloneNode(true);
  };

  return renderWrapper(cloneComponent)(root, state);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;2-15 구성 요소 기반 애플리케이션의 부팅 함수&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 DOM 요소에서 렌더링을 시작하려면 애플리케이션의 루트를 렌더링하는 메서드를 제공해야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import getTodos from &quot;./getTodos.js&quot;;
import todosView from &quot;./view/todos.js&quot;;
import counterView from &quot;./view/counter.js&quot;;
import filtersView from &quot;./view/filters.js&quot;;

import registry from &quot;./registry.js&quot;;

registry.add(&quot;todos&quot;, todosView);
registry.add(&quot;counter&quot;, counterView);
registry.add(&quot;filters&quot;, filtersView);

const state = {
  todos: getTodos(),
  currentFilter: &quot;All&quot;,
};

window.requestAnimationFrame(() =&amp;gt; {
  const main = document.querySelector(&quot;.todoapp&quot;);
  const newMain = registry.renderRoot(main, state);
  main.replaceWith(newMain);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;구성 요소 레지스트리를 사용하는 컨트롤러&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1421&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CF4Zz/btsIkGSMM97/cdQQRC1ZVkKTGOyXN3G96K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CF4Zz/btsIkGSMM97/cdQQRC1ZVkKTGOyXN3G96K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CF4Zz/btsIkGSMM97/cdQQRC1ZVkKTGOyXN3G96K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCF4Zz%2FbtsIkGSMM97%2FcdQQRC1ZVkKTGOyXN3G96K%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;1421&quot; height=&quot;352&quot; data-origin-width=&quot;1421&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;h4 data-ke-size=&quot;size20&quot;&gt;가상 DOM&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리액트에 의해 유명해진 가상 DOM 개념은 선언적 렌더링 엔진의 성능을 개선시키는 방법입니다.&lt;/li&gt;
&lt;li&gt;UI 표현은 메모리에 유지되고 &amp;lsquo;실제&amp;rsquo; DOM과 동기화 됩니다.&lt;/li&gt;
&lt;li&gt;실제 DOM은 가능한 적은 작업을 수행되어야 합니다. 우리는 이 과정을 조정(reconciliation)이라고 부릅니다.&lt;/li&gt;
&lt;li&gt;가상 DOM의 핵심은 diff 알고리즘입니다. 이 알고리즘은 실제 DOM을 문서에서 분리된 새로운 DOM element의 사본으로 바꾸는 가장 빠른 방법을 찾아냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;간단한 가상 DOM 구현&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const isNodeChanged = (node1, node2) =&amp;gt; {
  const n1Attributes = node1.attributes;
  const n2Attributes = node2.attributes;
  if (n1Attributes.length !== n2Attributes.length) {
    return true;
  }

  const differentAttribute = Array.from(n1Attributes).find((attribute) =&amp;gt; {
    const { name } = attribute;
    const attribute1 = node1.getAttribute(name);
    const attribute2 = node2.getAttribute(name);

    return attribute1 !== attribute2;
  });

  if (differentAttribute) {
    return true;
  }

  if (
    node1.children.length === 0 &amp;amp;&amp;amp;
    node2.children.length === 0 &amp;amp;&amp;amp;
    node1.textContent !== node2.textContent
  ) {
    return true;
  }

  return false;
};

const applyDiff = (parentNode, realNode, virtualNode) =&amp;gt; {
  if (realNode &amp;amp;&amp;amp; !virtualNode) {
    realNode.remove();
    return;
  }

  if (!realNode &amp;amp;&amp;amp; virtualNode) {
    parentNode.appendChild(virtualNode);
    return;
  }

  if (isNodeChanged(virtualNode, realNode)) {
    realNode.replaceWith(virtualNode);
    return;
  }

  const realChildren = Array.from(realNode.children);
  const virtualChildren = Array.from(virtualNode.children);

  const max = Math.max(realChildren.length, virtualChildren.length);
  for (let i = 0; i &amp;lt; max; i++) {
    applyDiff(realNode, realChildren[i], virtualChildren[i]);
  }
};

export default applyDiff;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 크게 두 부분으로 나뉩니다:&amp;nbsp;isNodeChanged&amp;nbsp;함수와&amp;nbsp;applyDiff&amp;nbsp;함수입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;isNodeChanged&amp;nbsp;함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 두 노드(HTML 요소)를 비교하여 변경이 필요한지 여부를 판단합니다. 다음과 같은 경우에 변경이 필요하다고 판단합니다:&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;/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;h4 data-ke-size=&quot;size20&quot;&gt;applyDiff&amp;nbsp;함수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 실제 DOM 노드(realNode)와 가상 DOM 노드(virtualNode)를 비교하여, 필요한 변경사항을 실제 DOM에 적용합니다. 다음과 같은 경우에 대해 처리합니다:&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;: 실제 노드를 DOM에서 제거합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제 노드는 없지만 가상 노드가 있는 경우&lt;/b&gt;: 가상 노드를 실제 DOM에 추가합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;두 노드가 다른 경우&lt;/b&gt;:&amp;nbsp;isNodeChanged&amp;nbsp;함수를 사용하여 두 노드가 다르다고 판단되면, 실제 노드를 가상 노드로 교체합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자식 노드 비교&lt;/b&gt;: 실제 노드와 가상 노드의 자식 노드들을 재귀적으로 비교하여, 각 자식 노드에 대해서도&amp;nbsp;applyDiff&amp;nbsp;함수를 호출합니다. 이 과정에서 자식 노드들 사이의 차이점도 적용됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 DOM의 변경사항을 최소화하여 성능을 개선하는 데 도움을 줍니다. 가상 DOM과 실제 DOM 사이의 차이점만을 적용함으로써, 불필요한 DOM 조작을 줄이고 애플리케이션의 반응 속도를 향상시킵니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/h3&gt;
&lt;figure id=&quot;og_1719979543963&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[리액트] Virtual DOM과 diffing&quot; data-og-description=&quot;주제&quot; data-og-host=&quot;joong-sunny.github.io&quot; data-og-source-url=&quot;https://joong-sunny.github.io/react/react2/&quot; data-og-url=&quot;https://joong-sunny.github.io/react/react2/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/im0ZD/hyWrKG8cxD/K7LLB74owIAUEbT0FkMRa0/img.jpg?width=1266&amp;amp;height=1236&amp;amp;face=0_0_1266_1236,https://scrap.kakaocdn.net/dn/i7mpx/hyWvVUxKF3/f5Ks5KN9kg2WKIsWwael30/img.png?width=1406&amp;amp;height=662&amp;amp;face=0_0_1406_662,https://scrap.kakaocdn.net/dn/Ly07G/hyWrNjzOS8/qvo6CAe9ihGSdfsjHo7ZYk/img.png?width=1280&amp;amp;height=595&amp;amp;face=0_0_1280_595&quot;&gt;&lt;a href=&quot;https://joong-sunny.github.io/react/react2/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://joong-sunny.github.io/react/react2/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/im0ZD/hyWrKG8cxD/K7LLB74owIAUEbT0FkMRa0/img.jpg?width=1266&amp;amp;height=1236&amp;amp;face=0_0_1266_1236,https://scrap.kakaocdn.net/dn/i7mpx/hyWvVUxKF3/f5Ks5KN9kg2WKIsWwael30/img.png?width=1406&amp;amp;height=662&amp;amp;face=0_0_1406_662,https://scrap.kakaocdn.net/dn/Ly07G/hyWrNjzOS8/qvo6CAe9ihGSdfsjHo7ZYk/img.png?width=1280&amp;amp;height=595&amp;amp;face=0_0_1280_595');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[리액트] Virtual DOM과 diffing&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;주제&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;joong-sunny.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719979543612&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Element: classList property - Web APIs | MDN&quot; data-og-description=&quot;The Element.classList is a read-only property that returns a live DOMTokenList collection of the class attributes of the element. This can then be used to manipulate the class list.&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/classList&quot; data-og-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/classList&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bO2tMH/hyWrSkQs7u/STkcNSIuf9iRsABiM39vA0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/classList&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/classList&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bO2tMH/hyWrSkQs7u/STkcNSIuf9iRsABiM39vA0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Element: classList property - Web APIs | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Element.classList is a read-only property that returns a live DOMTokenList collection of the class attributes of the element. This can then be used to manipulate the class list.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719979545298&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Element: replaceWith() method - Web APIs | MDN&quot; data-og-description=&quot;The Element.replaceWith() method replaces this Element in the children list of its parent with a set of Node or string objects. String objects are inserted as equivalent Text nodes.&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceWith&quot; data-og-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceWith&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bxKBNZ/hyWvQZZStb/YZYQMeTNjTMLhuh83ERTd1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceWith&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/replaceWith&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bxKBNZ/hyWvQZZStb/YZYQMeTNjTMLhuh83ERTd1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Element: replaceWith() method - Web APIs | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Element.replaceWith() method replaces this Element in the children list of its parent with a set of Node or string objects. String objects are inserted as equivalent Text nodes.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719979542421&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;  웹 애니메이션 최적화 requestAnimationFrame 가이드&quot; data-og-description=&quot;자바스크립트 웹 애니메이션 웹페이지의 애니메이션을 구현할때 CSS의 animatoin , transition , transform 속성을 통해 구현할 수도 있지만, 보다 사용자와의 복잡한 상호작용을 구현하게 하기 위해 Javasc&quot; data-og-host=&quot;inpa.tistory.com&quot; data-og-source-url=&quot;https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C#%EB%94%94%EC%8A%A4%ED%94%8C%EB%A0%88%EC%9D%B4_%EC%A3%BC%EC%82%AC%EC%9C%A8%EC%97%90_%EB%A7%9E%EA%B2%8C_%ED%98%B8%EC%B6%9C_%ED%9A%9F%EC%88%98&quot; data-og-url=&quot;https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ntgYp/hyWrZK5Gg1/2VTeUk2nKk6k8oMCG92wt1/img.jpg?width=800&amp;amp;height=397&amp;amp;face=0_0_800_397,https://scrap.kakaocdn.net/dn/RGAYU/hyWrR0AaWU/AKKhbsWAteA17kIjXRHoAk/img.jpg?width=800&amp;amp;height=397&amp;amp;face=0_0_800_397,https://scrap.kakaocdn.net/dn/mGlTe/hyWrMEYRDI/kDReuKeChN7Ngwliwq933k/img.png?width=1073&amp;amp;height=888&amp;amp;face=0_0_1073_888&quot;&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C#%EB%94%94%EC%8A%A4%ED%94%8C%EB%A0%88%EC%9D%B4_%EC%A3%BC%EC%82%AC%EC%9C%A8%EC%97%90_%EB%A7%9E%EA%B2%8C_%ED%98%B8%EC%B6%9C_%ED%9A%9F%EC%88%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C#%EB%94%94%EC%8A%A4%ED%94%8C%EB%A0%88%EC%9D%B4_%EC%A3%BC%EC%82%AC%EC%9C%A8%EC%97%90_%EB%A7%9E%EA%B2%8C_%ED%98%B8%EC%B6%9C_%ED%9A%9F%EC%88%98&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ntgYp/hyWrZK5Gg1/2VTeUk2nKk6k8oMCG92wt1/img.jpg?width=800&amp;amp;height=397&amp;amp;face=0_0_800_397,https://scrap.kakaocdn.net/dn/RGAYU/hyWrR0AaWU/AKKhbsWAteA17kIjXRHoAk/img.jpg?width=800&amp;amp;height=397&amp;amp;face=0_0_800_397,https://scrap.kakaocdn.net/dn/mGlTe/hyWrMEYRDI/kDReuKeChN7Ngwliwq933k/img.png?width=1073&amp;amp;height=888&amp;amp;face=0_0_1073_888');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;  웹 애니메이션 최적화 requestAnimationFrame 가이드&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트 웹 애니메이션 웹페이지의 애니메이션을 구현할때 CSS의 animatoin , transition , transform 속성을 통해 구현할 수도 있지만, 보다 사용자와의 복잡한 상호작용을 구현하게 하기 위해 Javasc&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;inpa.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719979542365&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Window: requestAnimationFrame() method - Web API | MDN&quot; data-og-description=&quot;화면에 애니메이션을 업데이트할 준비가 될 때마다 이 메서드를 호출해야 합니다. 이는 브라우저가 다음 리페인트를 수행하기 전에 애니메이션 함수를 호출하도록 요청합니다. 콜백의 수는 보&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bnidqU/hyWvL5taop/Za8YYkbTBxCwtRfrpHQKuk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bnidqU/hyWvL5taop/Za8YYkbTBxCwtRfrpHQKuk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Window: requestAnimationFrame() method - Web API | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;화면에 애니메이션을 업데이트할 준비가 될 때마다 이 메서드를 호출해야 합니다. 이는 브라우저가 다음 리페인트를 수행하기 전에 애니메이션 함수를 호출하도록 요청합니다. 콜백의 수는 보&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719979540060&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Performance: now() method - Web APIs | MDN&quot; data-og-description=&quot;The performance.now() method returns a high resolution timestamp in milliseconds. It represents the time elapsed since Performance.timeOrigin (the time when navigation has started in window contexts, or the time when the worker is run in Worker and Service&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Performance/now&quot; data-og-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Performance/now&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gV6JY/hyWvQZZSrs/RzGCwGtKyrghJiKkDkdzU0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Performance/now&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Performance/now&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gV6JY/hyWvQZZSrs/RzGCwGtKyrghJiKkDkdzU0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Performance: now() method - Web APIs | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The performance.now() method returns a high resolution timestamp in milliseconds. It represents the time elapsed since Performance.timeOrigin (the time when navigation has started in window contexts, or the time when the worker is run in Worker and Service&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719979536434&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;북마클릿 만들고 북마크(즐겨찾기)에 올리기(예제 : 브라우저 사이즈 표시)&quot; data-og-description=&quot;북마클릿 만들고 북마크(즐겨찾기)에 올리기(예제 : 브라우저 사이즈 표시) 자바스크립트를 어느 정도 이해하고 간단한 스크립트를 작성할 수있다면 브라우저에서 원하는 기능을 구현할 수있는&quot; data-og-host=&quot;nuknukhan.tistory.com&quot; data-og-source-url=&quot;https://nuknukhan.tistory.com/54&quot; data-og-url=&quot;https://nuknukhan.tistory.com/54&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ZvN3n/hyWrSrBuBd/Z0DU2Kk3LYk4moMeGocW1k/img.jpg?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360,https://scrap.kakaocdn.net/dn/oZhIR/hyWvP05Ntl/lpcv9bQqJy3wgGivkmXFLK/img.jpg?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360,https://scrap.kakaocdn.net/dn/kSCsi/hyWvWTrsEA/cfgyIxymLdO0nGZjY3XKbK/img.jpg?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360&quot;&gt;&lt;a href=&quot;https://nuknukhan.tistory.com/54&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nuknukhan.tistory.com/54&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ZvN3n/hyWrSrBuBd/Z0DU2Kk3LYk4moMeGocW1k/img.jpg?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360,https://scrap.kakaocdn.net/dn/oZhIR/hyWvP05Ntl/lpcv9bQqJy3wgGivkmXFLK/img.jpg?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360,https://scrap.kakaocdn.net/dn/kSCsi/hyWvWTrsEA/cfgyIxymLdO0nGZjY3XKbK/img.jpg?width=360&amp;amp;height=360&amp;amp;face=0_0_360_360');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;북마클릿 만들고 북마크(즐겨찾기)에 올리기(예제 : 브라우저 사이즈 표시)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;북마클릿 만들고 북마크(즐겨찾기)에 올리기(예제 : 브라우저 사이즈 표시) 자바스크립트를 어느 정도 이해하고 간단한 스크립트를 작성할 수있다면 브라우저에서 원하는 기능을 구현할 수있는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nuknukhan.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719979539973&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - mrdoob/stats.js: JavaScript Performance Monitor&quot; data-og-description=&quot;JavaScript Performance Monitor. Contribute to mrdoob/stats.js development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/mrdoob/stats.js/&quot; data-og-url=&quot;https://github.com/mrdoob/stats.js&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eb0AEg/hyWrUQwbNY/6Wl2nt0GvbFglQ5ixGwz21/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_125_1045_213&quot;&gt;&lt;a href=&quot;https://github.com/mrdoob/stats.js/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/mrdoob/stats.js/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eb0AEg/hyWrUQwbNY/6Wl2nt0GvbFglQ5ixGwz21/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_125_1045_213');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - mrdoob/stats.js: JavaScript Performance Monitor&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JavaScript Performance Monitor. Contribute to mrdoob/stats.js development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719979536061&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Node - Web APIs | MDN&quot; data-og-description=&quot;The DOM Node interface is an abstract base class upon which many other DOM API objects are based, thus letting those object types to be used similarly and often interchangeably. As an abstract class, there is no such thing as a plain Node object. All objec&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Node&quot; data-og-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Node&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bQ7rwd/hyWrXfnira/auf8agH3ka2jQWLM5y2a21/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Node&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Node&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bQ7rwd/hyWrXfnira/auf8agH3ka2jQWLM5y2a21/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Node - Web APIs | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The DOM Node interface is an abstract base class upon which many other DOM API objects are based, thus letting those object types to be used similarly and often interchangeably. As an abstract class, there is no such thing as a plain Node object. All objec&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1719979531028&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;What is the Document Object Model?&quot; data-og-description=&quot;What is the Document Object Model? Editors Jonathan Robie, Texcel Research Introduction The Document Object Model (DOM) is a programming API for HTML and XML documents. It defines the logical structure of documents and the way a document is accessed and ma&quot; data-og-host=&quot;www.w3.org&quot; data-og-source-url=&quot;https://www.w3.org/TR/WD-DOM/introduction.html&quot; data-og-url=&quot;https://www.w3.org/TR/WD-DOM/introduction.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.w3.org/TR/WD-DOM/introduction.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.w3.org/TR/WD-DOM/introduction.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;What is the Document Object Model?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;What is the Document Object Model? Editors Jonathan Robie, Texcel Research Introduction The Document Object Model (DOM) is a programming API for HTML and XML documents. It defines the logical structure of documents and the way a document is accessed and ma&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.w3.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>독서/2024</category>
      <author>_OIL</author>
      <guid isPermaLink="true">https://rocketengine.tistory.com/194</guid>
      <comments>https://rocketengine.tistory.com/entry/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%97%86%EB%8A%94-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C-2%EC%9E%A5-%EB%A0%8C%EB%8D%94%EB%A7%81#entry194comment</comments>
      <pubDate>Wed, 3 Jul 2024 13:06:41 +0900</pubDate>
    </item>
    <item>
      <title>[프레임워크 없는 프론트엔드 개발] - 1장. 프레임워크에 대한 이야기</title>
      <link>https://rocketengine.tistory.com/entry/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%97%86%EB%8A%94-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C-1%EC%9E%A5-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1장. 프레임워크에 대한 이야기&lt;/h2&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;blockquote data-ke-style=&quot;style3&quot;&gt;  1장에서는 프레임워크에 대한 저자의 생각과 프레임워크 없이 개발하는 방법을 배우는 것이 왜 중요한지에 대한 저자의 믿음으로 시작한다. 프레임워크 없이 작업하는 방법을 배우고 나면 이 방법이 자신의 프로젝트에 적합한 선택인지 알 수 있게 될 것이다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프레임워크란?&lt;/h3&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크는 특정 문제를 해결하기 위한&amp;nbsp;뼈대&amp;nbsp;또는&amp;nbsp;틀을 제공하는&amp;nbsp;구조를 말합니다. - GPT&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;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;와&amp;nbsp;&lt;b&gt;규칙&lt;/b&gt;을 제공하여 개발자가&amp;nbsp;&lt;b&gt;복잡한 작업&lt;/b&gt;을&amp;nbsp;&lt;b&gt;효율적으로 수행&lt;/b&gt;할 수 있도록 돕는&amp;nbsp;&lt;b&gt;소프트웨어 환경&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용 가능한 코드&lt;/b&gt;와&amp;nbsp;&lt;b&gt;구조&lt;/b&gt;를 제공하여&amp;nbsp;&lt;b&gt;개발 시간&lt;/b&gt;을 단축하고&amp;nbsp;&lt;b&gt;코드 품질&lt;/b&gt;을 향상시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관성&lt;/b&gt;과&amp;nbsp;&lt;b&gt;표준화&lt;/b&gt;를 유지하여&amp;nbsp;&lt;b&gt;팀 개발&lt;/b&gt;을 용이하게 합니다.&lt;/li&gt;
&lt;/ul&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;nbsp;&lt;b&gt;Spring, Django, Ruby on Rails&lt;/b&gt;&amp;nbsp;등은 웹 애플리케이션 개발에 필요한&amp;nbsp;&lt;b&gt;기본적인 구조&lt;/b&gt;와&amp;nbsp;&lt;b&gt;기능&lt;/b&gt;을 제공합니다.&lt;/li&gt;
&lt;li&gt;모바일 개발 프레임워크:&amp;nbsp;&lt;b&gt;React Native, Flutter&lt;/b&gt;&amp;nbsp;등은 모바일 앱 개발에 필요한&amp;nbsp;&lt;b&gt;UI 구성 요소&lt;/b&gt;,&amp;nbsp;&lt;b&gt;네트워킹 기능&lt;/b&gt;,&amp;nbsp;&lt;b&gt;데이터베이스 연동&lt;/b&gt;&amp;nbsp;등을 제공합니다.&lt;/li&gt;
&lt;li&gt;데이터 분석 프레임워크:&amp;nbsp;&lt;b&gt;Pandas, Scikit-learn&lt;/b&gt;&amp;nbsp;등은 데이터 분석에 필요한&amp;nbsp;&lt;b&gt;데이터 처리&lt;/b&gt;,&amp;nbsp;&lt;b&gt;머신러닝 알고리즘&lt;/b&gt;,&amp;nbsp;&lt;b&gt;시각화 도구&lt;/b&gt;&amp;nbsp;등을 제공합니다.&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;미리 만들어진 구성 요소를 사용하여 개발 시간을 절약할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 품질 향상:&lt;/b&gt;&amp;nbsp;일관성과 표준화를 유지하여 코드 품질을 향상시킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수 편의성:&lt;/b&gt;&amp;nbsp;코드 구조가 일관성 있게 구성되어 유지보수가 편리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팀 개발 효율성 증대:&lt;/b&gt;&amp;nbsp;팀원 간의 협업을 용이하게 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;b&gt;결론적으로 프레임워크는 개발자가&amp;nbsp;더 빠르고 효율적으로&amp;nbsp;소프트웨어를 개발할 수 있도록 돕는&amp;nbsp;필수적인 도구입니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프레임워크와 라이브러리의 비교&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프레임워크&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앵귤러: 서비스, 구성 요소와 파이프 같은 기본 요소를 사용해 애플리케이션을 빌드하는 데 필요한 구조를 제공한다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로대쉬(Loadash)와 모멘트 같이(Moment) 애플리케이션 코드를 어떻게 구성해야 하는지에 대해 특별한 형식을 요구하지 않는다.&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;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vlhje/btsIaRFuFWU/vjkGPLp5IyOsXqHk9PpQIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vlhje/btsIaRFuFWU/vjkGPLp5IyOsXqHk9PpQIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vlhje/btsIaRFuFWU/vjkGPLp5IyOsXqHk9PpQIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVlhje%2FbtsIaRFuFWU%2FvjkGPLp5IyOsXqHk9PpQIK%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;974&quot; height=&quot;468&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  그렇다면 React는 라이브러리일까? 아니면 프레임워크일까? 정답은 아래에  &lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프레임워크 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Moment.js같은 라이브러리는 개발자가 어떻게 코드에 통합하는지 강요하지 않는다. 이에 반해 앵귤러는 매우 독선적이다. 다음 절에서는 몇 가지 제약 조건을 알아본다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 언어&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앵귤러는 TypeScript를 사용한다. TypeScript는 일반 자바스크립트 컴파일 되는 자바스크립트의 형식화된 슈퍼 세트다. 유형 검사 외에도 어노테이션 같은 원본 언어에서는 존재하지 않는 여러 기능을 제공한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 의존성 주입&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요소가 앵귤러 애플리케이션에서 통신할 수 있게 하려면 유형에 따라 의존성 주입 메커니즘을 사용해 요소를 주입해야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  의존성 주입(Dependency Injection, DI)은 애플리케이션의 클래스들이 자신의 의존성을 직접 생성하지 않고 외부에서 제공받는 설계 패턴이다. 이 방식은 코드의 재사용성, 테스트 용이성, 유지보수성을 크게 향상시킨다. - GPT&lt;br /&gt;&lt;a href=&quot;https://www.angular.kr/guide/dependency-injection&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;앵귤러 의존성 주입 이해하기&lt;/a&gt;&lt;/blockquote&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root' // root 인젝터에 등록되어 애플리케이션 전역에서 사용 가능
})
export class DataService {
  constructor() { }

  getData() {
    return ['Data1', 'Data2', 'Data3'];
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;서비스 정의 및 데코레이션&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-data',
  templateUrl: './data.component.html',
  styleUrls: ['./data.component.css']
})
export class DataComponent implements OnInit {
  data: string[];

  // DataService 의존성을 주입
  constructor(private dataService: DataService) { }

  ngOnInit() {
    this.data = this.dataService.getData();
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;의존성 주입&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// 특정 모듈에서만 서비스 제공
@NgModule({
  providers: [DataService]
})
export class DataModule { }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;의존성 등록&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 옵저버블&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앵귤러는 옵저버블(observable)을 사용한 반응형 프로그래밍용 라이브러리인 RxJS를 기반으로 설계됐다. 그래서 데이터를 가져오러면 Observable객체의 subscribe메서드를 사용해야된다. 이 접근 방식은 HTTP 요청이 프라미스 처럼 설계되는 다른 프론트엔드 프레임워크와 다르다&lt;/p&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;// 1-5) 옵저버블을 사용하지 않은 앵귤러 서비스

import axios from 'axios';
const URL = '&amp;lt;http://example.api.com/&amp;gt;';

export default {
    list() {
        return axios.get(URL);
    }
}

// 1-6) 옵저버블을 사용하지 않는 앵귤러 구성 요소
import people from 'people.js';

export class PeopleList {
    load() {
        people
            .list()
            .then(people =&amp;gt; {
                this.people = people
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;옵저버블을 사용한 앵규러의 데이터 통신 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DataService {

  private apiUrl = '&amp;lt;https://jsonplaceholder.typicode.com/posts&amp;gt;';

  constructor(private http: HttpClient) { }

  getPosts(): Observable {
    return this.http.get(this.apiUrl);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;서비스 생성&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { Component, OnInit } from '@angular/core';
import { DataService } from '../data.service';

@Component({
  selector: 'app-posts',
  templateUrl: './posts.component.html',
  styleUrls: ['./posts.component.css']
})
export class PostsComponent implements OnInit {

  posts: any[] = [];

  constructor(private dataService: DataService) { }

  ngOnInit(): void {
    this.dataService.getPosts().subscribe(
      data =&amp;gt; this.posts = data,
      error =&amp;gt; console.error('There was an error!', error)
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;컴포넌트에서 서비스 사용&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;div *ngIf=&quot;posts.length &amp;gt; 0&quot;&amp;gt;
  &amp;lt;h2&amp;gt;Posts&amp;lt;/h2&amp;gt;
  &amp;lt;ul&amp;gt;
    &amp;lt;li *ngFor=&quot;let post of posts&quot;&amp;gt;
      {{ post.title }}
    &amp;lt;/li&amp;gt;
  &amp;lt;/ul&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;div *ngIf=&quot;posts.length === 0&quot;&amp;gt;
  &amp;lt;p&amp;gt;No posts available.&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;HTML 템플릿 설정&lt;/span&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;리액트 홈페이지에서 리액트는 &amp;lsquo;사용자 인터페이스 구축을 위한 자바스크립트 라이브러리&amp;rsquo;라고 정의돼 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3018&quot; data-origin-height=&quot;1376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfxxSM/btsH8nft0GY/YpfLRCnb3NkKEiy9Mk8aGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfxxSM/btsH8nft0GY/YpfLRCnb3NkKEiy9Mk8aGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfxxSM/btsH8nft0GY/YpfLRCnb3NkKEiy9Mk8aGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfxxSM%2FbtsH8nft0GY%2FYpfLRCnb3NkKEiy9Mk8aGK%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;3018&quot; height=&quot;1376&quot; data-origin-width=&quot;3018&quot; data-origin-height=&quot;1376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;그러나 현실은 이보다 훨씬 더 복잡하다!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;리액트의 주요 제약 사항&lt;/h4&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;DOM을 직접 조작하는 대신 구성 요소의 상태를 수정한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선언적 패턴으로&lt;/b&gt; PoseExample &lt;b&gt;컴포넌트 구현&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import React, { useState } from 'react';
import posed from 'react-pose';

const Box = posed.div({
  hidden: { opacity: 0 },
  visible: { opacity: 1 },
  transition: {
    ease: 'linear',
    duration: 500
  }
});

const PoseExample = () =&amp;gt; {
  const [isVisible, setIsVisible] = useState(true);

  const toggle = () =&amp;gt; {
    setIsVisible(!isVisible);
  };

  const pose = isVisible ? 'visible' : 'hidden';

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;Box className='box' pose={pose} /&amp;gt;
      &amp;lt;button onClick={toggle}&amp;gt;Toggle&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default PoseExample;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;PoseExample&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;b&gt;명령형패턴으로 PoseExample 컴포넌트 구현&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;const PoseExample = () =&amp;gt; {
  const [isVisible, setIsVisible] = useState(true);
  const boxRef = useRef(null);

  useEffect(() =&amp;gt; {
    const box = boxRef.current;
    if (isVisible) {
      box.style.opacity = 1;
    } else {
      box.style.opacity = 0;
    }
    box.style.transition = 'opacity 0.5s linear';
  }, [isVisible]);

  const toggle = () =&amp;gt; {
    setIsVisible(!isVisible);
  };

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;div ref={boxRef} className='box' /&amp;gt;
      &amp;lt;button onClick={toggle}&amp;gt;Toggle&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default PoseExample;

// Box.css
.box {
  width: 100px;
  height: 100px;
  background-color: red;
  opacity: 1; /* 초기 상태 */
  transition: opacity 0.5s linear; /* 트랜지션 정의 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  리액트 개발자라면 명령형 패턴으로 PoseExample 컴포넌트를 구현 한 예제가 매우 이상하게 보일 것이다. 명령형으로 Box를 움직이기 때문이다! 이 &amp;lsquo;기묘함(strangeness)&amp;rsquo; 이 리액트는 라이브러리가 아닌 프레임워크라고 저자가 믿는 이유다. 저자는 이것이 코드 때문이 아니라 리액트 커뮤니티에서 이를 사용할 때 수용한 제약 사항 때문이라고 믿는다. 다시 말해 선언적 패턴은 리액트 방식의 일부이다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팁: 작업을 처리할 때 &amp;lsquo;프레임워크 방식&amp;rsquo;을 사용하고 있다면 프레임워크라고 볼 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자바스크립트 프레임워크 연혁&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;제이쿼리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2006년 존 레식이 만든 제이쿼리는 모든 자바스크립트 프레임워크의 모체가 됐다. 오늘날 프론트엔드 개발자들이 제이쿼리를 우습게 여기는 경향이 있지만 제이쿼리는 현대 웹 개발의 초석 역할을 했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;lt;aside&amp;gt;   사실 jQuery로 만든 레거시 프로젝트가 너무 많기에 아직까지도 굳건히 순위 안에 들고 있다.&lt;br /&gt;&lt;a href=&quot;https://survey.stackoverflow.co/2023/#most-popular-technologies-webframe-prof&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Stack OverflowStack Overflow Developer Survey 2023&lt;/a&gt;&lt;br /&gt;​&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2070&quot; data-origin-height=&quot;1354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkBrdr/btsH8GyTG2j/TsJzhjksmf9KkQOASAEcik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkBrdr/btsH8GyTG2j/TsJzhjksmf9KkQOASAEcik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkBrdr/btsH8GyTG2j/TsJzhjksmf9KkQOASAEcik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkBrdr%2FbtsH8GyTG2j%2FTsJzhjksmf9KkQOASAEcik%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;2070&quot; height=&quot;1354&quot; data-origin-width=&quot;2070&quot; data-origin-height=&quot;1354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;앵귤러JS&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앵귤러JS는 2009년 미스코 헤브리에 의해 부수적인 프로젝트로 개발됐다. 나중에 그는 구글의 직원이 됐고 이런 이유로 앵귤러JS는 지금은 구글 엔지니어가 관리한다. 1.0 버전은 2011년 5월에 릴리스됐다. 앵귤러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;양방향 데이터 바인딩 덕분에 개발자는 웹 애플리케이션을 빠르게 작성할 수 있게 됐다. 그러나 양방향 데이터 바인딩이 대규모 애플리케이션에는 적합하지 않기 때문에 시간이 지나자 많은 개발자가 앵귤러JS를 떠났다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  구글은 앵귤러JS의 단점을 보완하기 위해 앵귤러(앵귤러2)를 출시 했지만 앵귤러JS로 생긴 부정적인 선입견과 아키텍처의 대규모 변화 그리고 높은 러닝커브로 많은 웹개발자들이 Angular대신 React나 Vue를 선택했다. 그러나 구글은 6개월에 한번씩 메이저 버전을 발표하는등 매우 부지런한 행보를 보이고 있다.&lt;br /&gt;&lt;a href=&quot;https://angular.dev/reference/releases#release-frequency%20&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;릴리즈 정책&lt;/a&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;리액트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2011년 페이스북에서 만들어 2013년 오픈소스로 공개한 리액트는 현재 가낭 인기 있는 프론트엔드 라이브러리다. 리액트는 선언적 패러다임으로 동작한다. 일반적으로 DOM을 직접 수정하는 대신 setState 메서드로 상태를 변경하면 리액트가 나머지 작업을 수행한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술 부채&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프레임워크 비용&lt;/h4&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;h4 data-ke-size=&quot;size20&quot;&gt;기술 투자&lt;/h4&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;8장에서는 프레임워크가 프로젝트의 자산이 될지 판단하는 데 사용하는 몇 가지 기술적 방법과 &amp;lsquo;비용&amp;rsquo;이 적게 드는 프레임워클 선택하는 방법을 알아본다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &lt;a href=&quot;https://nextjs.org/&quot;&gt;NextJS&lt;/a&gt;는 프레임워크 일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NextJS 공식 문서는 웹용 React 프레임워크로 소개하고 있다. 차근차근 NextJS의 제약 조건을 알아보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3020&quot; data-origin-height=&quot;1426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfmZSy/btsH9ss0YlQ/3kQayRG3VlokmKVClmSsi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfmZSy/btsH9ss0YlQ/3kQayRG3VlokmKVClmSsi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfmZSy/btsH9ss0YlQ/3kQayRG3VlokmKVClmSsi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfmZSy%2FbtsH9ss0YlQ%2F3kQayRG3VlokmKVClmSsi1%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;3020&quot; height=&quot;1426&quot; data-origin-width=&quot;3020&quot; data-origin-height=&quot;1426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제약 조건(app router 기준)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/routing&quot;&gt;1. Routing&lt;/a&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 13에서 소개된 app router는 레이아웃, 중첩 라우팅, 로딩 상태, 오류 처리 등을 지원하는 서버 컴포넌트 위에 구축된 파일 시스템 기반 라우터다. 이를 사용하기위해 우리는 NextJS에서 설정한 폴더 구조를 엄격하게 따라야한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로의 각 폴더는 경로 세그먼트를 타나낸다. 각 경로 세그먼트는 URL 경로의 해당 세그먼트에 매핑된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blarz2/btsH8yVsEh5/WRVUB2q6Uj47UFQHKC8VKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blarz2/btsH8yVsEh5/WRVUB2q6Uj47UFQHKC8VKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blarz2/btsH8yVsEh5/WRVUB2q6Uj47UFQHKC8VKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fblarz2%2FbtsH8yVsEh5%2FWRVUB2q6Uj47UFQHKC8VKK%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;1600&quot; height=&quot;594&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/rendering&quot;&gt;2. &lt;b&gt;Rendering&lt;/b&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NextJS app router는 서버와 클라이언트 컴포넌트를 모두 지원하기 때문에 서버측에서 작동되어야하는 코드와 클라이언트 측에서 작동되어야하는 코드가 구분되어야한다. 이를 해결하기 위해 NextJS는 &quot;use client&quot; , &amp;ldquo;use server&quot;지시어를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;'use client'
 
import { useState } from 'react'
 
export default function Counter() {
  const [count, setCount] = useState(0)
 
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;You clicked {count} times&amp;lt;/p&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;Click me&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;클라이언트 컴포넌트 예시&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래와 같이서버와 클라이언트 컴포넌트를 사용할 때 권장되는 몇 가지 구성 패턴을 다룬다&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 170px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;What do you need to do?&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Server Component&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Client Component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Fetch data&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Access backend resources (directly)&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Keep sensitive information on the server (access tokens, API keys, etc)&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Keep large dependencies on the server / Reduce client-side JavaScript&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Add interactivity and event listeners (onClick(),&amp;nbsp;onChange(), etc)&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Use State and Lifecycle Effects (useState(),&amp;nbsp;useReducer(),&amp;nbsp;useEffect(), etc)&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Use browser-only APIs&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Use custom hooks that depend on state, effects, or browser-only APIs&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Use&amp;nbsp;&lt;a href=&quot;https://react.dev/reference/react/Component&quot;&gt;https://react.dev/reference/react/Component&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/data-fetching&quot;&gt;3. Data Fetching&lt;/a&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 컴포넌트에서 비동기/대기 기능을 통해 데이터 가져오기를 간소화하고 요청 메모화, 데이터 캐싱 및 재검증을 위한 확장된 가져오기 API를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 네이티브 &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Fetch_API&quot;&gt;fetch Web API를&lt;/a&gt; 확장하여 서버에서 각 불러오기 요청에 대한 캐싱 및 재검증 동작을 구성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;async function getData() {
  const res = await fetch('&amp;lt;https://api.example.com/&amp;gt;...')
  // The return value is *not* serialized
  // You can return Date, Map, Set, etc.
 
  if (!res.ok) {
    // This will activate the closest `error.js` Error Boundary
    throw new Error('Failed to fetch data')
  }
 
  return res.json()
}
 
export default async function Page() {
  const data = await getData()
 
  return
 }&lt;/code&gt;&lt;/pre&gt;
&lt;main&gt;&lt;/main&gt;&lt;main&gt;&lt;/main&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/optimizing&quot;&gt;4. &lt;b&gt;Optimizations&lt;/b&gt;&lt;/a&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js에는 애플리케이션의 속도와 핵심 &lt;a href=&quot;https://web.dev/articles/vitals?hl=ko&quot;&gt;Web Vitals&lt;/a&gt;을 개선하기 위해 설계된 다양한 최적화 기능이 내장되어 있다. 대표적인 예로 Next.js 이미지 컴포넌트는 자동 이미지 최적화를 위한 기능으로 HTML &amp;lt;img&amp;gt; 요소를 확장한 Image 컴포넌트 사용을 권장한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Image from 'next/image'
import profilePic from './me.png'
 
export default function Page() {
  return (
    &amp;lt;Image
      src={profilePic}
      alt=&quot;Picture of the author&quot;
      // width={500} automatically provided
      // height={500} automatically provided
      // blurDataURL=&quot;data:...&quot; automatically provided
      // placeholder=&quot;blur&quot; // Optional blur-up while loading
    /&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  저자의 시점에서 본다면 NextJS는 프로젝트 시작부터 끝까지 강한 제약들이 존재한다. 웹 개발의 선두를 달리고 있지만 과연 5년뒤에도 NextJS가 추구하는 아키텍처, 폴더 구조들이 변하지 않는다고 장담 할 수 있을까? 지금까지 나는 프로젝트 시작부터 매우 큰 부채를 떠안고 시작한게 아닌가 싶다. 앞으로 스터디를 진행하면서 우리가 사용하는 프레임워크가 프로젝트의 자산이 될지 판단하는 방법을 터득할 생각에 기대가 된다.&lt;/blockquote&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;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.youtube.com/watch?v=aAs36UeLnTg&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[10분&amp;nbsp;테코톡]&amp;nbsp;호프의&amp;nbsp;프론트엔드에서&amp;nbsp;컴포넌트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yozm.wishket.com/magazine/detail/2083/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;선언형 프로그래밍으로 이해하기 쉬운 코드 작성하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://angular.dev/reference/releases#release-frequency%20&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Angular 릴리즈 정책&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Next JS - Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/vitals?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Web Vitals &lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>독서</category>
      <author>_OIL</author>
      <guid isPermaLink="true">https://rocketengine.tistory.com/193</guid>
      <comments>https://rocketengine.tistory.com/entry/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%97%86%EB%8A%94-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C-1%EC%9E%A5-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0#entry193comment</comments>
      <pubDate>Mon, 24 Jun 2024 01:01:47 +0900</pubDate>
    </item>
    <item>
      <title>개발자를 넘어 기술 리더로 가는 길 - 타냐 라일리</title>
      <link>https://rocketengine.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EB%84%98%EC%96%B4-%EA%B8%B0%EC%88%A0-%EB%A6%AC%EB%8D%94%EB%A1%9C-%EA%B0%80%EB%8A%94-%EA%B8%B8-%ED%83%80%EB%83%90-%EB%9D%BC%EC%9D%BC%EB%A6%AC</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;I'm Okay I'm Fine 괜찮아 괜찮아 댕랭랭랭레~&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근래에 일을 하는 이유와 현 경력 지속 여부 그리고 돈을 버는 이유와 행복의 기준이 무엇인지 재 정립이 필요하다고 느끼는 와중에 &lt;b&gt;개발자를 넘어 기술 리더로 가는 길&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;프로젝트들이&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;매니저(PM)와&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;기술&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;자격으로&lt;b&gt;&amp;nbsp;'다양한&amp;nbsp;기술적&amp;nbsp;지식과&amp;nbsp;풍부한&amp;nbsp;경험'&lt;/b&gt;이&amp;nbsp;기반되어&amp;nbsp;&lt;b&gt;'빅&amp;nbsp;픽처&amp;nbsp;관점의&amp;nbsp;사고력'&lt;/b&gt;과&amp;nbsp;&lt;b&gt;'성공적인&amp;nbsp;프로젝트&amp;nbsp;실행력'&lt;/b&gt;그리고&amp;nbsp;&lt;b&gt;'조직&amp;nbsp;차원의&amp;nbsp;레벨&amp;nbsp;업'&amp;nbsp;&lt;/b&gt;이라는&amp;nbsp;영향력이&amp;nbsp;뒷받침되어야한다고&amp;nbsp;말한다.&amp;nbsp;어려워도&amp;nbsp;너무&amp;nbsp;어렵다.&amp;nbsp;솔직히&amp;nbsp;말해서&amp;nbsp;이&amp;nbsp;분야에서&amp;nbsp;20~30년&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;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;스태프 엔지니어가 되고 싶은가? 더 높은 기술자 직급을 열망하지 않아도 괜찮다. 매니저 진로로 이직하거나(또는 왔다 갔다 하는 것) 좋아하는 일을 계속하면서 시니어 레벨에 머무르는 것도 좋다. 어떤 상태이든 여러분이 조직의 목표를 달성하는 것을 돕고 주변 엔지니어들의 실력을 향상시키면서 기술적인 역량을 계속해서 구축하고자 한다면, 이 책을 계속 읽어보기를 바란다.&quot;&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;라는 문구가 끝까지 책을 읽을 수 있는 원동력이 되었던거 같다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;text-align: left;&quot;&gt;마지막으로 책을 읽으면서 알 수 없는 언짢음이 지속적으로 올라왔다. 감정의 원인을 생각해 보니 너무 많은 책임을 요구해 시작도 전에 지레 겁을 먹은 거 같다. 책에서 제시하는 일들을 즐기면서 할 수 있을까? 내 그릇을 키우려면 앞으로 더 큰 도전들을 수행해야 되는데 버겁지 않을까? 쓸데없는 고민은 그만하고 긍정적으로 생각하며 마무리한다.&lt;/span&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;nbsp;힘으로&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;것은&amp;nbsp;극히&amp;nbsp;제한적이다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;어떻게든 속한 조직의 자원과 인력을 최대로 활용해 최고의 결과를 도출해야된다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;너무 많은 사이드 업무와 도움에 휘말릴수록 여러분이 정말로 책임져야 할 일을 할 시간이 없어진다. 분별력을 가져라! -401&lt;/li&gt;
&lt;li&gt;코드를 작성하는 것은 소프트웨어 엔지니어링의 한 단계일 뿐이다. -198&lt;/li&gt;
&lt;li&gt;상사에게 보고한다는 것은 다른 팀에 관해서 불평한다거나 조직 내에서 소란을 일으키는 행위가 아니다. 그보다는 여러분을 도와줄 힘이 있는 사람과 정중하게 대화를 나누고, 함께 문제를 해결하려는 것을 의미한다. -223&lt;/li&gt;
&lt;li&gt;여러분의 계획 부족이 저희의 비상사태는 아닙니다. -227&lt;/li&gt;
&lt;li&gt;엔지니어링은 컴퓨터 시스템과 소통하는 수준에서 끝나는 것이 아니라 이는 여러분이 타인과 이야기하는 방식과 관련이 있다. -259&lt;/li&gt;
&lt;li&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;7년&amp;nbsp;이상의&amp;nbsp;코드&amp;nbsp;작성과&amp;nbsp;배포&amp;nbsp;경력을&amp;nbsp;갖추는&amp;nbsp;것을&amp;nbsp;의미한다&amp;nbsp;-262&lt;/li&gt;
&lt;/ul&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;458&quot; data-origin-height=&quot;589&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cL5zRb/btsH0V2630b/dSTdiRCyTx62owbd9DbLjK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cL5zRb/btsH0V2630b/dSTdiRCyTx62owbd9DbLjK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cL5zRb/btsH0V2630b/dSTdiRCyTx62owbd9DbLjK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcL5zRb%2FbtsH0V2630b%2FdSTdiRCyTx62owbd9DbLjK%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;458&quot; height=&quot;589&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;589&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>독서/2024</category>
      <category>개발자</category>
      <category>개발자를 넘어 기술 리더로 가는 길</category>
      <category>독후감</category>
      <author>_OIL</author>
      <guid isPermaLink="true">https://rocketengine.tistory.com/192</guid>
      <comments>https://rocketengine.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EB%84%98%EC%96%B4-%EA%B8%B0%EC%88%A0-%EB%A6%AC%EB%8D%94%EB%A1%9C-%EA%B0%80%EB%8A%94-%EA%B8%B8-%ED%83%80%EB%83%90-%EB%9D%BC%EC%9D%BC%EB%A6%AC#entry192comment</comments>
      <pubDate>Sat, 15 Jun 2024 23:05:59 +0900</pubDate>
    </item>
    <item>
      <title>맥킨지는 일하는 방식이 다르다 - 에단 라지엘</title>
      <link>https://rocketengine.tistory.com/entry/%EB%A7%A5%ED%82%A8%EC%A7%80%EB%8A%94-%EC%9D%BC%ED%95%98%EB%8A%94-%EB%B0%A9%EC%8B%9D%EC%9D%B4-%EB%8B%A4%EB%A5%B4%EB%8B%A4-%EC%97%90%EB%8B%A8-%EB%9D%BC%EC%A7%80%EC%97%98</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;일잘러의 3가지 접근 방식&lt;/b&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;b&gt;1부 문제 해결 방식&lt;/b&gt;&lt;/h3&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;문제의 크기에 압도되거나 경험이 전무한 프로젝트에 투입되는 경우에 3가지의 접근 방식을 놓쳐 간단한 문제조차도 오랜 시간 허비하거나 심한 경우 프로젝트가 망가지는 것도 보았다. 현재까지는 프로젝트 결과에 대한 책임이 적은 위치에 있었기에 반면교사로 삼을 뿐이지만 언제든 큰 책임과 역할을 요구할 수 있기에 작은 문제가 발생해도 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;&lt;b&gt;어떤 비즈니스 문제도 동일한것은 없다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 업무 스타일은 예스맨에 가까운데 그 이유는 새로운 요구사항이나 문제가 들어올 때 이전에 비슷한 경험을 해본 경우가 있다면 동일한 접근 방식을 적용해 빠르게 해결할 수 있을 거 같기 때문이다. 그래서 '기간 내에 할 수 있어요' 또는 '10~15분 정도 걸릴 거예요'를 하루에도 2~5번 정도 남발한다. 물론 같이 협업하는 기획자나 디자이너들은 이 태도를 반기지만 의도치 않게 완전히 다른 접근 방식을 요구하는 문제라서 일정이 연장된다면 동료 개발자에게 허세만 가득한 개발자가 되는 위험 부담이 있다. 마치 김민재 선수가 예측 수비를 성공하면 최고의 센터 백이 되는 것이지만 실패하면 팀에 실점을 안기는 양날과 같은 행동이다. 그래서 최상의 해결책을 찾기 위해 각각의 문제에 어떻게 접근해야 하는지를 소개하는 책의 소 제목들이 인상 깊었다.&lt;/p&gt;
&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;개발자는 회사 내의 파벌 싸움에 큰 영향이 없을 거라 생각했는데 이 생각은 근래에 프리랜서를 한지 2개월 만에 바뀌었다. 그럼에도 문제를 풀어야 한다면 컨설턴트가 아닌 개발자로서 어떤 태도를 취해야 될지 고민이 많은 상황이다.&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;&lt;b&gt;프로젝트 수주&lt;/b&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;/b&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;b&gt;3부 커뮤니케이션 방식&lt;/b&gt;&lt;/h3&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;상대방에게 알기 쉬운 방식으로 논리를 전개하는 사람은 그 자체로 매우 똑똑하고 배려 깊은 사람인 거 같다. 최근에 RESTful 하지 못한 api를 받아봤는데 내색하진 않았지만 해당 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;b&gt;정보가 계속 흐르게 하라&lt;/b&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;/b&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;b&gt;마치며,&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 향후 5년 이내로 개발자를 계속할 수 있을지는 의문이다. 하지만 사회 구성원으로서 문제를 해결하고 가치를 생산하는 일을 좋아하기 때문에 그리고 그러한 행위를 통해 돈을 (효율적으로 많이) 버는 것을 좋아하기 때문에 어떤 형태로든 일을 할 것이다. 누군가 나에게 어떤 가치관으로 일을 하고 있냐고 물어본다면 맥킨지스럽게 일하는 것을 추구한다고 말해봐야 되겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bT4w1Z/btsHccSkLaY/suf5WZ4AKzhpUzh0Zr0pak/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bT4w1Z/btsHccSkLaY/suf5WZ4AKzhpUzh0Zr0pak/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bT4w1Z/btsHccSkLaY/suf5WZ4AKzhpUzh0Zr0pak/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT4w1Z%2FbtsHccSkLaY%2Fsuf5WZ4AKzhpUzh0Zr0pak%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;752&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>독서</category>
      <category>개발자 독후감</category>
      <category>맥킨지</category>
      <category>컨설팅</category>
      <author>_OIL</author>
      <guid isPermaLink="true">https://rocketengine.tistory.com/191</guid>
      <comments>https://rocketengine.tistory.com/entry/%EB%A7%A5%ED%82%A8%EC%A7%80%EB%8A%94-%EC%9D%BC%ED%95%98%EB%8A%94-%EB%B0%A9%EC%8B%9D%EC%9D%B4-%EB%8B%A4%EB%A5%B4%EB%8B%A4-%EC%97%90%EB%8B%A8-%EB%9D%BC%EC%A7%80%EC%97%98#entry191comment</comments>
      <pubDate>Sun, 5 May 2024 20:32:51 +0900</pubDate>
    </item>
    <item>
      <title>소프트 스킬 - 존 손메즈</title>
      <link>https://rocketengine.tistory.com/entry/%EC%86%8C%ED%94%84%ED%8A%B8-%EC%8A%A4%ED%82%AC-%EC%A1%B4-%EC%86%90%EB%A9%94%EC%A6%88</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;용두사미&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&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;당신은&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;&lt;b&gt;Part 1경력&lt;/b&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;때마다&amp;nbsp;찾아보면&amp;nbsp;인생을&amp;nbsp;더&amp;nbsp;쪼을&amp;nbsp;수&amp;nbsp;있어&amp;nbsp;보인다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Part 2 셀프 마케팅&lt;/b&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;있는&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;누적되어&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;b&gt;Part 3  학습, Part4 생산성&lt;/b&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;거&amp;nbsp;같다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Part 5  재무관리, Part6 건강, Part7 마이드셋&lt;/b&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;저자가&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;같다.&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;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;184&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cM9XS3/btsGcLihZIN/hVAKwKbLWSN59kxO5RcuDk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cM9XS3/btsGcLihZIN/hVAKwKbLWSN59kxO5RcuDk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cM9XS3/btsGcLihZIN/hVAKwKbLWSN59kxO5RcuDk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcM9XS3%2FbtsGcLihZIN%2FhVAKwKbLWSN59kxO5RcuDk%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;184&quot; height=&quot;273&quot; data-origin-width=&quot;184&quot; data-origin-height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>독서</category>
      <category>독후가</category>
      <category>소프트 스킬</category>
      <author>_OIL</author>
      <guid isPermaLink="true">https://rocketengine.tistory.com/190</guid>
      <comments>https://rocketengine.tistory.com/entry/%EC%86%8C%ED%94%84%ED%8A%B8-%EC%8A%A4%ED%82%AC-%EC%A1%B4-%EC%86%90%EB%A9%94%EC%A6%88#entry190comment</comments>
      <pubDate>Sat, 30 Mar 2024 22:19:21 +0900</pubDate>
    </item>
    <item>
      <title>테크 커리어 - 돈 존스</title>
      <link>https://rocketengine.tistory.com/entry/%ED%85%8C%ED%81%AC-%EC%BB%A4%EB%A6%AC%EC%96%B4-%EB%8F%88-%EC%A1%B4%EC%8A%A4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Own Your Life&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책 도입 부분에서 저자는 잠시 멈춰서 자신이 원하는 인생(성공)을 정의하라고 권한다. 그래야 무턱대고 더 많은 것을 얻기 위한 무한 경쟁에 휩쓸리지 않고 자신의 인생을 주도적으로 이끌 수 있음을 강조한다.&amp;nbsp;하지만 나는 시간이 없기에 성공의 정의를 머릿속으로 1분 정도 짧게 정의하고 페이지를 넘기다 보니 저자가 말하는 대로 행하는 것이 과연 나한테 무슨 의미가 있는지 괴리감을 빈번하게 느꼈다.&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;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;&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;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;Own Your Tech Carrer(자신의 기술 경력은 자신이 소유하라)&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;184&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctETm5/btsGf983DP5/TLOvJmwiZfdVjv7rjAOUdk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctETm5/btsGf983DP5/TLOvJmwiZfdVjv7rjAOUdk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctETm5/btsGf983DP5/TLOvJmwiZfdVjv7rjAOUdk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctETm5%2FbtsGf983DP5%2FTLOvJmwiZfdVjv7rjAOUdk%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;184&quot; height=&quot;273&quot; data-origin-width=&quot;184&quot; data-origin-height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>독서</category>
      <category>독후감</category>
      <category>테크 커리어</category>
      <author>_OIL</author>
      <guid isPermaLink="true">https://rocketengine.tistory.com/189</guid>
      <comments>https://rocketengine.tistory.com/entry/%ED%85%8C%ED%81%AC-%EC%BB%A4%EB%A6%AC%EC%96%B4-%EB%8F%88-%EC%A1%B4%EC%8A%A4#entry189comment</comments>
      <pubDate>Sat, 30 Mar 2024 22:14:00 +0900</pubDate>
    </item>
    <item>
      <title>NextJS next-redux-wrapper hydrate 버그 수정</title>
      <link>https://rocketengine.tistory.com/entry/NextJS-next-redux-wrapper-hydrate-%EB%B2%84%EA%B7%B8-%EC%88%98%EC%A0%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;next-redux-wrappe 공식 문서에서 next js에서 store를 관리하는 방법 2가지를 소개합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kirill-konshin/next-redux-wrapper?tab=readme-ov-file#server-and-client-state-separation&quot;&gt;서버와 클라이언트 스토어를 분리하는 방법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kirill-konshin/next-redux-wrapper?tab=readme-ov-file#state-reconciliation-during-hydration&quot;&gt;클라이언트 쪽 스토어에 서버 쪽 스토어를 덮어씌우는 방법&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중 2번째 방법을 사용하고 있었는데 아마도 초기 리덕스 설정을 할 때&amp;nbsp;_app.tsx에&amp;nbsp;wrapper.getInitialAppProps를 사용하면 자동적으로 서버와 클라이언트 store를 관리해 주는 것으로 착각한 거 같습니다. 즉, 지금까지 의미 없는 서버 쪽 store를 호출해 hydrate를 계속 발생시키고 있었던 겁니다. 서버 쪽에서 의미 없는 hydrate를 계속 발생시키니 rootReducer코드를 보면 서버가 보내는&amp;nbsp;&amp;nbsp;action.payload에 store의 초기값이 담겨 오므로 클라이언트의 store가 계속 초기화되는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;const rootReducer = (state: any, action: any) =&amp;gt; {
  switch (action.type) {
    case HYDRATE:
      return { ...state, ...action.payload };
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 store가 계속 초기화 되서 각 페이지마다 이용 서비스를 체크하는 saga(api 통신 후 전역스토어에 저장해 주는 역할)를 호출하지 않으면 전역 store에 관리되고 있는&amp;nbsp;menu가 초기화되므로 사이드바의 메뉴 필터가 정상적으로 작동하지 않는 문제가 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 해결을 위한 2가지 방법&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;wrapper.getInitialAppProps으로 감싸져 있으니 서버쪽에서 서비스를 체크하는 api를 호출한 후&amp;nbsp;menu의 전역 상태값을 넣어주자&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 페이지를 이동할때마다 중복 api가 호출됩니다. 클라이언트 호출에서 서버 호출로 바뀐 것뿐 근본적인 문제가 해결되지 않습니다. 또한&amp;nbsp;menu 말고 다른 전역 상태값들도 신경 써야 되므로 매우 비효율적이고 광범위한 작업이 됩니다. 2. wrapper.getInitialAppProps를 지워 의미 없는 HYDRATE를 방지하자! getInitialProps는 nextjs에서 지양하는 방법이기도 하고 굳이 여기서 서버 쪽 store를 호출해 HYDRATE를 발생시켜 스토어를 초기화시킬 이유가 없습니다.&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;nbsp;&lt;a href=&quot;https://github.com/kirill-konshin/next-redux-wrapper?tab=readme-ov-file#state-reconciliation-during-hydration&quot;&gt;next-redux-wrapper&lt;/a&gt;&lt;/p&gt;</description>
      <category>기억보단 기록을/Next JS (Pages Router)</category>
      <category>Hydrate</category>
      <category>next-redux-wrapper</category>
      <category>nextjs</category>
      <author>_OIL</author>
      <guid isPermaLink="true">https://rocketengine.tistory.com/187</guid>
      <comments>https://rocketengine.tistory.com/entry/NextJS-next-redux-wrapper-hydrate-%EB%B2%84%EA%B7%B8-%EC%88%98%EC%A0%95#entry187comment</comments>
      <pubDate>Sun, 31 Dec 2023 19:28:26 +0900</pubDate>
    </item>
    <item>
      <title>Routing - Pages and Layouts</title>
      <link>https://rocketengine.tistory.com/entry/Routing-Pages-and-Layouts</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;페이지 라우터는 페이지 개념을 기반으로 구축된 파일 시스템 기반 라우터입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;파일이 페이지 디렉터리에 추가되면 자동으로 경로로 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Index routes&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;라우터는 index라는 이름의 파일을 디렉터리의 루트로 자동 라우팅합니다.&lt;/span&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 style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;pages/index.js&amp;nbsp;&amp;rarr;&amp;nbsp;/&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;pages/blog/index.js&amp;nbsp;&amp;rarr;&amp;nbsp;/blog&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Nested routes&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;라우터는 중첩 파일을 지원합니다. 중첩된 폴더 구조를 만들면 파일은 여전히 동일한 방식으로 자동으로 라우팅 됩니다.&lt;/span&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 style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;pages/blog/first-post.js&amp;nbsp;&amp;rarr;&amp;nbsp;/blog/first-post&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;pages/dashboard/settings/username.js&amp;nbsp;&amp;rarr;&amp;nbsp;/dashboard/settings/username&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Pages with Dynamic Routes&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Next.js는 동적 경로가 있는 페이지를 지원합니다. 예를 들어 pages/posts/[id]. js라는 파일을 만들면 posts/1, posts/2 등에서 액세스 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Layout Pattern&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;React 모델을 사용하면 페이지를 일련의 컴포넌트로 분해할 수 있습니다. 이러한 컴포넌트 중 상당수는 페이지 간에 재사용되는 경우가 많습니다. 예를 들어 모든 페이지에 동일한 navigation bar와 footer가 있을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Navbar from './navbar'
import Footer from './footer'

export default function Layout({ children }) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Navbar /&amp;gt;
      &amp;lt;main&amp;gt;{children}&amp;lt;/main&amp;gt;
      &amp;lt;Footer /&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Examples&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Single Shared Layout with Custom App&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;전체 애플리케이션에 레이아웃이 하나만 있는 경우 재사용될 &amp;lt;Layout /&amp;gt; 컴포넌트를 만들어 애플리케이션 전체를 래핑 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Layout from '../components/layout'

export default function MyApp({ Component, pageProps }) {
  return (
    &amp;lt;Layout&amp;gt;
      &amp;lt;Component {...pageProps} /&amp;gt;
    &amp;lt;/Layout&amp;gt;
  )
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Per-Page Layouts(페이지별 레이아웃)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여러 레이아웃이 필요한 경우, 페이지에 getLayout 속성을 추가하여 레이아웃에 대한 React 컴포넌트를 반환할 수 있습니다. 이를 통해 페이지 단위로 레이아웃을 정의할 수 있습니다. 함수를 반환하기 때문에 원하는 경우 복잡한 중첩 레이아웃을 가질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import type { ReactElement } from &quot;react&quot;;
import Layout from &quot;@/components/Layout&quot;;
import NestedLayout from &quot;@/components/NestedLayout&quot;;
import { NextPageWithLayout } from &quot;@/pages/_app&quot;;

const Page: NextPageWithLayout = () =&amp;gt; {
  return &amp;lt;div className=&quot;bg-amber-600&quot;&amp;gt;본문&amp;lt;/div&amp;gt;;
};

Page.getLayout = function getLayout(page: ReactElement) {
  return (
    &amp;lt;Layout&amp;gt;
	    &amp;lt;NestedLayout&amp;gt;{page}&amp;lt;/NestedLayout&amp;gt;
    &amp;lt;/Layout&amp;gt;
  );
};

export default Page;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import type { ReactElement, ReactNode } from &quot;react&quot;;
import type { NextPage } from &quot;next&quot;;
import type { AppProps } from &quot;next/app&quot;;
import &quot;../styles/globlas.css&quot;;

export type NextPageWithLayout&amp;lt;P = {}, IP = P&amp;gt; = NextPage&amp;lt;P, IP&amp;gt; &amp;amp; {
  getLayout?: (page: ReactElement) =&amp;gt; ReactNode;
};

type AppPropsWithLayout = AppProps &amp;amp; {
  Component: NextPageWithLayout;
};

export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  // Use the layout defined at the page level, if available
  const getLayout = Component.getLayout ?? ((page) =&amp;gt; page);

  return getLayout(&amp;lt;Component {...pageProps} /&amp;gt;);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;페이지 사이를 탐색할 때 SPA(단일 페이지 애플리케이션) 경험을 위해 페이지 상태(입력 값, 스크롤 위치 등)를 유지하려고 합니다. 이 레이아웃 패턴은 페이지 전환 사이에 React 컴포넌트 트리가 유지되기 때문에 상태 지속성을 가능하게 합니다. 컴포넌트 트리를 통해 React는 상태를 유지하기 위해 어떤 요소가 변경되었는지 파악할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;상세 설명&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;1. `NextPageWithLayout` 타입 정의:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697469843413&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export type NextPageWithLayout&amp;lt;P = {}, IP = P&amp;gt; = NextPage&amp;lt;P,IP&amp;gt; &amp;amp; {
	getLayout?: (page: ReactElement) =&amp;gt; ReactNode;
};&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- `NextPageWithLayout`는 `NextPage`의 확장된 타입입니다. 이 타입은 페이지 컴포넌트에 레이아웃을 적용하는 데 사용됩니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- `getLayout`은 페이지 컴포넌트에서 레이아웃을 정의할 때 사용할 수 있는 함수입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;2.&amp;nbsp;`AppPropsWithLayout`&amp;nbsp;타입&amp;nbsp;정의:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697469865274&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type AppPropsWithLayout = AppProps &amp;amp; {
	Component: NextPageWithLayout;
};&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp;- `AppPropsWithLayout`는 앱의 props에 페이지 컴포넌트와 레이아웃을 관리하기 위한 확장된 타입입니다. `Component` 속성은 `NextPageWithLayout` 유형을 가지고 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;3. `MyApp` 컴포넌트:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697469908180&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
	// Use the layout defined at the page level, if available
	const getLayout = Component.getLayout ?? ((page) =&amp;gt; page);
	return getLayout(&amp;lt;Component {...pageProps} /&amp;gt;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- `MyApp` 컴포넌트는 앱의 메인 컴포넌트로 사용됩니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- `Component`는 현재 페이지 컴포넌트를 나타냅니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- `pageProps`는 현재 페이지 컴포넌트의 프롭스(props)입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- `Component.getLayout`은 페이지 컴포넌트에서 정의한 레이아웃 함수입니다. 페이지 컴포넌트가 이 함수를 정의한 경우 페이지 컴포넌트의 레이아웃 함수가 사용됩니다. 그렇지 않으면 기본적으로 페이지 컴포넌트 자체가 레이아웃으로 사용됩니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;- 마지막으로, `getLayout` 함수를 사용하여 현재 페이지 컴포넌트와 프롭스를 레이아웃 함수에 전달하고, 레이아웃 된 페이지를 반환합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이렇게&amp;nbsp;설정된&amp;nbsp;`MyApp`&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;/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;size18&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;❓ 중첩 레이아웃을 사용하면 상태값이 유지되는 이유는 뭘까요?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;레이아웃 변경은 페이지의 일부 컴포넌트 트리를 변경하므로 React는 레이아웃 변경에 따라 최소한의 DOM 조정만 수행할 것입니다.&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예를 들어, 헤더, 사이드바 및 메인 콘텐츠로 구성된 레이아웃을 가진 페이지를 생각해 보십시오. 사용자가 이 페이지를 다른 페이지로 이동할 때, React는 현재 페이지와 새 페이지 간의 레이아웃 변경에 따라 레이아웃을 효율적으로 조정합니다. 새로운 페이지의 레이아웃이 이전 페이지와 다른 경우, React는 필요한 변경만 DOM에 반영하며 이러한 변경은 reconciliation 프로세스를 통해 관리됩니다.&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;요약하면, Next.js의 per-page layout를 사용하면 각 페이지에 대해 사용자 정의 레이아웃을 정의할 수 있으며, React의 reconciliation 프로세스는 이러한 레이아웃 변경을 효율적으로 관리하고 렌더링 성능을 최적화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://react-ko.dev/learn/preserving-and-resetting-state&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[state 보존 및 재설정] 참고&lt;/a&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그래서&amp;nbsp;&lt;a href=&quot;https://github.com/Ibaslogic/nextjs-nested-layout-pages-dir&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[nextjs-nested-layout-pages-dir]의&lt;/a&gt; dashboard하위의 페이지들이 고유의 레이아웃을 만들어도 인풋의 입력값이 유지되는 것을 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/441759614&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/pRsot/hyUgTqDs8U/dMlD6P5VPKrcK8BKfqaFpK/img.jpg?width=1080&amp;amp;height=1829&amp;amp;face=0_0_1080_1829,https://scrap.kakaocdn.net/dn/qNMP8/hyUdWWThgE/IJnW7WoH63AAPZLMOTs5b1/img.jpg?width=1080&amp;amp;height=1829&amp;amp;face=0_0_1080_1829&quot; data-video-width=&quot;320&quot; data-video-height=&quot;542&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;1456&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/441759614?service=daum_tistory&quot; width=&quot;320&quot; height=&quot;542&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Per-page layout pattern이 생긴 배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;부제: 왜 getLayout은 함수여야 할까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 블로그 링크: &lt;a href=&quot;https://adamwathan.me/2019/10/17/persistent-layout-patterns-in-nextjs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://adamwathan.me/2019/10/17/persistent-layout-patterns-in-nextjs/&lt;/a&gt; (저자: Adam Wathan-tailwindCSS 만든 사람)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React, Angular, Vue와 같은 SPA의 가장 큰 장점 중 하나는 페이지 이동 시에도 전체 문서를 처음부터 다시 렌더링 하지 않고도 사이트를 탐색할 수 있다는 점이었습니다. &amp;nbsp;왜냐면 페이지를 새로 받아와 렌더링 하는 것이 아니기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 스크롤 위치를 저장하고 다음 페이지 이동 시 스크롤 위치를 복원하는 복잡한 작업 없이도 사이드바 컴포넌트처럼 변경되지 않는 UI부분의 스크롤 위치를 보존하는 등의 작업을 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 NextJs를 사용해 보면 Link를 통해 페이지 이동시 전체 화면을 처음부터 다시 렌더링 한다는 것을 알 수 있습니다. 그렇다면 NextJs는 페이지 이동시에도 변하지 않는 UI의 상태값을 유지하기 위해 어떤 방법을 시도해왔을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;iframe src=&quot;https://codesandbox.io/embed/1-single-layout-for-entire-site-in-custom-app-fqj0g?fontsize=14&amp;hidenavigation=1&amp;theme=dark&quot;
     style=&quot;width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;&quot;
     title=&quot;1 - Single layout for entire site in custom App&quot;
     allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot;
     sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;
   &gt;&lt;/iframe&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;가로로 스크롤할 수 있는 탭 목록이 포함된 account-settings 섹션으로, 이 섹션을 클릭하면 다른 하위 섹션으로 이동할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 데모에서는 페이지에 필요한 레이아웃 구성 요소를 각 페이지마다 동일하게 넣어주고 있습니다. 이 접근 방식의 문제점은 account-settings 페이지 중 하나를 방문하여 탭 목록을 오른쪽 끝까지 스크롤한 다음 'Security' 탭을 클릭하면 페이지가 이동되면서 탭의 가로 스크롤이 왼쪽 끝으로 초기화됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 reconciliation개념에 따라 동일한 component tree를 갖고 있으면 스크롤 위치 같은 상태값이 유지되어야 하는데 뭔가 이상합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 각 페이지의 가장 상위 component는 우리가 보고 있는 그 SiteLayout가 아니라 바로 _app.js파일에 전달되는 props 중에 Component(Page component)입니다. 페이지 이동시 Page component가 바뀌므로(가장 최상위 component가 바뀌므로) React는 모든 children을 버리고 처음부터 다시 그리게 됩니다. &lt;b&gt;즉, 이전 페이지에서 이미 DOM의 같은 위치에 해당 컴포넌트를 렌더링 했더라도 각 페이지가 SiteLayout 또는 AccountSettingsLayout의 새로운 복사본을 렌더링 합니다.&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;그래서 NextJs에서 위 데모처럼 코드를 짜면 서버 기반 애플리케이션 UI처럼 느껴지는 단일 페이지 애플리케이션이 돼버립니다. 으악!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법 1: 사용자 지정 &amp;lt;App&amp;gt; 컴포넌트에서 단일 공유 레이아웃(single shared layout) 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이트에 persistent layout component를 추가하는 한 가지 방법은 custom App component를 만들고 컴포넌트 트리에서 항상 현재 Page component 위에 사이트 레이아웃을 렌더링 하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import SiteLayout from '../components/SiteLayout'

export default function MyApp({ Component, pageProps }) {
  return (
    &amp;lt;SiteLayout&amp;gt;
      &amp;lt;Component {...pageProps} /&amp;gt;
    &amp;lt;/SiteLayout&amp;gt;
  )
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SiteLayout 컴포넌트는 페이지 전환 시 재사용되므로 검색 필드에 입력한 내용이 그대로 유지됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 AccountSettingLayout처럼 일부 페이지에서 공유하는 추가적인 Layout이 있다면?&lt;/b&gt; Account setting의 하위 페이지들은 AccountSettingLayout을 사용해야 됩니다. 그리고 AccountSettingLayout는 가로 스크롤이 있는 tab bar를 포함하고 있습니다. AccountSettingLayout은 App component에 포함되어있지 않기 때문에 tab bar의 scroll 위치를 기억할 수 없습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법 2: 현재 URL을 기반으로 &amp;lt;App&amp;gt;에서 다른 레이아웃 렌더링하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 공유 레이아웃(single shared layout)으로 가능한 것보다 요구 사항이 약간 더 복잡하다면 현재 URL을 기반으로 다른 트리를 렌더링 하는 것으로 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;router.pathname을 검사하여 현재 사이트의 어느 'section'에 있는지 파악하고 해당 레이아웃 트리를 렌더링 할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;class MyApp extends App {
  render() {
    const { Component, pageProps, router } = this.props
    if (router.pathname.startsWith('/account-settings/')) {
      return (
        &amp;lt;SiteLayout&amp;gt;
          &amp;lt;AccountSettingsLayout&amp;gt;
            &amp;lt;Component {...pageProps}&amp;gt;&amp;lt;/Component&amp;gt;
          &amp;lt;/AccountSettingsLayout&amp;gt;
        &amp;lt;/SiteLayout&amp;gt;
      )
    }
    return (
      &amp;lt;SiteLayout&amp;gt;
        &amp;lt;Component {...pageProps}&amp;gt;&amp;lt;/Component&amp;gt;
      &amp;lt;/SiteLayout&amp;gt;
    )
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우, /account-settings/ 아래 페이지들은 같은 layout component tree를 공유할 수 있고 따라서 SiteLayout 뿐 아니라 AccountSettingsLayout 역시 재 사용 되어 페이지 이동을 하더라도 상태를 유지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 접근 방식의 단점은 &lt;b&gt;각 페이지 component 별로 어떤 Layout을 사용하는지 Page 파일만 봐서는 알기 어려운 문제점이 있으며&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이아웃을 URL에 연결하기 때문에 레이아웃을 변경해야 하는 경우(예: /account-settings/delete가 완전히 다른 레이아웃을 사용하는 경우) 매우 구체적인 조건부 로직을 점점 더 많이 추가해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 소규모 사이트에서는 이 방법이 효과적일 수 있지만, 대규모 사이트에서는 좀 더 선언적이고 유연한 방법이 필요할 것입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법 3: Page component에 정적 'layout' 속성 추가하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 지남에 따라 앱 컴포넌트의 복잡성이 증가하는 것을 방지하는 한 가지 방법은 페이지 레이아웃을 정의하는 책임을 앱 컴포넌트가 아닌 페이지 컴포넌트로 옮기는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Page Component가 export 하는 page component 객체에 &amp;lsquo;layout&amp;rsquo;이라는 property를 추가하고 그 layout property를 앱 컴포넌트 내부에서 읽어주면 됩니다 대신 Layout component를 중첩해 선언할 수는 없기 때문에, 이 경우 중첩된 Layout을 담는 하나의 새로운 Layout이 필요합니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;//components/AccountSettingsLayout.jsx
const AccountSettingsLayout = () =&amp;gt; &amp;lt;SiteLayout&amp;gt;{/* ... */}&amp;lt;/SiteLayout&amp;gt;

export default AccountSettingsLayout

&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// /pages/account-settings/basic-information.js
import AccountSettingsLayout from '../../components/AccountSettingsLayout'

const AccountSettingsBasicInformation = () =&amp;gt; &amp;lt;div&amp;gt;{/* ... */}&amp;lt;/div&amp;gt;

AccountSettingsBasicInformation.layout = AccountSettingsLayout

export default AccountSettingsBasicInformation

&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import React from 'react'
import App from 'next/app'

class MyApp extends App {
  render() {
    const { Component, pageProps } = this.props
    const Layout = Component.layout || (children =&amp;gt; &amp;lt;&amp;gt;{children}&amp;lt;/&amp;gt;)
    return (
      &amp;lt;Layout&amp;gt;
        &amp;lt;Component {...pageProps}&amp;gt;&amp;lt;/Component&amp;gt;
      &amp;lt;/Layout&amp;gt;
    )
  }
}
export default MyApp

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 완벽해 보이지만 한 가지 문제가 있습니다. SiteLayout의 state가 보존되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 이 예제에서 AccountSettingsLayout이 내부적으로 SiteLayout을 사용하기 때문에 실제 최상위 레이아웃 컴포넌트가 SiteLayout에서 AccountSettingsLayout으로 전환되고 원래 있던 SiteLayout이 파괴되고 AccountSettingsLayout 내부에서 생성된 새 SiteLayout 인스턴스로 대체되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, SiteLayout을 layout으로 쓰는 페이지에서 AccountSettingsLayout을 layout으로 쓰는 페이지로 이동할 경우, react 관점에서 최상의 component가 바뀌는 것이므로 새롭게 SiteLayout instance를 생성합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법 4: Page component에 getLayout 함수 추가하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 layout 프로퍼티 대신 정적 함수를 사용하면 복잡한 레이아웃 트리를 반환할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// /pages/account-settings/basic-information.js

import SiteLayout from '../../components/SiteLayout'
import AccountSettingsLayout from '../../components/AccountSettingsLayout'
const AccountSettingsBasicInformation = () =&amp;gt; &amp;lt;div&amp;gt;{/* ... */}&amp;lt;/div&amp;gt;

AccountSettingsBasicInformation.getLayout = page =&amp;gt; (
	&amp;lt;SiteLayout&amp;gt;
		&amp;lt;AccountSettingsLayout&amp;gt;{page}&amp;lt;/AccountSettingsLayout&amp;gt;
	&amp;lt;/SiteLayout&amp;gt;
)

export default AccountSettingsBasicInformation

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 다음 _app.js에서 현재 페이지에 전달된 함수를 호출하여 전체 트리를 가져올 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// /pages/_app.js
import React from 'react'
import App from 'next/app'

class MyApp extends App {
	render() {
		const { Component, pageProps, router } = this.props
		const getLayout = Component.getLayout || (page =&amp;gt; page)
		return getLayout(&amp;lt;Component {...pageProps}&amp;gt;&amp;lt;/Component&amp;gt;)
	}
}
export default MyApp

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이 예제에서는 getLayout이라는 이름을 사용했지만, 프레임워크 기능이나 다른 어떤 것이든 원하는 대로 사용할 수 있습니다).&lt;/p&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;nbsp;&lt;/p&gt;</description>
      <category>기억보단 기록을/Next JS (Pages Router)</category>
      <category>Next JS</category>
      <category>Per-Page Layouts</category>
      <category>페이지별 레이아웃</category>
      <author>_OIL</author>
      <guid isPermaLink="true">https://rocketengine.tistory.com/186</guid>
      <comments>https://rocketengine.tistory.com/entry/Routing-Pages-and-Layouts#entry186comment</comments>
      <pubDate>Tue, 17 Oct 2023 00:13:44 +0900</pubDate>
    </item>
    <item>
      <title>노션에서 옵시디언으로 전환하기</title>
      <link>https://rocketengine.tistory.com/entry/%EB%85%B8%EC%85%98%EC%97%90%EC%84%9C-%EC%98%B5%EC%8B%9C%EB%94%94%EC%96%B8%EC%9C%BC%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;노션에서 옵시디언으로 옮기는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵시디언의 장점과 옵시디언 vs노션을 비교하는 자료들은 찾아보면 많기 때문에 길게 풀어놓지는 않겠습니다. 노션과 &lt;a href=&quot;https://xenostudy.tistory.com/697#_13&quot;&gt;옵시디언 비교 글,&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=n-O-7PpsZOs&quot;&gt;옵시디언과 노션의 비교 유뷰트,&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=h6rxKbbgI28&quot;&gt;노매드 코더의 옵시디언 소개 유튜브&lt;/a&gt; , &lt;a href=&quot;https://olait.tistory.com/5&quot;&gt;옵시디언 고수의 블로그&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 노션을 이용해 다양한 글 &amp;amp; 자료 &amp;amp; 일정을 관리하고 있지만 페이지의  용량이 늘어날수록 느려지는 속도, 정규화된 템플릿으로 커스텀에 제한이 많음, 슬슬 물리기 시작하는 노션의 UI 등으로 인해 지속적으로 노션을 대체할 만한 앱을 둘러보고 있었습니다. 많은 대체 앱들 중 옵시디언을 선택한 이유는 바로 아래와 같은 멋진 그래프 때문이죠&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/sWASbPZ.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그래프는 내 개인 노션에 있는 글들을 옵시디언으로 옮겼을때 어떤 글들이 어떻게 관계를 갖고 있는지 그래프로 표현해준 것입니다. 아직은 옵시디언에 최적화가 안되었기 때문에 비교적 단순한 그래프이지만 말 그대로 옵시디언을 이용해 글을 작성하면 제2의 두뇌를 눈으로 볼 수 있습니다. 하나에 꽂히면 계획한 일정은 저~뒤로 미루는 편이라 이번 추석 연휴의 이틀을 옵시디언을 갖고 노는 데 사용했어요   그래도 여러 삽질 끝에 옵시디언을 어떻게 사용하면 좋을지 두각이 잡혔기 때문에 느낀 점을 글로 남겨봅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;옵시디언을 이용해 해보고 싶었던 것&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;노션에 있는 글들을 모두 옵시디언으로 옮겨 멋진 그래프를 본다&lt;/li&gt;
&lt;li&gt;옵시디언에 작성된 글들은 자동으로 Github의 특정 repository 백업되어 내가 사용하고 있는 기기(개인 맥북, 회사 맥북, 아이폰, 아이패드)들에 동기화되어야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;옵시디언은 동기화를 위한 유료 서비스를 제공하지만 요금이 월 10달러라 과감히 포기&lt;/li&gt;
&lt;li&gt; remotely save커스텀 플러그인을 사용해 ICould, dDopbox, S3, OneDrive등과 같은 곳에 파일을  업로드해 동기화할 수 있지만 동기화 속도가 느리고 무료버전은 용량에 제한이 있어서 포기&lt;/li&gt;
&lt;li&gt;Github을 연동하면 개발자 친화적이고, 동기화 속도도 빠르고, 용량도 무제한이고, 글만 작성해도 자동으로 커밋이 찍히기 때문에 잔디를 심어야 한다는 압박감을 해소할 수 있어서 채택!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;옵시디언에 있는 글들이 클릭 한 번으로 개인 블로그, Github, 노션에 배포 및 수정이 되면 좋겠다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개인 블로그, Github, 노션에 올릴 수 있는 커스텀 플러그인들은 많지만 아직 확인을 안 해봄.&lt;/li&gt;
&lt;li&gt;업로된 글이 수정 가능한지도 잘 모르겠음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;옵시디언 실행 -&amp;gt; 새 보관소 생성 클릭 후 생성합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/wghAN2d.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://i.imgur.com/BL0kHvM.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우측하단 설정 아이콘 클릭 -&amp;gt; 커뮤니티 플러그인 선택 -&amp;gt; 커뮤니티 플러그인 탐색 버튼 클릭 -&amp;gt; importer 검색 후 설치 -&amp;gt; 설치 완료 되면 활성화 버튼 클릭 -&amp;gt; 활성화가 되면 좌측 사이드바에 표시된 것처럼 importer 아이콘이 생성됩니다.&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;img src=&quot;https://i.imgur.com/1VDtLg2.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;노션 콘텐츠 내보내기, **주의할 점으로 내보내기 형식은 Html로 해야 됩니다.&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;img src=&quot;https://i.imgur.com/nMgzIJB.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;추출된 zip 파일 importer에서 선택 후 &lt;b&gt;import 버튼&lt;/b&gt; 클릭 합니다.&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;img src=&quot;https://i.imgur.com/UhSjicB.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;import가 성공하면 노션에 작성한 글들이 옮겨와 지고 단축키 cmd + G로 그래프 뷰 보기를 활성화하면 아래와 같이 비슷한 그래프를 볼 수 있습니다.&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;img src=&quot;https://i.imgur.com/6qfno0l.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬에 있는 글들을 GtiHub repository에 백업하기 (mac os 기준)&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;github private repository 만들기&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;img src=&quot;https://i.imgur.com/WAW6Le8.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt; 옵시디언에 서 git을 원활하게 사용해 줄 수 있게 만든 커스텀 플러그인 Obsidian Git가 있지만 해당 플러그인에 git-lfs가 없어어 인지 저처럼 큰 용량을 import해온 경우 Obsidian Git가 정상적으로 작동하지 않으므로 터미널을  사용해 git 작업을 하는 게 훨씬 속편 합니다. Obsidian Git은 추후 자동 백업을 편하게 해 주는데 도움이 되므로 작은 파일 단위로 보낼 때 사용하면 매우 편해요!
&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;git init&lt;/li&gt;
&lt;li&gt;git remote set-url origin  &quot;생성한 github repository url&quot;&lt;/li&gt;
&lt;li&gt;git add.&lt;/li&gt;
&lt;li&gt;git commit -m &amp;ldquo;first commit&amp;rdquo;&lt;/li&gt;
&lt;li&gt;git push origin main&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;정상적으로 git push가 완료되면 옵시디언에 있는 글들이 github에 업로드된 것을 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;img src=&quot;https://i.imgur.com/FTUZo3A.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Obsidian Git플러그인으로 자동 동기화 &amp;amp; 백업 환경 구축하기&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우측하단 설정 아이콘 클릭 -&amp;gt; 커뮤니티 플러그인 선택 -&amp;gt; 커뮤니티 플러그인 탐색 버튼 클릭 -&amp;gt; Obsidian Git검색 후 설치 -&amp;gt; 설치 완료 되면 활성화 버튼 클릭&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;img src=&quot;https://i.imgur.com/yAPIBZ0.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;로컬에 있는 글들을 GtiHub repository에 백업하기 (mac os 기준)&lt;/code&gt;에서 이미 git 연동을 했기 때문에 별도의 git 환경설정은 하지 않습니다.&lt;/li&gt;
&lt;li&gt;좌측 하단 환경 설정 아이콘 클릭 -&amp;gt; Obsidian Git 클릭 이동을 하면 Git Backup setting을 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;img src=&quot;https://i.imgur.com/vMnJI68.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;자동 백업 환경 설정을 해줍니다. &lt;a href=&quot;https://error-storage.tistory.com/46&quot;&gt;이미지 출처&lt;/a&gt; (선택)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;① 자동으로 commit과 push를 해주는 설정입니다. ON으로 바꿉시다.&lt;/li&gt;
&lt;li&gt;②.③ commit과 push 주기입니다. 동일한 시간으로 맞추시면 됩니다. 5로 맞추면 5분마다 변경된 파일이 있으면 깃헙 저장소에 전송을 한다고 생각하시면 됩니다.&lt;/li&gt;
&lt;li&gt;④ pull 주기입니다. 저는 1로 설정하였습니다. 1분마다 깃헙 저장소에서 내용들을 끌고 옵니다.&lt;/li&gt;
&lt;/ul&gt;
저는 git 환경이 익숙해서 위 예시처럼 특정 간격으로 백업하는 것보다 &lt;code&gt;Obsidian Git: Create backup&lt;/code&gt;을 &lt;code&gt;cmd+Shift+S&lt;/code&gt; 단축키로 등록해서 저장할 때마다 자동으로 git repository에 push 되도록 설정했습니다.&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://i.imgur.com/jHuTGaT.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;br /&gt;그 외 git을 다룰 줄 아는 개발자라면 설명문 봐도 어떻게 작동하는지 알 수 있으니 자세한 내용은 생략하겠습니다. 혹 자동 배포에 관해 궁금한 점이 있으면 댓글로 남겨주세요!&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;img src=&quot;https://i.imgur.com/aD4yRkE.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아이패드 &amp;amp; 아이폰 환경에서 옵시디언 연동하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 옵시디언 앱을 설치하고 Obsidian Git으로 github에 올라간 옵시디언 저장소를 클론 해오면 간단하게 될 줄 알았습니다. 그런데&amp;hellip; 위에서 말한 것처럼 Obsidian Git은 아직 대용량 파일을 전송하거나 전달받지 못하므로 노션에 있는 글들을 생각 없이 때려 박아 비대하게 커져 버린 저의 옵시디언 저장소를 당겨오지 못하는 상황이 발생했습니다. 그래서 &lt;a href=&quot;https://publish.obsidian.md/git-doc/Getting+Started#Clone+via+Working+Copy+on+iOS&quot;&gt;Obsidian Git 공식문서&lt;/a&gt;를 찾아보니 &amp;nbsp;&lt;a href=&quot;https://workingcopy.app/&quot;&gt;Working Copy&lt;/a&gt;라는 앱을 이용해 IOS 환경에 git repository를 가져온 후 해당 폴더를 복붙 하라고 해서 그대로 진행했습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@joshuara7235/%EC%98%B5%EC%8B%9C%EB%94%94%EC%96%B8-%EC%82%AC%EC%9A%A9%ED%95%B4-%EB%B3%B4%EC%8B%A4%EB%9E%98%EC%9A%94&quot;&gt;Github을 이용한 옵시디언 백업 환경 구성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://error-storage.tistory.com/46&quot;&gt;Obsidian git 자동 백업 환경 설정&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기억보단 기록을/옵시디언</category>
      <category>OBSIDIAN</category>
      <author>_OIL</author>
      <guid isPermaLink="true">https://rocketengine.tistory.com/185</guid>
      <comments>https://rocketengine.tistory.com/entry/%EB%85%B8%EC%85%98%EC%97%90%EC%84%9C-%EC%98%B5%EC%8B%9C%EB%94%94%EC%96%B8%EC%9C%BC%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%98%EA%B8%B0#entry185comment</comments>
      <pubDate>Thu, 5 Oct 2023 23:38:34 +0900</pubDate>
    </item>
  </channel>
</rss>