<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>옹토리</title>
    <link>https://ongheong.tistory.com/</link>
    <description>옹헤의 개발 + 잡담 티스토리입니다!</description>
    <language>ko</language>
    <pubDate>Sat, 8 Aug 2026 23:54:58 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>옹헤옹</managingEditor>
    <image>
      <title>옹토리</title>
      <url>https://tistory1.daumcdn.net/tistory/6072213/attach/3af46e1b487a4cf0806c8552d59ac995</url>
      <link>https://ongheong.tistory.com</link>
    </image>
    <item>
      <title>useMemo, useCallback, React.memo에 대해서</title>
      <link>https://ongheong.tistory.com/entry/useMemo-useCallback-Reactmemo%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 면접에서 useMemo, useCallback, React.memo에 대한 질문이 들어왔습니다. useMemo와 useCallback은 예전에 스터디에서 발표를 한 적이 있어 대답할 수 있었지만, React.memo에 대해서는 제대로 대답하지 못했습니다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;대충 컴포넌트 메모이제이션 아닌가요&lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 기회에 리액트의 세 훅에 대해 다시 정리해보고, 간단한 예제를 통해 적용 방법을 익히는 시간을 가져 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMemo와 useCallback, React.memo 모두 React에서 성능 최적화를 위해 자주 사용되는 hook입니다. 즉 컴포넌트의 불필요한 렌더링을 방지하고 성능을 개선하기 위해 사용됩니다. 이 hook들은 기능은 비슷하지만, 어떤 것을 메모이제이션하는지에 따라 다릅니다.&lt;/p&gt;
&lt;aside&gt;&lt;/aside&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;  &lt;b&gt;메모이제이션이란?&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 계산한 결과를 메모리에 저장하여, 동일한 계산을 반복하지 않도록 하는 기술입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 계산한 값을 재사용하여 애플리케이션의 속도를 높일 수 있다. &amp;rArr; 성능을 향상시킬 수 있다. ex) DP&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useMemo의 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMemo는 재랜더링 사이에 계산 결과를 메모이제이션할 수 있게 해주는 React Hook입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const cachedValue = useMemo(calculateValue, dependencies)&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;calculateValue: 메모이제이션하려는 값을 계산하는 함수.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순수 함수이며, 인자를 받지 않음&lt;/li&gt;
&lt;li&gt;초기 렌더링에서 함수를 호출함.&lt;/li&gt;
&lt;li&gt;다음 렌더링에서 &lt;code&gt;dependencies&lt;/code&gt;가 변경되지 않으면 이전에 계산된 값을 반환&lt;/li&gt;
&lt;li&gt;변경되면 calculateValue를 재호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;dependencies: calculateValue 코드 내에서 참조된 모든 반응형 값들의 목록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인라인 형태로 작성되어야 함 &amp;rarr; [dep1, dep2, &amp;hellip; ]&lt;/li&gt;
&lt;li&gt;React는 &lt;code&gt;Object.js&lt;/code&gt; 비교를 통해 의존성 배열에 있는 값들을 이전 값과 비교함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Object.js 비교란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;=== (엄격 동등 연산자) 와 비슷하지만 약간 다릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwiRMu/btsOk4gioIf/kFtjH39ya70i2sWKaFIwKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwiRMu/btsOk4gioIf/kFtjH39ya70i2sWKaFIwKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwiRMu/btsOk4gioIf/kFtjH39ya70i2sWKaFIwKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwiRMu%2FbtsOk4gioIf%2FkFtjH39ya70i2sWKaFIwKK%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;439&quot; height=&quot;418&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cachedValue (반환값)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 렌더링에서 calculateValue를 호출한 결과를 반환&lt;/li&gt;
&lt;li&gt;다음 렌더링에서 종속성을 확인하고
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저장된 값을 반환하거나&lt;/li&gt;
&lt;li&gt;calculateValue를 재호출한 값을 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;사용 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useMemo } from 'react';

function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () =&amp;gt; filterTodos(todos, tab),
    [todos, tab]
  );
  // ...
}&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useMemo를 사용하는 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMemo를 이용해서 성능을 개선하는 과정을 보겠습니다. 다음 코드에서는 상위 컴포넌트인 App에서 사용자의 입력 값이 변경될 때마다 PrimeNumbers 컴포넌트에 calculatePrimeNumbers 함수가 재실행됩니다.&lt;/p&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;const calculatePrimeNumbers = (max) =&amp;gt; {
  let primes = [];
  for (let i = 2; i &amp;lt;= max; i++) {
    let isPrime = true;
    for (let j = 2; j * j &amp;lt;= i; j++) {
      if (i % j === 0) {
        isPrime = false;
        break;
      }
    }
    if (isPrime) primes.push(i);
  }

  return primes;
};

function PrimeNumbers ({ max }) {
  const primes = calculatePrimeNumbers(max);
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;{max} 이하의 소수: {primes.join(', ')}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

function App() {
  const [max, setMax] = useState(10);
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input
        type=&quot;number&quot;
        value={max}
        onChange={(e) =&amp;gt; setMax(Number(e.target.value))}
      /&amp;gt;
      &amp;lt;PrimeNumbers max={max} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default App;&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;만약 동일한 입력 값이 들어올 경우에도 함수는 재실행되어 불필요한 계산을 할 것입니다. 이런 상황에서 useMemo를 적용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const calculatePrimeNumbers = (max) =&amp;gt; {
  // 소수 계산 로직은 동일
};

const PrimeNumbers = ({ max }) =&amp;gt; {
  const primes = useMemo(() =&amp;gt; calculatePrimeNumbers(max), [max]);
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;{max} 이하의 소수: {primes.join(', ')}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PrimeNumbers에서 useMemo의 인자로 콜백함수와 max가 담긴 배열을 넘겨주면, 입력 값이 변하지 않는 한 이전에 계산된 소수 목록을 재사용합니다. 이를 통해 성능을 향상시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 useMemo는 재랜더링에 대해서 불필요한 작업을 생략하여 성능을 최적화해주는 것이지, 처음 렌더링을 더 빠르게 만들어주는 것은 아닙니다. 그래서 상호작용이 잦은 경우 (페이지나 전체 섹션이 바뀌는 경우) 에는 불필요합니다. 값이 자주 변경되지 않거나 계산이 간단한 경우에도 메모이제이션 자체가 비용을 초과할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 useMemo로 최적화하는 것이 유용한 경우는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useMemo에 입력하려는 계산이 매우 느리고, 의존성이 거의 변경되지 않는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같은 경우입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;메모이제이션한 값을 특정 Hook의 의존성으로 이용할 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존성 값이 자주 변경되지 않아 계산을 최소화할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748790050744&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import React, { useState, useMemo, useEffect } from 'react';

function Parent() {
  const [num, setNum] = useState(1);

  // useMemo로 계산된 값
  const memoizedValue = useMemo(() =&amp;gt; {
    console.log('값을 계산 중...');
    return num * 1000;
  }, [num]);

  // memoizedValue를 useEffect의 의존성으로 사용
  useEffect(() =&amp;gt; {
    console.log('memoizedValue가 변경되었습니다:', memoizedValue);
  }, [memoizedValue]);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;div&amp;gt;{memoizedValue}&amp;lt;/div&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setNum(num + 1)}&amp;gt;num 증가&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default Parent;&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;React.memo로 감싸진 컴포넌트에 props로 전달할 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useCallback의 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useCallback은 리렌더링 간에 함수 정의를 메모이제이션하는 React Hook입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const cachedFn = useCallback(fn, dependencies)&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;fn: 캐싱할 함수값으로, 어떤 인자나 반환값도 가질 수 있음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 렌더링에서 이 함수를 반환함 &amp;rarr; 함수 메모이제이션&lt;/li&gt;
&lt;li&gt;다음 렌더링에서 dependencies 값이 이전과 같다면 같은 함수 반환&lt;/li&gt;
&lt;li&gt;dependencies 값이 변경되었다면, 이번 렌더링에서 전달한 함수를 반환 &amp;rarr; 함수 메모이제이션&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dependencies&lt;/code&gt;: fn 내에서 참조되는 모든 반응형 값의 목록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useMemo와 동일한 형태, 비교방법을 가집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
// 리렌더링 간에 메모이제이션할 함수 정의와 의존성 배열을 인자로 넘겨줍니다.
  const handleSubmit = useCallback((orderDetails) =&amp;gt; {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);
  // ...&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useCallback을 사용하는 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 useCallback을 이용해서 성능을 개선하는 과정을 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드처럼 자식 컴포넌트에 handleClick 함수를 props로 전달한다고 해봅시다. 부모 컴포넌트가 렌더링될 때마다 새로운 함수 객체가 생성되고, 자식 컴포넌트는 불필요하게 재렌더링될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import React, { useState } from 'react';

function Child({ onClick }) {
  return &amp;lt;button onClick={onClick}&amp;gt;클릭&amp;lt;/button&amp;gt;;
}

function Parent() {
  const [count, setCount] = useState(0);
    // Parent 컴포넌트가 렌더링될 때마다 새로 생성됨
  const handleClick = () =&amp;gt; {
    console.log('버튼 클릭');
  };

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;클릭 수: {count}&amp;lt;/p&amp;gt;
      // 그에 따른 불필요한 리렌더링 발생
      &amp;lt;Child onClick={handleClick} /&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;증가&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default Parent;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서 useCallback을 사용한다면, useCallback의 의존성 배열이 비어있기 때문에 handleClick 함수는 마운트 시 한 번만 생성되고, 그 이후로는 변경되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Parent 컴포넌트가 리렌더링되더라도 동일한 함수가 재사용되며, 자식 컴포넌트에서의 불필요한 재랜더링이 일어나지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import React, { useState, useCallback } from 'react';

function Child({ onClick }) {
  return &amp;lt;button onClick={onClick}&amp;gt;클릭&amp;lt;/button&amp;gt;;
}

function Parent() {
  const [count, setCount] = useState(0);

  // useCallback을 사용하여 handleClick 함수의 재생성을 방지
  const handleClick = useCallback(() =&amp;gt; {
    console.log('버튼 클릭');
  }, []);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;클릭 수: {count}&amp;lt;/p&amp;gt;
      &amp;lt;Child onClick={handleClick} /&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;증가&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default Parent;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useCallback 역시 useMemo 처럼 상호작용이 잦은 상황에는 불필요합니다. 간단하거나 자주 변경되는 함수에 useCallback을 사용하면 불필요한 메모리 사용을 일으킬 수 있습니다. 따라서 다음과 같은 경우에 유용하게 사용할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;memo로 감싸진 컴포넌트에 props로 넘길 때&lt;/li&gt;
&lt;li&gt;넘긴 함수가 어떤 Hook의 의존성으로 사용될 때&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748790147378&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState, useEffect, useCallback } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  
  const handleClick = useCallback(() =&amp;gt; {
    console.log('버튼 클릭됨');
  }, []); 

  // useEffect에서 handleClick을 의존성으로 사용
  useEffect(() =&amp;gt; {
    console.log('컴포넌트가 마운트되었습니다.');
  }, [handleClick]); // handleClick이 변경될 때만 실행

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;클릭 수: {count}&amp;lt;/p&amp;gt;
      &amp;lt;button onClick={handleClick}&amp;gt;클릭&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;증가&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default Parent;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useMemo와 useCallback의 비용 계산&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMemo와 useCallback을 통해 최적화하기에 적합한 상황은, 비용이 높은 로직을 여러 번 계산해야 하는 상황입니다. 만약 사용하는 코드의 비용에 대해 확인하고 싶다면, 해당 코드를 콘솔 로그로 감싸서 소요 시간을 측정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기록된 시간이 크다면 기존 코드에 useMemo, useCallback을 적용하여 소요 시간을 비교하고, 최적화를 하는 것이 좋습니다. 공식 문서에서는 1ms 이상 기록될 경우 큰 비용이라고 나와 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// useMemo를 사용하지 않았을 때
console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');

// useMemo를 사용했을 때
console.time('filter array');
const visibleTodos = useMemo(() =&amp;gt; {
  return filterTodos(todos, tab); // todo와 tab이 변경되지 않은 경우 건너뜁니다.
}, [todos, tab]);
console.timeEnd('filter array');

// useCallback을 사용했을 때
const filterTodos = useCallback((todos, tab) =&amp;gt; {
  console.time('filter array');
  const visible = todos.filter(todo =&amp;gt; todo.tab === tab);
  console.timeEnd('filter array');
  return visible;
}, [todos, tab]);&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React.memo의 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memo를 사용하면 컴포넌트의 Props가 변경되지 않은 경우 리렌더링을 건너뛸 수 있습니다. 즉, React.memo는 컴포넌트를 메모이제이션하는 훅입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 가능한 이유는 memo를 사용하는 React 컴포넌트는 항상&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;순수한 렌더링 로직&lt;/b&gt;을 가지기 때문입니다. 즉 동일한 입력에 대해 항상 동일한 결과를 반환하므로, props가 변경되지 않는다면 React가 리렌더링될 필요가 없습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;React.memo 훅의 정의는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;const MemoizedComponent = memo(SomeComponent, arePropsEqual?)&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;훅의 매개변수로는 메모할 Component와, optional한 arePropsEqual이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Component: 원본 컴포넌트는 수정되지 않고, 새로운 메모이제이션 컴포넌트가 반환됩니다. 인수로 함수형 컴포넌트, forwardRef 모두 가능합니다.&lt;/li&gt;
&lt;li&gt;arePropsEqual: 컴포넌트의 이전 Props와 새 Props를 받아 true/false를 리턴하는 비교 함수입니다. Object.js 비교를 사용하여 컴포넌트의 모든 props를 이전 값과 비교합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훅은 위의 매개변수를 인자로 받아 새로운 React 컴포넌트를 반환합니다. 이는 매개변수 컴포넌트와 동일하게 동작하는 컴포넌트이지만, 부모 컴포넌트가 리렌더링되어도 props가 동일할 경우에 리렌더링되지 않습니다.&lt;/p&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;실제 코드는 다음과 같이 작성할 수 있습니다. 가장 먼저 메모할 컴포넌트를 memo 로 감싸고, 기존 컴포넌트 대신 반환된 값을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Hello = memo(function Hello({ name }) {
	return &amp;lt;h1&amp;gt;밥은 먹고 다니니?&amp;lt;/h1&amp;gt;;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 컴포넌트를 사용하는 부모 컴포넌트 MyApp에서, 만약 address state 변수가 바뀐다면 MyApp 컴포넌트는 리렌더링됩니다. 하지만 Hello 컴포넌트의 props는 바뀌지 않았으므로 리렌더링되지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;export default function MyApp() {
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');
  return (
    &amp;lt;&amp;gt;
     &amp;lt;Hello name={name} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;memo된 컴포넌트를 리렌더링하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memo된 컴포넌트는 props를 변경하지 않고도 업데이트할 수 있습니다. memo는 부모에서 해당 컴포넌트로 전달되는 props에만 적용되므로, 컴포넌트의 state가 변경되거나, 사용 중인 Context가 변경되면 리렌더링됩니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Context 정의
const CountContext = createContext(0);

// memo된 자식 컴포넌트
const Child = memo(() =&amp;gt; {
  const contextValue = useContext(CountContext);
  const [localCount, setLocalCount] = useState(0);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;Context 값: {contextValue}&amp;lt;/p&amp;gt;
      &amp;lt;p&amp;gt;Local Count: {localCount}&amp;lt;/p&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setLocalCount((c) =&amp;gt; c + 1)}&amp;gt;로컬 +1&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
});

const App = () =&amp;gt; {
  const [parentCount, setParentCount] = useState(0);

  return (
    &amp;lt;CountContext.Provider value={parentCount}&amp;gt;
      &amp;lt;h1&amp;gt;부모 Count: {parentCount}&amp;lt;/h1&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setParentCount((c) =&amp;gt; c + 1)}&amp;gt;부모 +1&amp;lt;/button&amp;gt;
      &amp;lt;Child /&amp;gt;
    &amp;lt;/CountContext.Provider&amp;gt;
  );
};

export default App;
&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;위 코드에서 CountContext 에 전달되는 parentCount props가 변경된다면 메모된 컴포넌트인 Child는 리렌더링됩니다. 또한 Child 내의 버튼 컴포넌트를 통해 localCount state 값이 업데이트되어도 리렌더링됩니다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;React.memo와 useMemo, useCallback 함께 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React.memo는 리렌더링 비용이 많이 들고, 컴포넌트가 동일한 props로 자주 리렌더링 될 경우에 유용합니다. 따라서 props가 변경되는 횟수를 최소화한다면, memo를 더 유용하게 사용할 수 있습니다. 이를 위해 useMemo와 useCallback을 활용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 props가 객체인 경우, useMemo를 사용하여 부모 컴포넌트가 props를 매번 다시 만드는 것을 막을 수 있습니다. 함수인 경우, 컴포넌트 외부에 함수를 선언하여 변경되지 않도록 하거나, useCallback 을 사용하여 함수 선언을 메모이제이션할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 자식 컴포넌트를 React.memo로 메모
const Child = memo(({ user, onClick }: { user: { name: string }; onClick: () =&amp;gt; void }) =&amp;gt; 
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;p&amp;gt;이름: {user.name}&amp;lt;/p&amp;gt;
      &amp;lt;button onClick={onClick}&amp;gt;클릭&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
});
const App = () =&amp;gt; {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('옹헤');

  // 객체의 참조값을 useMemo로 메모
  const user = useMemo(() =&amp;gt; ({ name }), [name]);

  // 함수 선언을 useCallback으로 메모
  const handleClick = useCallback(() =&amp;gt; {
    console.log('useCallback으로 메모한 함수');
  }, []);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;부모 컴포넌트&amp;lt;/h1&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount((c) =&amp;gt; c + 1)}&amp;gt;부모 count: {count}&amp;lt;/button&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setName((n) =&amp;gt; (n === '삼순' ? '삼순' : '옹헤'))}&amp;gt;이름 변경&amp;lt;/button&amp;gt;
      &amp;lt;Child user={user} onClick={handleClick} /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <category>Memo</category>
      <category>react</category>
      <category>react hook</category>
      <category>React.memo</category>
      <category>useCallback</category>
      <category>useMemo</category>
      <category>성능최적화</category>
      <author>옹헤옹</author>
      <guid isPermaLink="true">https://ongheong.tistory.com/14</guid>
      <comments>https://ongheong.tistory.com/entry/useMemo-useCallback-Reactmemo%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C#entry14comment</comments>
      <pubDate>Sun, 1 Jun 2025 23:51:09 +0900</pubDate>
    </item>
    <item>
      <title>모자딥 18장 함수와 일급 객체</title>
      <link>https://ongheong.tistory.com/entry/%EB%AA%A8%EC%9E%90%EB%94%A5-18%EC%9E%A5-%ED%95%A8%EC%88%98%EC%99%80-%EC%9D%BC%EA%B8%89-%EA%B0%9D%EC%B2%B4</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Group 1.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4GEE5/btsOfwcWtRl/LvMPdsISnvoKMjDGy2oyhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4GEE5/btsOfwcWtRl/LvMPdsISnvoKMjDGy2oyhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4GEE5/btsOfwcWtRl/LvMPdsISnvoKMjDGy2oyhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4GEE5%2FbtsOfwcWtRl%2FLvMPdsISnvoKMjDGy2oyhk%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;1200&quot; height=&quot;628&quot; data-filename=&quot;Group 1.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;이번 장에서 공부할 내용&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 일급 객체가 무엇인지에 대해 배웁니다.&lt;br /&gt;2. 일급 객체에 해당하는 함수 객체의 프로퍼티에 대해 배웁니다.&lt;br /&gt;3. 해당 챕터에서 나온 코드를 실습합니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 일급 객체&lt;/h2&gt;
&lt;aside&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍 언어에서의 일급 객체는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;변수에 담을 수 있고, 함수에 넘기거나 반환할 수도 있는 값&lt;/span&gt;을 말합니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;자바스크립트에서 함수는 아래 조건을 모두 만족하여 일급 객체에 해당합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/aside&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;런타임에 생성할 수 있습니다. (식별자가 없는 리터럴로 생성할 수 있습니다.)&lt;/li&gt;
&lt;li&gt;변수나 자료구조에 저장할 수 있습니다.&lt;/li&gt;
&lt;li&gt;함수의 매개변수에 전달할 수 있고, 반환값으로 사용할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 1번 조건 만족
const increase = function (num) {
    return ++num;
};
// 2번 조건 만족
const predicates = {increase};
// 3번 조건 만족
function makeCounter(predicates) {
    let num = 0;
    return function() {
        num = predicates(num);
        return num;
    };
}&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수는 일급 객체이므로, 객체와 동일하게 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;값과 동일하게 취급할 수 있습니다.&lt;/li&gt;
&lt;li&gt;값을 사용할 수 있는 곳이라면 어디서든 리터럴로 정의할 수 있으며, 런타임에 함수 객체로 평가됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;함수가 일반 객체와 다른 점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 객체와 다르게 호출할 수 있습니다.&lt;/li&gt;
&lt;li&gt;일반 객체에는 없는 함수 고유의 프로퍼티가 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 함수 객체의 프로퍼티&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 객체를 브라우저에서 console.dir() 메서드를 사용해 출력하면, 다음과 같은 속성을 가지고 있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkrOas/btsOhXNrHsA/BY2MvAfKWiEaOtcN7fAHu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkrOas/btsOhXNrHsA/BY2MvAfKWiEaOtcN7fAHu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkrOas/btsOhXNrHsA/BY2MvAfKWiEaOtcN7fAHu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkrOas%2FbtsOhXNrHsA%2FBY2MvAfKWiEaOtcN7fAHu0%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;336&quot; height=&quot;342&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❓&lt;b&gt;console.log()와 console.dir()의 차이&lt;/b&gt;&lt;br /&gt;- console.log(): 매개변수의 값을 콘솔에 출력합니다. &lt;br /&gt;- console.dir(): 객체의 속성을 콘솔에 출력합니다. 객체를 나열하거나, 객체의 트리 구조(JSON)를 표시하는 데 사용됩니다.&lt;/blockquote&gt;
&lt;aside&gt;&lt;/aside&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 객체의 고유한 프로퍼티들은 Object.getOwnPropertyDescriptors로 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image 1.png&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;313&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VupOJ/btsOhk97BRv/ej6Md91Hf66WkmoXVo9Cik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VupOJ/btsOhk97BRv/ej6Md91Hf66WkmoXVo9Cik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VupOJ/btsOhk97BRv/ej6Md91Hf66WkmoXVo9Cik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVupOJ%2FbtsOhk97BRv%2Fej6Md91Hf66WkmoXVo9Cik%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;612&quot; height=&quot;313&quot; data-filename=&quot;image 1.png&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;313&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로퍼티에는 arguments, caller, length, name, prototype이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 객체에는 다른 객체에서 상속받은 프로퍼티도 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image 2.png&quot; data-origin-width=&quot;265&quot; data-origin-height=&quot;92&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bk4Ajc/btsOhfuctBI/LZTmo0K99uFrqE4BsvUN9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bk4Ajc/btsOhfuctBI/LZTmo0K99uFrqE4BsvUN9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk4Ajc/btsOhfuctBI/LZTmo0K99uFrqE4BsvUN9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbk4Ajc%2FbtsOhfuctBI%2FLZTmo0K99uFrqE4BsvUN9K%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;265&quot; height=&quot;92&quot; data-filename=&quot;image 2.png&quot; data-origin-width=&quot;265&quot; data-origin-height=&quot;92&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;proto&lt;/b&gt; 는 접근자 프로퍼티로, Object.prototype 객체의 프로퍼티를 상속받은 것입니다.&lt;/li&gt;
&lt;li&gt;Object.prototype 객체의 프로퍼티는 모든 객체가 상속받을 수 있으므로, &lt;b&gt;proto&lt;/b&gt; 접근자 프로퍼티는 모든 객체가 사용가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 arguments 프로퍼티&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;arguments 프로퍼티는 arguments 객체입니다.&lt;/li&gt;
&lt;li&gt;이 객체는 함수 호출 시에 전달된 인수들의 정보를 담고 있는 순회 가능한 유사 배열 객체입니다.&lt;/li&gt;
&lt;li&gt;함수 내부에서 지역 변수처럼 사용되므로 외부에서는 참조할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image 3.png&quot; data-origin-width=&quot;607&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQhkqK/btsOfyPofES/O4T9rTPKPntOppVvnQOuN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQhkqK/btsOfyPofES/O4T9rTPKPntOppVvnQOuN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQhkqK/btsOfyPofES/O4T9rTPKPntOppVvnQOuN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQhkqK%2FbtsOfyPofES%2FO4T9rTPKPntOppVvnQOuN0%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;607&quot; height=&quot;752&quot; data-filename=&quot;image 3.png&quot; data-origin-width=&quot;607&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바스크립트는 매개변수와 인수의 개수가 일치하는지 확인하지 않으므로, 매개변수 개수만큼 인수를 전달하지 않아도 에러가 발생하지 않습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매개변수 &amp;gt; 인수 : 인수가 전달되지 않은 매개변수는 undefinded 상태 그대로입니다.&lt;/li&gt;
&lt;li&gt;매개변수 &amp;lt; 인수 : 암묵적으로 arguments 객체의 프로퍼티로 보관됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;함수가 호출되면, 함수 몸체 내에서 암묵적으로 매개변수가 선언되고 undefined로 초기화 됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이후 인수가 할당됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;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;edited_image 3.png&quot; data-origin-width=&quot;607&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ch7zvb/btsOgMznghf/2vZKfsmTlE1MewKgoec250/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ch7zvb/btsOgMznghf/2vZKfsmTlE1MewKgoec250/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ch7zvb/btsOgMznghf/2vZKfsmTlE1MewKgoec250/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fch7zvb%2FbtsOgMznghf%2F2vZKfsmTlE1MewKgoec250%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;607&quot; height=&quot;201&quot; data-filename=&quot;edited_image 3.png&quot; data-origin-width=&quot;607&quot; data-origin-height=&quot;201&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;argument 객체는 다음 값들을 프로퍼티로 가집니다.&amp;nbsp;&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인수 &amp;rarr; 프로퍼티 키는 인수의 순서&lt;/li&gt;
&lt;li&gt;callee : 함수 자신&lt;/li&gt;
&lt;li&gt;length : 인수의 개수&lt;/li&gt;
&lt;li&gt;Symbol: arguments 객체를 이터러블로 만들기 위한 프로퍼티&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748435870490&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function hello(name) {
	const iterator = arguments[Symbol.iterator]();
  console.log(iterator.next());
  console.log(iterator.next());
  console.log(iterator.next());
  console.log(iterator.next());

  return &quot;Hello &quot; + name;
}

console.log(hello('onghe', 'samsoon', 'jelly'));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image 4.png&quot; data-origin-width=&quot;253&quot; data-origin-height=&quot;75&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/budrtb/btsOhjDp19j/hqTXk18cDKbpvHUXW7gBj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/budrtb/btsOhjDp19j/hqTXk18cDKbpvHUXW7gBj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/budrtb/btsOhjDp19j/hqTXk18cDKbpvHUXW7gBj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbudrtb%2FbtsOhjDp19j%2FhqTXk18cDKbpvHUXW7gBj0%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;253&quot; height=&quot;75&quot; data-filename=&quot;image 4.png&quot; data-origin-width=&quot;253&quot; data-origin-height=&quot;75&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;arguments 객체를 통해 가변 인자 함수(매개변수 개수 확정X)를 구현할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748436104091&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function sum() {
	let res = 0;
	for (let i = 0; i &amp;lt; arguments.length; i++) {
		res += arguments[i];
	}
	return res;
}&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;arguments 객체는 실제 배열이 아닌 유사 배열 객체입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유사 배열 객체: length 프로퍼티를 가진 객체 &amp;rarr; for문으로 순회 가능&lt;/li&gt;
&lt;li&gt;배열 메서드는 사용할 수 없습니다.&lt;/li&gt;
&lt;li&gt;간접 호출을 해야 하는 번거로움을 해결하기 위해 Rest 파라미터가 도입되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 caller 프로퍼티&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECMAScript 사양에 포함되지 않은 비표준 프로퍼티로, 참고로만 알아두면 좋습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;caller 프로퍼티는 함수 자신을 호출한 함수를 가리킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 length 프로퍼티&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;length 프로퍼티는 함수를 정의할 때 선언한 매개변수의 개수를 가리킵니다.&lt;/li&gt;
&lt;li&gt;arguments 객체의 length 프로퍼티와 함수 객체의 length 프로퍼티의 값은 다를 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;arguments 객체의 length 프로퍼티: 인자 개수&lt;/li&gt;
&lt;li&gt;함수 객체의 length 프로퍼티: 매개변수 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 name 프로퍼티&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;name 프로퍼티는 함수 이름을 나타냅니다.&lt;/li&gt;
&lt;li&gt;ES5와 ES6에서, 익명 함수 표현식에서의 동작이 서로 다릅니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ES5: 빈 문자열을 값으로 가집니다.&lt;/li&gt;
&lt;li&gt;ES6: 함수 객체를 가리키는 변수 이름을 값으로 가집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748436191488&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기명
var namedFunc = function foo() {};
console.log(namedFunc.name); // foo
// 익명 -&amp;gt; 이때 동작이 다름!
var anonymousFunc = function() {};
console.log(anonymousFunc.name); // anonymousFunc&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.5 proto 접근자 프로퍼티&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 객체는 &lt;code&gt;[[Prototype]]&lt;/code&gt; 이라는 내부 슬롯을 가집니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;[[Prototype]]&lt;/code&gt; : 프로토타입 객체를 가리킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;proto 접근자 프로퍼티는 [[Prototype]] 이 가리키는 프로토타입 객체에 접근하기 위해 사용하는 프로퍼티입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[[Prototype]] 내부 슬롯에 직접 접근할 수는 없지만, proto 프로퍼티를 통해 간접적으로 프로토타입 객체에 접근할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const obj = {};
// 객체 리터럴 방식으로 생성한 객체의 프로토타입 객체는 Object.prototype입니다.
console.log(obj.__proto__ === Object.prototype); // true
// 객체 리터럴 방식으로 생성한 객체는 Object.prototype의 프로퍼티(프로토타입 객체)를 상속받습니다.
// hasOwnProperty(): 프로퍼티 키를 인수로 받고, 그게 객체 고유의 프로퍼티 키일 때 true를 반환합니다.
console.log(obj.hasOwnProperty('__proto__')); // false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.6 prototype 프로퍼티&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;prototype 프로퍼티는 constructor만이 가지는 프로퍼티입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;constructor: 생성자 함수로 호출할 수 있는 함수 객체&lt;/li&gt;
&lt;li&gt;일반 객체, 생성자 함수로 호출할 수 없는 객체는 해당 프로퍼티가 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748436218026&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 함수 객체는 prototype 프로퍼티를 가집니다.
(function () {}).hasOwnProperty('prototype'); // true
// 일반 객체는 가지지 않습니다.
({}).hasOwnProperty('prototype); // false&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 프로퍼티는 함수가 객체를 생성하는 생성자 함수로 호출될 때, 생성자 함수가 만들 인스턴스의 프로토타입 객체를 가리킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JS</category>
      <category>javascript</category>
      <category>딥다이브</category>
      <category>모던자바스크립트</category>
      <category>모던자바스크립트딥다이브</category>
      <category>모자딥</category>
      <category>일급객체</category>
      <category>함수</category>
      <author>옹헤옹</author>
      <guid isPermaLink="true">https://ongheong.tistory.com/13</guid>
      <comments>https://ongheong.tistory.com/entry/%EB%AA%A8%EC%9E%90%EB%94%A5-18%EC%9E%A5-%ED%95%A8%EC%88%98%EC%99%80-%EC%9D%BC%EA%B8%89-%EA%B0%9D%EC%B2%B4#entry13comment</comments>
      <pubDate>Wed, 28 May 2025 21:26:13 +0900</pubDate>
    </item>
    <item>
      <title>PWA 푸시알림 기능 조사</title>
      <link>https://ongheong.tistory.com/entry/PWA-%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EA%B8%B0%EB%8A%A5-%EC%A1%B0%EC%82%AC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Push API / Notifications API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;푸시알림 기능에 대해 자세히 알아보기 전에, 알림 기능에서 자주 쓰이는 두 API에 대해 살펴보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Push API&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Push API는 server에서 service worker로 알림을 보내기 위해 사용하는 API입니다.&lt;/li&gt;
&lt;li&gt;브라우저가 서버로부터 푸시 메시지를 수신할 수 있는 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;브라우저(웹앱)의 활성 여부와 관계 없이 푸시 메시지를 받을 수 있다는 장점이 있습니다.&lt;/li&gt;
&lt;li&gt;서비스 워커가 활성화되면, Push API의 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;PushManager.subscribe()&lt;/span&gt; 메서드를 사용하여 푸시 알림을 구독할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Notification API&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Notification API는 브라우저가 사용자에게 알림배너(UI)를 띄우기 위해 사용하는 API입니다.&lt;/li&gt;
&lt;li&gt;service worker에 의해 호출되며, 사용자에게 새로운 정보를 보여주거나 업데이트를 알립니다.&lt;/li&gt;
&lt;li&gt;Notification API의 &lt;span style=&quot;background-color: #f3c000; color: #333333; text-align: left;&quot;&gt;Notification.requestPermission()&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt; 메서드를 사용하여 브라우저가 사용자에게 알림 권한을 요청합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;둘의 공통점: service worker&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;활성화된 서비스워커가 있어야 합니다.&lt;/li&gt;
&lt;li&gt;서비스워커의 특성 상 백그라운드에서도 수신이 가능합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;따라서 사용자가 앱의 페이지를 보고 있지 않거나, 앱이 종료되었을 때도 알림을 보여줄 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Push API 기반의 푸시 알림 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Push API가 사용되는 푸시 알림 기능의 경우, 다음처럼 크게 5단계로 이루어집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDNhFM/btsN298fI6l/mRLH7EOGm99thmbT4zGtCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDNhFM/btsN298fI6l/mRLH7EOGm99thmbT4zGtCk/img.png&quot; data-alt=&quot;이미지 출처: https://godsenal.com/posts/web-push-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0/#%EA%B4%80%EB%A0%A8-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDNhFM/btsN298fI6l/mRLH7EOGm99thmbT4zGtCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDNhFM%2FbtsN298fI6l%2FmRLH7EOGm99thmbT4zGtCk%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;713&quot; height=&quot;416&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처: https://godsenal.com/posts/web-push-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0/#%EA%B4%80%EB%A0%A8-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC&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;b&gt;1. 사용자에게 알림 권한을 요청하여, 권한이 허용된 경우 서비스워커가 등록됩니다.&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;2. 등록 후 활성화된 서비스워커에서 push 구독을 요청합니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;pushManager.subscribe()&lt;/code&gt;를 호출하여, 브라우저의 Push Service에서 push 구독 정보(&lt;code&gt;Subscription&lt;/code&gt;)를 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Subscription&lt;/code&gt; 에는 다음 정보가 포함됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;endpoint URL: 푸시 서버에 메시지를 보낼 주소&lt;/li&gt;
&lt;li&gt;public key: 사용자의 공개키 &amp;rarr; 메시지 암호화에 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;1번, 2번 과정 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1747573136400&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 1. 알림 권한 요청
Notification.requestPermission().then(permission =&amp;gt; {
  if (permission === 'granted') {
    // 2. 서비스워커 등록
    navigator.serviceWorker.register('/sw.js').then(registration =&amp;gt; {
      // 3. 구독 요청
      registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: '&amp;lt;VAPID_PUBLIC_KEY&amp;gt;', // 서버 공개키
      }).then(subscription =&amp;gt; {
        // 4. 이 subscription 객체를 백엔드 서버로 전송
        console.log('Push Subscription:', subscription);
      });
    });
  }
});&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 생성된 subscription 정보를 웹앱 서버로 전송합니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;백엔드 서버는 이 정보를 저장하고, 나중에 푸시 알림을 보낼 때 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 웹앱 서버에서 웹푸시 서버로 HTTP POST 요청을 보냅니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;알림을 보내야 할 때, 웹앱 서버는 저장해둔 endpoint를 사용해 브라우저의 push server를 결정합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) &lt;a href=&quot;https://fcm.googleapis.com/&quot;&gt;https://fcm.&lt;/a&gt;googleapis&lt;a href=&quot;https://fcm.googleapis.com/&quot;&gt;.com/&lt;/a&gt;... &amp;rarr; 크롬&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서버는 메시지 내용을 준비하고, 이를 암호화합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) JSON 객체 &amp;rarr; {title: &amp;ldquo;제목&amp;rdquo;, body: &amp;ldquo;본문&amp;rdquo;}&lt;/li&gt;
&lt;li&gt;VAPID 방식으로 메시지를 암호화하고 인증합니다. &amp;rarr; &lt;a href=&quot;https://web.dev/articles/push-notifications-web-push-protocol?hl=ko#perform_the_encryption&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;웹 푸시 프로토콜&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;공개키/개인키 쌍을 이용하여 인증된 발신자임을 push server에 증명합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;웹푸시 서버는 이 요청을 받아서, 해당 endpoint를 가진 사용자의 브라우저로 메시지를 전달합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;❓WebPush 서버란?&lt;/b&gt;&lt;br /&gt;브라우저 제조사가 제공하는 푸시 서비스 중계 서버입니다. 즉 푸시 알림을 대신 해주는 서비스입니다.&lt;br /&gt;ex) 구글(Chrome): Firebase Cloud Messaging(FCM)&lt;br /&gt;&lt;br /&gt;이를 사용하는 이유는 브라우저가 꺼져 있을 때도 메시지를 저장할 수 있고, 다시 활성화될 때 전달할 수 있다는 장점 때문입니다. 또한 VAPID 발급, 암호화 과정 등을 쉽게 구현할 수 있습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. push 메시지가 브라우저로 전달됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebPush 서버는 목표로 하는 endpoint를 가진 브라우저의 Push Socket에 푸시 메시지를 전송합니다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;암호화된 메시지를 복호화하여 보냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;브라우저는 서비스워커를 활성화시켜 &lt;code&gt;onpush()&lt;/code&gt; 이벤트를 실행시킵니다.&lt;/li&gt;
&lt;li&gt;서비스워커가 &lt;code&gt;showNotification()&lt;/code&gt; 을 사용해 알림 UI를 표시합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FCM이란&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image 1.png&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;1372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AhGxv/btsN2YlyKzr/aCzwmnvVE0yExNKkUs3lz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AhGxv/btsN2YlyKzr/aCzwmnvVE0yExNKkUs3lz1/img.png&quot; data-alt=&quot;출처: https://velog.io/@chy8165/FCM%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%9B%B9-%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84-%EC%9B%B9-PWA&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AhGxv/btsN2YlyKzr/aCzwmnvVE0yExNKkUs3lz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAhGxv%2FbtsN2YlyKzr%2FaCzwmnvVE0yExNKkUs3lz1%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;1942&quot; height=&quot;1372&quot; data-filename=&quot;image 1.png&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;1372&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://velog.io/@chy8165/FCM%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%9B%B9-%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84-%EC%9B%B9-PWA&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Firebase Cloud Messaging의 약자로, 구글에서 제공하는 푸시 서비스 중계 서버입니다.&lt;/li&gt;
&lt;li&gt;WebPush 서버의 역할뿐만 아니라, subscription까지 처리해주는 인터페이스를 제공합니다.&lt;/li&gt;
&lt;li&gt;크로스 플랫폼으로 메시지를 제공할 때 관리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;무료로 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FCM을 이용한 웹 푸시알림 흐름&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;클라이언트&lt;/b&gt;(브라우저)에서 알림 허용 후 &lt;b&gt;FCM에 구독 요청&lt;/b&gt;을 합니다. 이때 해당 브라우저에게 할당해 줄 디바이스 토큰을 요청합니다.&lt;/li&gt;
&lt;li&gt;FCM이 요청한 구독에 대해 &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;/li&gt;
&lt;li&gt;웹앱 서버는 알림이 필요한 상황에서 &lt;b&gt;암호화된 메세지 + 디바이스 토큰을 FCM에 전송&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;FCM은 받은 디바이스 토큰을 바탕으로, 토큰에 해당하는 기기로 &lt;b&gt;푸시 알림 메세지를 보냅니다&lt;/b&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스 적용 시 고려 사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;iOS 웹앱 푸시알림&lt;/h3&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/SKuXx/btsN10Lr6fp/OkQ4tFZx6XtXtdxtYDJdW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SKuXx/btsN10Lr6fp/OkQ4tFZx6XtXtdxtYDJdW1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;376&quot; data-filename=&quot;image 2.png&quot; style=&quot;width: 71.7287%; margin-right: 10px;&quot; data-widthpercent=&quot;72.57&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SKuXx/btsN10Lr6fp/OkQ4tFZx6XtXtdxtYDJdW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSKuXx%2FbtsN10Lr6fp%2FOkQ4tFZx6XtXtdxtYDJdW1%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;814&quot; height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KjyMy/btsN2GMajcK/vkEQZKm35qkTZq7hpoRY0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KjyMy/btsN2GMajcK/vkEQZKm35qkTZq7hpoRY0K/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;924&quot; data-filename=&quot;image 3.png&quot; style=&quot;width: 27.1085%;&quot; data-widthpercent=&quot;27.43&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KjyMy/btsN2GMajcK/vkEQZKm35qkTZq7hpoRY0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKjyMy%2FbtsN2GMajcK%2FvkEQZKm35qkTZq7hpoRY0K%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;756&quot; height=&quot;924&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;이미지 출처: https://dong-kim.tistory.com/53&lt;/figcaption&gt;
&lt;/figure&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;안드로이드의 경우 사이트에 접속할 때 자동으로 알림 권한 요청이 가능하지만, iOS는 사용자가 직접 누르게 유도해야 합니다.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iOS에서, UI에 &amp;ldquo;푸시 알림 켜기&amp;rdquo; 버튼을 통해 알림 권한 요청을 띄우는 식으로 구현 가능합니다.&lt;/li&gt;
&lt;li&gt;iOS 17부터는 옵션 설정이 완화되어 자동 호출이 가능해졌습니다. 하지만 정책상 권장되는 UX는 기존 방식입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사전 준비(FE 관점)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FireBase 프로젝트를 생성합니다.&lt;/li&gt;
&lt;li&gt;앱(웹) 등록 후 SDK, 비밀키 파일을 백엔드에 전달합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Web Push 프로토콜 표준은 VAPID 인증 방식을 사용해야 하므로, 해당 인증서를 생성하여 FCM 토큰을 전달합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;firebase message 용 서비스워커 스크립트를 생성합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일명은 반드시 firebase-message-sw.js 로 생성해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그밖의 고려 사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iOS의 경우 &amp;ldquo;홈 화면에 추가하기&amp;ldquo; 가이드 노출&lt;/li&gt;
&lt;li&gt;사용자 권한 요청 알림을 언제, 어떤 형태로 제시할지&lt;/li&gt;
&lt;li&gt;권한 거부에 대해 어떻게 처리할지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Re-engageable_Notifications_Push&quot;&gt;https://developer.mozilla.org/ko/docs/Web/Progressive_web_apps/Tutorials/js13kGames/Re-engageable_Notifications_Push&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Push_API&quot;&gt;https://developer.mozilla.org/ko/docs/Web/API/Push_API&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://godsenal.com/posts/web-push-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0/#%EA%B4%80%EB%A0%A8-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC&quot;&gt;https://godsenal.com/posts/web-push-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0/#%EA%B4%80%EB%A0%A8-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@chy8165/FCM%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%9B%B9-%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84-%EC%9B%B9-PWA&quot;&gt;https://velog.io/@chy8165/FCM%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%9B%B9-%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84-%EC%9B%B9-PWA&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@chy8165/%EC%B5%9C%EC%A2%85-%EC%9B%B9-%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84&quot;&gt;https://velog.io/@chy8165/%EC%B5%9C%EC%A2%85-%EC%9B%B9-%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://headf1rst.github.io/TIL/push-notification&quot;&gt;https://headf1rst.github.io/TIL/push-notification&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dong-kim.tistory.com/53&quot;&gt;https://dong-kim.tistory.com/53&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>PWA</category>
      <category>FCM</category>
      <category>Push</category>
      <category>push api</category>
      <category>pwa</category>
      <category>푸시알림</category>
      <author>옹헤옹</author>
      <guid isPermaLink="true">https://ongheong.tistory.com/12</guid>
      <comments>https://ongheong.tistory.com/entry/PWA-%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EA%B8%B0%EB%8A%A5-%EC%A1%B0%EC%82%AC#entry12comment</comments>
      <pubDate>Sun, 18 May 2025 21:41:07 +0900</pubDate>
    </item>
    <item>
      <title>서비스워커란?</title>
      <link>https://ongheong.tistory.com/entry/%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9B%8C%EC%BB%A4%EB%9E%80</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스워커란?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 출처(사이트)의 하나 혹은 그 이상의 페이지를 제어하는 스크립트&lt;/li&gt;
&lt;li&gt;이벤트 기반 워커로서 JavaScript로 작성된 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 워커는 자신이 제어하는 페이지에서 발생하는 이벤트를 수신할 수 있다.&lt;/li&gt;
&lt;li&gt;웹에서 네트워크 요청과 같은 이벤트를 가로채어 수정할 수 있고, 이를 다시 페이지로 돌려보낼 수 있다.&lt;/li&gt;
&lt;li&gt;또한, 서비스에서 사용하는 리소스를 캐싱할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스워커 사용법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 서비스워커 만들기&lt;/h3&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;// app.js
if (&quot;serviceWorker&quot; in navigator) {
  navigator.serviceWorker
    .register(&quot;/sw.js&quot;)
    .then(function (registration) {
      console.log(&quot;Service Worker registered with scope:&quot;, registration.scope);
    })
    .catch(function (err) {
      console.log(&quot;Service Worker registration failed&quot;, err);
    });
}&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;브라우저가 서비스워커를 지원한다면 &amp;rarr; navigator.serviceWorker.register를 호출&lt;/li&gt;
&lt;li&gt;navigator.serviceWorker.register: 서비스 워커 스크립트의 URL, option 객체의 2개 인자를 받는 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 웹에서 컨텐츠 가져오기&lt;/h3&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;// /public/sw.js
self.addEventListener(&quot;fetch&quot;, function (event) {
  console.log(&quot;Fetch request for: &quot;, event.request.url);
  if (event.request.url.includes(&quot;/img/logo.png&quot;)) {
    event.respondWith(fetch(&quot;/img/logo-flipped.png&quot;));
  }
});&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;웹사이트에서 발생하는 모든 fetch 요청을 중간에 가로채서 분석하고 조작한다.&lt;/li&gt;
&lt;li&gt;들어오는 요청 중 /img/logo-flipped.png인 요청을 가로채서 &amp;rarr; 다른 로고(/img/logo-flipped.png)를 fetch로 response를 생성하고 &amp;rarr; 원래 request 이벤트에 응답한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Scope로 요청의 범위 조정하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 워커는 서비스 워커 파일의 위치나 navigator.serviceWorker.register에서 지정해준 Scope 옵션에 따라 요청의 범위를 조정할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스워커를 root 폴더에 저장했으므로 모든 요청을 다 거친다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1746973010261&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// js 폴더에서 발생한 요청만 전달 
navigator.serviceWorker.register(&quot;sw.js&quot;, {scope: &quot;/js&quot;}) 
// js 폴더 내부를 대상으로 하는 요청만 전달 
navigator.serviceWorker.register(&quot;/js/sw.js&quot;)&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;만약 위 코드에서 js 폴더 내에 index.html 파일을 넣는다면
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스워커는 /js/index.html 에서 발생하는 요청만 전달받는다!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 오프라인 요청 감지하기&lt;/h3&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;self.addEventListener(&quot;fetch&quot;, function (event) {
  event.respondWith(
    fetch(event.request).catch(function () {
      return new Response(&quot;hello world !\n&quot;);
    })
  );
});&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;모든 fetch 요청에 대해, 예외가 발생하면 catch로 받아 새로운 응답을 보내준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오프라인이면 당연히 fetch 요청에서 에러가 발생하므로!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이를 이용해서, 오프라인이면 특정 html 파일을 응답하도록 만들 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;self.addEventListener(&quot;fetch&quot;, function (event) {
    event.respondWith(
        fetch(event.request).catch(function () {
            return fetch('/offine.html');
        })
    );
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CacheStorage API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프라인 상태일 때 어떻게 상태를 알고 요청을 받아서 넘길 수 있을까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;온라인일 때 해당 파일도 같이 받아와서 캐시해야 한다.&lt;/li&gt;
&lt;li&gt;그럼 파일을 캐시하는 과정을 한번 알아보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;install Event에서 파일 캐싱하기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTucN6/btsNS5S4tx9/p7XYm1vTZl3wG4y4HOupSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTucN6/btsNS5S4tx9/p7XYm1vTZl3wG4y4HOupSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTucN6/btsNS5S4tx9/p7XYm1vTZl3wG4y4HOupSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTucN6%2FbtsNS5S4tx9%2Fp7XYm1vTZl3wG4y4HOupSK%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;696&quot; height=&quot;233&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 워커의 install 이벤트는 첫번째 단계 &amp;ldquo;설치 중&amp;rdquo; 에서 단 한번 발생한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 워커가 가장 처음 등록된 직후&lt;/li&gt;
&lt;li&gt;이벤트가 활성화 되기 전&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서비스 워커가 페이지를 제어하고 fetch 이벤트 수신을 시작하기 전에, 오프라인화 가능한 모든 파일을 캐싱할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;self.addEventListener(&quot;install&quot;, function (event) {
    event.waitUntil(
        caches.open(&quot;c&quot;).then(function (cache) {
            return cache.add(&quot;/offline.html&quot;);
        })
    );
});&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;install 이벤트는 설치 단계에서 새로운 서비스 워커가 등록된 직후 호출된다.&lt;/li&gt;
&lt;li&gt;파일을 가져와 캐시에 저장하는 일이 비동기적으로 일어나기 때문에, 해당 이벤트가 완료될 때까지 install 이벤트를 연기해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;waitUntil&lt;/code&gt; : 전달된 프로미스가 resolve될 때까지 이벤트의 수명을 연장한다.&lt;/li&gt;
&lt;li&gt;캐시가 정상적으로 되지 않을 때 install 자체를 취소해야 할 수도 있으므로, install에서 waitUntil을 해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CacheStorage로부터 응답 받기&lt;/h3&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;self.addEventListener(&quot;fetch&quot;, function (event) {
    event.respondWith(
        fetch(event.request).catch(function () {
            return caches.match(&quot;./offline.html&quot;);
        })
    );
});&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;오프라인 감지 시 CacheStorage에서 caches.match를 사용하여 캐시된 데이터 중 원하는 파일을 반환받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여러개 캐싱하고 원하는 응답 받기&lt;/h3&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;const CACHE_NAME = &quot;git-cache&quot;;
const CACHE_URLS = [
    &quot;/offline.html&quot;,
    &quot;/img/logo-header.png&quot;,
];

/** cache.addAll을 통해 여러 파일을 객체로 묶어 한번에 캐싱 */
self.addEventListener(&quot;install&quot;, function (event){
    event.waitUntil(
        caches.open(CACHE_NAME).then(function (cache) {
            return cache.addAll(CACHE_URLS);
        })
    );
});

/** fetch 응답 로직
* request fetch 과정에서 실패해서 catch로 넘어오면 
* -&amp;gt; 캐시에서 해당 request가 있는지 match해서 -&amp;gt; 있다면 응답 리턴
*/
self.addEventListener(&quot;fetch&quot;, function (event) {
    event.respondWith(
        fetch(event.request).catch(function() {
            return caches.match(event.request).then(function(response){
                if (response) {
                    return response;
                }
                else if (event.request.headers.get(&quot;accept&quot;).includes(&quot;text/html&quot;)){
                    return caches.match(&quot;/offline.html&quot;);
                }
            });
        })
    );
});&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;addAll 함수를 사용하여 원하는 파일들을 한번에 캐싱한다.&lt;/li&gt;
&lt;li&gt;match 후 캐시된 파일을 찾을 때 then으로 예외를 처리하는 이유
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cache.match는 찾지 못하면 undefined를 반환&lt;/li&gt;
&lt;li&gt;프로미스를 reject하기 위해 then으로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스워커의 생명주기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2480&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kBTQm/btsNSwQ4JiG/f3OXprRwR7uVnrwTsQR3W0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kBTQm/btsNSwQ4JiG/f3OXprRwR7uVnrwTsQR3W0/img.png&quot; data-alt=&quot;https://www.oreilly.com/library/view/building-progressive-web/9781491961643/ch04.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kBTQm/btsNSwQ4JiG/f3OXprRwR7uVnrwTsQR3W0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkBTQm%2FbtsNSwQ4JiG%2Ff3OXprRwR7uVnrwTsQR3W0%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;747&quot; height=&quot;126&quot; data-origin-width=&quot;2480&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.oreilly.com/library/view/building-progressive-web/9781491961643/ch04.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생명주기 5단계&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;설치 중 (Installing)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;navigator.serviceWorker.register&lt;/code&gt; 를 사용하여 새로운 서비스 워커를 등록할 때, js 다운로드 및 파싱 후 &lt;code&gt;installing&lt;/code&gt;에 들어간다.&lt;/li&gt;
&lt;li&gt;설치가 무사히 완료된다면(promise가 resolve되면) &lt;code&gt;installed&lt;/code&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;code&gt;redundant(중복)&lt;/code&gt; 상태에 빠진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;install 이벤트 콜백 내에서 &lt;code&gt;waitUntil&lt;/code&gt; 함수를 사용해서 installing 상태를 연장할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;설치 됨 / 대기 중(installed/waiting)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 워커가 성공적으로 설치되면 &amp;rarr; &lt;code&gt;installed&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 활성화된 다른 서비스워커가 앱을 제어하고 있는게 아니라면(단독이라면) &amp;rarr; &lt;code&gt;activating&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;다른 서비스워커가 앱을 제어하고 있다면 &amp;rarr; &lt;code&gt;waiting&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;활성화 중(Activating)
&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;code&gt;waitUntil&lt;/code&gt; 을 사용하여 호출을 연장할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;활성화 중(Activating)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스워커가 활성화되면 페이지를 제어하고, fetch 이벤트와 같은 동작 이벤트를 받을 준비가 된다.&lt;/li&gt;
&lt;li&gt;서비스워커가 활성화되기 전에 로딩이 시작된 페이지는 제어할 수 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지 로딩이 시작되기 전에만 페이지 제어 권환을 가져올 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;중복(Redunant)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 워커가 설치 중 실패하거나 새로운 버전으로 교체되면 &lt;code&gt;Redunant(중복 상태)&lt;/code&gt;가 된다.&lt;/li&gt;
&lt;li&gt;이 상태에서는 앱에 아무런 영향을 미칠 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스워커의 수명&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 사이트를 방문하면 &amp;rarr; 서비스 워커가 시작되고 &amp;rarr; 페이지에서 이벤트 처리를 완료하면 &amp;rarr; 종료된다.&lt;/li&gt;
&lt;li&gt;다른 이벤트가 나중에 들어온다면 다시 서비스워커는 시작되고 &amp;rarr; 종료된다.&lt;/li&gt;
&lt;li&gt;때문에 이 이벤트 리스너 코드의 실행-종료가 중요하다. 서비스워커 작업 도중이어도 이벤트 리스터 코드의 실행이 종료되면 정상적으로 작동하지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때 &lt;code&gt;waitUntil&lt;/code&gt; 을 사용해서 브라우저가 서비스워커 작업이 완료될 때까지 기다리게 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1746973091270&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;self.addEventListener(&quot;push&quot;, function (event) { 
	event.waitUntil( fetch(&quot;/updates&quot;).then(function () { 
    		return self.registration.showNotification(&quot;New Update&quot;); 
        }) 
    ); 
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스 워커 업데이트&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 워커 내의 코드를 바꾸고 새로고침했을 때 업데이트된 내용이 반영되지 않을 수 있다.&lt;/li&gt;
&lt;li&gt;페이지를 새로고침 하면 브라우저는 서비스 워커 스크립트에 대한 업데이트가 있는지 확인하고, 업데이트가 있다면 새로운 서비스 워커를 설치(install)한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 바로 교체되지 않고 대기중(waiting) 상태에 남는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;현재 활성화된 서비스 워커가 제어하는 페이지가 더 이상 열려 있지 않아야 &amp;rArr; 새 서비스 워커가 활성화된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 탭과 윈도우 창이 종료되거나&lt;/li&gt;
&lt;li&gt;범위를 벗어난 새로운 페이지로 이동할 때까지 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스 워커 캐시 관리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시명에 버전 넘버를 달고, 파일이 변경될 때마다 버전 숫자를 증가시키는 방법이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;var CACHE_NAME = &quot;gih-cache-v2&quot;;
var CACHED_URLS = [
  &quot;/index-offline.html&quot;,
  &quot;https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css&quot;,
  &quot;/css/gih-offline.css&quot;,
  &quot;/img/jumbo-background-sm.jpg&quot;,
  &quot;/img/logo-header.png&quot;,
];

self.addEventListener(&quot;install&quot;, function (event) {
  event.waitUntil(
    caches.open(CACHE_NAME).then(function (cache) {
      return cache.addAll(CACHED_URLS);
    })
  );
});

self.addEventListener(&quot;fetch&quot;, function (event) {
  event.respondWith(
    fetch(event.request).catch(function () {
      return caches.match(event.request).then(function (response) {
        if (response) {
          return response;
        } else if (event.request.headers.get(&quot;accept&quot;).includes(&quot;text/html&quot;)) {
          return caches.match(&quot;/index-offline.html&quot;);
        }
      });
    })
  );
});&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;혹은 workbox 같은 pwa 라이브러리를 이용해서 캐싱을 쉽게 할 수 있다.
&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://developer.chrome.com/docs/workbox/what-is-workbox?hl=ko&quot;&gt;https://developer.chrome.com/docs/workbox/what-is-workbox?hl=ko&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;이번 서비스에서도 workbox의 옵션을 이용해서 캐싱을 관리했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@hancihu/%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9B%8C%EC%BB%A4%EC%9D%98-%EC%83%9D%EB%AA%85-%EC%A3%BC%EA%B8%B0%EC%99%80-%EC%BA%90%EC%8B%9C-%EA%B4%80%EB%A6%AC&quot;&gt;https://velog.io/@hancihu/%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9B%8C%EC%BB%A4%EC%9D%98-%EC%83%9D%EB%AA%85-%EC%A3%BC%EA%B8%B0%EC%99%80-%EC%BA%90%EC%8B%9C-%EA%B4%80%EB%A6%AC&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1746972595573&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[PWA] 서비스 워커의 생명 주기와 캐시 관리&quot; data-og-description=&quot;본 포스팅은 아래 링크의 만들면서 배우는 프로그레시브 웹 앱 책을 보며 공부한 내용을 스스로 정리한 것 입니다.만들면서 배우는 프로그레시브 웹 앱이번 포스팅부터 사용할 실습 코드는 포&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@hancihu/%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9B%8C%EC%BB%A4%EC%9D%98-%EC%83%9D%EB%AA%85-%EC%A3%BC%EA%B8%B0%EC%99%80-%EC%BA%90%EC%8B%9C-%EA%B4%80%EB%A6%AC&quot; data-og-url=&quot;https://velog.io/@hancihu/서비스-워커의-생명-주기와-캐시-관리&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bidshA/hyYRvGUJkh/hoTezOzipZW74ucdnF5IRk/img.png?width=1280&amp;amp;height=787&amp;amp;face=0_0_1280_787,https://scrap.kakaocdn.net/dn/AK234/hyYRjTZNcU/3xPx0G3pz5QQV6YP3M9Iu1/img.png?width=1280&amp;amp;height=787&amp;amp;face=0_0_1280_787,https://scrap.kakaocdn.net/dn/ekrwHI/hyYRmpEhup/P4fNKwoSrnlIzXsayZ7gUK/img.png?width=1280&amp;amp;height=787&amp;amp;face=0_0_1280_787&quot;&gt;&lt;a href=&quot;https://velog.io/@hancihu/%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9B%8C%EC%BB%A4%EC%9D%98-%EC%83%9D%EB%AA%85-%EC%A3%BC%EA%B8%B0%EC%99%80-%EC%BA%90%EC%8B%9C-%EA%B4%80%EB%A6%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@hancihu/%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9B%8C%EC%BB%A4%EC%9D%98-%EC%83%9D%EB%AA%85-%EC%A3%BC%EA%B8%B0%EC%99%80-%EC%BA%90%EC%8B%9C-%EA%B4%80%EB%A6%AC&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bidshA/hyYRvGUJkh/hoTezOzipZW74ucdnF5IRk/img.png?width=1280&amp;amp;height=787&amp;amp;face=0_0_1280_787,https://scrap.kakaocdn.net/dn/AK234/hyYRjTZNcU/3xPx0G3pz5QQV6YP3M9Iu1/img.png?width=1280&amp;amp;height=787&amp;amp;face=0_0_1280_787,https://scrap.kakaocdn.net/dn/ekrwHI/hyYRmpEhup/P4fNKwoSrnlIzXsayZ7gUK/img.png?width=1280&amp;amp;height=787&amp;amp;face=0_0_1280_787');&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;[PWA] 서비스 워커의 생명 주기와 캐시 관리&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;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PWA</category>
      <category>pwa</category>
      <category>서비스워커</category>
      <category>서비스워커 생명주기</category>
      <author>옹헤옹</author>
      <guid isPermaLink="true">https://ongheong.tistory.com/11</guid>
      <comments>https://ongheong.tistory.com/entry/%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9B%8C%EC%BB%A4%EB%9E%80#entry11comment</comments>
      <pubDate>Sun, 11 May 2025 23:07:12 +0900</pubDate>
    </item>
    <item>
      <title>PWA란?</title>
      <link>https://ongheong.tistory.com/entry/PWA%EB%9E%80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bippeF/btsNJv0ao3v/NWS6qdtZKTcQkmZ9IW9J8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bippeF/btsNJv0ao3v/NWS6qdtZKTcQkmZ9IW9J8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bippeF/btsNJv0ao3v/NWS6qdtZKTcQkmZ9IW9J8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbippeF%2FbtsNJv0ao3v%2FNWS6qdtZKTcQkmZ9IW9J8k%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;512&quot; height=&quot;512&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;글 작성 목적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 PWA를 사용했는데, 완성까지의 시간이 촉박하여 제대로 알아보지 못하고 적용하게 되었습니다. 이로 인해 PWA가 동작하는 서비스워커 환경에서 추가적인 이슈들이 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 일을 기회로 삼아 PWA란 무엇인지, 서비스워커란 무엇인지에 대해 정리하고 넘어가고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그래서, PWA가 뭔데?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PWA&lt;/code&gt;는 &lt;code&gt;Progressive Web App&lt;/code&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;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 웹 앱 (Web App)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구동 방식이 앱처럼 보이는, 웹 브라우저를 통해 실행 가능한 애플리케이션입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;링크(URL)을 통해 간단하게 접근할 수 있고, 별도의 설치 과정이 필요없습니다.&lt;/li&gt;
&lt;li&gt;접근성이 좋고, 사용할 수 있는 범위가 넓습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점 &lt;b&gt;❌&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디바이스 접근 권한에 제한이 있어 특정 디바이스 기능을 사용하지 못할 수도 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 네이티브 앱 (Native App)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모바일 운영체제나, 데스크톱 운영체제에 직접 설치되는 애플리케이션입니다.&lt;/li&gt;
&lt;li&gt;앱스토어(iOS)나 플레이스토어(Android)을 통해 설치한 앱을 뜻합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점 &lt;b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;✅&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디바이스의 권한에 제한이 없고, 하드웨어 기능을 직접 활용할 수 있습니다. ex) 카메라, 마이크&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점 &lt;b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;❌&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플랫폼에 맞춰 고유한 언어나 도구를 사용하여 개발해야 합니다.&lt;/li&gt;
&lt;li&gt;앱스토어로 직접 설치해야 해서 접근성이 다소 떨어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 방식의 장점만을 모아 만든 PWA는 이런 장점을 가지게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 브라우저에서 실행 가능하여 &lt;b&gt;별도의 다운로드가 불필요&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네이티브 앱과 유사한 사용자 경험&lt;/b&gt;을 제공할 수 있습니다.&lt;/li&gt;
&lt;li&gt;앱을 출시할 경우, 기존 네이티브 앱과 비교하여 &lt;b&gt;사이즈를 크게 줄일 수 있습니다&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;b&gt;  PWA로 생성한 앱을 스토어에 제출할 수 있나요?&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/b&gt;PWA는 Google이 2015년에 제안하고 주도적으로 지원해온 기술로, PlayStore에 출시하는 방법이 &lt;a href=&quot;https://web.dev/explore/progressive-web-apps?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;문서&lt;/a&gt;로 잘 정리되어 있습니다. &lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;하지만 단순히 PWA만 적용한 앱이라면 iOS 앱스토어에는 제출할 수 없습니다&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-size: 1em; letter-spacing: 0px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;Apple의 &lt;a href=&quot;https://developer.apple.com/app-store/review/guidelines/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;App Store Review Guidelines&lt;/a&gt;에 따르면, 앱은 단순한 웹사이트의 포장(wrapped website)이 아닌, 고유한 기능과 사용자 경험을 제공해야 합니다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PWA를 iOS 앱으로 제출하려면 웹 콘텐츠를 WebView로 감싼 네이티브 앱을 생성해야 합니다. &lt;a href=&quot;https://docs.pwabuilder.com/#/builder/app-store&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PWABuilder&lt;/a&gt;와 같은 도구를 사용하면 PWA를 기반으로 한 iOS 앱 패키지를 쉽게 생성할 수 있습니다.&lt;/li&gt;
&lt;li&gt;그렇다고 해서 심사를 100% 통과할 수 있는 것은 아니며, 네이티브 기능을 추가해야 통과 가능성이 높아지는 것으로 추측됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://brainhub.eu/library/pwa-on-ios&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;현재(2025년) iOS의 PWA 사용자 경험 제공 정도&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/PWA/comments/1bpqsdr/has_any_one_successfully_used_pwabuilder_to_ship/?rdt=56922&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;실제 적용된 사례&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에, 여전히 &lt;b&gt;한계점&lt;/b&gt;도 존재합니다. 일부 브라우저에서는 지원이 제한된다거나, 네이티브 앱에 비해서는 제한적인 하드웨어 접근 권한이 주어진다는 점 등이 그렇습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 &lt;a href=&quot;https://jakearchibald.github.io/isserviceworkerready/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;주요 브라우저&lt;/a&gt;에서는 모두 PWA를 지원하고 있습니다.&lt;/li&gt;
&lt;li&gt;네이티브 기능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;접근 가능 : Geolocation, Device Orientation &amp;amp; Motion, camera &amp;amp; microphone, vibration 등&lt;/li&gt;
&lt;li&gt;접근 불가능 : System access, Low-level hardware access&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 PWA가 필수적으로 갖는 특징들이 있습니다. 정확히 말하자면 PWA가 실행되는 service worker의 특징이라고 볼 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;백그라운드에서 실행되는 JavaScript 파일인 &lt;b&gt;service worker&lt;/b&gt;가 지원되는 브라우저에서만 동작합니다.&lt;/li&gt;
&lt;li&gt;보안 상의 이유로 &lt;b&gt;HTTPS에서만 동작&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;앱의 메타 정보(앱의 이름, 아이콘, 배경색 등)가 나와 있는 &lt;b&gt;manifest.json&lt;/b&gt; 파일을 필요로 합니다.&lt;/li&gt;
&lt;li&gt;일반적으로 index.html 파일을 메인 엔트리 포인트로 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PWA를 프로젝트에 적용한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 이러한 설계들과 요구사항들이 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자에게 정산 확인을 알리기 위해, 네이티브 기능 중 하나인 &lt;b&gt;푸시 알림 기능&lt;/b&gt;이 필요했습니다.&lt;/li&gt;
&lt;li&gt;제한된 개발 시간과 팀의 기술 역량을 고려하여 &lt;b&gt;웹 기반으로 개발&lt;/b&gt;을 진행하되, 모바일 사용자 비중이 높을 것으로 예상되어 &lt;b&gt;Mobile-First Design&lt;/b&gt;을 적용했습니다.&lt;/li&gt;
&lt;li&gt;다양한 사용자 접근성을 고려하여, &lt;b&gt;PC 환경에서도 원활하게 사용&lt;/b&gt;할 수 있도록 설계했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 특징들을 종합적으로 고려하여, 제한된 리소스 내에서 사용자 경험을 높이기 위해 프로젝트에 PWA를 적용하게 되었습니다.  &lt;/p&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 PWA란 무엇인지, 이 기술을 왜 프로젝트에 적용했는지에 대해 살펴봤습니다. 다음으로는 PWA를 프로젝트에 적용한 실제 과정과, PWA가 동작하는 서비스워커의 작동원리에 대한 글로 돌아오겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a href=&quot;https://ongheong.tistory.com/entry/%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9B%8C%EC%BB%A4%EB%9E%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;다음 글: 서비스워커란?&lt;/a&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1747039725265&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;서비스워커란?&quot; data-og-description=&quot;서비스워커란?정의특정 출처(사이트)의 하나 혹은 그 이상의 페이지를 제어하는 스크립트이벤트 기반 워커로서 JavaScript로 작성된 파일기능서비스 워커는 자신이 제어하는 페이지에서 발생하는&quot; data-og-host=&quot;ongheong.tistory.com&quot; data-og-source-url=&quot;https://ongheong.tistory.com/entry/%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9B%8C%EC%BB%A4%EB%9E%80&quot; data-og-url=&quot;https://ongheong.tistory.com/entry/%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9B%8C%EC%BB%A4%EB%9E%80&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/wNcJZ/hyYVeRe9RI/hrrQE0Qz31iBpDcJH8PNNk/img.png?width=800&amp;amp;height=267&amp;amp;face=0_0_800_267,https://scrap.kakaocdn.net/dn/r0Kjr/hyYVbtr1HX/ZZMiQX2RAPtfKNOZKL5FDk/img.png?width=800&amp;amp;height=267&amp;amp;face=0_0_800_267,https://scrap.kakaocdn.net/dn/4B3N6/hyYPniqtwQ/DbKtj4Ixp1JXjpVeNrS4jK/img.png?width=2236&amp;amp;height=748&amp;amp;face=0_0_2236_748&quot;&gt;&lt;a href=&quot;https://ongheong.tistory.com/entry/%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9B%8C%EC%BB%A4%EB%9E%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ongheong.tistory.com/entry/%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9B%8C%EC%BB%A4%EB%9E%80&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/wNcJZ/hyYVeRe9RI/hrrQE0Qz31iBpDcJH8PNNk/img.png?width=800&amp;amp;height=267&amp;amp;face=0_0_800_267,https://scrap.kakaocdn.net/dn/r0Kjr/hyYVbtr1HX/ZZMiQX2RAPtfKNOZKL5FDk/img.png?width=800&amp;amp;height=267&amp;amp;face=0_0_800_267,https://scrap.kakaocdn.net/dn/4B3N6/hyYPniqtwQ/DbKtj4Ixp1JXjpVeNrS4jK/img.png?width=2236&amp;amp;height=748&amp;amp;face=0_0_2236_748');&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;서비스워커란?정의특정 출처(사이트)의 하나 혹은 그 이상의 페이지를 제어하는 스크립트이벤트 기반 워커로서 JavaScript로 작성된 파일기능서비스 워커는 자신이 제어하는 페이지에서 발생하는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ongheong.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>PWA</category>
      <category>pwa</category>
      <category>PWA란</category>
      <category>네이티브앱</category>
      <category>웹앱</category>
      <author>옹헤옹</author>
      <guid isPermaLink="true">https://ongheong.tistory.com/10</guid>
      <comments>https://ongheong.tistory.com/entry/PWA%EB%9E%80#entry10comment</comments>
      <pubDate>Sun, 4 May 2025 14:41:52 +0900</pubDate>
    </item>
    <item>
      <title>[리액트 공식문서] 상호작용성 더하기 - (3) 렌더링 그리고 커밋</title>
      <link>https://ongheong.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9%EC%84%B1-%EB%8D%94%ED%95%98%EA%B8%B0-3-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%BB%A4%EB%B0%8B</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트가 화면에 표시되는 과정은 다음의 3단계로 이루어집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 렌더링 트리거&lt;/h3&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;background-color: #f6e199;&quot;&gt;React에서 컴포넌트를 호출&lt;/span&gt;하는 것입니다. 이러한 렌더링은 다음 두 경우에서 트리거됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 컴포넌트의 &lt;span style=&quot;background-color: #f6e199;&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;createRoot()에 화면에 보일 DOM 노드(루트 노드)를 인수로 넣어 호출 -&amp;gt; 객체에 할당&lt;/li&gt;
&lt;li&gt;할당된 객체로 render() 메서드를 호출 -&amp;gt; 렌더링 트리거&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1736830631004&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'))
root.render(&amp;lt;App /&amp;gt;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 컴포넌트의 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;state가 업데이트&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;set 함수를 통해 state를 업데이트하여 추가적인 렌더링을 트리거합니다.&lt;/li&gt;
&lt;li&gt;컴포넌트의 state를 업데이트하면 자동으로 렌더링 대기열에 추가됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. React 컴포넌트 렌더링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;렌더링을 트리거한 후, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;React는 컴포넌트를 호출&lt;/span&gt;하여 화면에 어떤 내용이 표시될지 확인합니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 렌더링의 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;React는 루트 컴포넌트를 호출합니다.&lt;/li&gt;
&lt;li&gt;React는 Virtual DOM 트리를 생성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;state 업데이트로 렌더링 될 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;React는 업데이트가 일어난 컴포넌트를 호출합니다.&lt;/li&gt;
&lt;li&gt;새로 Virtual DOM 트리를 구성하고, 이전 렌더링(이전 Virtual DOM)과 비교하여 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;변경된 속성을 계산&lt;/span&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 업데이트된 컴포넌트가 다른 컴포넌트를 반환하면 그 컴포넌트도 이후에 렌더링됩니다. 중첩된 컴포넌트가 더 이상 없을 때까지 렌더링이 재귀적으로 계속됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&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;이전 state를 변경하면 안된다. 즉, state는 불변성(immutability)을 유지해야 한다. -&amp;gt; 항상 새로운 상태 객체를 만들어 React에 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Strict Mode&lt;/span&gt;를 이용하여 개발한다면 각 컴포넌트의 함수를 두 번 호출하므로, 순수하지 않은 함수로 인한 실수를 잡는 것에 도움이 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736834047983&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(
  &amp;lt;StrictMode&amp;gt;
    &amp;lt;App /&amp;gt;
  &amp;lt;/StrictMode&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Strick Mode 컴포넌트로 감싼 하위 컴포넌트들에 대해 모두 적용되기 때문에 일부분만 활성화할 수도 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버그를 찾기 위해 순수 함수여야 하는 특정 함수들이 두 번 호출됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 함수, useState, set 함수, useMemo, useReducer에 전달한 함수&lt;/li&gt;
&lt;li&gt;constructor, render, shouldComponentUpdate 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;버그를 찾기 위해 effects를 두 번 실행합니다. -&amp;gt; 셋업 + 클린업 사이클을 실행합니다.&lt;/li&gt;
&lt;li&gt;더 이상 사용되지 않는 API 사용 여부를 확인합니다.&lt;/li&gt;
&lt;li&gt;개발 환경에서만 실행되며, 빌드에는 영향을 미치지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. React가 DOM에 변경사항을 커밋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트가 렌더링 된 이후에, React는 DOM을 수정합니다. 즉 변경 사항을 실제 DOM에 적용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 렌더링의 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;appendChild() DOM API를 사용 -&amp;gt; 생성한 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;모든 DOM 노드를 화면에 표시&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;리렌더링의 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;React는 렌더링하는 동안 계산된 작업을 적용하여 DOM이 최신 렌더링 출력과 일치하도록 만듬&lt;/li&gt;
&lt;li&gt;React는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경&lt;/span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;894&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PEF1H/btsLNxYJTwR/nAE7u2cdZIL9EUNT9VkXY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PEF1H/btsLNxYJTwR/nAE7u2cdZIL9EUNT9VkXY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PEF1H/btsLNxYJTwR/nAE7u2cdZIL9EUNT9VkXY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPEF1H%2FbtsLNxYJTwR%2FnAE7u2cdZIL9EUNT9VkXY1%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;894&quot; height=&quot;511&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;511&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 브라우저 페인트(페인팅)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React가 DOM을 업데이트하면 브라우저는 화면을 다시 그립니다.&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 문서&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.react.dev/learn/render-and-commit&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.react.dev/learn/render-and-commit&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1736835096802&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;렌더링 그리고 커밋 &amp;ndash; React&quot; data-og-description=&quot;The library for web and native user interfaces&quot; data-og-host=&quot;ko.react.dev&quot; data-og-source-url=&quot;https://ko.react.dev/learn/render-and-commit&quot; data-og-url=&quot;https://ko.react.dev/learn/render-and-commit&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/boGJsa/hyX0ld0rpU/KnW8r7KqcpNW1FXlZhIlqk/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/gucq2/hyX0pUZGsH/ew3WhkvVo7o5pg6t9s3prK/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567&quot;&gt;&lt;a href=&quot;https://ko.react.dev/learn/render-and-commit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.react.dev/learn/render-and-commit&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/boGJsa/hyX0ld0rpU/KnW8r7KqcpNW1FXlZhIlqk/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/gucq2/hyX0pUZGsH/ew3WhkvVo7o5pg6t9s3prK/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;렌더링 그리고 커밋 &amp;ndash; React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The library for web and native user interfaces&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.react.dev&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;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.react.dev/reference/react/StrictMode&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.react.dev/reference/react/StrictMode&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1736835062096&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;&amp;lt;StrictMode&amp;gt; &amp;ndash; React&quot; data-og-description=&quot;The library for web and native user interfaces&quot; data-og-host=&quot;ko.react.dev&quot; data-og-source-url=&quot;https://ko.react.dev/reference/react/StrictMode&quot; data-og-url=&quot;https://ko.react.dev/reference/react/StrictMode&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/d3SLiF/hyX0qzCwi8/g6fh9OtlO4dzKn92KKEgT0/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/cdLdJt/hyX0zQQ5gE/wbjJnveyDBYLBMZ1Ng0aYk/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567&quot;&gt;&lt;a href=&quot;https://ko.react.dev/reference/react/StrictMode&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.react.dev/reference/react/StrictMode&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/d3SLiF/hyX0qzCwi8/g6fh9OtlO4dzKn92KKEgT0/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/cdLdJt/hyX0zQQ5gE/wbjJnveyDBYLBMZ1Ng0aYk/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;StrictMode&amp;gt; &amp;ndash; React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The library for web and native user interfaces&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.react.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>React</category>
      <author>옹헤옹</author>
      <guid isPermaLink="true">https://ongheong.tistory.com/9</guid>
      <comments>https://ongheong.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9%EC%84%B1-%EB%8D%94%ED%95%98%EA%B8%B0-3-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%BB%A4%EB%B0%8B#entry9comment</comments>
      <pubDate>Tue, 14 Jan 2025 15:10:04 +0900</pubDate>
    </item>
    <item>
      <title>[리액트 공식문서] 상호작용성 더하기 - (2) State: 컴포넌트의 기억 저장소</title>
      <link>https://ongheong.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9%EC%84%B1-%EB%8D%94%ED%95%98%EA%B8%B0-2-State-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-%EA%B8%B0%EC%96%B5-%EC%A0%80%EC%9E%A5%EC%86%8C-1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트가 상호 작용의 결과로 화면의 내용을 변경해야 할 때(렌더링해야 할 때)가 있습니다. 이때 필요한 데이터를 저장하는 컴포넌트별 메모리를 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;State&lt;/b&gt;&lt;/span&gt;라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이 데이터를 일반 변수에 저장하지 않을까요? 일반 변수(지역 변수)는 렌더링 간에 유지되지 않고, 변수를 변경해도 렌더링이 일어나지 않기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 저희는 컴포넌트를 새로운 데이터로 업데이트하기 위해 &lt;b&gt;&lt;span style=&quot;color: #000000; background-color: #c0d1e7;&quot;&gt;useState&lt;/span&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;useState처럼 &quot;use&quot;로 시작하는 모든 함수는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;훅(hook)&lt;/span&gt;&lt;/b&gt;이라고 부릅니다. 훅의 특징은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React가 렌더링 중일 때만 사용할 수 있는 함수입니다.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;컴포넌트의 최상위 또는 커스텀 훅에서만 호출할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useState 훅은 우리가 필요한 두 가지 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 렌더링 간에 데이터를 유지하기 위한 &lt;b&gt;state 변수&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 변수를 업데이트한 후 React가 컴포넌트를 다시 렌더링하도록 만드는 &lt;b&gt;state setter 함수&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736819480453&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const [index, setIndex] = useState(0);&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useState는 몇가지 &lt;span style=&quot;background-color: #c0d1e7;&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;호출 시 const [something, setSomething] 과 같이 선언하는 것이 일반적입니다.&lt;/li&gt;
&lt;li&gt;useState의 인수는 state 변수의 초기값입니다. 따라서 위 예시에서 index의 초기값은 0입니다.&lt;/li&gt;
&lt;li&gt;컴포넌트가 렌더링될 때마다, 저장한 값을 가진 state 변수와, state 변수를 업데이트하고 렌더링하도록 만드는 state setter 함수가 반환됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useState 훅은 다음처럼 &lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;사용&lt;/span&gt;할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1736819029032&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. useState를 react에서 가져온다.
import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
// 2. state 변수와 setter 함수를 선언하고, useState()가 반환하는 배열로 초기화한다.
  const [index, setIndex] = useState(0);

// 3. 이벤트 핸들러나 JSX에서 사용한다.
  function handleClick() {
    setIndex(index + 1);
  }

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={handleClick}&amp;gt;
        Next
      &amp;lt;/button&amp;gt;
      &amp;lt;h3&amp;gt;
        ({index + 1} of {sculptureList.length})
      &amp;lt;/h3&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서의 useState의 &lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;실제 작동 방식&lt;/span&gt;은 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 컴포넌트가 처음 렌더링됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인수가 0이므로 [0, setIndex]를 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 상호작용에 따라 state를 업데이트합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버튼 클릭 시 setIndex(index + 1)를 호출합니다.&lt;/li&gt;
&lt;li&gt;index는 0이므로 setIndex(1)입니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;따라서 index는 1이 되고, 렌더링이 실행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 컴포넌트가 다시 렌더링됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React는 index가 1임을 기억하기 때문에 [1, setIndex]를 반환합니다.&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 컴포넌트에 여러 개의 state 변수를 가질 수 있습니다. 하지만 두 state 변수가 자주, 함께 변경된다면 하나의 객체 state 변수를 사용하는 것이 좋습니다. ex) 필드가 많은 폼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 변수가 있을 경우, React는 useState 호출 순서를 기준으로 각 상태 값과 setter를 연결합니다. React는 모든 컴포넌트에 대해 한 쌍의 state 배열을 가지고 있고, cursor 포인터를 통해 현재 어떤 useState 호출을 처리 중인지 추적합니다.&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;Hook 호출 시 커서는 0부터 시작하여 순서대로 증가하고, 현재 커서 위치에 해당 Hook의 상태 값이 저장됩니다. 컴포넌트가 리렌더링될 때 커서를 다시 0으로 초기화하며, 훅이 호출될 때마다 커서를 증가시키며 값을 읽어옵니다.&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/cML9mt/btsLMEwD9Tt/SiqO0xlLRuWkF4DToOW6Hk/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cML9mt/btsLMEwD9Tt/SiqO0xlLRuWkF4DToOW6Hk/tfile.dat&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;720&quot; data-is-animation=&quot;false&quot; style=&quot;width: 51.7647%; margin-right: 10px;&quot; data-widthpercent=&quot;52.37&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cML9mt/btsLMEwD9Tt/SiqO0xlLRuWkF4DToOW6Hk/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcML9mt%2FbtsLMEwD9Tt%2FSiqO0xlLRuWkF4DToOW6Hk%2Ftfile.dat&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;630&quot; height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DVbGs/btsLMZNZOTY/mz9Kf6WIafkG7QAQ9l0BAk/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DVbGs/btsLMZNZOTY/mz9Kf6WIafkG7QAQ9l0BAk/tfile.dat&quot; data-origin-width=&quot;627&quot; data-origin-height=&quot;788&quot; data-is-animation=&quot;false&quot; style=&quot;width: 47.0725%;&quot; data-widthpercent=&quot;47.63&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DVbGs/btsLMZNZOTY/mz9Kf6WIafkG7QAQ9l0BAk/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDVbGs%2FbtsLMZNZOTY%2Fmz9Kf6WIafkG7QAQ9l0BAk%2Ftfile.dat&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;627&quot; height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훅이 어떻게 동작하는지에 대해, 핵심 원리는 JavaScript의 Closure 라는 글이 있습니다. 훅의 동작 원리에 대한 글은 추후 따로 분리하여 정리해보도록 하겠습니다.&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;State는 선언한 컴포넌트에 대해 비공개로 되어 있으며, 부모 컴포넌트는 이를 변경할 수 없습니다. 따라서 동일한 컴포넌트를 두 번 렌더링한다면, 각 인스턴스는 완전히 격리된 state를 가집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736821976106&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Gallery from './Gallery.js';

export default function Page() {
  return (
    &amp;lt;div className=&quot;Page&quot;&amp;gt;
      {/* Gallery 안의 state는 해당 인스턴스에 지역적이므로, 두 Gallery의 state는 서로 격리되어 있음 */}
      &amp;lt;Gallery /&amp;gt;
      &amp;lt;Gallery /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 두 인스턴스가 state를 동기화하도록 만들고 싶다면, 자식 컴포넌트에서 state를 제거하고 공통 부모 컴포넌트에 이를 추가하여 prop으로 전달하는 방법이 있습니다.&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고한 문서&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.react.dev/learn/state-a-components-memory&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.react.dev/learn/state-a-components-memory&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1736822249718&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;State: 컴포넌트의 기억 저장소 &amp;ndash; React&quot; data-og-description=&quot;The library for web and native user interfaces&quot; data-og-host=&quot;ko.react.dev&quot; data-og-source-url=&quot;https://ko.react.dev/learn/state-a-components-memory&quot; data-og-url=&quot;https://ko.react.dev/learn/state-a-components-memory&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bjs402/hyX0t366lY/v8L4NINNeEuI9pHcxqdfT0/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/dbKUZ5/hyX0q7qiST/xGS3CEAhRRvAnPghs8EAfK/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567&quot;&gt;&lt;a href=&quot;https://ko.react.dev/learn/state-a-components-memory&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.react.dev/learn/state-a-components-memory&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bjs402/hyX0t366lY/v8L4NINNeEuI9pHcxqdfT0/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/dbKUZ5/hyX0q7qiST/xGS3CEAhRRvAnPghs8EAfK/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567');&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;State: 컴포넌트의 기억 저장소 &amp;ndash; React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The library for web and native user interfaces&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.react.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1736822256700&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;React hooks: not magic, just arrays&quot; data-og-description=&quot;Untangling the rules around the proposal using diagrams&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e&quot; data-og-url=&quot;https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5AirH/hyX0kMS05M/lKIIJfZHu8o3w8oFjOzrQ1/img.jpg?width=1200&amp;amp;height=940&amp;amp;face=0_0_1200_940&quot;&gt;&lt;a href=&quot;https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5AirH/hyX0kMS05M/lKIIJfZHu8o3w8oFjOzrQ1/img.jpg?width=1200&amp;amp;height=940&amp;amp;face=0_0_1200_940');&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;React hooks: not magic, just arrays&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Untangling the rules around the proposal using diagrams&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://it-eldorado.com/posts/069bc5ac-7609-43a6-9fd4-f8b8fc84764b&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://it-eldorado.com/posts/069bc5ac-7609-43a6-9fd4-f8b8fc84764b&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1736822272931&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;React Hook 동작(구현) 원리 간단히 알아보기 :: IT 엘도라도&quot; data-og-description=&quot;요즘에는 React로 개발할 때 클래스를 많이 사용하지 않는 듯하다.&amp;nbsp;React 16.8 버전부터 도입된 Hook API가 상당한 인기를 얻고 있기 때문이다. 필자 역시도 React를 처음 공부할 때는 클래스 컴포넌트&quot; data-og-host=&quot;it-eldorado.com&quot; data-og-source-url=&quot;https://it-eldorado.com/posts/069bc5ac-7609-43a6-9fd4-f8b8fc84764b&quot; data-og-url=&quot;https://it-eldorado.com/posts/069bc5ac-7609-43a6-9fd4-f8b8fc84764b&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://it-eldorado.com/posts/069bc5ac-7609-43a6-9fd4-f8b8fc84764b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://it-eldorado.com/posts/069bc5ac-7609-43a6-9fd4-f8b8fc84764b&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;React Hook 동작(구현) 원리 간단히 알아보기 :: IT 엘도라도&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;요즘에는 React로 개발할 때 클래스를 많이 사용하지 않는 듯하다.&amp;nbsp;React 16.8 버전부터 도입된 Hook API가 상당한 인기를 얻고 있기 때문이다. 필자 역시도 React를 처음 공부할 때는 클래스 컴포넌트&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;it-eldorado.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;</description>
      <category>React</category>
      <category>react</category>
      <category>State</category>
      <category>useState</category>
      <category>리액트 공식문서</category>
      <author>옹헤옹</author>
      <guid isPermaLink="true">https://ongheong.tistory.com/8</guid>
      <comments>https://ongheong.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9%EC%84%B1-%EB%8D%94%ED%95%98%EA%B8%B0-2-State-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%9D%98-%EA%B8%B0%EC%96%B5-%EC%A0%80%EC%9E%A5%EC%86%8C-1#entry8comment</comments>
      <pubDate>Tue, 14 Jan 2025 11:38:23 +0900</pubDate>
    </item>
    <item>
      <title>[리액트 공식문서] 상호작용성 더하기 - (1) 이벤트에 응답하기</title>
      <link>https://ongheong.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-1-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9%EC%84%B1-%EB%8D%94%ED%95%98%EA%B8%B0-1-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EC%97%90-%EC%9D%91%EB%8B%B5%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이벤트 핸들러란 무엇일까요?&lt;/b&gt; 사용자 상호작용에 따라 실행되는 사용자 정의 함수 입니다. 이를 사용하기 위해서 다음 단계를 거칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 사용할 함수를 컴포넌트 내에 선언한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 함수 내부 로직을 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. JSX의 onClick 속성에 함수를 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이벤트 핸들러에는 중요한 특징이 몇가지 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 컴포넌트 내부에서 선언합니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;따라서 이벤트 핸들러는 해당 컴포넌트의 prop에 접근할 수 있습니다.&lt;/li&gt;
&lt;li&gt;이벤트 핸들러를 선언할 때 JSX와 구분하거나, JSX 내에서 인라인으로 사용하거나, 화살표 함수를 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. &quot;handle&quot; 로 시작한 명명법이 일반적입니다.&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 이벤트 핸들러로 전달한 함수들은 호출이 아닌 &quot;전달&quot;입니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 &amp;lt;button onClick={handleClick()}&amp;gt; 과 같이 괄호를 붙인다면, 렌더링 과정 중에서 즉시 함수가 실행됩니다.&lt;/li&gt;
&lt;li&gt;따라서 괄호를 제거하거나, 화살표 함수를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 이벤트 핸들러는 prop으로 전달할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736774519004&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function Button({ onClick, children }) {
  return (
    &amp;lt;button onClick={onClick}&amp;gt;
      {children}
    &amp;lt;/button&amp;gt;
  );
}

function PlayButton({ movieName }) {
  function handlePlayClick() {
    alert(`Playing ${movieName}!`);
  }

  return (
    &amp;lt;Button onClick={handlePlayClick}&amp;gt;
      Play &quot;{movieName}&quot;
    &amp;lt;/Button&amp;gt;
  );
}

export default function Toolbar() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;PlayButton movieName=&quot;Kiki's Delivery Service&quot; /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 성질을 이용하여 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;디자인 시스템&lt;/b&gt;&lt;/span&gt;을 정의할 수 있습니다.&lt;/li&gt;
&lt;li&gt;버튼과 같은 컴포넌트는 일반적으로 동작을 지정하지 않고 스타일만 지정하며, 이를 JSX 내에서 사용하는 다른 컴포넌트가 이벤트 핸들러를 전달하도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 이벤트 핸들러 prop는 관습적으로 on으로 시작하는 명명법이 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌트인 컴포넌트(div, button 등)는 브라우저 이벤트 이름만 지원합니다. ex) onClick, ref, style 등&lt;/li&gt;
&lt;li&gt;사용자 정의 컴포넌트는 이벤트 핸들러 prop의 이름을 원하는 대로 정의할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. 이벤트 핸들러에 적절한 HTML 태그를 사용해야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 CSS와 JS를 사용한다면, 모든 HTML 요소들을 div로만 만들 수 있습니다. 하지만 작업에 올바른 HTML 요소를 사용하는 것이 좋습니다. 이를 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;시멘틱 마크업&lt;/b&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;키보드 접근성이 내장되어 있어, tab 등의 &lt;span style=&quot;background-color: #a6bc00;&quot;&gt;키를 이용해 버튼을 이동하거나 활성화&lt;/span&gt;할 수 있습니다.&lt;/li&gt;
&lt;li&gt;파일 사이즈가 가볍고 반응형으로 바꾸기 쉬워 &lt;span style=&quot;background-color: #a6bc00;&quot;&gt;모바일 환경을 지원하기에 더 적합합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;검색 엔진은 태그 내부 키워드에 더 많은 가중치를 부여하기 때문에, &lt;span style=&quot;background-color: #a6bc00;&quot;&gt;SEO에 좋습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크린 리더를 사용한다면 시멘틱 마크업을 적용한 페이지들을 보다 자세하게 테스트할 수 있습니다. 적용한 코드 예시는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1736776106150&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;h1&amp;gt;My heading&amp;lt;/h1&amp;gt;

&amp;lt;p&amp;gt;This is the first section of my document.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;I'll add another paragraph here too.&amp;lt;/p&amp;gt;

&amp;lt;ol&amp;gt;
  &amp;lt;li&amp;gt;Here is&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;a list for&amp;lt;/li&amp;gt;
  &amp;lt;li&amp;gt;you to read&amp;lt;/li&amp;gt;
&amp;lt;/ol&amp;gt;

&amp;lt;h2&amp;gt;My subheading&amp;lt;/h2&amp;gt;

&amp;lt;p&amp;gt;
  This is the first subsection of my document. I'd love people to be able to
  find this content!
&amp;lt;/p&amp;gt;

&amp;lt;h2&amp;gt;My 2nd subheading&amp;lt;/h2&amp;gt;

&amp;lt;p&amp;gt;
  This is the second subsection of my content, which I think is more interesting
  than the last one.
&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. 이벤트는 자식 컴포넌트에서 부모 컴포넌트로 전파(bubble)됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전파 시 이벤트가 일어난 자식 컴포넌트 -&amp;gt; 부모 컴포넌트 순으로 bubbling이 일어납니다.&lt;/li&gt;
&lt;li&gt;이벤트 오브젝트를 통해 불필요한 전파를 막을 수 있습니다.&lt;/li&gt;
&lt;li&gt;이벤트 오브젝트란 이벤트 핸들러가 받을 수 있는 유일한 매개변수로, 이벤트의 정보를 읽을 수 있으며, e로 호출하는 것이 일반적입니다.&lt;/li&gt;
&lt;li&gt;자식 컴포넌트의 이벤트 핸들러 함수 내부에서 e.stopPropagation() 함수를 호출한다면 bubbling을 막을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8. 브라우저 이벤트의 기본 동작을 방지할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;form의 기본 브라우저 동작은 내부의 버튼 클릭 시 페이지 전체를 리로드하는 것입니다.&lt;/li&gt;
&lt;li&gt;e.preventDefault() 함수를 이벤트 핸들러 함수 내부에서 호출한다면 이를 막을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;9. 이벤트 핸들러는 사이드 이펙트를 사용하기에 적합한 위치입니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수와 달리 이벤트 핸들러는 순수할 필요가 없어, 무언가를 변경하기에 알맞습니다.&lt;/li&gt;
&lt;li&gt;정보를 수정하기 전, 먼저 그 정보를 저장하기 위해 state를 이용할 수 있습니다.&lt;/li&gt;
&lt;/ul&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고문서&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ko.react.dev/learn/responding-to-events&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.react.dev/learn/responding-to-events&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1736777167683&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;이벤트에 응답하기 &amp;ndash; React&quot; data-og-description=&quot;The library for web and native user interfaces&quot; data-og-host=&quot;ko.react.dev&quot; data-og-source-url=&quot;https://ko.react.dev/learn/responding-to-events&quot; data-og-url=&quot;https://ko.react.dev/learn/responding-to-events&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/uYFcD/hyX0m4Tt2V/K86FieYyUozVC337FAaCP0/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/b9OBEb/hyX0ydakF9/jSpqlZNBKDKG8eTEh9p3nk/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567&quot;&gt;&lt;a href=&quot;https://ko.react.dev/learn/responding-to-events&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.react.dev/learn/responding-to-events&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/uYFcD/hyX0m4Tt2V/K86FieYyUozVC337FAaCP0/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567,https://scrap.kakaocdn.net/dn/b9OBEb/hyX0ydakF9/jSpqlZNBKDKG8eTEh9p3nk/img.png?width=1080&amp;amp;height=567&amp;amp;face=0_0_1080_567');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;이벤트에 응답하기 &amp;ndash; React&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The library for web and native user interfaces&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.react.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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://developer.mozilla.org/ko/docs/Learn_web_development/Core/Accessibility/HTML&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.mozilla.org/ko/docs/Learn_web_development/Core/Accessibility/HTML&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>React</category>
      <category>react</category>
      <category>리액트</category>
      <category>리액트 공식문서</category>
      <category>이벤트 핸들러</category>
      <author>옹헤옹</author>
      <guid isPermaLink="true">https://ongheong.tistory.com/6</guid>
      <comments>https://ongheong.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-1-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9%EC%84%B1-%EB%8D%94%ED%95%98%EA%B8%B0-1-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EC%97%90-%EC%9D%91%EB%8B%B5%ED%95%98%EA%B8%B0#entry6comment</comments>
      <pubDate>Mon, 13 Jan 2025 23:08:30 +0900</pubDate>
    </item>
    <item>
      <title>프론트엔드 개발자 지망생의 2024년 되돌아보기</title>
      <link>https://ongheong.tistory.com/entry/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%A7%80%EB%A7%9D%EC%83%9D%EC%9D%98-2024%EB%85%84-%EB%90%98%EB%8F%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRgaRs/btsLABtYWVM/udJKzBHCYsHnaBvwsvTklK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRgaRs/btsLABtYWVM/udJKzBHCYsHnaBvwsvTklK/img.jpg&quot; data-alt=&quot;대충 2024년을 설명하는 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRgaRs/btsLABtYWVM/udJKzBHCYsHnaBvwsvTklK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRgaRs%2FbtsLABtYWVM%2FudJKzBHCYsHnaBvwsvTklK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;564&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대충 2024년을 설명하는 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;글을 적게 된 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일기장에나 적어둘 것 같은 이 글을, 2024년의 마지막 날에 작성하게 된 까닭은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 벌써 1년동안 뭘 했는지 가물가물하다. 지금 적어두지 않으면 전부 잊고 나서 나중에 1년을 낭비했다고 후회할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 1년 동안 잘못한 것은 스스로 반성하고 잘한 것은 칭찬하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 블로그를 꾸준히 쓰는 습관을 들여보고 싶은데, 가장 먼저 떠오르는 주제가 이것뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 비루한 글 솜씨에도 불구하고 적어보게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 아마 이 글을 읽는 사람은 별로 없겠지만, 혹시나 저와 비슷한 상황에서 힘들어하는 사람이라면 읽으며 때로는 공감하고 때로는 비웃으며 힘든 마음을 털어버리기를 바랍니다. 저 또한 &quot;2024 프론트엔드 취업&quot; 같은 키워드를 검색하며 여러 블로그에서 많은 위안을 받았으니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 그래서, 이 프론트엔드 개발자(지망생)는 대체&lt;b&gt; 1년 동안 무엇을 한 것일까요?&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;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1 ~ 2월: 일단 자격증부터 따고 보자&lt;br /&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 12월, 저는 험난한 막학기 22학점을 거치고 드디어 필수 이수 학점을 달성했습니다. 하지만 영어 성적이 없었기 때문에 졸업을 할 수가 없었죠. 그래서 같은 상황의 동기와 손잡고 사이좋게 토익 스피킹 학원을 등록했습니다. 동시에 정보처리기사 자격증도 준비하고, 코딩테스트를 대비한 알고리즘 문제들도 풀기 시작했습니다.&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/SNdcY/btsLAUUGqcj/gTDkXiVVZFGzD0WjQecsk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SNdcY/btsLAUUGqcj/gTDkXiVVZFGzD0WjQecsk0/img.png&quot; width=&quot;325&quot; height=&quot;441&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;407&quot; data-is-animation=&quot;false&quot; data-filename=&quot;blob&quot; style=&quot;width: 26.9003%; margin-right: 10px;&quot; data-widthpercent=&quot;27.54&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SNdcY/btsLAUUGqcj/gTDkXiVVZFGzD0WjQecsk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSNdcY%2FbtsLAUUGqcj%2FgTDkXiVVZFGzD0WjQecsk0%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;300&quot; height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IQV87/btsLCnhbc59/x1fXJwmOMjeVBtauhWS0gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IQV87/btsLCnhbc59/x1fXJwmOMjeVBtauhWS0gk/img.png&quot; data-origin-width=&quot;1229&quot; data-origin-height=&quot;1254&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;294&quot; data-widthpercent=&quot;36.62&quot; data-filename=&quot;blob&quot; style=&quot;width: 35.7672%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IQV87/btsLCnhbc59/x1fXJwmOMjeVBtauhWS0gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIQV87%2FbtsLCnhbc59%2Fx1fXJwmOMjeVBtauhWS0gk%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;1229&quot; height=&quot;1254&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QlPIr/btsLClKoqS2/tUvu3oKw4xlvNek4JKFJAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QlPIr/btsLClKoqS2/tUvu3oKw4xlvNek4JKFJAK/img.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;417&quot; data-is-animation=&quot;false&quot; data-filename=&quot;blob&quot; style=&quot;width: 35.0069%;&quot; data-widthpercent=&quot;35.84&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QlPIr/btsLClKoqS2/tUvu3oKw4xlvNek4JKFJAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQlPIr%2FbtsLClKoqS2%2FtUvu3oKw4xlvNek4JKFJAK%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;400&quot; height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;분명 2024 하반기 취업이 목표였으나...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정보처리기사 준비는 그 유명한 시나공의 기본서와 기출문제집 하나씩을 사서 꾸준히 풀었습니다. 코딩테스트는 기본 개념도 자리잡히지 않아서, 인프런에서 개념 강의를 찾아 들었던 기억이 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 본격적인 프론트엔드 개발자 준비를 위해 현업자 밋업을 기웃거렸는데, 소플님의 프론트엔드 밋업 행사에도 참여했습니다. 덕분에 프론트엔드 개발자가 되기 위해 어떤 순서로, 무엇을 공부해야 하는지 가닥을 잡을 수 있었습니다 (직접 쓰신 개발 책도 증정해주셔서 돈없는 취준생에게 많은 도움이 되었습니다! 짱짱  ). 이때의 좋은 기억으로 이후에 열린 개발자 행사에도 적극적으로 지원하고 참여하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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/bkvvwc/btsLz7mpKxq/KvKqj3I9jRlkTdkJmPWSdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkvvwc/btsLz7mpKxq/KvKqj3I9jRlkTdkJmPWSdK/img.png&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;824&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;170&quot; data-widthpercent=&quot;40.79&quot; data-filename=&quot;edited_blob&quot; style=&quot;width: 39.8451%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkvvwc/btsLz7mpKxq/KvKqj3I9jRlkTdkJmPWSdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkvvwc%2FbtsLz7mpKxq%2FKvKqj3I9jRlkTdkJmPWSdK%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;890&quot; height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uOczB/btsLCISJnGm/w0Qt8q1hJqF8fIyOccu7K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uOczB/btsLCISJnGm/w0Qt8q1hJqF8fIyOccu7K1/img.png&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1568&quot; data-is-animation=&quot;false&quot; width=&quot;400&quot; height=&quot;711&quot; data-widthpercent=&quot;28.18&quot; data-filename=&quot;blob&quot; style=&quot;width: 27.5266%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uOczB/btsLCISJnGm/w0Qt8q1hJqF8fIyOccu7K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuOczB%2FbtsLCISJnGm%2Fw0Qt8q1hJqF8fIyOccu7K1%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;1170&quot; height=&quot;1568&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CQLFy/btsLArL6gS9/mvhQsyNWOxSsN29Mk7Gph1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CQLFy/btsLArL6gS9/mvhQsyNWOxSsN29Mk7Gph1/img.png&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;1400&quot; data-is-animation=&quot;false&quot; style=&quot;width: 30.3027%;&quot; data-widthpercent=&quot;31.03&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CQLFy/btsLArL6gS9/mvhQsyNWOxSsN29Mk7Gph1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCQLFy%2FbtsLArL6gS9%2FmvhQsyNWOxSsN29Mk7Gph1%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;1150&quot; height=&quot;1400&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;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3 ~ 5월: 이렇게 자소서를 많이 쓰는데 취업이 안된다고?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 취업 시즌의 3월이었습니다. 하지만 어떤 기업을 가고 싶은지, 어떤 분야를 가고 싶은지조차 확실하지 않았습니다. 그래서 일단 무작정 다 써보자, 라는 마인드로 자소서를 난사하기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업 플랫폼은 대기업 ~ 중견기업 공고가 주로 올라오는 자소설닷컴을 이용했습니다. 지원한 기업은 공기업, 금융업, 제조업, 서비스... 등 분야를 가리지 않았습니다. 그곳에서 '전공자 우대', 'IT 분야' 라는 단어가 들어가면 그 즉시 자소서를 작성하여 제출했습니다. 하루에 6개의 자소서를 작성한 적도 있었고, 생일에도 저녁을 먹고 돌아와 다시 자소서를 작성한 적도 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 마구잡이로 작성한 자소서는 29개. 그 중에서 서류를 합격한 것은 9개. 그 안에서 면접까지 간 것은, 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;어디에서 막혀버린 것일까요. 그렇습니다. 전부 코딩테스트의 문턱을 넘지 못했습니다. 겨울부터 문제를 풀었지만, 설렁설렁 풀어왔던 것인지, 아니면 그냥 그쪽 머리가 없는지는 알 수 없습니다(사실 2개 다 해당한다고 생각합니다). 또한 공기업 필기 시험도 몇 번 보았지만 전부 떨어졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서류에서도 우수수 떨어지는데 간신히 붙은 몇개는 모두 코딩 테스트, 혹은 필기 시험에서 탈락이라니. 어디 가서 전공자라고 말하기 창피하고 갈수록 자신감이 낮아지는 우울한 나날이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그나마 좌절하여 방에 틀어박히고 은둔청년으로 전직하지 않을 수 있었던 이유는 같은 목표(취업 혹은 시험)를 향해 달려가는 좋은 주변 사람들이 있었고, 돈없는 취준생에게 무료로 장소를 대여해준 &quot;취준컴퍼니&quot; 프로그램이 있었고, 운동을 조금씩 꾸준히 해왔던 습관이 있어서라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 사람들과 소통하고, 밖에 나갈 이유를 만들기 위해 정말 여러 자잘한 활동들을 해왔습니다. 앞서 말한 취준 컴퍼니, 항해 99의 코테 스터디, 삼성생명에서 진행한 삼성금융아카데미 교육 프로그램, 카페 아르바이트, 오전 기상 스터디 등등 ...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buutCR/btsLCGtYL8F/kE206RD4hZGnI5KPt1xw9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buutCR/btsLCGtYL8F/kE206RD4hZGnI5KPt1xw9K/img.png&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;1486&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;35.03&quot; style=&quot;width: 34.2135%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buutCR/btsLCGtYL8F/kE206RD4hZGnI5KPt1xw9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuutCR%2FbtsLCGtYL8F%2FkE206RD4hZGnI5KPt1xw9K%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;1402&quot; height=&quot;1486&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJq4UM/btsLBXbM7xC/NL4qnD77OGyNi0z9ho2JRK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJq4UM/btsLBXbM7xC/NL4qnD77OGyNi0z9ho2JRK/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;720&quot; data-filename=&quot;IMG_6500.JPG&quot; style=&quot;width: 36.2634%; margin-right: 10px;&quot; data-widthpercent=&quot;37.13&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJq4UM/btsLBXbM7xC/NL4qnD77OGyNi0z9ho2JRK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJq4UM%2FbtsLBXbM7xC%2FNL4qnD77OGyNi0z9ho2JRK%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;720&quot; height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9lsCj/btsLAgRx1hR/AKRik9WEn3PNQGL7SXJ7A0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9lsCj/btsLAgRx1hR/AKRik9WEn3PNQGL7SXJ7A0/img.jpg&quot; data-origin-width=&quot;2160&quot; data-origin-height=&quot;2880&quot; data-is-animation=&quot;false&quot; data-filename=&quot;IMG_6322.JPG&quot; style=&quot;width: 27.1975%;&quot; data-widthpercent=&quot;27.84&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9lsCj/btsLAgRx1hR/AKRik9WEn3PNQGL7SXJ7A0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9lsCj%2FbtsLAgRx1hR%2FAKRik9WEn3PNQGL7SXJ7A0%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;2160&quot; height=&quot;2880&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 지금 와서 생각해보면, 자소서를 그렇게 많이 쓴건 아닌가... 하는 생각이 있습니다. 또 이런 자잘한 활동들을 하지 않고 취업 준비만 바짝 했더라면 진즉에 어디든 들어가지 않았을까 하는 생각도 듭니다. 그래도 그때 당시의 힘듦으로 시야가 좁아졌던 상황에서는 이런 활동 하나하나가 뭐라도 하고 있다는 증거같았고, 그런 것들이 절실했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6월 ~ 9월: 인생 첫 앱 런칭, 첫 부트캠프&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6월에는 긍정적인 소식이 2개나 찾아왔습니다.&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/bmFBkF/btsLCqSkl3M/PlRWWfbkGQYGK2UKWlshZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmFBkF/btsLCqSkl3M/PlRWWfbkGQYGK2UKWlshZK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1576&quot; data-origin-height=&quot;1047&quot; data-filename=&quot;edited_IMG_6803.jpg&quot; data-widthpercent=&quot;59.25&quot; style=&quot;width: 58.5578%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmFBkF/btsLCqSkl3M/PlRWWfbkGQYGK2UKWlshZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmFBkF%2FbtsLCqSkl3M%2FPlRWWfbkGQYGK2UKWlshZK%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;1576&quot; height=&quot;1047&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1cA5L/btsLBxj5jxX/HUyLgRRTgii1Rkis80REOk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1cA5L/btsLBxj5jxX/HUyLgRRTgii1Rkis80REOk/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1130&quot; data-filename=&quot;IMG_8383.jpg&quot; style=&quot;width: 40.2794%;&quot; data-widthpercent=&quot;40.75&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1cA5L/btsLBxj5jxX/HUyLgRRTgii1Rkis80REOk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1cA5L%2FbtsLBxj5jxX%2FHUyLgRRTgii1Rkis80REOk%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;1170&quot; height=&quot;1130&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 앱 런칭 및 운영을 할 수 있는 기회였습니다. 스타트업에서 기술 스택을 착착 쌓아 만능이 된 개발자 친구를 따라 Epson에서 주최한 개발 공모전에 참여해 네컷사진을 찍을 수 있는 사진 앱을 Flutter로 개발했고, 본선에 진출했습니다. (2인 팀 중에서는 유일한 본선 진출 팀이었습니다  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 상은 타지 못했지만 거기에서 멈추지 않았습니다. 무수한 재심사 끝에 iOS 앱스토어에 인생 첫 앱을 런칭하게 되었습니다. 내가 만든 서비스를 실제로 사용하는 익명의 사람이 있고, 그에 따른 피드백이 있다는 건 정말 신기하고 소중한 경험이었습니다. 정말 많이 부족했던 저와 함께 해준 열정적인 그 친구에게 다시 한번 고마움과 감사함을 보냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;두 번째는 LG U+와 멀티캠퍼스가 진행하는 유레카 SW 교육 프론트엔드 1기에 참여하게 된 것입니다. 이전에도 유명한 부트캠프들을 지원했지만 그놈의 코딩 테스트에서 와르르 떨어지고 말았습니다. 그런 저를 받아주고, 심지어 채용 혜택도 준다니 거절할 이유가 없었습니다. 무엇보다 대면으로 프론트엔드 전용 교육을 진행한다는 것이 매우 마음에 들었습니다.&lt;/p&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시간 반을 넘게 지옥철을 타고 오는 사람들, 9시부터 6시까지의 교육을 모두 그날 기록하고 정리하는 사람들, 교육을 마치고도 저녁 10시까지 공부하고 가는 사람들 등... 세상은 넓고 열심히 사는 사람들은 많다는게 실감이 났습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 수업에서도 배울 점이 많았습니다. 비록 프론트엔드 전문 교육이라기엔 다소 아쉬웠지만, 그래도 Java와 객체지향에 대해 제대로 다시 한번 짚어보고, Spring Boot를 배우고 적용하며 그토록 궁금했던 백엔드 개발에 대한 기초도 습득할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그곳에서 또 좋았던 점은 사람들과 자율적으로 모여 했던 스터디였습니다. 그동안 궁금했던 것을 주제로 발표도 해보고, 리액트 공식 문서도 공부해보고, 코테 문제도 같이 풀어보고, 매주마다 새로운 요구사항이 주어지는 미니 프로젝트도 해보고, 코드리뷰도 해보고, ... 그렇게 저는 스터디 매우 좋아 인간이 되어 한번에 3개의 스터디에 참여하기도 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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/yOvlW/btsLAgqrLZI/h7MYbbJvOMsVqYqvEpn9kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yOvlW/btsLAgqrLZI/h7MYbbJvOMsVqYqvEpn9kk/img.png&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;1522&quot; data-is-animation=&quot;false&quot; style=&quot;width: 20.834%; margin-right: 10px;&quot; data-widthpercent=&quot;21.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yOvlW/btsLAgqrLZI/h7MYbbJvOMsVqYqvEpn9kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyOvlW%2FbtsLAgqrLZI%2Fh7MYbbJvOMsVqYqvEpn9kk%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;1412&quot; height=&quot;1522&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lZcKM/btsLBjGofnY/ALXEUquGCNfEWD3K8gDTR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lZcKM/btsLBjGofnY/ALXEUquGCNfEWD3K8gDTR1/img.png&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;758&quot; data-is-animation=&quot;false&quot; data-filename=&quot;blob&quot; style=&quot;width: 49.7136%; margin-right: 10px;&quot; data-widthpercent=&quot;50.9&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lZcKM/btsLBjGofnY/ALXEUquGCNfEWD3K8gDTR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlZcKM%2FbtsLBjGofnY%2FALXEUquGCNfEWD3K8gDTR1%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;1678&quot; height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vLA3p/btsLAd737JJ/8SP5DMvSNZCYJrn2S1K1I0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vLA3p/btsLAd737JJ/8SP5DMvSNZCYJrn2S1K1I0/img.png&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;856&quot; data-is-animation=&quot;false&quot; style=&quot;width: 27.1268%;&quot; data-widthpercent=&quot;27.77&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vLA3p/btsLAd737JJ/8SP5DMvSNZCYJrn2S1K1I0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvLA3p%2FbtsLAd737JJ%2F8SP5DMvSNZCYJrn2S1K1I0%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;1034&quot; height=&quot;856&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같이 교육 받는 사람들 뿐만 아니라, 그곳을 관리하고 알뜰살뜰 챙겨주시던 매니저님들과, 교육과정의 처음부터 끝까지 친절하게 멘토링을 해주셨던 LG U+ 현직자 멘토님도 너무 좋고 감사했습니다. 정말 좋은 사람들을 올해 분수에 넘치도록 많이 만났습니다.&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;300&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbOV3y/btsLBjTU0eN/L8IEPELkhUdKg9vfLJ1uS1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbOV3y/btsLBjTU0eN/L8IEPELkhUdKg9vfLJ1uS1/img.jpg&quot; data-alt=&quot;모두 사..사...암튼 그러합니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbOV3y/btsLBjTU0eN/L8IEPELkhUdKg9vfLJ1uS1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbOV3y%2FbtsLBjTU0eN%2FL8IEPELkhUdKg9vfLJ1uS1%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;300&quot; height=&quot;220&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;220&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;10월 ~ 12월: 그저 프로젝트, 프로젝트, ... 프로젝트!&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9월부터 다시 채용 공고들이 쏟아져 나왔지만, 무작정 난사했던 상반기와 달리 하반기는 그 절반도 지원하지 못했습니다. 비겁한 변명이지만 프로젝트를 하느라 정말, 정말 바빴습니다...&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;622&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XCKwS/btsLBrqZc2H/jiMzNDtiPnrGmGnhp5ivsk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XCKwS/btsLBrqZc2H/jiMzNDtiPnrGmGnhp5ivsk/img.jpg&quot; data-alt=&quot;발등에 불 떨어진 정도가 아니고, 그저 F I R E 자체&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XCKwS/btsLBrqZc2H/jiMzNDtiPnrGmGnhp5ivsk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXCKwS%2FbtsLBrqZc2H%2FjiMzNDtiPnrGmGnhp5ivsk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;392&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;발등에 불 떨어진 정도가 아니고, 그저 F I R E 자체&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;프로젝트 하나가 끝나면 잠깐 쉬었다가, 다시 새로운 프로젝트에 들어가고, 그게 끝나면 다시 새로운 프로젝트... 프로젝트의 연속이었습니다. 1일 N커밋을 찍고 PR을 날리고 남의 코드에 따봉과 LGTM을 날리는 나날이 반복되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 와중에 정말 다행스럽게도 모든 팀 프로젝트에서 좋은 팀원들만을 만났습니다. 다들 열정적이었고, 프로젝트에 책임감이 넘쳤고, 저보다 많은 것을 알고 있어 배울 점이 많았던 사람들이었습니다. 역시나 저만 잘하면 되는 상황에서 1인분 이상을 하기 위해 부단히도 노력했던 기억이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 노력 속에서 정말 많은 것을 배웠습니다. 고작 useEffect와 useState 밖에 모르는데 심지어 그 두 개도 헷갈리는 상태의(not state) 인간에서 출발했습니다. 프로젝트를 하며 CSS 컴포넌트 커스텀도 해보고, props drilling도 겪어보며 머리 싸매보고, 상태 관리 라이브러리도 써보고, Storybook을 사용한 공통 컴포넌트의 장점도 알게 되고, 네이버 지도 API도 써보고, QA 및 유저 테스트도 진행하고, ... 구구절절 적기에 구차할 정도로 넓고 얕게 배울 수 있었던 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Jira로 관리하며 MVP를 빠르게 개발해 사용자를 모아 에자일 프로세스를 그대로 적용한 프로젝트를 해보고, 여러 컨벤션을 지켜 가며 개발했던 것이 정말 소중한 경험이었습니다. 지망생에 불과하지만 스스로 나는 현업 개발자다 라고 최면을 걸며 체계적으로 개발하는 것에 몰두할 수 있었습니다.&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/zxKYu/btsLAhppL0C/RqfMwcdVWiskiEdm98zmQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zxKYu/btsLAhppL0C/RqfMwcdVWiskiEdm98zmQK/img.png&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;1440&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;25.79&quot; style=&quot;width: 25.1866%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zxKYu/btsLAhppL0C/RqfMwcdVWiskiEdm98zmQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzxKYu%2FbtsLAhppL0C%2FRqfMwcdVWiskiEdm98zmQK%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;1322&quot; height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/trlNb/btsLCrcF61D/lDy1eh2pSBcC3DPv9Jc0L1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/trlNb/btsLCrcF61D/lDy1eh2pSBcC3DPv9Jc0L1/img.jpg&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2338&quot; data-is-animation=&quot;false&quot; style=&quot;width: 13.7291%; margin-right: 10px;&quot; data-widthpercent=&quot;14.06&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/trlNb/btsLCrcF61D/lDy1eh2pSBcC3DPv9Jc0L1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtrlNb%2FbtsLCrcF61D%2FlDy1eh2pSBcC3DPv9Jc0L1%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;1170&quot; height=&quot;2338&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pwAIL/btsLB34guOA/DLR92cVKgufjoVJwUYjvKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pwAIL/btsLB34guOA/DLR92cVKgufjoVJwUYjvKk/img.png&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;1044&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;60.15&quot; style=&quot;width: 58.7587%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pwAIL/btsLB34guOA/DLR92cVKgufjoVJwUYjvKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpwAIL%2FbtsLB34guOA%2FDLR92cVKgufjoVJwUYjvKk%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;2236&quot; height=&quot;1044&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 뛰어난 사람들을 만나 최우수상, 우수상을 수상한 것도 너무 좋았지만, &quot;그냥 기능이 돌아가기만 하면 된다&quot; 라는 생각에서 깨어나 코드를 어떻게 짜야 효율적일까, 어떻게 해야 사용자 경험을 높일 수 있을까 라는 생각을 하게 된 것만으로도 더 성장한 것 같다고 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&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/Cq3I8/btsLCHma7wc/vo29GzACP6BISpaoZTtK5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cq3I8/btsLCHma7wc/vo29GzACP6BISpaoZTtK5K/img.png&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1102&quot; data-is-animation=&quot;false&quot; data-filename=&quot;blob&quot; style=&quot;width: 29.0977%; margin-right: 10px;&quot; data-widthpercent=&quot;29.79&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cq3I8/btsLCHma7wc/vo29GzACP6BISpaoZTtK5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCq3I8%2FbtsLCHma7wc%2Fvo29GzACP6BISpaoZTtK5K%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;1170&quot; height=&quot;1102&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cARJsW/btsLzYwu1MQ/9e9T4Ylo2Q5KA13y30cIm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cARJsW/btsLzYwu1MQ/9e9T4Ylo2Q5KA13y30cIm1/img.png&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;227&quot; data-is-animation=&quot;false&quot; data-filename=&quot;edited_blob&quot; data-widthpercent=&quot;42.15&quot; style=&quot;width: 41.1702%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cARJsW/btsLzYwu1MQ/9e9T4Ylo2Q5KA13y30cIm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcARJsW%2FbtsLzYwu1MQ%2F9e9T4Ylo2Q5KA13y30cIm1%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;341&quot; height=&quot;227&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3KDpx/btsLCDjJ2Cp/T7fz8VKvOz4XR0W85BLNKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3KDpx/btsLCDjJ2Cp/T7fz8VKvOz4XR0W85BLNKk/img.png&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;1058&quot; data-is-animation=&quot;false&quot; style=&quot;width: 27.4065%;&quot; data-widthpercent=&quot;28.06&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3KDpx/btsLCDjJ2Cp/T7fz8VKvOz4XR0W85BLNKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3KDpx%2FbtsLCDjJ2Cp%2FT7fz8VKvOz4XR0W85BLNKk%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;1058&quot; height=&quot;1058&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 모든 프로젝트가 끝났을까요..? 아닙니다. 기존 프로젝트가 끝나면? 다시 또 새로운 프로젝트가 시작되는거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qyQ9Q/btsLAtXnQQd/KkdVtUKeStYYvPWe40CZRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qyQ9Q/btsLAtXnQQd/KkdVtUKeStYYvPWe40CZRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qyQ9Q/btsLAtXnQQd/KkdVtUKeStYYvPWe40CZRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqyQ9Q%2FbtsLAtXnQQd%2FKkdVtUKeStYYvPWe40CZRK%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;1050&quot; height=&quot;600&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 감사하게도 DND 12기에 프론트엔드 개발로 참여할 수 있게 되었습니다. 앞으로 최소 8주 동안은 열심히 새로운 프로젝트 안에서 구르면서 달릴 것 같습니다.&lt;/p&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&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/bGXFxL/btsLAWZhYLz/rOKKktWRLYT7RwOlqyvEnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGXFxL/btsLAWZhYLz/rOKKktWRLYT7RwOlqyvEnk/img.png&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;676&quot; data-is-animation=&quot;false&quot; style=&quot;width: 57.9786%; margin-right: 10px;&quot; data-widthpercent=&quot;58.66&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGXFxL/btsLAWZhYLz/rOKKktWRLYT7RwOlqyvEnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGXFxL%2FbtsLAWZhYLz%2FrOKKktWRLYT7RwOlqyvEnk%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;1052&quot; height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DPV50/btsLAYQm0Jl/2izOclbnYzzGVctNOSpwi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DPV50/btsLAYQm0Jl/2izOclbnYzzGVctNOSpwi0/img.png&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;786&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;41.34&quot; style=&quot;width: 40.8586%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DPV50/btsLAYQm0Jl/2izOclbnYzzGVctNOSpwi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDPV50%2FbtsLAYQm0Jl%2F2izOclbnYzzGVctNOSpwi0%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;862&quot; height=&quot;786&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2024년 요약 및 2025년 다짐&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회고가 구구절절 길었습니다. 내용을 간략하게 정리하여 잘한 점과 아쉬운 점을 뽑자면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잘한 점 : 은둔청년이 되지 않으려고 열심히 노력한 것, 무엇이든지 적극적으로 배우려 한 것, 작년보다 개발 실력이 나아진 것&lt;/li&gt;
&lt;li&gt;아쉬운 점 : 아직도 코테를 못하는 것, 앞서나가는 팀원이 되지 못한 것, 단기간에 여러 기술을 익히느라 기본기를 제대로 쌓지 않은 것, 프로젝트에 집중하느라 정작 취준은 뒤로한 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 적어보니 계속 유지해야 할 장점과 보완해야 할 단점이 명확하게 잘 보입니다. 2025년의 제가 해야 할 일들도 회고록을 적으니 계속 생겨났습니다. 2024년을 돌아보는 것은 이정도로 끝내고, 곧 올 2025년의 다짐을 적어보겠습니다.&lt;/p&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;1. 뭘 하더라도 1순위는 취업이다. 지금처럼 열심히 살더라도, 곁다리로 빠지지 말자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 무분별 자소서 난사 X, 프론트엔드 직무를 뽑는 회사 위주로 지원하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 1일 최소 1코테는 진짜 필수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 프론트엔드 개발자로 나아가기 위한 기본기를 잘 쌓자. 알게 된 건 블로그로 잘 정리해두자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 믿고 의지할 수 있는 팀원이 되자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저걸 모두 지킬 수 있을지는 잘 몰라도, 일단 기대와 희망을 가지는게 중요합니다. 그래야 새로운 한 해를 두려움 없이 맞이할 수 있습니다. 무수한 탈락과 실패를 딛고 희미한 성공의 가능성을 믿으며 다시 열심히 도전해볼 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uNxAS/btsLB1FnV8C/gXXWF73jahNA8uAS9Iu0R1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uNxAS/btsLB1FnV8C/gXXWF73jahNA8uAS9Iu0R1/img.jpg&quot; data-alt=&quot;그래도 뭐... 볕들 날 있겠지...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uNxAS/btsLB1FnV8C/gXXWF73jahNA8uAS9Iu0R1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuNxAS%2FbtsLB1FnV8C%2FgXXWF73jahNA8uAS9Iu0R1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;356&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;712&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;마지막으로, 제가 가장 좋아하는 컬럼의 한 부분을 적어보며 2024년 회고를 마무리하겠습니다. 한 해 가장 여러번 읽었던 컬럼의 한 부분입니다.&lt;/span&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;기대하세요. 내일의 날씨, 이따가의 점심메뉴, 오랜만의&amp;nbsp;시내 외출,&amp;nbsp;개봉할 영화와 새로운 드라마. 실망할 수도 있겠지만 실패에도 다시 일어나는 힘은, 지치지 않는 기대에서 나옵니다. 오늘 점심으로 먹은&amp;nbsp;달걀샌드위치가&amp;nbsp;형편없었대도, 저녁으로 먹을 소고기덮밥은 괜찮을 수 있습니다. 이번 학기의 학점이 개판이었대도, 내일 보기로 한 영화는 재미있을 수 있습니다. &lt;br /&gt;&lt;b&gt;우리의 취미는 '기대하는 것'. 백 번을 실망한대도.&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;- 실패에 우아할 것, 허지원 심리학과 조교수&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 전문을 읽어보는 것을 추천합니다 : &lt;a href=&quot;https://www.psychiatricnews.net/news/articleView.html?idxno=11210&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.psychiatricnews.net/news/articleView.html?idxno=11210&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1735576288898&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;실패에 우아할 것. - 정신의학신문&quot; data-og-description=&quot;[정신의학신문 : 허지원 고려대학교 심리학과 교수] 인간의 정보처리에 대해 강의할 때면 시스템에 주요한 손상 있거나 적합하지 않은 새로운 정보가 입력되는 경우, 전체 시스템이 파국적으로 &quot; data-og-host=&quot;www.psychiatricnews.net&quot; data-og-source-url=&quot;https://www.psychiatricnews.net/news/articleView.html?idxno=11210&quot; data-og-url=&quot;http://www.psychiatricnews.net/news/articleView.html?idxno=11210&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/W0jeh/hyXSDlZvHA/vPQfTcUkCHsPBQwGJJk1G0/img.jpg?width=640&amp;amp;height=426&amp;amp;face=0_0_640_426&quot;&gt;&lt;a href=&quot;https://www.psychiatricnews.net/news/articleView.html?idxno=11210&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.psychiatricnews.net/news/articleView.html?idxno=11210&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/W0jeh/hyXSDlZvHA/vPQfTcUkCHsPBQwGJJk1G0/img.jpg?width=640&amp;amp;height=426&amp;amp;face=0_0_640_426');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;실패에 우아할 것. - 정신의학신문&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[정신의학신문 : 허지원 고려대학교 심리학과 교수] 인간의 정보처리에 대해 강의할 때면 시스템에 주요한 손상 있거나 적합하지 않은 새로운 정보가 입력되는 경우, 전체 시스템이 파국적으로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.psychiatricnews.net&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>잡담</category>
      <category>2024 회고</category>
      <category>2025 다짐</category>
      <category>부트캠프 후기</category>
      <category>프로젝트</category>
      <category>프론트엔드 취업</category>
      <author>옹헤옹</author>
      <guid isPermaLink="true">https://ongheong.tistory.com/5</guid>
      <comments>https://ongheong.tistory.com/entry/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%A7%80%EB%A7%9D%EC%83%9D%EC%9D%98-2024%EB%85%84-%EB%90%98%EB%8F%8C%EC%95%84%EB%B3%B4%EA%B8%B0#entry5comment</comments>
      <pubDate>Tue, 31 Dec 2024 01:38:06 +0900</pubDate>
    </item>
    <item>
      <title>CORS 에러 핸들링 : 개념 편</title>
      <link>https://ongheong.tistory.com/entry/CORS-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81-%EA%B0%9C%EB%85%90-%ED%8E%B8</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;주제 선정 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 서버와 연동하는 웹 프로젝트를 했을 때, 개발자 도구 콘솔 창에 다음과 같은 에러가 자주 발생했었다. 그때 클라이언트의 문제인지, 서버의 문제인지 잘 몰라서, 프론트를 맡은 내가 해결할 수 있는 방법으로 어찌저찌 해치웠던 경험이 있었다.&amp;nbsp;&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;1280&quot; data-origin-height=&quot;73&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OhCcV/btsIIiQD3ND/5K7qSxkGmHOMattYiemuk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OhCcV/btsIIiQD3ND/5K7qSxkGmHOMattYiemuk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OhCcV/btsIIiQD3ND/5K7qSxkGmHOMattYiemuk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOhCcV%2FbtsIIiQD3ND%2F5K7qSxkGmHOMattYiemuk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;73&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;73&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이후로도 제대로는 이해하지 못하고 넘어간 문제였는데, 브라우저 동작 방식에 대해 조금 더 이해하게 된 지금은 확실하게 정리해볼 수 있을 것 같아 위와 같은 주제를 선정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CORS가 뭐야?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러를 파악하기 위해서는 우선 CORS가 무엇인지부터 이해해야 한다. MDN에서는 CORS를 다음과 같이 소개하고 있다.&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 51.3953%; height: 145px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 145px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 145px;&quot;&gt;&lt;span&gt;&lt;b&gt;Cross-Origin Resource Sharing(CORS, 교차 출처 리소스 공유)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가 HTTP 헤더를 사용하여&lt;/li&gt;
&lt;li&gt;한 출처에서 실행 중이 웹 어플리케이션이&lt;/li&gt;
&lt;li&gt;다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록&lt;/li&gt;
&lt;li&gt;브라우저에 알려주는 체제이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출처란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처(origin)는 도메인 + 프로토콜 + 포트를 합친 것이다. ex) &lt;a href=&quot;http://example.com:80&quot;&gt;http://example.com:80&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 3개만 동일하면 같은 출처이므로, 다음 둘의 url이 같은 출처임을 유의하자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) &lt;a href=&quot;http://example.com/app1/index.html과&quot;&gt;http://example.com/app1/index.html &lt;/a&gt;과 &lt;a href=&quot;http://example.com/app2/app.html&quot;&gt;http://example.com/app2/app.html&lt;/a&gt; 은 같은 출처!&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처 정책에는 cross-orign 정책과 same-origin 정책이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;cross-orgin 정책
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 도메인의 리소스에 접근할 수 있음.&lt;/li&gt;
&lt;li&gt;&amp;lt;img&amp;gt;, &amp;lt;video&amp;gt;, &amp;lt;script&amp;gt;, &amp;lt;link&amp;gt; 태그 등이 해당.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;same-orgin 정책 (SOP 정책)
&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;XMLHttpRequest, Fetch API 스크립트 등이 해당.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안의 이유로 브라우저는 스크립트에서 시작한 cross-orgin HTTP 요청을 제한하며, 기본적으로 하나의 서버 연결만 허용한다. 동일 출처 정책을 따르는 웹 애플리케이션에서 다른 출처의 리소스를 불러오려면, 그 출처(다른 출처)에서 올바른 CORS 헤더를 포함한 응답을 반환해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dD31KR/btsIJdgXmiV/xQ8aSjGxwanXJceNET5rB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dD31KR/btsIJdgXmiV/xQ8aSjGxwanXJceNET5rB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dD31KR/btsIJdgXmiV/xQ8aSjGxwanXJceNET5rB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdD31KR%2FbtsIJdgXmiV%2FxQ8aSjGxwanXJceNET5rB1%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;957&quot; height=&quot;675&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Why SOP?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 동일 출처 정책을 굳이 사용하는 이유는 뭘까? 바로 보안 취약점을 막기 위해서라고 한다. 이러한 제약이 없다면 CSRF나 XSS와 같은 해킹 방법을 이용해서 코드를 심고, 그 코드가 실행되어 개인 정보를 가로챌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CSRF(Cross-Site Request Forgery)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 자신의 의지와는 무관하게 자신의 권한으로 공격자가 의도한 행위를 특정 웹사이트에 요청하게 하는 공격.&lt;/li&gt;
&lt;li&gt;공격자는 사용자가 이미 인증한 세션을 악용하여 사용자가 원하지 않는 요청을 수행하도록 함.&lt;/li&gt;
&lt;li&gt;ex) 사용자가 악성 스크립트 페이지를 실행하게 하여 session ID 등의 정보를 갈취한 후 서버에 요청하여 사용자의 데이터를 수정 및 삭제함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;XSS(Cross-Site Scriping)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 애플리케이션에 악성 스크립트를 삽입하여 사용자의 브라우저에서 실행되도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgvg3d/btsIIKs8MIt/AXwVSoJYEv3iZfC5erBNmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgvg3d/btsIIKs8MIt/AXwVSoJYEv3iZfC5erBNmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgvg3d/btsIIKs8MIt/AXwVSoJYEv3iZfC5erBNmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgvg3d%2FbtsIIKs8MIt%2FAXwVSoJYEv3iZfC5erBNmK%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;782&quot; height=&quot;401&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;401&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 이미지처럼 사용자가 특정 script가 심어진 evil.com 페이지에 접속했다면, 접속과 동시에 script가 실행되어 AJAX 호출로 은행 API를 호출할 수 있다. 그러면 공격자가 나의 은행 계좌를 삭제해버릴 수도 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 보안상의 약점에도 불구하고, 클라이언트에서 출처가 다른 서버에서 제공하는 API를 사용할 일은 자주 존재한다. 따라서 다른 도메인으로도 요청하기 위해 CORS라는 해결법이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CORS의 동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS 요청에는 Simple Request와 Preflighted Request 의 두 방식이 존재한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Simple Request&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;simple request 방식을 사용하기 위해서는 다음과 같은 조건을 만족해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET, HEAD, POST 중 하나의 메서드를 사용한다.&lt;/li&gt;
&lt;li&gt;브라우저가 자동으로 만드는 Request Header 이외에는 다음 헤더만 추가할 수 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Accept&lt;/li&gt;
&lt;li&gt;Accept-Language&lt;/li&gt;
&lt;li&gt;Content-Language&lt;/li&gt;
&lt;li&gt;Content-Type&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;헤더가 Content-Type 일 경우 헤더를 다음의 값들로만 작성해야 한다는 추가조건이 있다. 다음의 3가지 타입은 브라우저에서 기본적으로 안전하다고 간주되어 바로 요청이 가능하다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;application/x-www-form-urlencoded&lt;/li&gt;
&lt;li&gt;multipart/form-data&lt;/li&gt;
&lt;li&gt;text/plain&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmcYT4/btsIIBJeRgE/EkCabeupSuEiW1ArvcU6vK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmcYT4/btsIIBJeRgE/EkCabeupSuEiW1ArvcU6vK/img.png&quot; data-alt=&quot;위 이미지에서 client는 클라이언트의 브라우저, server는 요청할 api 서버이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmcYT4/btsIIBJeRgE/EkCabeupSuEiW1ArvcU6vK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmcYT4%2FbtsIIBJeRgE%2FEkCabeupSuEiW1ArvcU6vK%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;1023&quot; height=&quot;490&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;위 이미지에서 client는 클라이언트의 브라우저, server는 요청할 api 서버이다.&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;p data-ke-size=&quot;size16&quot;&gt;1. 클라이언트에서 다른 출처의 서버에 보낼 요청을 준비한다. 요청은 XMLHttpRequest나 fetch API를 사용하여 만들 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721617789274&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function simpleRequest(){
    const xhr = new XMLHttpRequest();
    const url = 'https://bar.other/resources/public-data/';

    xhr.open('GET', url);
    xhr.onreadystatechange = someHandler;
    xhr.send();
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 브라우저에서 클라이언트의 js 코드를 실행하여 실제 HTTP 요청을 생성한다. 이때 해당 요청에 host와 orgin request header 등을 추가하여 서버로 보낸다.&lt;/p&gt;
&lt;pre id=&quot;code_1721612890826&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;## browser request
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example&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;여기서 Host 헤더는 요청이 전송될 서버의 출처를, Origin 헤더는 요청이 시작된 서버의 출처(클라이언트 서버의 출처)를 말한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 서버에서 origin 요청 헤더를 확인한다. orgin 요청 값이 허용되면, Access-Control-Allow-Origin 헤더를 추가하여 응답한다. 해당 해더 값으로는 모든 도메인에서 접근할 수 있는 *로 응답하거나, orgin 헤더로 응답하는 방법이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1721614404699&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 200 OK #요청 처리함
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: * #이때 https://foo.example로 설정할 수도 있다!
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml&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;4. 브라우저는 서버의 응답을 받고, 클라이언트 코드에 응답을 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Preflighted Request&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 요청은 원하는 출처의 서버에 바로 요청을 보내는 것이지만, 이 요청은 그렇지 않다. 우선 OPTIONS 메서드를 통해 다른 도메인의 리소스로 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;preflighted request는 cross origin 요청이 서버의 유저 데이터에 영향을 줄 수 있을 경우에 사용한다. 즉 simple request의 조건을 충족하지 못하는 경우에는 모두 preflight 처리를 해야한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;preflight_correct.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1076&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xtF4S/btsIIoRqcjG/QbM5iLEEDkzCzQbmVQ6LDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xtF4S/btsIIoRqcjG/QbM5iLEEDkzCzQbmVQ6LDK/img.png&quot; data-alt=&quot;위 이미지에서 client는 클라이언트의 브라우저, server는 요청할 api 서버이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xtF4S/btsIIoRqcjG/QbM5iLEEDkzCzQbmVQ6LDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxtF4S%2FbtsIIoRqcjG%2FQbM5iLEEDkzCzQbmVQ6LDK%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;1024&quot; height=&quot;1076&quot; data-filename=&quot;preflight_correct.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1076&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;위 이미지에서 client는 클라이언트의 브라우저, server는 요청할 api 서버이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 클라이언트 서버에서는 다음과 같이 요청을 보낸다.&lt;/p&gt;
&lt;pre id=&quot;code_1721615414242&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const xhr = new XMLHttpRequest();
const url = 'https://bar.other/resources/post-here/';

xhr.open('POST', url);

xhr.setRequestHeader('X-PINGOTHER', 'pingpong'); // custom header
xhr.setRequestHeader('Content-Type', 'application/xml');

xhr.onreadystatechange = handler;
xhr.send('&amp;lt;person&amp;gt;&amp;lt;name&amp;gt;Arun&amp;lt;/name&amp;gt;&amp;lt;/person&amp;gt;');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 POST 요청이고, 요청과 함께 보낼 XML body를 만들고 있다. 또한 Ping-Other이라는 비표준 HTTP 요청 헤더를 설정했다. content-type이 application/xml 이고, 사용자 정의 헤더가 있으므로 preflighted 처리된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 브라우저는 클라이언트 코드가 preflight 요청을 보내야 하는지 확인하고, 실제 요청 전에 OPTIONS 요청으로 preflight 요청을 서버로 보낸다. 서버가 해당 요청을 허용하는지 확인하기 위함이다.&lt;/p&gt;
&lt;pre id=&quot;code_1721619157783&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;OPTIONS /resources/post-here/ HTTP/1.1 #OPTIONS 메서드를 사용한 preflight request임을 나타냄
Host: bar.other #실제 요청을 전송할 서버의 출처
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST #실제 요청을 전송할 때 사용할 메소드
Access-Control-Request-Headers: X-PINGOTHER, Content-Type #실제 요청을 전송할 때 사용할 헤더&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;preflight 요청은 밑의 두 줄이 핵심이다. 실제 요청을 어떤 식으로 전달할지 미리 알려주는 부분인데, Access-Control-Request-Method 헤더는 POST 메서드로 전송된다는 것을, Access-Control-Request-Headers는 사용자 정의 헤더와 같이 전송된다는 것을 알려준다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request header의 X-pingother는 클라이언트 코드에서 정의한 사용자 정의 헤더 이름이다. 이름은 해당 js 코드에서 아무거나 설정하면 된다!&lt;/p&gt;
&lt;/div&gt;
&lt;/div&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. 서버는 preflight 요청을 받고 실제 요청을 허용할지의 여부를 알려준다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721619803591&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example #실제 요청을 전송할 출처
Access-Control-Allow-Methods: POST, GET, OPTIONS #실제 요청을 전송할 때 사용가능한 메서드
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type #사용자 지정 헤더
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive&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;Access로 시작하는 응답 코드를 살펴보면, simple 요청과 비교하여 origin 코드 이외에 3개의 코드이 추가된 것을 볼 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Access-Control-Allow-Methods : 클라이언트가 요청한 리소스에 대해 POST, GET, OPTIONS 메서드를 사용할 수 있다고 알려준다.&lt;/li&gt;
&lt;li&gt;Access-Control-Allow-Headers : 실제 요청에 사용자 정의 헤더를 사용할 수 있음을 알려준다.&lt;/li&gt;
&lt;li&gt;Access-Control-Max-Age : preflight 요청에 대한 응답을 캐시할 수 있는 시간(요청을 다시 보내지 않아도 되는 시간)을 알려준다. (위 시간은 24시간!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 브라우저는 preflight 응답을 확인하고 실제 요청을 보낸다.&lt;/p&gt;
&lt;pre id=&quot;code_1721620153604&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache

&amp;lt;person&amp;gt;&amp;lt;name&amp;gt;Arun&amp;lt;/name&amp;gt;&amp;lt;/person&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;5. 서버는 실제 요청을 처리한 후 브라우저로 응답을 보내고, 브라우저는 이를 클라이언트로 보낸다.&lt;/p&gt;
&lt;pre id=&quot;code_1721620232422&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;## server real response
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼 CORS 에러는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 우리는 CORS가 무엇인지, 왜 사용하는지, 동작방식이 무엇인지 이해했다. 그럼 CORS 에러가 무엇인지는 쉽게 알 수 있다. CORS 정책을 위반하면 발생하는 에러인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러, 어떻게 해결할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 내가 했던 방법에 대해 소개하고자 한다. 내가 마주친 CORS 에러는 개발 환경에서 이미 배포한 서버의 Access-Control-Allow-Origin에 내 로컬 서버를 추가해주지 않아서 발생한 문제였다. 하지만 실제 배포한 웹에서는 하나의 api 주소만 사용할 것이므로, proxy를 이용하여 로컬 서버를 요청할 서버 주소로 변경했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1721621949047&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//package.json
{ #요청할 서버 주소
	&quot;proxy&quot;: &quot;http://apiserver.com:5000&quot;,
}

//요청 메소드
export const getAPI = async () =&amp;gt; {
//    const response = await fetch(&quot;http://apiserver.com:5000/api&quot;);
    const response = await fetch(&quot;/api&quot;);
    return await response.json();
};&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;위와 같은 내용을 React 프로젝트의 package.json 항목에 추가해주면 된다. 그럼 다음과 같은 방식으로 요청이 처리된다.&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;332&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chRpnF/btsIHgGhW1R/vKQSu3NMKgym9PxucnABW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chRpnF/btsIHgGhW1R/vKQSu3NMKgym9PxucnABW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chRpnF/btsIHgGhW1R/vKQSu3NMKgym9PxucnABW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchRpnF%2FbtsIHgGhW1R%2FvKQSu3NMKgym9PxucnABW0%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;332&quot; height=&quot;205&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;205&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 브라우저는 클라이언트 코드를 읽고 요청을 React 개발 서버(요청과 동일한 서버 주소)로 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. React 개발 서버는 package.json의 프록시 설정을 확인하고, 해당 프록시 주소로 자신이 받은 요청을 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. API 서버는 요청을 처리한 후 응답을 생성하여 React 개발 서버로 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. React 개발 서버는 받은 응답을 브라우저로 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 브라우저는 React 개발 서버로 받은 응답을 클라이언트 코드로 전달한다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+출처: &lt;a href=&quot;https://create-react-app.dev/docs/proxying-api-requests-in-development/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://create-react-app.dev/docs/proxying-api-requests-in-development/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 단순한 코드 한 줄로 나의 문제를 해결할 수 있었다. 위의 방법은 하나의 api 주소만 사용한 것인데, 이외에도 webpack dev server의 proxy 기능을 사용하거나, http-proxy-middleware를 사용하는 등의 다양한 방법이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이러한 프록시 설정은 개발 환경에서만 가능하다. 실제 배포 시 프록시 설정을 클라이언트에 두는 것은 보안 문제가 있고, 정적으로 정의되어 있기 때문에 런타임에 동적으로 변경할 수 없어 유연하지 못하다. 실제 배포를 위해서는 어떻게 문제를 해결하는지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버에서 Access-Control-Allow-Origin 헤더 세팅하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 정석적인 해결 방법이다. 아주 다양한 세팅 방식이 있었고, 서버마다 방식이 조금씩 다르다. 내가 서버에 대한 지식이 많지 않으므로, Spring에서 어떻게 설정하는지만 알아보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1721623313444&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@CrossOrigin(origin=&quot;*&quot;, allowedHeaders = &quot;*&quot;)
@Controller
public class MainController {
	@GetMapping(path = &quot;/&quot;)
	public String main(Model model) {
		return &quot;main&quot;;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@CrossOrigin annotation에 origins, allowHeaders 등의 속성 값을 넣어 CORS 에러를 해결할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;origins : access-control-allow-origin에 배치될 값&lt;/li&gt;
&lt;li&gt;allowedHeaders : access-control-allow-header에 배치될 값&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프록시 서버 이용(구축)하기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRQy2f/btsIHmT2y2X/Zzae8jecputCFZTVook63K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRQy2f/btsIHmT2y2X/Zzae8jecputCFZTVook63K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRQy2f/btsIHmT2y2X/Zzae8jecputCFZTVook63K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRQy2f%2FbtsIHmT2y2X%2FZzae8jecputCFZTVook63K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;492&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 수정할 수 없는 상황이거나, OpenAPI를 사용하는 경우에는 클라이언트에서 처리를 해줘야 한다. 이를 위해 개발 환경 뿐만이 아닌 테스트, 배포 환경에서도 사용할 수 있는 프록시 서버를 적용할 수 있고, 다음과 같은 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보안 강화 : 클라이언트 코드에서 서버의 주소를 노출하지 않고 요청할 수 있다.&lt;/li&gt;
&lt;li&gt;재요청 시 응답 속도 증가 : 브라우저가 보낸 요청들을 프록시 서버에 캐시하여 재요청 시 서버를 거치지 않고 데이터를 주고 받을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 현재 나와 있는 무료 프록시 서버 대여 서비스를 이용하거나, 프록시 서버를 직접 구축하는 방법 등이 존재한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 무료 프록시 대여 서비스 이용 &lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/raravel/cors-proxy&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/raravel/cors-proxy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1721625743540&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - raravel/cors-proxy: Bypass CORS blocking api.&quot; data-og-description=&quot;Bypass CORS blocking api. Contribute to raravel/cors-proxy development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/raravel/cors-proxy&quot; data-og-url=&quot;https://github.com/raravel/cors-proxy&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bxxKUD/hyWCI2yoGF/oZ9IGkcRK6tqqUPOR9kdqK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/raravel/cors-proxy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/raravel/cors-proxy&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bxxKUD/hyWCI2yoGF/oZ9IGkcRK6tqqUPOR9kdqK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - raravel/cors-proxy: Bypass CORS blocking api.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Bypass CORS blocking api. Contribute to raravel/cors-proxy development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre id=&quot;code_1721625755126&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script src='https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js'&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    axios({
        url: 'https://cors-proxy.org/api/',
        method: 'get',
        headers: {
            'cors-proxy-url' : 'https://google.com/' //요청을 보낼 서버의 url
        },
    }).then((res) =&amp;gt; {
        console.log(res.data);
    })
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 프록시 서버 구축&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nginx를 사용하여 프록시 서버를 만드는 방법 : &lt;a href=&quot;https://velog.io/@jeff0720/2018-11-18-2111-%EC%9E%91%EC%84%B1%EB%90%A8-iojomvsf0n&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@jeff0720/2018-11-18-2111-%EC%9E%91%EC%84%B1%EB%90%A8-iojomvsf0n&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1721625942715&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Nginx를 사용하여 프록시 서버 만들기&quot; data-og-description=&quot;서론 안녕하세요, 영훈입니다. 회사에 입사한 후 인프라에 중요성에 대해서 깨닳게된 후 꾸준히 공부하고 있습니다. 오늘은 제가 공부한 Nginx의 개념에 대해서 간단히 정리한 후 Nginx 설치 및 사&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@jeff0720/2018-11-18-2111-%EC%9E%91%EC%84%B1%EB%90%A8-iojomvsf0n&quot; data-og-url=&quot;https://velog.io/@jeff0720/2018-11-18-2111-작성됨-iojomvsf0n&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/51Cxs/hyWCH3DUzJ/UQJqJeA74KuWzi1kAMkvN1/img.png?width=1024&amp;amp;height=596&amp;amp;face=0_0_1024_596,https://scrap.kakaocdn.net/dn/fNbr0/hyWCNv1AQB/flNfSaEvVfwte6ktOM01iK/img.png?width=1024&amp;amp;height=596&amp;amp;face=0_0_1024_596,https://scrap.kakaocdn.net/dn/buxuMy/hyWCGDGF5N/UMUkNt5sIxqWqK8DtOEnP0/img.png?width=1052&amp;amp;height=592&amp;amp;face=0_0_1052_592&quot;&gt;&lt;a href=&quot;https://velog.io/@jeff0720/2018-11-18-2111-%EC%9E%91%EC%84%B1%EB%90%A8-iojomvsf0n&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@jeff0720/2018-11-18-2111-%EC%9E%91%EC%84%B1%EB%90%A8-iojomvsf0n&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/51Cxs/hyWCH3DUzJ/UQJqJeA74KuWzi1kAMkvN1/img.png?width=1024&amp;amp;height=596&amp;amp;face=0_0_1024_596,https://scrap.kakaocdn.net/dn/fNbr0/hyWCNv1AQB/flNfSaEvVfwte6ktOM01iK/img.png?width=1024&amp;amp;height=596&amp;amp;face=0_0_1024_596,https://scrap.kakaocdn.net/dn/buxuMy/hyWCGDGF5N/UMUkNt5sIxqWqK8DtOEnP0/img.png?width=1052&amp;amp;height=592&amp;amp;face=0_0_1052_592');&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;Nginx를 사용하여 프록시 서버 만들기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론 안녕하세요, 영훈입니다. 회사에 입사한 후 인프라에 중요성에 대해서 깨닳게된 후 꾸준히 공부하고 있습니다. 오늘은 제가 공부한 Nginx의 개념에 대해서 간단히 정리한 후 Nginx 설치 및 사&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;netlify.toml을 사용하여 서버를 세팅하는 방법 : &lt;a href=&quot;https://docs.netlify.com/routing/redirects/rewrites-proxies/#proxy-to-another-service&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.netlify.com/routing/redirects/rewrites-proxies/#proxy-to-another-service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1721626461193&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Rewrites and proxies&quot; data-og-description=&quot;Use rewrite rules to fetch a location behind the scenes while the URL in the visitor&amp;rsquo;s address bar remains the same. Proxy to another service or site.&quot; data-og-host=&quot;docs.netlify.com&quot; data-og-source-url=&quot;https://docs.netlify.com/routing/redirects/rewrites-proxies/#proxy-to-another-service&quot; data-og-url=&quot;https://docs.netlify.com/routing/redirects/rewrites-proxies/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/czJSpA/hyWCAKfdVi/QQKq5IGrZ8IfkpDB1lKPC1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://docs.netlify.com/routing/redirects/rewrites-proxies/#proxy-to-another-service&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.netlify.com/routing/redirects/rewrites-proxies/#proxy-to-another-service&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/czJSpA/hyWCAKfdVi/QQKq5IGrZ8IfkpDB1lKPC1/img.png?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;Rewrites and proxies&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Use rewrite rules to fetch a location behind the scenes while the URL in the visitor&amp;rsquo;s address bar remains the same. Proxy to another service or site.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.netlify.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;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&amp;lt;출처&amp;gt;&lt;br /&gt;1. [MDN] CORS 문서 : &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/CORS&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.mozilla.org/ko/docs/Web/HTTP/CORS&lt;/a&gt;
&lt;figure id=&quot;og_1721633439541&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;교차 출처 리소스 공유 (CORS) - HTTP | MDN&quot; data-og-description=&quot;교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/CORS&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/CORS&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bpItY8/hyWCBbkSPd/tkY5hcomaKVGkXFxWkdJM1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080,https://scrap.kakaocdn.net/dn/nZwBW/hyWCGjqEG7/7otZ2FTLeiRGcyi6aOO0sK/img.png?width=957&amp;amp;height=675&amp;amp;face=0_0_957_675&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/CORS&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/CORS&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bpItY8/hyWCBbkSPd/tkY5hcomaKVGkXFxWkdJM1/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080,https://scrap.kakaocdn.net/dn/nZwBW/hyWCGjqEG7/7otZ2FTLeiRGcyi6aOO0sK/img.png?width=957&amp;amp;height=675&amp;amp;face=0_0_957_675');&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;교차 출처 리소스 공유 (CORS) - HTTP | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. [개발자 난기수의 공간] &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;교차 출처 리소스 공유 (CORS) : 개발자라면 한번은 보았던 것 : &lt;a href=&quot;https://nankisu.tistory.com/70&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nankisu.tistory.com/70&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1721633645937&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;교차 출처 리소스 공유 (CORS) : 개발자라면 한번은 보았던 것&quot; data-og-description=&quot;동일 출처 정책(Same-Origin Policy) 사이드 프로젝트를 진행하다보면 자주 보는 것이 하나있는데 바로 CORS 에러다. CORS 에러는 브라우저의 동일 출처 정책(Same-Origin Policy)을 위반하면 발생하는 에러인&quot; data-og-host=&quot;nankisu.tistory.com&quot; data-og-source-url=&quot;https://nankisu.tistory.com/70&quot; data-og-url=&quot;https://nankisu.tistory.com/70&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b6DKv8/hyWCOhqVY4/9SEfIjtgkocngPK2yuDpUK/img.png?width=800&amp;amp;height=383&amp;amp;face=0_0_800_383,https://scrap.kakaocdn.net/dn/d9N2YQ/hyWCF5UicF/qjI480MKuhK7o7xxhnWbYK/img.png?width=800&amp;amp;height=383&amp;amp;face=0_0_800_383,https://scrap.kakaocdn.net/dn/oOYqj/hyWCO2O9Kk/gu8SINoTF4MqVAjE6i9bqK/img.png?width=921&amp;amp;height=317&amp;amp;face=0_0_921_317&quot;&gt;&lt;a href=&quot;https://nankisu.tistory.com/70&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nankisu.tistory.com/70&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b6DKv8/hyWCOhqVY4/9SEfIjtgkocngPK2yuDpUK/img.png?width=800&amp;amp;height=383&amp;amp;face=0_0_800_383,https://scrap.kakaocdn.net/dn/d9N2YQ/hyWCF5UicF/qjI480MKuhK7o7xxhnWbYK/img.png?width=800&amp;amp;height=383&amp;amp;face=0_0_800_383,https://scrap.kakaocdn.net/dn/oOYqj/hyWCO2O9Kk/gu8SINoTF4MqVAjE6i9bqK/img.png?width=921&amp;amp;height=317&amp;amp;face=0_0_921_317');&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;교차 출처 리소스 공유 (CORS) : 개발자라면 한번은 보았던 것&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;동일 출처 정책(Same-Origin Policy) 사이드 프로젝트를 진행하다보면 자주 보는 것이 하나있는데 바로 CORS 에러다. CORS 에러는 브라우저의 동일 출처 정책(Same-Origin Policy)을 위반하면 발생하는 에러인&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nankisu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. [Inpa Dev] 악명 높은 CORS 개념 &amp;amp; 해결법 - 정리 끝판왕 : &lt;a href=&quot;https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F#%EC%9A%94%EC%B2%AD_%EB%B0%A9%EC%8B%9D%EC%97%90_%EB%94%B0%EB%9D%BC_%EB%8B%A4%EB%A5%B8_cors_%EB%B0%9C%EC%83%9D_%EC%97%AC%EB%B6%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F#%EC%9A%94%EC%B2%AD_%EB%B0%A9%EC%8B%9D%EC%97%90_%EB%94%B0%EB%9D%BC_%EB%8B%A4%EB%A5%B8_cors_%EB%B0%9C%EC%83%9D_%EC%97%AC%EB%B6%80&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1721633701946&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;  악명 높은 CORS 개념 &amp;amp; 해결법 - 정리 끝판왕  &quot; data-og-description=&quot;악명 높은 CORS 에러 메세지 웹 개발을 하다보면 반드시 마주치는 멍멍 같은 에러가 바로 CORS 이다. 웹 개발의 신입 신고식이라고 할 정도로, CORS는 누구나 한 번 정도는 겪게 된다고 해도 과언이 &quot; data-og-host=&quot;inpa.tistory.com&quot; data-og-source-url=&quot;https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F#%EC%9A%94%EC%B2%AD_%EB%B0%A9%EC%8B%9D%EC%97%90_%EB%94%B0%EB%9D%BC_%EB%8B%A4%EB%A5%B8_cors_%EB%B0%9C%EC%83%9D_%EC%97%AC%EB%B6%80&quot; data-og-url=&quot;https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bAHOlM/hyWCIuLI9A/KIXCxuXAIKPb0koNfZTZo0/img.png?width=800&amp;amp;height=466&amp;amp;face=0_0_800_466,https://scrap.kakaocdn.net/dn/cbxWq7/hyWCIO4aAX/vx97tEiJhjU1lfQn75OLK1/img.png?width=800&amp;amp;height=466&amp;amp;face=0_0_800_466,https://scrap.kakaocdn.net/dn/bkZu58/hyWCO2PaKU/SYPrjf53EB2oyBcIpjxYW1/img.png?width=1200&amp;amp;height=700&amp;amp;face=0_0_1200_700&quot;&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F#%EC%9A%94%EC%B2%AD_%EB%B0%A9%EC%8B%9D%EC%97%90_%EB%94%B0%EB%9D%BC_%EB%8B%A4%EB%A5%B8_cors_%EB%B0%9C%EC%83%9D_%EC%97%AC%EB%B6%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F#%EC%9A%94%EC%B2%AD_%EB%B0%A9%EC%8B%9D%EC%97%90_%EB%94%B0%EB%9D%BC_%EB%8B%A4%EB%A5%B8_cors_%EB%B0%9C%EC%83%9D_%EC%97%AC%EB%B6%80&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bAHOlM/hyWCIuLI9A/KIXCxuXAIKPb0koNfZTZo0/img.png?width=800&amp;amp;height=466&amp;amp;face=0_0_800_466,https://scrap.kakaocdn.net/dn/cbxWq7/hyWCIO4aAX/vx97tEiJhjU1lfQn75OLK1/img.png?width=800&amp;amp;height=466&amp;amp;face=0_0_800_466,https://scrap.kakaocdn.net/dn/bkZu58/hyWCO2PaKU/SYPrjf53EB2oyBcIpjxYW1/img.png?width=1200&amp;amp;height=700&amp;amp;face=0_0_1200_700');&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;  악명 높은 CORS 개념 &amp;amp; 해결법 - 정리 끝판왕  &lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;악명 높은 CORS 에러 메세지 웹 개발을 하다보면 반드시 마주치는 멍멍 같은 에러가 바로 CORS 이다. 웹 개발의 신입 신고식이라고 할 정도로, CORS는 누구나 한 번 정도는 겪게 된다고 해도 과언이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;inpa.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>스터디</category>
      <category>CORS</category>
      <category>CORS 에러</category>
      <category>preflighted request</category>
      <category>proxy</category>
      <category>simple request</category>
      <category>SoP</category>
      <author>옹헤옹</author>
      <guid isPermaLink="true">https://ongheong.tistory.com/4</guid>
      <comments>https://ongheong.tistory.com/entry/CORS-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81-%EA%B0%9C%EB%85%90-%ED%8E%B8#entry4comment</comments>
      <pubDate>Mon, 22 Jul 2024 15:23:41 +0900</pubDate>
    </item>
  </channel>
</rss>