<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발을항해</title>
    <link>https://seyeon.tistory.com/</link>
    <description>웹 개발 공부합니다  &amp;zwj; </description>
    <language>ko</language>
    <pubDate>Wed, 6 May 2026 16:36:53 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yeonDev</managingEditor>
    <image>
      <title>개발을항해</title>
      <url>https://tistory1.daumcdn.net/tistory/4662260/attach/5df66f30972343b19625b2f3daddeff9</url>
      <link>https://seyeon.tistory.com</link>
    </image>
    <item>
      <title>[Softeer] Lv.2 나무 공격 + 입출력 방법</title>
      <link>https://seyeon.tistory.com/120</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&lt;/b&gt;: &lt;a href=&quot;https://softeer.ai/practice/9657&quot;&gt;https://softeer.ai/practice/9657&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737980592172&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;Softeer - 현대자동차그룹 SW인재확보플랫폼&quot; data-og-description=&quot;&quot; data-og-host=&quot;softeer.ai&quot; data-og-source-url=&quot;https://softeer.ai/practice/9657&quot; data-og-url=&quot;https://softeer.ai/practice/9657&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://softeer.ai/practice/9657&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://softeer.ai/practice/9657&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;Softeer - 현대자동차그룹 SW인재확보플랫폼&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;softeer.ai&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Node.js 입출력&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프티어에서는 우측 상단의 공식 문서를 참고하여 문제 풀이를 할 수 있습니다. 입출력 방법은 &lt;a href=&quot;https://nodejs.org/docs/latest-v12.x/api/readline.html#readline_example_tiny_cli&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt; 예시를 참고해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1737979879958&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const readline = require('readline');
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});
// ...

// 한 줄씩 입력받기
rl.on('line', (line) =&amp;gt; {
  switch (line.trim()) {
    case 'hello':
      console.log('world!');
      break;
    default:
      console.log(`Say what? I might have heard '${line.trim()}'`);
      break;
  }
  // ...
}).on('close', () =&amp;gt; {
  // readline 이벤트 종료
  console.log('Have a great day!');
  process.exit(0);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 풀이&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격을 할 때마다 &lt;u&gt;일일이 그래프를 탐색하면 &lt;b&gt;비효율적&lt;/b&gt;&lt;/u&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;b&gt;단계&lt;/b&gt;:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;행마다&lt;/b&gt; 환경 파괴범이 몇 명인지 배열에 저장합니다. &lt;span style=&quot;color: #666666;&quot;&gt;(인덱스가 0이면 첫 번째 행을 뜻함)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;L ~ R에 해당하는 행의 개수를 배열에서 찾아 &lt;b&gt;1씩 감소&lt;/b&gt;시킵니다. &lt;span style=&quot;color: #ef5369;&quot;&gt;(단, 0 이하는 0으로 유지합니다)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;공격을 모두 수행한 후, 배열의 개수를 &lt;b&gt;모두 더해서&lt;/b&gt; 정답을 출력합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1737980385068&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const readline = require('readline');

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

const inputs = [];

// 입력하기
rl.on('line', (input) =&amp;gt; {
    inputs.push(input);
});

// 출력하기
rl.on('close', () =&amp;gt; {
    const [n, m] = inputs[0].split(&quot; &quot;).map(Number);
    const graph = inputs.slice(1, n + 1).map(line =&amp;gt; line.split(&quot; &quot;).map(Number));
    const attacks = inputs.slice(n + 1).map(line =&amp;gt; line.split(&quot; &quot;).map(Number));
    
    const countPerRow = Array(n).fill(0); // 각 행의 환경 파괴범 수

    for (let i = 0; i &amp;lt; n; i++) {
        for (let j = 0; j &amp;lt; m; j++) {
            if (graph[i][j] === 1) countPerRow[i]++; // 카운트
        }
    }

    // 공격 진행
    for (const [L, R] of attacks) {
        for (let i = L - 1; i &amp;lt; R; i++) {
            countPerRow[i] = countPerRow[i] &amp;gt; 0 ? countPerRow[i] - 1 : 0;
        }
    }

    const answer = countPerRow.reduce((acc, curr) =&amp;gt; acc + curr); // 합계
    console.log(answer);
    
    process.exit(0);
});&lt;/code&gt;&lt;/pre&gt;</description>
      <category>  알고리즘</category>
      <category>Softeer</category>
      <category>나무공격</category>
      <category>문제풀이</category>
      <category>소프티어</category>
      <category>알고리즘</category>
      <category>입출력</category>
      <category>코딩테스트</category>
      <author>yeonDev</author>
      <guid isPermaLink="true">https://seyeon.tistory.com/120</guid>
      <comments>https://seyeon.tistory.com/120#entry120comment</comments>
      <pubDate>Mon, 27 Jan 2025 21:22:30 +0900</pubDate>
    </item>
    <item>
      <title>textarea에 링크 삽입 기능 만들기 (with React)</title>
      <link>https://seyeon.tistory.com/119</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;어느 평화로운 날, 새로운 요구사항이 생겼습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;관리자 코멘트 입력란에 '링크 삽입' 기능 추가해 주세요.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코멘트 입력란은 &lt;b&gt;textarea&lt;/b&gt;를 사용 중이었는데, 말 그대로 텍스트만 입력됩니다. 어떻게 구현해야 될지 몰랐지만 일단 &lt;span style=&quot;color: #dddddd;&quot;&gt;&lt;s&gt;안&lt;/s&gt;&lt;/span&gt;된다고 대답하고 좀 더 고민해 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크를 삽입하는 방법이 여러 가지 떠올랐는데,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;링크를 a 태그로 감싸서 텍스트로 저장한 후, HTML로 파싱하여 보여준다.&lt;/li&gt;
&lt;li&gt;코멘트 입력란을 현재 블로그 기능에 사용 중인 웹 에디터로 교체한다.&lt;/li&gt;
&lt;li&gt;마크다운으로 입력 후 JSX로 변환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방법 모두 검색해 보면 관련 라이브러리가 있습니다. 하지만 짧은 코멘트를 위해 라이브러리를 설치한다고? 링크 많이 넣어봤자 2, 3개일 텐데, 마크다운이나 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;차마 그러지 못해서 링크 삽입을 &lt;b&gt;직접 구현&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ &lt;b&gt;기능 구체화&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코멘트를 입력할 때는 textarea를 사용하고, 코멘트를 저장한 후 보여줄 때는 div를 사용했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 텍스트를 붙여넣기(Paste)하면, 입력된 텍스트가 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;URL인지 아닌지&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&amp;nbsp;&lt;/span&gt;판단한다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;URL이 아니면 그냥 입력한다.&lt;/li&gt;
&lt;li&gt;URL이면 마크다운 문법처럼 (Caption)[URL] 형태로 변환하여 입력한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;붙여넣기 시, 사용자가 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;선택한 텍스트&lt;/b&gt;가 있는지&lt;/span&gt; 확인한다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;선택한 텍스트가 있으면, 현재 커서 위치에 (선택한 텍스트)[URL] 형태로 변환하여 입력하고 선택한 텍스트는 삭제한다.&lt;/li&gt;
&lt;li&gt;선택한 텍스트가 없으면, 현재 커서 위치에 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;(URL)[URL]&lt;span&gt; 형태로 변환하여 입력한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;USER 단에서 (Caption)[URL]를 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Link로 변환&lt;/b&gt;&lt;/span&gt;하여 보여준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;  &lt;b&gt;구현하기&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ URL인지 아닌지 판단하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL 판단은 &lt;b&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/URL/URL&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;URL() 생성자&lt;/a&gt;&lt;/b&gt;를 사용했습니다. URL 생성자에 URL이 아닌 텍스트를 넣으면 에러를 던지기 때문에 다음과 같이 &lt;b&gt;try-catch문&lt;/b&gt;을 사용하여 일반 텍스트일 때와 링크일 때 서로 다른 작업을 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력한 값은 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;event.nativeEvent.data&lt;/span&gt;&lt;/b&gt;에서 가져왔습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1735907205788&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;textarea
    value={comment}
    onChange={(e) =&amp;gt; {
      try {
        const input = (e.nativeEvent as InputEvent).data;
        new URL(input);
        // URL이면 링크 삽입
      } catch (error) {
        // URL이 아니면
        setComment(e.target.value);
      }
    }}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2️⃣ URL 삽입하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력한 텍스트가 URL 형태이면 마크다운에서 링크를 나타내는 문법처럼 텍스트를 입력합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문법: (Caption)[URL]&lt;/li&gt;
&lt;li&gt;예시: (네이버)[&lt;a href=&quot;https://www.naver.com/&quot;&gt;https://www.naver.com/&lt;/a&gt;]&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변환한 텍스트를 현재 커서 위치에 입력하기 위해 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;event.currentTarget.selectionStart&lt;/b&gt;&lt;/span&gt; 값을 사용했습니다. &lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;텍스트가 랜더링되지 않았어도 selectionStart 값은 입력값의 길이를 포함&lt;/b&gt;&lt;/span&gt;하고 있기 때문에 그 길이를 뺀 값을 커서 위치로 사용했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1735908027324&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;textarea
    value={comment}
    onChange={(e) =&amp;gt; {
      try {
        const input = (e.nativeEvent as InputEvent).data;
        new URL(input);

        // URL이면 링크 삽입
        const inputLength = input?.length ?? 0;
        const link = `(${input})[${input}]`;
        const cursorPosition = e.currentTarget.selectionStart - inputLength;

        setComment(
          (prev) =&amp;gt;
            prev.substring(0, cursorPosition) +
            link +
            prev.substring(cursorPosition),
        );
      } catch (error) {
        // URL이 아니면
        setComment(e.target.value);
      }
    }}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;327&quot; data-origin-height=&quot;49&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9dTcy/btsLCEY1r46/bQN5SfTGMhEoNWF6HU4ZK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9dTcy/btsLCEY1r46/bQN5SfTGMhEoNWF6HU4ZK1/img.png&quot; data-alt=&quot;textarea에 입력한 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9dTcy/btsLCEY1r46/bQN5SfTGMhEoNWF6HU4ZK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9dTcy%2FbtsLCEY1r46%2FbQN5SfTGMhEoNWF6HU4ZK1%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;327&quot; height=&quot;49&quot; data-origin-width=&quot;327&quot; data-origin-height=&quot;49&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;textarea에 입력한 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3️⃣ 커서 위치 되돌리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;textarea에 URL을 붙여 넣으면 마크다운 문법처럼 잘 입력됐습니다. 하지만 substring을 사용하여 전체 텍스트를 다시 랜더링하니 &lt;b&gt;커서가 맨 마지막으로 이동&lt;/b&gt;했습니다. 다음은 &lt;b&gt;커서가 입력한 URL 뒤에 오도록 수정&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;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;useRef&lt;/b&gt;&lt;/span&gt;를 사용하여 커서 위치를 담을 변수(cursorPositionRef)를 생성합니다. &lt;b&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;Change 이벤트가 발생하면 이전 커서 위치를 해당 변수에 저장&lt;/span&gt;&lt;/b&gt;합니다. 그 다음 u&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;seEffect를 사용하여 cursorPositionRef 값을 textarea 커서 위치로 설정&lt;/b&gt;&lt;/span&gt;해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL이 아닌 일반 텍스트를 입력할 때도 있으니, &lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;커서 위치를 설정한 후에는 ref를 undefined로 초기화&lt;/b&gt;&lt;/span&gt;했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1735909412216&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const cursorPositionRef = useRef&amp;lt;number&amp;gt;();

/** 링크 삽입 후 커서가 맨 마지막으로 이동하는 것을 방지 */
useEffect(() =&amp;gt; {
    if (!cursorPositionRef.current) return;

    const textarea = document.activeElement; // focused element
    
    (textarea as HTMLTextAreaElement).setSelectionRange(
      cursorPositionRef.current,
      cursorPositionRef.current,
    );
    cursorPositionRef.current = undefined;
}, [editingComment]);
//...

return &amp;lt;textarea
    value={comment}
    onChange={(e) =&amp;gt; {
      try {
        const input = (e.nativeEvent as InputEvent).data;
        new URL(input);

        // URL이면 링크 삽입
        const inputLength = input?.length ?? 0;
        const link = `(${input})[${input}]`;
        const cursorPosition = e.currentTarget.selectionStart - inputLength;
        // 커서 위치 저장
        cursorPositionRef.current = cursorPosition + inputLength * 2 + 4;

        setComment(
          (prev) =&amp;gt;
            prev.substring(0, cursorPosition) +
            link +
            prev.substring(cursorPosition),
        );
      } catch (error) {
        // URL이 아니면
        setComment(e.target.value);
      }
    }}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4️⃣ 선택한 텍스트 가져오기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 텍스트를 선택하고 URL을 붙여넣으면 선택한 텍스트가 캡션에 입력되도록 구현한 과정입니다. 사용자의 선택을 감지하기 위해 &lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;Select 이벤트&lt;/b&gt;&lt;/span&gt;를 사용하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737001296905&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; const selectionRef = useRef&amp;lt;string&amp;gt;();
 
 return &amp;lt;textarea
    onSelect={() =&amp;gt; {
      // 사용자가 선택한 텍스트 저장
      const selected = window.getSelection()?.toString();

      if (selected) selectionRef.current = selected;
      if (selected?.trim() === '') {
        selectionRef.current = undefined;
      }
    }}
    //...
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택한 텍스트를 Change 이벤트가 발생했을 때 사용해야 되기 때문에 커서 위치와 마찬가지로 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;useRef&lt;/b&gt;&lt;/span&gt;에 선택한 텍스트를 저장했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;Select 이벤트는 입력 필드를 그냥 클릭했을 때도 발생&lt;/b&gt;&lt;/span&gt;하는데, 이때는 선택한 텍스트가 빈 문자열로 넘어옵니다. 그러면&amp;nbsp; &lt;b&gt;selectionRef에 undefined를 저장하여 빈 캡션에 URL이 들어가지 않도록 방지&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;선택한 텍스트는 Change 핸들러에서 다음과 같이 사용했습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737003833708&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;textarea
    onSelect={() =&amp;gt; {
      // 사용자가 선택한 텍스트 저장
      const selected = window.getSelection()?.toString();

      if (selected) selectionRef.current = selected;
      if (selected?.trim() === '') {
        selectionRef.current = undefined;
      }
    }}
    onChange={(e) =&amp;gt; {
      try {
        const input = (e.nativeEvent as InputEvent).data;
        new URL(input ?? '');
        
        const inputLength = input?.length ?? 0;
        const cursorPosition =
          e.currentTarget.selectionStart - inputLength;
        let link = '';
		
        // 선택한 텍스트가 있으면
        if (selectionRef.current) {
          link = `(${selectionRef.current})[${input}]`;
          // 이전 커서 위치 저장
          cursorPositionRef.current =
            cursorPosition +
            inputLength +
            selectionRef.current.length +
            4;
        } else {
          link = `(${input})[${input}]`;
          cursorPositionRef.current =
            cursorPosition + inputLength * 2 + 4;
        }

        setEditingComment(
          (prev) =&amp;gt;
            prev.substring(0, cursorPosition) +
            link +
            prev.substring(
              cursorPosition +
                (selectionRef.current?.length ?? 0),
            ),
        );
      } catch (error) {
        // URL이 아니면
        setEditingComment(e.target.value);
      }
    }}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택한 텍스트가 있으면 캡션에 input 대신 선택한 텍스트를 넣어서 링크를 만들어줬습니다. 커서 위치도 input 길이 대신 선택한 텍스트 길이를 반영하여 cursorPosition에 저장하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 텍스트가 캡션에 잘 들어가는 걸 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;519&quot; data-origin-height=&quot;74&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HwFpK/btsLP7ZEkfC/hZbcFqjSVzJgKAYYC1Ak91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HwFpK/btsLP7ZEkfC/hZbcFqjSVzJgKAYYC1Ak91/img.png&quot; data-alt=&quot;텍스트에 링크 삽입&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HwFpK/btsLP7ZEkfC/hZbcFqjSVzJgKAYYC1Ak91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHwFpK%2FbtsLP7ZEkfC%2FhZbcFqjSVzJgKAYYC1Ak91%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;519&quot; height=&quot;74&quot; data-origin-width=&quot;519&quot; data-origin-height=&quot;74&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;텍스트에 링크 삽입&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5️⃣ Link로 변환하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자가 입력한 URL을 (Caption)[URL] 형태로 변환하여 저장했으니, 사용자에게 보여줄 때는 (Caption)[URL]를 Link로 변환하여 보여줬습니다. 아래는 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;String.replace&lt;/b&gt;&lt;/a&gt; 메소드로 URL을 찾아 Link로 변환하는 함수입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737008119254&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 링크는 Link element로 변경 */
const parseLink = (text: string) =&amp;gt; {
  const regex = /\((.*?)\)\[(.*?)\]/g; // (Caption)[URL] 형태
  let startIndex = 0;
  const result = [];

  text.replace(regex, (match, caption, url, offset, string) =&amp;gt; {
    const element = (
      &amp;lt;Link
      	key={offset}
        to={url}
        className=&quot;text-primary underline&quot;
        target=&quot;_blank&quot;
        rel=&quot;noopener noreferrer&quot;
      &amp;gt;
        {caption}
      &amp;lt;/Link&amp;gt;
    );

    result.push(string.substring(startIndex, offset), element);
    startIndex = offset + match.length; // 다음 시작 인덱스 갱신
    return '';
  });
  result.push(text.substring(startIndex)); // 마지막 텍스트 저장

  return result;
};

//...

return &amp;lt;div&amp;gt;{parseLink(comment)}&amp;lt;/div&amp;gt;&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;String.replace 메소드를 사용하여 전체 텍스트에서 (Caption)[URL] 부분을 찾습니다.&lt;/li&gt;
&lt;li&gt;replace로 받은 caption과 url로 Link element를 생성하고,&lt;/li&gt;
&lt;li&gt;이전 텍스트와 element를 순서대로 배열에 저장합니다.&lt;/li&gt;
&lt;li&gt;이때 다음 시작 인덱스를 replace의 offset과 match 길이를 더하여 갱신합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'아니, replace에 이런 arguments가 있었어?'라는 생각이 들 정도로 replace가 많은 값들을 넘겨주더라고요. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_the_replacement&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN 문서&lt;/a&gt;를 살펴보면 &lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;replace의 arguments&lt;/b&gt;&lt;/span&gt;는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1737008667052&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function replacer(match, p1, p2, /* &amp;hellip;, */ pN, offset, string, groups) {
  return replacement;
}&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;&lt;b&gt;match&lt;/b&gt;: 매칭된 문자열 (예: (네이버)[&lt;a href=&quot;https://www.naver.com/&quot;&gt;https://www.naver.com/&lt;/a&gt;])&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p1, p2, /* &amp;hellip;, */ pN&lt;/b&gt;: regex에 매칭된 문자열에서 n번째 문자열 (예: p1 = 네이버, p2 = &lt;a href=&quot;https://www.naver.com/&quot;&gt;https://www.naver.com/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;offset&lt;/b&gt;: 매칭된 문자열의 시작 인덱스&lt;/li&gt;
&lt;li&gt;&lt;b&gt;string&lt;/b&gt;: 전체 문자열&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✨ 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 과정을 거쳐 관리자가 입력한 링크가 텍스트에 잘 삽입되었습니다. 다른 에디터에 비하면 허접한 링크 삽입 기능이지만, 관리자분들이 불편함 없이 잘 사용하길 바라며.. 이만 줄이겠습니다. 읽어주셔서 감사합니다 :)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Qsip/btsLP1ZEmH2/BSIKPzulU5o8ghiPg33DKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Qsip/btsLP1ZEmH2/BSIKPzulU5o8ghiPg33DKk/img.png&quot; data-alt=&quot;USER 페이지 코멘트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Qsip/btsLP1ZEmH2/BSIKPzulU5o8ghiPg33DKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Qsip%2FbtsLP1ZEmH2%2FBSIKPzulU5o8ghiPg33DKk%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;463&quot; height=&quot;138&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;USER 페이지 코멘트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  챌린지</category>
      <category>link변환</category>
      <category>React</category>
      <category>textArea</category>
      <category>url변환</category>
      <category>url삽입</category>
      <category>개발챌린지</category>
      <category>링크삽입</category>
      <category>주소변환</category>
      <author>yeonDev</author>
      <guid isPermaLink="true">https://seyeon.tistory.com/119</guid>
      <comments>https://seyeon.tistory.com/119#entry119comment</comments>
      <pubDate>Fri, 3 Jan 2025 22:38:12 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 16235 나무 재테크 - Node.js</title>
      <link>https://seyeon.tistory.com/118</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제: &lt;a href=&quot;https://www.acmicpc.net/problem/16235&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/16235&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 풀이&lt;/b&gt;&lt;/h3&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;i&gt;trees[x, y ,z] &lt;/i&gt;&lt;/li&gt;
&lt;li&gt;나이가 어린 나무부터 양분을 섭취하므로, &lt;i&gt;trees&lt;/i&gt;&amp;nbsp;배열을 나이를 기준으로 &lt;b&gt;오름차순&lt;/b&gt; 정렬한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반복문&lt;/b&gt;을 통해 K번 봄, 여름, 가을, 겨울을 거친다.&lt;/li&gt;
&lt;li&gt;죽은 나무 &lt;i&gt;deadTrees&lt;/i&gt;, 살아있는 나무 &lt;i&gt;aliveTrees&lt;/i&gt;로 배열을 생성하여 한 해를 보낸다.&lt;/li&gt;
&lt;li&gt;&lt;i&gt;trees&lt;/i&gt; 배열을 비우고, 가을에 새로 번식한 나무와 함께 살아있는 나무 &lt;i&gt;aliveTrees&lt;/i&gt;를 &lt;i&gt;trees&lt;/i&gt; 배열에 저장한다.&lt;br /&gt;(이때, 번식한 나무의 나이가 1이고 &lt;i&gt;aliveTrees&lt;/i&gt;는 이미 오름차순 정렬되어 있으므로,&lt;b&gt; 다시 정렬할 필요가 없다&lt;/b&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;전체 코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1733023426820&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const fs = require(&quot;fs&quot;);
const [[n, m, k], ...data] = fs.readFileSync(&quot;/dev/stdin&quot;).toString().trim().split(&quot;\n&quot;)
    .map(line =&amp;gt; line.split(&quot; &quot;).map(Number));
const A = data.slice(0, n);
let trees = data.slice(n);
const graph = Array.from(new Array(n), () =&amp;gt; Array(n).fill(5)); // 현재 양분
const dr = [-1, -1, -1, 0, 1, 1, 1, 0];
const dc = [-1, 0, 1, 1, 1, 0, -1, -1];

// 나이 오름차순 정렬
trees.sort((a, b) =&amp;gt; {
    return a[2] - b[2];
})

// K년 후
for (let i = 0; i &amp;lt; k; i++) {
    const aliveTrees = [];
    const deadTrees = [];
    
    /* 봄 */
    for (const [r, c, a] of trees) {
        // 죽은 나무
        if (graph[r-1][c-1] &amp;lt; a) {
            deadTrees.push([r, c, a]);
            continue;
        }
        // 나무 양분 섭취
        graph[r-1][c-1] -= a;
        aliveTrees.push([r, c, a+1]);
    }
    trees = []; // 기존 배열 비우기
    
    /* 여름 */
    for (const [r, c, a] of deadTrees) {
        graph[r-1][c-1] += Math.floor(a / 2); // 죽은 나무 양분 저장
    }
    
    /* 가을 */
    for (const [r, c, a] of aliveTrees) {
        if (a % 5 !== 0) continue; // 번식할 수 없는 나무
        
        for (let d = 0; d &amp;lt; 8; d++) {
            const nr = r + dr[d];
            const nc = c + dc[d];
            
            // 땅을 벗어나면 패스
            if (nr &amp;lt; 1 || nr &amp;gt; n || nc &amp;lt; 1 || nc &amp;gt; n) continue;
            trees.push([nr, nc, 1]); // 새 나무 번식
        }
    }
    trees.push(...aliveTrees); // 살아있는 나무 저장
    
    /* 겨울 */
    for (let r = 0; r &amp;lt; n; r++) {
        for (let c = 0; c &amp;lt; n; c++) {
            graph[r][c] += A[r][c]; // 양분 추가
        }
    }
}

console.log(trees.length);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>  알고리즘</category>
      <category>16235</category>
      <category>node.js</category>
      <category>구현</category>
      <category>나무재테크</category>
      <category>문제풀이</category>
      <category>백준</category>
      <category>알고리즘</category>
      <category>전체코드</category>
      <category>코딩테스트</category>
      <author>yeonDev</author>
      <guid isPermaLink="true">https://seyeon.tistory.com/118</guid>
      <comments>https://seyeon.tistory.com/118#entry118comment</comments>
      <pubDate>Sun, 1 Dec 2024 12:26:54 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] 연도별 대장균 크기의 편차 구하기 - SQL</title>
      <link>https://seyeon.tistory.com/117</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/299310&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/299310&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1732844136934&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;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/299310&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GGxLf/hyXDgxVjxr/T39MsC1qY44n8ViAiuD1L1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bMHt1z/hyXGKc2Yev/JHzX2kPf09g471PWacg1rK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/299310&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/299310&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GGxLf/hyXDgxVjxr/T39MsC1qY44n8ViAiuD1L1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bMHt1z/hyXGKc2Yev/JHzX2kPf09g471PWacg1rK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&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;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 먼저 연도별 가장 큰 대장균의 크기(MAX_SIZE)를 구한다.&lt;/p&gt;
&lt;pre id=&quot;code_1732844211809&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT MAX(SIZE_OF_COLONY) AS MAX_SIZE, YEAR(DIFFERENTIATION_DATE) AS YEAR
FROM ECOLI_DATA 
GROUP BY YEAR&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;2. 1번 테이블(T)과 ECOLI_DATA 테이블을 조인하여, 각 대장균의 크기 편차를 구한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ECOLI_DATA 테이블에 없는 연도로 조인해야 하므로, LEFT JOIN을 사용하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT T.YEAR, (T.MAX_SIZE - SIZE_OF_COLONY) AS YEAR_DEV, ID
FROM ECOLI_DATA E
LEFT JOIN T
ON YEAR(E.DIFFERENTIATION_DATE) = T.YEAR
&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;3. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;연도(YEAR)와 편차(YEAR_DEV)로 오름차순 정렬한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732844313084&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT T.YEAR, (T.MAX_SIZE - SIZE_OF_COLONY) AS YEAR_DEV, ID
FROM ECOLI_DATA E
LEFT JOIN T
ON YEAR(E.DIFFERENTIATION_DATE) = T.YEAR
ORDER BY T.YEAR, YEAR_DEV; -- 정렬 추가&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;전체 코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1732844363010&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT T.YEAR, (T.MAX_SIZE - SIZE_OF_COLONY) AS YEAR_DEV, ID
FROM ECOLI_DATA E
LEFT JOIN 
    (SELECT MAX(SIZE_OF_COLONY) AS MAX_SIZE, YEAR(DIFFERENTIATION_DATE) AS YEAR
    FROM ECOLI_DATA 
    GROUP BY YEAR) T
ON YEAR(E.DIFFERENTIATION_DATE) = T.YEAR
ORDER BY T.YEAR, YEAR_DEV;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>  알고리즘</category>
      <category>sql</category>
      <category>문제풀이</category>
      <category>알고리즘</category>
      <category>연도별 대장균 크기의 편차 구하기</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <author>yeonDev</author>
      <guid isPermaLink="true">https://seyeon.tistory.com/117</guid>
      <comments>https://seyeon.tistory.com/117#entry117comment</comments>
      <pubDate>Fri, 29 Nov 2024 10:41:39 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 15686 치킨 배달 - Node.js</title>
      <link>https://seyeon.tistory.com/116</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/15686&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/15686&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 풀이&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;n, m의 크기가 작으므로, &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;으로 치킨집을 M개 선택하는 모든 경우를 구한다. 선택한 치킨집은 별도의 배열에 저장한다.&lt;/li&gt;
&lt;li&gt;치킨집 M개를 모두 선택했을 때, 선택한 치킨집을 기반으로 각 집마다 &lt;b&gt;치킨 거리&lt;/b&gt;를 계산하고, 도시의 치킨 거리를 구한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1732331206322&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const fs = require(&quot;fs&quot;);
const [[n, m], ...graph] = fs.readFileSync(&quot;/dev/stdin&quot;).toString().trim().split(&quot;\n&quot;)
    .map(line =&amp;gt; line.split(&quot; &quot;).map(Number));
const houses = []; // 집의 좌표 배열
const chickens = []; // 치킨 좌표 배열
const selectedChickens = []; // 선택된 치킨집
let answer = Infinity;

// 집, 치킨 좌표 저장
for (let r = 0; r &amp;lt; n; r++) {
    for (let c = 0; c &amp;lt; n; c++) {
        if (graph[r][c] === 1) houses.push([r, c]);
        if (graph[r][c] === 2) chickens.push([r, c]);        
    }
}

// 완전 탐색 수행하고 정답 출력
dfs(0, 0);
console.log(answer);

function dfs(count, startIndex) {
    // M개를 모두 선택했으면 정답 갱신 및 종료
    if (count === m) {
        answer = Math.min(answer, calculateCityChickenDist());
        return;
    }
    
    for (let i = startIndex; i &amp;lt; chickens.length; i++) {
        // M개를 선택할 수 없으면 종료
        if (chickens.length - i + count &amp;lt; m) return;
        // 백트래킹
        selectedChickens.push(chickens[i]);
        dfs(count + 1, i + 1);
        selectedChickens.pop();
    }
}

// 선택한 치킨집으로 기반으로, 도시의 치킨 거리 계산
function calculateCityChickenDist() {
    let sum = 0;
    for (const [hr, hc] of houses) {
        let dist = Infinity;
        for (const [cr, cc] of selectedChickens) {
            dist = Math.min(dist, calculateDist(hr, hc, cr, cc));
        }
        sum += dist;
    }
    
    return sum;
}

// 두 좌표 사이의 거리 계산
function calculateDist(r1, c1, r2, c2) {
    return Math.abs(r1-r2) + Math.abs(c1-c2);
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>  알고리즘</category>
      <category>15686</category>
      <category>node.js</category>
      <category>nodeJS</category>
      <category>문제풀이</category>
      <category>백준</category>
      <category>백트래킹</category>
      <category>알고리즘</category>
      <category>오블완</category>
      <category>치킨배달</category>
      <category>티스토리챌린지</category>
      <author>yeonDev</author>
      <guid isPermaLink="true">https://seyeon.tistory.com/116</guid>
      <comments>https://seyeon.tistory.com/116#entry116comment</comments>
      <pubDate>Sat, 23 Nov 2024 12:08:29 +0900</pubDate>
    </item>
    <item>
      <title>이미지 크기를 조정하고 Base64로 인코딩하기</title>
      <link>https://seyeon.tistory.com/114</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 이미지 주소나 이미지 파일을 업로드했을 때, 이미지 크기를 조정하고 Base64로 인코딩하는 과정을 알아보겠습니다.&lt;/p&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;클라이언트: Typescript&lt;/li&gt;
&lt;li&gt;서버: Node.js + Express +&amp;nbsp; Typescript&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;사용자가 업로드 한 이미지를 서버에서 Base64로 인코딩해야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이미지 가로x세로 크기 조정을 서버와 클라이언트 중 어디에서 할까?&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 용량이 큰 이미지를 업로드했을 때를 고려하여, &lt;b&gt;클라이언트에서 크기를 조정하고 용량을 줄인 후 서버로 전송&lt;/b&gt;하겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;클라이언트에서 서버로 이미지를 전송할 때 어떤 형태로 전송할까? (이미지 주소, 파일, Blob, Base64 등)&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트에서 Canvas를 사용하여 이미지 크기를 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;Canvas의 toBlob 함수를 사용하여 크기를 줄인 이미지를 이진 데이터 형태로 받을 수 있기 때문에 &lt;b&gt;Blob&lt;/b&gt;을 서버에 전송하겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+) 클라이언트에서 Base64로 인코딩해도 되지 않을까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FileReader 객체로 이미지를 읽은 후 Base64로 인코딩할 수 있습니다.&lt;/li&gt;
&lt;li&gt;하지만 어차피 서버에 이미지를 전달해야 하는 상황이고, 미미한 차이지만 Base64로 인코딩하면 사이즈가 기존보다 4/3 (약 33%) 증가하기 때문에 서버에서 인코딩하도록 하겠습니다.&lt;/li&gt;
&lt;li&gt;참고 자료: &lt;a href=&quot;https://bharatkalluri.com/posts/base64-size-increase-explanation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Why&amp;nbsp;is&amp;nbsp;a&amp;nbsp;base&amp;nbsp;64&amp;nbsp;encoded&amp;nbsp;file&amp;nbsp;33%&amp;nbsp;larger&amp;nbsp;than&amp;nbsp;the&amp;nbsp;original?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1730947117416&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;Why is a base 64 encoded file 33% larger than the original?&quot; data-og-description=&quot;Understanding what is base64 encoding &amp;amp; how it works&quot; data-og-host=&quot;bharatkalluri.com&quot; data-og-source-url=&quot;https://bharatkalluri.com/posts/base64-size-increase-explanation&quot; data-og-url=&quot;https://bharatkalluri.com/posts/base64-size-increase-explanation&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/MpmnK/hyXwlxzkYa/7m4K7mq57WrafBUipK5ko1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://bharatkalluri.com/posts/base64-size-increase-explanation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://bharatkalluri.com/posts/base64-size-increase-explanation&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/MpmnK/hyXwlxzkYa/7m4K7mq57WrafBUipK5ko1/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;Why is a base 64 encoded file 33% larger than the original?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Understanding what is base64 encoding &amp;amp; how it works&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;bharatkalluri.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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트에서 이미지 크기를 조정한다 (with Canvas)&lt;/li&gt;
&lt;li&gt;이미지 주소와 파일을 Blob 데이터로 변환하여 서버에 전송한다.&lt;/li&gt;
&lt;li&gt;서버에서 Blob 데이터를 Base64로 인코딩한다. (with Buffer)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  캔버스로 이미지 크기 조정하고 Blob으로 반환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 이미지 주소를 제출하거나 드롭다운으로 이미지 파일을 업로드합니다. &lt;b&gt;이미지 주소일 경우 img src 속성에 바로 넘기면 되고, 이미지 파일일 경우 URL.createObjectURL() 함수를 사용하여 url로 변환 후 img src 속성에 넘겨줍니다&lt;/b&gt;. 캔버스를 활용한 이미지 크기 조정 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730947529391&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function resizeImage(
  fileOrUrl: File | string,
  width: number,
  height: number
): Promise&amp;lt;Blob | null&amp;gt; {
  return new Promise((resolve, reject) =&amp;gt; {
    const img = new Image();
    // 외부 데이터를 캔버스에 그리고 Blob으로 변환할 경우 CORS 에러가 발생한다.
    // 이를 허용하기 위해 crossOrigin 속성을 사용한다.
    // 출처: https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
    img.crossOrigin = &quot;anonymous&quot;; 

    // 이미지 주소인 경우
    if (typeof fileOrUrl === &quot;string&quot;) img.src = fileOrUrl;
    // 파일인 경우
    else img.src = URL.createObjectURL(fileOrUrl as Blob);

    img.onload = () =&amp;gt; {
      const canvas = document.createElement(&quot;canvas&quot;);
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext(&quot;2d&quot;);
      ctx?.drawImage(img, 0, 0, width, height);
      
      // 크기를 조정한 이미지를 blob으로 반환
      canvas.toBlob((blob) =&amp;gt; {
        resolve(blob);
      }, &quot;image/png&quot;);
    };
    
    // [에러 처리] 이미지 주소가 아닌 주소를 넘기면 에러가 발생
    img.onerror = (error) =&amp;gt; reject(error); 
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Blob 데이터 서버로 전송하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blob은 이진데이터를 다루는 객체입니다. 이진 데이터를 서버로 전송하려면 Content-Type을 &lt;span style=&quot;background-color: #f2f1f1; color: #1b1b1b; text-align: start;&quot;&gt;application/octet-stream&lt;/span&gt; 으로 설정합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;175&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7nHMt/btsKzTnJYy3/1gUWV7NIo03GivH1V2yTU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7nHMt/btsKzTnJYy3/1gUWV7NIo03GivH1V2yTU1/img.png&quot; data-alt=&quot;출처: https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types#application&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7nHMt/btsKzTnJYy3/1gUWV7NIo03GivH1V2yTU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7nHMt%2FbtsKzTnJYy3%2F1gUWV7NIo03GivH1V2yTU1%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;780&quot; height=&quot;175&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;175&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types#application&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인이 구현한 api 주소로 &lt;b&gt;POST&lt;/b&gt; 요청을 날려줍니다. 코드는 아래와 같이 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730948057815&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function postImage(blob: Blob) {
  return await fetch(&quot;your api url&quot;, {
    method: &quot;POST&quot;,
    headers: { &quot;Content-Type&quot;: &quot;application/octet-stream&quot; },
    body: blob,
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  서버에서 이진 데이터를 Base64로 인코딩하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request body에 담긴 blob을 Buffer로 응답받습니다. 클라이언트에서 이진 데이터를 다루는 객체가 Blob이라면, &lt;b&gt;서버에서 이진 데이터를 다루는 객체는 Buffer&lt;/b&gt;라고 합니다. Buffer 객체에 toString 메소드를 사용하여 base64로 쉽게 인코딩할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1730948272851&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiRouter.post(&quot;your api url&quot;, encodeBase64);

async function encodeBase64(req: Request, res: Response) {
  const buffer = req.body as Buffer; // Buffer 타입 지정
  const base64 = buffer.toString(&quot;base64&quot;); // 인코딩 완료
  //...
  res.send(&quot;ok&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 이미지 크기 조정, 인코딩 모두 서버에서 하는 방식으로 구현했었습니다. 이미지 크기가 작으면 문제가 없었지만, 이미지 크기가 클 경우 인코딩할 때까지 7~8초의 시간이 걸리기도 했습니다   그러다 생각한 방법이 Canvas로 클라이언트에서 이미지 크기를 조정하고 서버로 전송하는 것이었습니다. 그 결과 이미지 크기가 커도 (너비가 약 1300~2000px 정도 되는 이미지여도) 2초대에서 이미지 전송 및 인코딩을 마칠 수 있었습니다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;(+ 이렇게 하면 디스크에 이미지를 저장하는 Multer와 이미지 크기 조정에 필요한 Sharp 라이브러리도 필요하지 않더라고요!)&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;/p&gt;</description>
      <category>  챌린지</category>
      <category>Base64</category>
      <category>BLOB</category>
      <category>buffer</category>
      <category>Canvas</category>
      <category>이미지처리</category>
      <category>이미지최적화</category>
      <category>이미지크기</category>
      <category>이미지크기조정</category>
      <category>인코딩</category>
      <author>yeonDev</author>
      <guid isPermaLink="true">https://seyeon.tistory.com/114</guid>
      <comments>https://seyeon.tistory.com/114#entry114comment</comments>
      <pubDate>Fri, 1 Nov 2024 14:35:08 +0900</pubDate>
    </item>
    <item>
      <title>백준 14503 로봇 청소기 - Node.js</title>
      <link>https://seyeon.tistory.com/113</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 &lt;b&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/14503&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;&lt;/b&gt;에서 확인&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제풀이&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N, M의 크기가 크지 않고 모든 방향을 탐색해야 하므로 &lt;b&gt;완전 탐색&lt;/b&gt;을 사용한다. 청소 가능한 방향을 우선으로 깊이 탐색하므로 &lt;b&gt;DFS&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;1.청소한 곳을 방문 처리할 visited 배열을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 문제에 적힌 대로 북동남서(0, 1, 2, 3) 방향 배열을 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1728960352930&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 북, 동, 남, 서
const dr = [-1, 0, 1, 0];
const dc = [0, 1, 0, -1];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 현재 위치를 방문처리 하고 DFS를 수행한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;POINT&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;반시계 방향으로 회전하므로 방향은 +1이 아닌 &lt;b&gt;+3&lt;/b&gt;으로 계산해야 한다.&lt;/li&gt;
&lt;li&gt;네 방향 모두 청소하여 후진할 때는 &lt;b&gt;방향을 유지한 채로&lt;/b&gt; 이동한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1728960812754&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const fs = require(&quot;fs&quot;);
const [[n, m], [r, c, d], ...graph] = fs.readFileSync(&quot;/dev/stdin&quot;).toString().trim().split(&quot;\n&quot;)
    .map(line =&amp;gt; line.split(&quot; &quot;).map(Number));
const visited = Array.from(new Array(n), () =&amp;gt; Array(m).fill(false));
// 북동남서
const dr = [-1, 0, 1, 0];
const dc = [0, 1, 0, -1];
let answer = 1; // 현재 위치 카운트

visited[r][c] = true; // 현재 위치 방문 처리
dfs(r, c, d);
console.log(answer);
        
function dfs(r, c, d) {    
    for (let i = 0; i &amp;lt; 4; i++) {
        d = (d + 3) % 4; // 반시계 방향으로 90도 회전
        const nr = r + dr[d];
        const nc = c + dc[d];
        
        // 범위를 벗어나면 패스
        if (nr &amp;lt; 0 || nr &amp;gt;= n || nc &amp;lt; 0 || nc &amp;gt;= m) continue;
        // 벽이면 패스
        if (graph[nr][nc] === 1) continue;
        // 아직 청소하지 않았으면
        if (graph[nr][nc] === 0 &amp;amp;&amp;amp; !visited[nr][nc]) {
            visited[nr][nc] = true;
            answer++;
            dfs(nr, nc, d);
            return; // 네 방향 탐색 종료
        }
    }
    
    // 네 방향 모두 이미 청소했으면
    d = (d + 2) % 4; // 후진 방향
    const nr = r + dr[d];
    const nc = c + dc[d];

    // 범위를 벗어나면 종료
    if (nr &amp;lt; 0 || nr &amp;gt;= n || nc &amp;lt; 0 || nc &amp;gt;= m) return;
    // 벽이면 종료
    if (graph[nr][nc] === 1) return;
    // 빈 칸이면 후진
    if (graph[nr][nc] === 0) dfs(nr, nc, (d + 2) % 4); // 방향을 유지한 채로 후진
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>  알고리즘</category>
      <category>14503</category>
      <category>dfs</category>
      <category>javascript</category>
      <category>nodeJS</category>
      <category>로봇청소기</category>
      <category>문제풀이</category>
      <category>백준</category>
      <category>알고리즘</category>
      <category>완전탐색</category>
      <author>yeonDev</author>
      <guid isPermaLink="true">https://seyeon.tistory.com/113</guid>
      <comments>https://seyeon.tistory.com/113#entry113comment</comments>
      <pubDate>Tue, 15 Oct 2024 11:55:41 +0900</pubDate>
    </item>
    <item>
      <title>이벤트 루프(Event Loop)를 알면 좋은 점</title>
      <link>https://seyeon.tistory.com/112</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣ &lt;b&gt;이벤트 루프란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Javascript가 브라우저에서 동작하는 방식입니다. 브라우저 JS 런타임은 이벤트 루프에 기반하여 실행되며, 구성은 다음과 같습니다.&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;Call Stack&lt;/b&gt;: 스택에 함수가 저장되었다가 순차적으로 실행된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Web API 컨테이너&lt;/b&gt;: &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;브라우저에서 제공하는 API&lt;/a&gt;가 콜 스택에서 호출되면, 함수가 Web API 컨테이너에 저장된다. 함수 실행 시점이 되면 함수(callback)를 콜백 큐로 이동시킨다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Callback Queue&lt;/b&gt;: 콜백 큐에 저장된 함수(callback)는 이벤트 루프에 의해 순차적으로 콜 스택으로 이동한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Event Loop&lt;/b&gt;: 콜 스택이 모두 비어있을 때 콜백 큐에 저장된 함수를 콜 스택으로 이동시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ecXpAU/btsJW5PFGXI/ebzFyXCJX0Fg9Pypp2Sq41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ecXpAU/btsJW5PFGXI/ebzFyXCJX0Fg9Pypp2Sq41/img.png&quot; data-alt=&quot;출처: ✨♻️ JavaScript Visualized: Event Loop (https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ecXpAU/btsJW5PFGXI/ebzFyXCJX0Fg9Pypp2Sq41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FecXpAU%2FbtsJW5PFGXI%2FebzFyXCJX0Fg9Pypp2Sq41%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;1000&quot; height=&quot;420&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: ✨♻️ JavaScript Visualized: Event Loop (https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣ &lt;b&gt;왜 이벤트 루프를 만들었을까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;싱글 스레드&lt;/b&gt;인 Javascript가 브라우저에서 &lt;b&gt;동시성&lt;/b&gt;(Concurrency)을 갖기 위해서 입니다.&lt;/p&gt;
&lt;div id=&quot;code_1728201805889&quot; data-ke-type=&quot;html&quot; data-source=&quot; &amp;lt;section
      style=&amp;quot;
        border: 1px solid lightgrey;
        border-radius: 1rem;
        padding: 1rem;
        margin: 1rem 0;
      &amp;quot;
    &amp;gt;
      &amp;lt;span
        &amp;gt;&amp;lt;span style=&amp;quot;font-weight: bold&amp;quot;&amp;gt;동시성&amp;lt;/span&amp;gt;(Concurrency)이란?&amp;lt;/span
      &amp;gt;
      &amp;lt;p&amp;gt;
        시간을 잘게 나누어 각 시간마다 서로 다른 일을 번갈아 작업한다. 마치
        동시에 여러 작업을 처리하는 것 같지만 실제로는 한 번에 하나의 일만 한다.
      &amp;lt;/p&amp;gt;
      &amp;lt;br /&amp;gt;
      &amp;lt;span
        &amp;gt;&amp;lt;span style=&amp;quot;font-weight: bold&amp;quot;&amp;gt;병렬성/평행성&amp;lt;/span&amp;gt;(Parallelism)이란?&amp;lt;/span
      &amp;gt;
      &amp;lt;p&amp;gt;
        같은 시간 동안 여러 작업을 처리하는 것으로, 멀티스레드가 병렬성을
        가진다.
      &amp;lt;/p&amp;gt;
    &amp;lt;/section&amp;gt;&quot;&gt;
&lt;section style=&quot;border: 1px solid lightgrey; border-radius: 1rem; padding: 1rem; margin: 1rem 0;&quot;&gt;&lt;span&gt;&lt;span style=&quot;font-weight: bold;&quot;&gt;동시성&lt;/span&gt;(Concurrency)이란?&lt;/span&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간을 잘게 나누어 각 시간마다 서로 다른 일을 번갈아 작업한다. 마치 동시에 여러 작업을 처리하는 것 같지만 실제로는 한 번에 하나의 일만 한다.&lt;/p&gt;
&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;font-weight: bold;&quot;&gt;병렬성/평행성&lt;/span&gt;(Parallelism)이란?&lt;/span&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 시간 동안 여러 작업을 처리하는 것으로, 멀티스레드가 병렬성을 가진다.&lt;/p&gt;
&lt;/section&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;API를 통해 데이터를 가져오거나 이벤트 처리 등 웹 서비스에서 비동기로 처리해야 하는 작업이 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;하지만 싱글 스레드인 JS는 한 번에 하나의 일만 순차적으로 할 수 있습니다. 즉, 중간에 긴 시간을 소요하는 작업이 있다면 JS는 다음 작업으로 넘어가지 못하고, 사용자 입장에서 웹이 멈췄다고 생각할 겁니다.&lt;/p&gt;
&lt;pre id=&quot;code_1728364698382&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;console.log(&quot;시작&quot;);
runLongTask();
console.log(&quot;긴 시간 뒤에 실행&quot;);

function runLongTask() {
    let count = 0;
    for (let i = 0; i &amp;lt; 1e9; i++) {
    	count++;
    }
}

// 시작
// (긴 시간 소요)
// 긴 시간 뒤에 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 Event Loop와 브라우저에서 제공하는 Web APIs 덕분에 JS는 복잡하고 긴 작업을&lt;b&gt; 비동기&lt;/b&gt;로 처리할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3️⃣&lt;b&gt; 이벤트 루프 동작 방식&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5sNrs/btsJZqTUiVY/Jj8WY2AakbhDVDMxmyhK0K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5sNrs/btsJZqTUiVY/Jj8WY2AakbhDVDMxmyhK0K/img.gif&quot; data-alt=&quot;출처: How JavaScript Works: Web APIs, Callback Queue, and Event Loop (https://dev.to/bipinrajbhar/how-javascript-works-web-apis-callback-queue-and-event-loop-2p3e)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5sNrs/btsJZqTUiVY/Jj8WY2AakbhDVDMxmyhK0K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/5sNrs/btsJZqTUiVY/Jj8WY2AakbhDVDMxmyhK0K/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;420&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: How JavaScript Works: Web APIs, Callback Queue, and Event Loop (https://dev.to/bipinrajbhar/how-javascript-works-web-apis-callback-queue-and-event-loop-2p3e)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에 보이는 코드를 실행하면 다음과 같은 결과가 나옵니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1728367060115&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;start
end
I sec delay&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS는 콜 스택에 저장된 함수를 순차적으로 실행하며, setTimeout도 순서에 맞게 실행됩니다. 다만 Web API 함수는 콜 스택에서 빠져나와 Web API 컨테이너에 저장되며, &lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;JS는 callback이 실행되길 기다리지 않고 바로 다음 작업을 합니다&lt;/b&gt;&lt;/span&gt;. 타이머를 체크하고 delay 만큼 대기하는 작업을 브라우저가 대신하며, 대기가 끝나면 setTimeout에 연결된 callback은 콜백 큐로 이동합니다. 이때 콜 스택이 비어있다면 ,이벤트 루프에 의해 콜백 큐에 저장된 함수를 실행합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;4️⃣&lt;b&gt; 이벤트 루프를 알면 좋은 점&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 루프 덕분에 JS가 브라우저에서 비동기 작업을 처리할 수 있다는 건 알겠습니다. 하지만 이 개념이 실전에서 어떻게 사용될 수 있을까요? 그건 콜백 큐를 더 자세히 살펴봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 큐는 실제로 세 가지 큐로 구성되어 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;(Macro)&lt;b&gt;Task Queue&lt;/b&gt;: setTimeout, setInterval, DOM, URL 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Animation Frame Queue&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Microtask Queue&lt;/b&gt;: Promise callback, Mutation observer API, await/async callback 등&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Web APIs 콜백은 우선순위를 가지며, 아래로 갈 수록 우선 순위가 높습니다 (Micro &amp;gt; Animation &amp;gt; Macro). Microtask는 큐를 모두 비울 때까지 작업을 수행합니다. 즉, &lt;b&gt;Microtask를 완료하기 전까지는 랜더링(Animation)과 Macrotask가 실행되지 않습니다&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;각 태스크의 우선순위와 동작 방식을 알고 함수를 작성해야 웹이 멈춰버리는 문제를 줄일 수 있습니다. Microtask를 처리하느라 다른 큐의 작업이 지연되는 예시를 보면 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729145221244&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 출처: Claude로 생성 및 수정

let frameCount = 0;

function animateElement() {
  const element = document.getElementById(&quot;animatedElement&quot;);
  element.style.transform = `translateX(${frameCount}px)`;
  frameCount++;

  requestAnimationFrame(animateElement);
}

function createMicrotasks() {
  Promise.resolve().then(() =&amp;gt; {
    let result = 0;
    for (let i = 0; i &amp;lt; 1e9; i++) {
      result += Math.random();
    }
  });
}

function badMicrotaskOverflow() {
  console.log(&quot;Starting animation and creating microtasks...&quot;);

  // Start the animation
  animateElement();

  // Create a large number of microtasks
  createMicrotasks();

  // Microtask 때문에 지연되는 Macrotask Queue
  setTimeout(() =&amp;gt; {
    console.log(&quot;Animation frame count:&quot;, frameCount);
    console.log(&quot;This message is delayed!&quot;);
  }, 0);
}

badMicrotaskOverflow();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼이 오른쪽으로 계속 이동하는 애니메이션을 구현했습니다. 하지만 Promise의 콜백을 처리 중일 때는 translateX 작업이 지연되며, 로딩이 끝난 후에야 translateX와 setTimeout이 수행됩니다. 위 코드를 실행하면 아래 이미지에서 보이는 것처럼 Microtask를 수행하느라 버튼이 표시되지 않는 것을 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;EventLoopDemo-ezgif.com-crop.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFaxSB/btsJ8MDX8sY/ZFCyJmtkPVtKHx0m3yjCcK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFaxSB/btsJ8MDX8sY/ZFCyJmtkPVtKHx0m3yjCcK/img.gif&quot; data-alt=&quot;Event Loop 데모: Microtask를 처리하느라 로딩 중인 걸 탭에서 확인할 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFaxSB/btsJ8MDX8sY/ZFCyJmtkPVtKHx0m3yjCcK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bFaxSB/btsJ8MDX8sY/ZFCyJmtkPVtKHx0m3yjCcK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;439&quot; data-filename=&quot;EventLoopDemo-ezgif.com-crop.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;439&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Event Loop 데모: Microtask를 처리하느라 로딩 중인 걸 탭에서 확인할 수 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업의 지연을 막기 위해 위 코드를 수정해보겠습니다. Microtask 작업을 한 번에 처리하지 않고 나눠서 작업하면, 그 동안 Animation Frame과 Macrotask도 수행할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1729148320271&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 출처: Claude로 생성 및 수정

let frameCount = 0;

function animateElement() {
  const element = document.getElementById(&quot;animatedElement&quot;);
  element.style.transform = `translateX(${frameCount}px)`;
  frameCount++;

  requestAnimationFrame(animateElement);
}

function createMicrotasks(
  totalIterations,
  batchSize = 1000000,
  currentIteration = 0
) {
  return new Promise((resolve) =&amp;gt; {
    let result = 0;
    const endIteration = Math.min(
      currentIteration + batchSize,
      totalIterations
    );

    for (let i = currentIteration; i &amp;lt; endIteration; i++) {
      result += Math.random();
    }

    if (endIteration &amp;lt; totalIterations) {
      // Use setTimeout to create next microtask
      setTimeout(() =&amp;gt; {
        createMicrotasks(totalIterations, batchSize, endIteration);
      }, 0);
      console.log(&quot;Microtask created for iteration:&quot;, endIteration);
    } else {
      console.log(&quot;All microtasks completed!&quot;);
      resolve(result);
    }
  });
}

function improvedMicrotaskHandling() {
  console.log(&quot;Starting animation and creating microtasks...&quot;);

  // Start the animation
  animateElement();

  // Divide a large number of microtasks
  const totalIterations = 1e9;
  createMicrotasks(totalIterations);

  setTimeout(() =&amp;gt; {
    console.log(&quot;Animation frame count:&quot;, frameCount);
  }, 0);
}

// Call the function to start the process
improvedMicrotaskHandling();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 실행하면 버튼이 움직이면서 Microtask도 수행하고 있음을 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ImprovedEventLoop-ezgif.com-crop.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFUWWG/btsKaMbaRR3/MTY5zFGXSh3KSNGaauUEm1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFUWWG/btsKaMbaRR3/MTY5zFGXSh3KSNGaauUEm1/img.gif&quot; data-alt=&quot;Microtask를 나눠서 작업 중...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFUWWG/btsKaMbaRR3/MTY5zFGXSh3KSNGaauUEm1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bFUWWG/btsKaMbaRR3/MTY5zFGXSh3KSNGaauUEm1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;439&quot; data-filename=&quot;ImprovedEventLoop-ezgif.com-crop.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;439&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Microtask를 나눠서 작업 중...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1e9번의 작업을 모두 수행한 후에 완료 메시지도 표시되었습니다 :)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-17 160435.png&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;121&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d3I5Iz/btsKahpeCht/wJNMggotrYXQ98GdNUj2JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d3I5Iz/btsKahpeCht/wJNMggotrYXQ98GdNUj2JK/img.png&quot; data-alt=&quot;모든 Microtask 작업 완료!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3I5Iz/btsKahpeCht/wJNMggotrYXQ98GdNUj2JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3I5Iz%2FbtsKahpeCht%2FwJNMggotrYXQ98GdNUj2JK%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;328&quot; height=&quot;121&quot; data-filename=&quot;스크린샷 2024-10-17 160435.png&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;121&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모든 Microtask 작업 완료!&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 이벤트 루프의 개념을 알아봤습니다. 우연히 책이나 강의를 통해 기술 지식을 얻고나면 '이걸 알면 뭐가 좋을까?'가 가장 궁금한데, 원티드에서는 그 부분도 다뤄줘서 좋았습니다   이벤트 루프를 모르면 발생할 수 있는 문제와 이를 해결하기 위한 방법까지 정리해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 추천드린 대로 아래 유튜브가 개념을 자세하게 다루고 있으니 한 번 보셔도 좋을 것 같습니다. 틀린 부분 있으면 댓글 부탁드립니다. 읽어주셔서 감사합니다  &amp;zwj;♀️ &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=eiC58R16hb8&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/A8tlR/hyXhJ74T7L/8VCEHgfrKLxt5VHtJnLKCk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=1126_554_1202_636&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/eiC58R16hb8&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;참고자료&lt;/b&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;a href=&quot;https://youtu.be/eiC58R16hb8?feature=shared&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/eiC58R16hb8?feature=shared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/bipinrajbhar/how-javascript-works-web-apis-callback-queue-and-event-loop-2p3e&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.to/bipinrajbhar/how-javascript-works-web-apis-callback-queue-and-event-loop-2p3e&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@burak.bburuk/what-is-the-event-loop-in-javascript-and-why-is-it-essential-to-understand-b11af520a28b&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@burak.bburuk/what-is-the-event-loop-in-javascript-and-why-is-it-essential-to-understand-b11af520a28b&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://iamsjy17.github.io/javascript/2019/07/20/how-to-works-js.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://iamsjy17.github.io/javascript/2019/07/20/how-to-works-js.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>✍ 기록</category>
      <category>animation-frame</category>
      <category>event-loop</category>
      <category>javascript</category>
      <category>macrotask</category>
      <category>microtask</category>
      <category>TaskQueue</category>
      <category>web-apis</category>
      <category>브라우저</category>
      <category>원티드</category>
      <category>이벤트루프</category>
      <author>yeonDev</author>
      <guid isPermaLink="true">https://seyeon.tistory.com/112</guid>
      <comments>https://seyeon.tistory.com/112#entry112comment</comments>
      <pubDate>Sun, 6 Oct 2024 17:00:08 +0900</pubDate>
    </item>
    <item>
      <title>[후기] 면접 단골 질문 - 이벤트 루프 다시 돌아보기</title>
      <link>https://seyeon.tistory.com/111</link>
      <description>&lt;figure id=&quot;og_1727680784168&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;[공지] 프리온보딩 FE 챌린지 9월 | 원티드&quot; data-og-description=&quot;이력서, 채용 공고, 연봉 정보는 물론 프리랜서 프로젝트까지! 신입 취업 및 경력직 이직, 커리어 성장에 필요한 모든 것을 만나보세요.&quot; data-og-host=&quot;www.wanted.co.kr&quot; data-og-source-url=&quot;https://www.wanted.co.kr/events/pre_challenge_fe_25_notice&quot; data-og-url=&quot;https://www.wanted.co.kr/events/pre_challenge_fe_25_notice&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ZFzrQ/hyXaHuVLWY/7RGeWRyMnHgtnl8wukRn00/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628&quot;&gt;&lt;a href=&quot;https://www.wanted.co.kr/events/pre_challenge_fe_25_notice&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.wanted.co.kr/events/pre_challenge_fe_25_notice&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ZFzrQ/hyXaHuVLWY/7RGeWRyMnHgtnl8wukRn00/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628');&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;[공지] 프리온보딩 FE 챌린지 9월 | 원티드&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;www.wanted.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업 준비 중인 0년차 신입으로서, 자기소개서 쓰는 것도 쉽지 않지만 제일 두려운 건 면접입니다   오늘도 어김없이 취업사이트를 둘러보다가 원티드에서 세션을 진행하길래 냅다 신청했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업 준비를 하다보면 문득 '&lt;span style=&quot;background-color: #f6e199;&quot;&gt;교육을 받고 싶긴한데, 부트 캠프만 몇 개월 듣기는 부담스럽다&lt;/span&gt;'라고 생각한 적이 많았습니다. 원티드에서 진행하는 세션은 신입뿐만 아니라 주니어 개발자도 대상으로 하기 때문에 하루 종일 진행하는 교육이 아니었습니다. 게다가 돈을 내야하는 것도 아닙니다. 완전 &lt;b&gt;혜자&lt;/b&gt;아니야? 이건 신청할 수 밖에 없었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;264&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8OzcE/btsJSfkVuCT/h7T7WJ0pItxsfY3jz34eNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8OzcE/btsJSfkVuCT/h7T7WJ0pItxsfY3jz34eNK/img.png&quot; data-alt=&quot;일정도 부담스럽지 않은데 심지어 공짜라고?!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8OzcE/btsJSfkVuCT/h7T7WJ0pItxsfY3jz34eNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8OzcE%2FbtsJSfkVuCT%2Fh7T7WJ0pItxsfY3jz34eNK%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;264&quot; height=&quot;196&quot; data-origin-width=&quot;264&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;일정도 부담스럽지 않은데 심지어 공짜라고?!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주 2회 주말에 3시간씩, 총 2주 동안 진행되었습니다. 줌에서 실시간으로 진행되고, 디스코드나 줌 채팅을 통해 마음껏 질문할 수 있습니다. 혹시 일정 때문에 강의를 놓치더라도 '&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;다시보기&lt;/b&gt;&lt;/span&gt;'를 신청하여 3개월 동안 영상을 볼 수 있습니다. 다시보기는 단돈&lt;b&gt; 1만원&lt;/b&gt;으로 신청할 수 있고, 신청 방법은 때가 되면 이메일과 디스코드로 친절히 안내해주십니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1825&quot; data-origin-height=&quot;789&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mAuvI/btsJSs5h0Ky/lJDBE0EM8KNYEGNmck6p40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mAuvI/btsJSs5h0Ky/lJDBE0EM8KNYEGNmck6p40/img.png&quot; data-alt=&quot;친절한 다시보기 안내&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mAuvI/btsJSs5h0Ky/lJDBE0EM8KNYEGNmck6p40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmAuvI%2FbtsJSs5h0Ky%2FlJDBE0EM8KNYEGNmck6p40%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;1825&quot; height=&quot;789&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1825&quot; data-origin-height=&quot;789&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;친절한 다시보기 안내&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 무엇을 배웠습니까❓&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목 그대로 '&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이벤트 루프&lt;/b&gt;'&lt;/span&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;비동기/동기&lt;/li&gt;
&lt;li&gt;블로킹/논블로킹&lt;/li&gt;
&lt;li&gt;Callback Queue&lt;/li&gt;
&lt;li&gt;Call Stack&lt;/li&gt;
&lt;li&gt;실행컨텍스트&lt;/li&gt;
&lt;li&gt;클로저&lt;/li&gt;
&lt;li&gt;Promise&lt;/li&gt;
&lt;li&gt;Async/Await&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 세션이 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;파편적으로 알고 있던 기술 개념을 복습&lt;/span&gt;해보는 시간이기도 했습니다. 기술 면접 준비를 위해 기술 개념을 공부하다보면 '이걸 그래서 실전에 어떻게 활용하는거지? 무슨 도움이 되는 거지?'하고 궁금할 때가 많았습니다. 그 부분을 박박 긁어준 세션이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님의 알찬 수업으로 유용한 지식을 얻었으니 기술 블로그에 개념을 하나씩 정리하며 복습하려고 합니다. 의지 박약으로 글쓰기를 더 이상 미루지 않기 위해 먼저 여기에 선언합니다 &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;583&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c672cN/btsJQVOFId9/rtaYyZ42cKGSWTb1EcJmz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c672cN/btsJQVOFId9/rtaYyZ42cKGSWTb1EcJmz0/img.png&quot; data-alt=&quot;노션에 소중하게 정리한 강의&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c672cN/btsJQVOFId9/rtaYyZ42cKGSWTb1EcJmz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc672cN%2FbtsJQVOFId9%2FrtaYyZ42cKGSWTb1EcJmz0%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;486&quot; height=&quot;396&quot; data-origin-width=&quot;583&quot; data-origin-height=&quot;475&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;노션에 소중하게 정리한 강의&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 이벤트 루프에 대해 더 찾아보다가, 이 개념을 아주아주 쉽게 설명한 유튜브 영상 하나 공유합니다. 이미 위에 적어놓은 개념을 알고 계시다면, 바로 영상을 봐주시길 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=eiC58R16hb8&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bELQWd/hyXaGQk5Ka/IOU7OcGaPQgU4pLpIjZkOK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=1126_554_1202_636&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/eiC58R16hb8&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 곧 다시 돌아오겠습니다. 원티드가 10월에도 세션을 열었으니 관심 있는 분들은 뭐가 있는지 둘러보세요! 주제가 매번 다른 거 같은데 진작 알았으면 좋았을 걸.. 세션은 &lt;a href=&quot;https://www.wanted.co.kr/events&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;원티드 커리어&lt;/a&gt; 탭에서 확인하실 수 있습니다.&lt;/p&gt;</description>
      <category>  챌린지</category>
      <category>기술개념</category>
      <category>기술면접</category>
      <category>세션</category>
      <category>원티드</category>
      <category>이벤트루프</category>
      <category>챌린지</category>
      <category>취업준비</category>
      <category>취준생</category>
      <category>후기</category>
      <author>yeonDev</author>
      <guid isPermaLink="true">https://seyeon.tistory.com/111</guid>
      <comments>https://seyeon.tistory.com/111#entry111comment</comments>
      <pubDate>Mon, 30 Sep 2024 16:52:14 +0900</pubDate>
    </item>
    <item>
      <title>비동기 처리 : 콜백 패턴부터 async/await까지 (with TMDB)</title>
      <link>https://seyeon.tistory.com/103</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 패턴부터 async/await 사용법을 &lt;a href=&quot;https://developer.themoviedb.org/docs/getting-started&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TMDB API (V3)&lt;/a&gt;에서 인기 영화 목록을 가져오는 사례와 함께 알아봅시다. 모던 자바스크립트 Deep Dive의 비동기 처리 내용을 읽고 테스트 한 내용입니다.&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;983&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB1EQG/btsEkySmHyT/reA3rQyHNmayc1pOCtzYx1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB1EQG/btsEkySmHyT/reA3rQyHNmayc1pOCtzYx1/img.jpg&quot; data-alt=&quot;도마뱀 책에게 감사를!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB1EQG/btsEkySmHyT/reA3rQyHNmayc1pOCtzYx1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB1EQG%2FbtsEkySmHyT%2FreA3rQyHNmayc1pOCtzYx1%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;232&quot; height=&quot;283&quot; data-origin-width=&quot;983&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;도마뱀 책에게 감사를!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;a href=&quot;https://developer.themoviedb.org/docs/getting-started&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TMDB&lt;/a&gt;에 로그인하고 &lt;b&gt;API key&lt;/b&gt;를 발급받습니다. 반복적으로 사용하는 URL과 Params, API key는 아래와 같이 상수로 정리했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707049863323&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// constant.js
const BASE_URL = &quot;https://api.themoviedb.org/3&quot;;
const API_KEY = &quot;your API key&quot;;
const BASE_PARAMS = &quot;language=ko-KR&amp;amp;region=410&quot;;

export { BASE_URL, API_KEY, BASE_PARAMS };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 TMDB에서 인기 영화 목록을 가져와 아래처럼 화면에 나타내고, 영화 이미지를 클릭하면 모달에 영화 상세 정보를 표시할 겁니다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;(모달 닫기 버튼은 안 만들었습니다..)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BYdau/btsEmCUuQfy/js7pwKCKMgiobpDRwQeKB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BYdau/btsEmCUuQfy/js7pwKCKMgiobpDRwQeKB0/img.png&quot; data-origin-width=&quot;1573&quot; data-origin-height=&quot;919&quot; data-is-animation=&quot;false&quot; data-filename=&quot;127.0.0.1_5500_.png&quot; style=&quot;width: 50.3631%; margin-right: 10px;&quot; data-widthpercent=&quot;50.96&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BYdau/btsEmCUuQfy/js7pwKCKMgiobpDRwQeKB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBYdau%2FbtsEmCUuQfy%2Fjs7pwKCKMgiobpDRwQeKB0%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;1573&quot; height=&quot;919&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pu0LT/btsEm8ei18f/Pg7678O44dn3xIH8imKMjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pu0LT/btsEm8ei18f/Pg7678O44dn3xIH8imKMjK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;919&quot; data-filename=&quot;127.0.0.1_5500_ (1).png&quot; style=&quot;width: 48.4741%;&quot; data-widthpercent=&quot;49.04&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pu0LT/btsEm8ei18f/Pg7678O44dn3xIH8imKMjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpu0LT%2FbtsEm8ei18f%2FPg7678O44dn3xIH8imKMjK%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;1514&quot; height=&quot;919&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(좌)영화 목록 / (우)영화 상세 정보&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML과 CSS는 다음처럼 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707054297018&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- index.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Asynchronous with TMDB&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot; /&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;main&amp;gt;
      &amp;lt;div id=&quot;movieList&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/main&amp;gt;
    &amp;lt;div id=&quot;modal&quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/body&amp;gt;
  &amp;lt;script type=&quot;module&quot; src=&quot;index.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1707054358471&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* style.css */
body {
  margin: 0;
  background-color: #252744;
}

main {
  display: flex;
  justify-content: center;
}

main #movieList {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}

#movieList img {
  width: 200px;
  height: 300px;
  background-color: grey;
  cursor: pointer;
}

#modal {
  z-index: 10;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: white;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣ &lt;b&gt;콜백 패턴 with XMLHttpRequest&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 패턴은 &lt;b&gt;비동기 처리가 완료된 후 실행할 함수를 콜백 함수로 전달&lt;/b&gt;하는 방법입니다. 콜백 함수를 사용하는 이유는 다른 동기 함수가 종료된 후에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;비동기 함수가&lt;span&gt; &lt;/span&gt;&lt;/span&gt;실행되기 때문입니다. 즉, 아래처럼 비동기 처리의 결과를 외부(상위 스코프)에 전달하려고 해도, 전역 컨텍스트가 종료된 후 setTimeout의 콜백 함수가 실행되기 때문에 원하는 결과를 얻을 수 없습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707051591377&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let result = 0;

setTimeout(() =&amp;gt; {
  result = 100;
  console.log(`내부: ${result}`);
}, 0);

console.log(`외부: ${result}`);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음처럼 외부 함수가 먼저 실행된 후, 콜백 함수가 실행된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;53&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J9Fku/btsEmCAbThL/AQBDECIQ1K4S3QU8Fre5i0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J9Fku/btsEmCAbThL/AQBDECIQ1K4S3QU8Fre5i0/img.png&quot; data-alt=&quot;외부 console.log 실행 후 비동기 처리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J9Fku/btsEmCAbThL/AQBDECIQ1K4S3QU8Fre5i0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ9Fku%2FbtsEmCAbThL%2FAQBDECIQ1K4S3QU8Fre5i0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;280&quot; height=&quot;53&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;53&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;외부 console.log 실행 후 비동기 처리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니 영화 목록을 가져오는 get 함수를 실행 한 후 화면에 영화 목록을 생성하려면, 영화 목록을 생성하는 작업을 &lt;b&gt;콜백 함수로 전달&lt;/b&gt;하여 영화 데이터를 받은 후 사용해야합니다. 콜백 함수로 받은 json을 출력해 보면 영화 정보가 담겨 있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707052973445&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// index.js
import { BASE_URL, API_KEY, BASE_PARAMS } from &quot;./constant.js&quot;;

const url = `${BASE_URL}/movie/popular?${BASE_PARAMS}&amp;amp;page=1`;

get(url, (json) =&amp;gt; {
  console.log(json);
  // 응답 받은 json으로 화면에 영화 목록을 생성한다.
});

/* url에 GET 요청을 보내고, 처리 결과를 callback에 전달한다. */
function get(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open(&quot;GET&quot;, url);

  // header 설정
  xhr.setRequestHeader(&quot;Authorization&quot;, `Bearer ${API_KEY}`);
  xhr.setRequestHeader(&quot;accept&quot;, &quot;application/json&quot;);

  xhr.send();
  xhr.onload = function () {
    if (xhr.status === 200) {
      // callback에 결과 전달
      callback(JSON.parse(xhr.response));
    } else {
      console.error(&quot;Error&quot;, xhr.status, xhr.statusText);
    }
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cENKBH/btsEne6lv9z/we9U7oRxaHesWIq1rYkSS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cENKBH/btsEne6lv9z/we9U7oRxaHesWIq1rYkSS1/img.png&quot; data-alt=&quot;JSON: 인기 영화 목록&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cENKBH/btsEne6lv9z/we9U7oRxaHesWIq1rYkSS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcENKBH%2FbtsEne6lv9z%2Fwe9U7oRxaHesWIq1rYkSS1%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;522&quot; height=&quot;106&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JSON: 인기 영화 목록&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에 영화 목록을 생성한 후에는 각 영화 img마다 클릭 이벤트를 추가해야 합니다. 사용자가 영화 img를 클릭하면, HTTP 요청을 보내 영화 id로 상세 정보를 가져오고, 그 정보를 모달에 추가하여 화면에 표시해야 합니다. 그러면 위 코드에서 최초로 실행한 get 함수는 다음과 같은 구조가 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707054181567&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;get(url, (json) =&amp;gt; {
  // 응답 받은 json으로 화면에 영화 목록을 생성한다...
  // 각 영화 img에 클릭 이벤트를 추가한다...
  // 클릭 이벤트가 발생하면 영화 id로 상세 정보를 불러온다.
  get(`${BASE_URL}/movie/${movieId}?${BASE_PARAMS}`, (json) =&amp;gt; {
      // ...
    });
});
// ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 간단한 기능만 구현하여 복잡하지 않지만 프로그램 규모가 커지고 기능이 복잡해지면, 콜백 함수를 받는 get 함수가 중첩되면서 코드 또한 복잡해지게 됩니다. 이런 현상을 &lt;b&gt;콜백 지옥&lt;/b&gt;(Callback Hell)이라고 합니다. 하지만 복잡한 콜백 패턴은 후속 처리 메소드를 사용하는 &lt;b&gt;Promise&lt;/b&gt;로 해결할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 콜백 패턴을 사용한 &lt;b&gt;전체 코드&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707055982064&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { BASE_URL, API_KEY, BASE_PARAMS } from &quot;./constant.js&quot;;

const url = `${BASE_URL}/movie/popular?${BASE_PARAMS}&amp;amp;page=1`;

get(url, (json) =&amp;gt; {
  createMovieList(json);

  // 각 영화 img에 클릭 이벤트를 추가한다.
  const imgs = document.querySelectorAll(&quot;img&quot;);
  imgs.forEach((img) =&amp;gt; img.addEventListener(&quot;click&quot;, handleClick));

  /* 영화 목록을 화면에 표시한다. */
  function createMovieList(json) {
    const list = document.querySelector(&quot;#movieList&quot;);

    json.results.forEach((movie) =&amp;gt; {
      const img = document.createElement(&quot;img&quot;);
      img.src = `https://image.tmdb.org/t/p/w200${movie.poster_path}`;
      img.alt = movie.title;
      img.dataset.id = movie.id;
      list.appendChild(img);
    });
  }

  /* 영화 img의 클릭 이벤트를 처리한다. */
  function handleClick(event) {
    const movieId = event.target.dataset.id;

    get(`${BASE_URL}/movie/${movieId}?${BASE_PARAMS}`, (json) =&amp;gt; {
      // 모달을 생성하고 body에 추가한다.
      const modal = createModal();
      const body = document.querySelector(&quot;body&quot;);
      body.appendChild(modal);
    });
  }

  /* 영화 정보를 담은 모달을 생성하고 반환한다. */
  function createModal() {
    const modal = document.querySelector(&quot;#modal&quot;);
    const h2 = document.createElement(&quot;h2&quot;);
    const overview = document.createElement(&quot;p&quot;);

    modal.innerHTML = &quot;&quot;;
    h2.innerText = json.title;
    overview.innerText = json.overview;
    modal.appendChild(h2);
    modal.appendChild(overview);

    modal.style.width = &quot;60%&quot;;
    modal.style.height = &quot;40%&quot;;
    modal.style.padding = &quot;24px 32px&quot;;
    modal.style.borderRadius = &quot;24px&quot;;

    return modal;
  }
});

/* url에 GET 요청을 보내고, 처리 결과를 callback에 전달한다. */
function get(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open(&quot;GET&quot;, url);

  // header 설정
  xhr.setRequestHeader(&quot;Authorization&quot;, `Bearer ${API_KEY}`);
  xhr.setRequestHeader(&quot;accept&quot;, &quot;application/json&quot;);

  xhr.send();
  xhr.onload = function () {
    if (xhr.status === 200) {
      callback(JSON.parse(xhr.response));
    } else {
      console.error(&quot;Error&quot;, xhr.status, xhr.statusText);
    }
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2️⃣&lt;b&gt;&lt;span&gt; 콜백 패턴 with &lt;/span&gt;Promise&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Promise는 ES6에서 등장한 생성자 함수입니다. Promise를 사용하면 비동기 작업을 동기처럼 처리할 수 있습니다. 앞서 살펴본 콜백 패턴은 비동기 처리 결과를 외부 변수에 전달할 수 없었지만, 비동기 함수가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Promise를 반환&lt;/b&gt;하도록 하면 외부에서 비동기 처리 결과를 받을 수 있습니다. 아래처럼 기존 get 함수가 Promise를 반환하도록 만들고 결과를 출력해 보면,&lt;/p&gt;
&lt;pre id=&quot;code_1707114532265&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const promise = get(`${BASE_URL}/movie/popular?${BASE_PARAMS}&amp;amp;page=1`);
console.log(promise);

/* url에 GET 요청을 보내고, 처리 결과를 Promise로 반환한다. */
function get(url) {
  return new Promise((resolve, reject) =&amp;gt; {
    const xhr = new XMLHttpRequest();
    xhr.open(&quot;GET&quot;, url);

    // header 설정
    xhr.setRequestHeader(&quot;Authorization&quot;, `Bearer ${API_KEY}`);
    xhr.setRequestHeader(&quot;accept&quot;, &quot;application/json&quot;);

    xhr.send();
    xhr.onload = function () {
      if (xhr.status === 200) {
      	// 응답을 성공적으로 받으면 resolve에 json(처리 결과)을 넘긴다.
        resolve(JSON.parse(xhr.response));
      } else {
      	// 실패하면 reject에 실패 이유를 전달한다.
        reject(`Error ${xhr.status}: ${xhr.statusText}`);
      }
    };
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래처럼 Promise 객체를 받은 것을 확인할 수 있습니다. PromiseState는 비동기 처리 상태를 나타내며, 처리 전이 &quot;&lt;b&gt;pending&lt;/b&gt;&quot;, 성공적으로 처리하면 &quot;&lt;b&gt;fullfilled&lt;/b&gt;&quot;, 에러가 발생하면 &quot;&lt;b&gt;rejected&lt;/b&gt;&quot;가 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-02-05 151853.png&quot; data-origin-width=&quot;276&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mp5eY/btsEmNBHrSE/JYd4h9aYyMtPewLAICuQB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mp5eY/btsEmNBHrSE/JYd4h9aYyMtPewLAICuQB0/img.png&quot; data-alt=&quot;Promise 반환 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mp5eY/btsEmNBHrSE/JYd4h9aYyMtPewLAICuQB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmp5eY%2FbtsEmNBHrSE%2FJYd4h9aYyMtPewLAICuQB0%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;276&quot; height=&quot;66&quot; data-filename=&quot;스크린샷 2024-02-05 151853.png&quot; data-origin-width=&quot;276&quot; data-origin-height=&quot;66&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Promise 반환 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;후속 처리 메소드&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기 영화 목록을 Promise로 받아왔으니, 영화 상세 정보를 모달에 입력하여 화면에 표시해야 합니다. Promise의 &lt;b&gt;then&lt;/b&gt;, &lt;b&gt;catch&lt;/b&gt; 메소드로&lt;b&gt; 응답 결과를 처리하거나 에러에 대응&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전과 마찬가지로 &amp;nbsp;인기 영화 목록을 화면에 표시하는 코드입니다. 함수를 분리하기 위해 Image Element를 중간에 프로미스로 반환했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707116616909&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const popular = get(`${BASE_URL}/movie/popular?${BASE_PARAMS}&amp;amp;page=1`);
const imgsPromise = popular
  .then((json) =&amp;gt; {
    createMovieList(json);
    // Image element[] 반환 (프로미스로 반환됨)
    return document.querySelectorAll(&quot;img&quot;);

    function createMovieList(json) {
      const list = document.querySelector(&quot;#movieList&quot;);

      json.results.forEach((movie) =&amp;gt; {
        const img = document.createElement(&quot;img&quot;);
        img.src = `https://image.tmdb.org/t/p/w200${movie.poster_path}`;
        img.alt = movie.title;
        img.dataset.id = movie.id;
        list.appendChild(img);
      });
    }
  })
  .catch(console.error);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;원래는 get이 두 번 중첩되었으나 비동기 함수가 Promise를 반환하도록 만들었기 때문에, 다음처럼 각 영화 img에 클릭 이벤트를 이전 get 함수와&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;중첩 없이&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;추가할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1707118640714&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//...
// 각 영화 img에 클릭 이벤트를 추가한다.
imgsPromise.then((imgs) =&amp;gt; {
  imgs.forEach((img) =&amp;gt; img.addEventListener(&quot;click&quot;, handleClick));

  function handleClick(event) {
    const id = event.target.dataset.id;
    const detail = get(`${BASE_URL}/movie/${id}?${BASE_PARAMS}`);

    detail
      .then((json) =&amp;gt; {
        // 영화 정보를 담은 모달을 생성하고 body에 추가한다.
        const modal = createModal(json);
        const body = document.querySelector(&quot;body&quot;);
        body.appendChild(modal);
      })
      .catch(console.error);
  }

  function createModal(detail) {
    //...
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 get 함수가 프로미스를 반환하도록 직접 구현했지만 HTTP 요청할 때는 &lt;b&gt;프로미스를 반환하는 fetch&lt;/b&gt; 함수를 사용할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3️⃣ fetch&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetch 함수는 위에서 구현한 get 함수처럼 &lt;b&gt;HTTP 응답을 프로미스로 반환&lt;/b&gt;합니다. 즉, fetch도 프로미스의 &lt;b&gt;후속 처리 메소드&lt;/b&gt;를 사용할 수 있습니다. 아래는 fetch 함수를 사용하여 get 함수를 새로 정의한 코드입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707126566403&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function get(url) {
  const options = {
    method: &quot;GET&quot;,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      accept: &quot;application/json&quot;,
    },
  };
  return fetch(url, options)
    .then((res) =&amp;gt; {
      if (res.ok) return res.json();
      throw new Error(res.statusText);
    })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetch가 반환한 Response 객체의 구조는 다음과 같습니다.&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;458&quot; data-origin-height=&quot;229&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yL3Ta/btsEm8lQT0M/vGHBQ2krwYibWK9qtn47Ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yL3Ta/btsEm8lQT0M/vGHBQ2krwYibWK9qtn47Ek/img.png&quot; data-alt=&quot;fetch가 반환한 Response&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yL3Ta/btsEm8lQT0M/vGHBQ2krwYibWK9qtn47Ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyL3Ta%2FbtsEm8lQT0M%2FvGHBQ2krwYibWK9qtn47Ek%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;458&quot; height=&quot;229&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;229&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;fetch가 반환한 Response&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetch는 &lt;b&gt;Response&lt;/b&gt; 객체를 반환하고,&lt;b&gt; ok&lt;/b&gt; 속성에 응답 성공 여부를 나타냅니다. 응답 결과는 &lt;b&gt;json&lt;/b&gt; 메소드를 사용하여 JSON으로 파싱할 수 있습니다. 이때, &lt;b&gt;json도 프로미스를 반환&lt;/b&gt;하므로, 중간에 then 메소드를 사용하여 JSON을 반환하는 과정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ok로 응답 성공 여부를 확인했음에도 catch 메소드를 사용하는 이유는 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/fetch&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;fetch가 &lt;b&gt;HTTP 에러는 reject 하지 않기 때문&lt;/b&gt;&lt;/a&gt;입니다. 따라서 then에서 한 번, catch에서 한 번 에러를 확인해줘야 합니다.&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;778&quot; data-origin-height=&quot;92&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4yXqm/btsEyOFTsUO/LaGjI5AW7WP1fHKmdhrRv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4yXqm/btsEyOFTsUO/LaGjI5AW7WP1fHKmdhrRv1/img.png&quot; data-alt=&quot;fetch does not reject on HTTP errors (출처: MDN)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4yXqm/btsEyOFTsUO/LaGjI5AW7WP1fHKmdhrRv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4yXqm%2FbtsEyOFTsUO%2FLaGjI5AW7WP1fHKmdhrRv1%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;778&quot; height=&quot;92&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;92&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;fetch does not reject on HTTP errors (출처: MDN)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 Response.ok가 false면 에러를 던져서 catch에 전달해 줍시다.&lt;/p&gt;
&lt;pre id=&quot;code_1707131339205&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// fetch 기본 구조
fetch(url)
    .then((res) =&amp;gt; {
      if (res.ok) return res.json();
      throw new Error(); // HTTP 에러 던지기
    })
    .catch((error) =&amp;gt; {
      // 에러 처리
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4️⃣ async / await&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로미스의 후속 처리 메소드로 콜백 지옥에서 벗어났지만, 실행 결과 뒤로 계속 이어지는 콜백 패턴은 여전히 복잡해 보입니다. 이를 위해 &lt;b&gt;후속 처리 메소드 없이도 비동기를 처리&lt;/b&gt;할 수 있는 async / await가 ES8에서 등장하게 됩니다.&lt;/p&gt;
&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;async&lt;/b&gt;를 앞에 붙여서 함수를 정의하면, 해당 함수는 비동기로 작동하며 프로미스를 반환합니다. &lt;b&gt;await&lt;/b&gt;는 async로 정의된 함수 내부에서 사용하며, 프로미스를 반환하는 함수가 &lt;b&gt;resolve 한 값&lt;/b&gt;을 받아올 수 있습니다. 즉, 프로미스가 아니라 비동기 처리되어 &lt;b&gt;fullfilled나 rejected 된 결과&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;이전에 작성한 get 함수에 async / await를 사용하면 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707226491953&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function get(url) {
  const options = {
    method: &quot;GET&quot;,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      accept: &quot;application/json&quot;,
    },
  };

  try {
    // fetch가 resolve 한 Response를 받아온다.
    const res = await fetch(url, options);
    return res.json();
  } catch (error) {
    console.error(error);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 처리는 후속 처리 메소드 catch 대신 &lt;b&gt;try...catch&lt;/b&gt; 문을 사용하면 됩니다. catch는 try 블록에서 예외가 발생하면 실행되며, 후속 처리 메소드와 달리 HTTP 에러도 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 async로 정의한 get 함수를 사용하여 인기 영화 목록을 생성하는 코드입니다. 콜백 패턴을 사용하지 않아서 가독성이 더 좋아진 모습입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707228303621&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(async function () {
  // 인기 영화 정보를 받아온다.
  const popular = await get(`${BASE_URL}/movie/popular?${BASE_PARAMS}&amp;amp;page=1`);
  createMovieList(popular);

  const imgs = document.querySelectorAll(&quot;img&quot;);
  imgs.forEach((img) =&amp;gt; img.addEventListener(&quot;click&quot;, handleClick));

  function createMovieList(json) {
    // ...
  }

  async function handleClick(event) {
    const id = event.target.dataset.id;
    // 영화 상세 정보를 받아온다.
    const detail = await get(`${BASE_URL}/movie/${id}?${BASE_PARAMS}`);
    const modal = createModal(detail);
    const body = document.querySelector(&quot;body&quot;);
    body.appendChild(modal);
  }

  function createModal(detail) {
    // ...
  }
})();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5️⃣ 인기 영화 무한 스크롤 with Generator&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XMLHttpRequest부터 async / await를 사용하여 비동기를 처리하는 방법을 알아봤습니다. 여기까지 온 김에 Generator를 사용하여 반복적으로 HTTP 요청을 보내 무한 스크롤을 만들어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제너레이터 함수는 &lt;b&gt;&lt;i&gt;yield&lt;/i&gt; &lt;/b&gt;표현식을 사용하여, &lt;b&gt;특정 시점&lt;/b&gt;에 코드를 실행했다가 일시정지 할 수 있습니다. &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Generator&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN에 나와있는 Generator 함수&lt;/a&gt;의 예시를 보면, next 메소드를 실행할 때마다 yield 표현식을 실행하며, 차례대로 1, 2, 3을 반환하고 일시정지 하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707230149750&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 출처: MDN
function* generator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generator(); // &quot;Generator { }&quot;

console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기 영화 목록을 보여주는 무한 스크롤을 만들려면, 사용자가 현재 영화 목록을 끝까지 스크롤했을 때(&lt;b&gt;특정 시점&lt;/b&gt;) 영화 목록을 추가로 불러와야 합니다(yield). 즉, 스크롤 위치(scrollY)와 창의 내 높이(innerHeight)를 더한 값이 웹 콘텐츠 전체 길이(body.scrollHeight)와 같아지면, 다음 페이지의 영화 목록을 표시합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;scroll.png&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z1up7/btsEtvA5qYz/4tVop4D6FahlXnBKImMPEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z1up7/btsEtvA5qYz/4tVop4D6FahlXnBKImMPEk/img.png&quot; data-alt=&quot;무한 스크롤에 필요한 값 확인!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z1up7/btsEtvA5qYz/4tVop4D6FahlXnBKImMPEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ1up7%2FbtsEtvA5qYz%2F4tVop4D6FahlXnBKImMPEk%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;656&quot; height=&quot;358&quot; data-filename=&quot;scroll.png&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;무한 스크롤에 필요한 값 확인!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 작성한 코드에 다음 제너레이터 함수를 추가합니다. 특정 시점이 되어 yield 표현식을 실행할 때마다 &lt;b&gt;page 값을 증가시키며 인기 영화 목록을 요청&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707231675542&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const popularGenerator = (async function* () {
  let page = 1;

  while (true) {
    if (page &amp;gt; 500) break; // page는 500 이하의 자연수
    // 다음 페이지 인기 영화 목록 가져오기
    yield await get(`${BASE_URL}/movie/popular?${BASE_PARAMS}&amp;amp;page=${++page}`);
  }
  return; // 종료
})();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 창을 스크롤했을 때 스크롤이 끝까지 내려갔는지 확인하기 위해, window에 scroll 이벤트 리스너를 추가합니다. 특정 시점은 앞서 설명한 대로 &lt;i&gt;&lt;b&gt;scrollY + innerHeight === body.scrollHeight&lt;/b&gt; &lt;/i&gt;가 되었을 때입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707231909534&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;window.addEventListener(&quot;scroll&quot;, handleScroll);

async function handleScroll() {
  const body = document.querySelector(&quot;body&quot;);

  if (window.innerHeight + window.scrollY &amp;gt;= body.scrollHeight) {
    const { done, value } = await popularGenerator.next();
    
    // 영화 목록이 남아있으면
    if (!done) createMovieList(value);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;done을 통해 generator 함수가 종료(return)되었는지 확인할 수 있습니다. 함수가 종료되지 않았으면 done이 false이고, next 메소드로 전달받은 인기 영화 목록을 화면에 표시합니다.&lt;/p&gt;
&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-filename=&quot;AsynchronouswithTMDB-Brave2024-02-0700-15-21-ezgif.com-crop.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf0ZRY/btsEx9DylEc/ZQFULVXJXkTWQBEf79nG00/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf0ZRY/btsEx9DylEc/ZQFULVXJXkTWQBEf79nG00/img.gif&quot; data-alt=&quot;인기 영화 무한 스크롤&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf0ZRY/btsEx9DylEc/ZQFULVXJXkTWQBEf79nG00/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bf0ZRY/btsEx9DylEc/ZQFULVXJXkTWQBEf79nG00/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;382&quot; data-filename=&quot;AsynchronouswithTMDB-Brave2024-02-0700-15-21-ezgif.com-crop.gif&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인기 영화 무한 스크롤&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 fetch와 async / await는 사용하면서도 그 이유를 생각해보지 못했습니다. 이번 글을 정리하며 콜백 패턴과 Promise에 대해 알게 되었고, 저와 같은 궁금증을 가지신 분들께 이 글이 도움이 되었으면 합니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;틀린 부분이 있다면 한 말씀 부탁드립니다. 감사합니다 :)&lt;/p&gt;</description>
      <category>✍ 기록</category>
      <category>async</category>
      <category>await</category>
      <category>fetch</category>
      <category>Generator</category>
      <category>Promise</category>
      <category>tmdb</category>
      <category>비동기</category>
      <category>콜백패턴</category>
      <author>yeonDev</author>
      <guid isPermaLink="true">https://seyeon.tistory.com/103</guid>
      <comments>https://seyeon.tistory.com/103#entry103comment</comments>
      <pubDate>Sun, 4 Feb 2024 23:16:04 +0900</pubDate>
    </item>
  </channel>
</rss>