<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>안나의 전두엽 어딘가  </title>
    <link>https://dksek3050.tistory.com/</link>
    <description>기억의 유한함을 기록의 무한함으로 ✍️
예비 개발자가 꿈꾸는 공간
여기는 안나의 개발 블로그  </description>
    <language>ko</language>
    <pubDate>Fri, 7 Aug 2026 03:15:58 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</managingEditor>
    <image>
      <title>안나의 전두엽 어딘가  </title>
      <url>https://tistory1.daumcdn.net/tistory/5331353/attach/e40578f8fa6e4584acce51f5aa420de8</url>
      <link>https://dksek3050.tistory.com</link>
    </image>
    <item>
      <title>[React] React 19에서의 폼 처리 방식: Form Action, useActionState, useFormStatus, useOptimistic</title>
      <link>https://dksek3050.tistory.com/182</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9iC8w%2FbtsNubNhm0f%2FHFWwAptJcoF0tjazRHsNK1%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Form Action이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 19에서는 &amp;lt;form action={someFunction}&amp;gt;처럼 action 속성에 함수를 전달할 수 있습니다. 이때 React는 다음과 같은 동작을 자동으로 수행합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;preventDefault() 자동 호출 &amp;rarr; 직접 막을 필요 없음&lt;/li&gt;
&lt;li&gt;FormData 객체를 해당 함수에 인자로 전달&lt;/li&gt;
&lt;li&gt;&amp;lt;input&amp;gt;, &amp;lt;select&amp;gt; 등에 설정된 name 속성을 기반으로 FormData 구성&lt;/li&gt;
&lt;li&gt;제출이 완료되면 자동으로 폼 초기화&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;form action={myAction}&amp;gt;
  &amp;lt;input name=&quot;email&quot; /&amp;gt;
  &amp;lt;button&amp;gt;Submit&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의: 모든 입력 요소에는 반드시 name 속성을 지정해야 합니다. 그래야 FormData에 포함됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  useActionState 훅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useActionState는 &lt;b&gt;폼의 상태와 액션 함수를 연결&lt;/b&gt;하여, 액션 함수의 반환값을 쉽게 UI에서 사용할 수 있도록 도와주는 React 19의 새로운 훅입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const [formState, formAction, isPending] = useActionState(actionFn, initialState);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;actionFn&lt;/b&gt;: 폼 제출 시 실행될 함수. (prevState, formData)를 인자로 받습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;initialState&lt;/b&gt;: 초기 상태&lt;/li&gt;
&lt;li&gt;반환 배열 :
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;formState: 현재 상태&lt;/li&gt;
&lt;li&gt;formAction: form 요소의 action에 전달할 수 있는 React가 감지 가능한 래핑된 함수
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;h3 data-ke-size=&quot;size23&quot;&gt;  React는 다음과 같은 일을 자동으로 해줘야 해요:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FormData를 자동으로 꺼내서 넘겨주고,&lt;/li&gt;
&lt;li&gt;상태 변화를 감지하고 (isPending 같은),&lt;/li&gt;
&lt;li&gt;폼 제출 중 상태를 관리하고,&lt;/li&gt;
&lt;li&gt;렌더링도 적절히 트리거해야 하죠.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚙️ 그래서 React는 우리가 전달한 함수를 직접 쓰지 않고, &lt;b&gt;내부적으로 새로운 함수를 만듭니다.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 새 함수는 다음과 같은 일을 합니다:&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function wrappedAction(formData) {
  // isPending = true 로 설정
  // 우리 액션(myAction)을 실행
  // 결과를 상태로 저장
  // isPending = false 로 변경
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 감싼 함수를 &lt;b&gt;React가 추적 가능한 형태로 만들어서&lt;/b&gt; formAction으로 돌려주는 겁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;isPending: 비동기 요청 중 여부
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;폼이 현재 제출 중일때 true, 그렇지 않을때는 false 값을 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예시: 유효성 검사 및 에러 처리&lt;/h3&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;function signupAction(prevState, formData) {
  const email = formData.get(&quot;email&quot;);
  const password = formData.get(&quot;password&quot;);

  const errors = [];

  if (!email.includes(&quot;@&quot;)) errors.push(&quot;잘못된 이메일 형식입니다.&quot;);
  if (!password || password.length &amp;lt; 6) errors.push(&quot;비밀번호는 6자 이상이어야 합니다.&quot;);

  if (errors.length &amp;gt; 0) {
    return { errors, enteredValues: { email, password } };
  }

  return { errors: null };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;const [formState, formAction] = useActionState(signupAction, { errors: null });

&amp;lt;form action={formAction}&amp;gt;
  &amp;lt;input name=&quot;email&quot; defaultValue={formState.enteredValues?.email} /&amp;gt;
  &amp;lt;input name=&quot;password&quot; type=&quot;password&quot; defaultValue={formState.enteredValues?.password} /&amp;gt;
  {formState.errors &amp;amp;&amp;amp; &amp;lt;ul&amp;gt;{formState.errors.map(e =&amp;gt; &amp;lt;li key={e}&amp;gt;{e}&amp;lt;/li&amp;gt;)}&amp;lt;/ul&amp;gt;}
  &amp;lt;button&amp;gt;Sign Up&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  useFormStatus: 제출 중 상태 추적&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useFormStatus는 현재 &amp;lt;form&amp;gt;의 &lt;b&gt;제출 상태와 관련된 다양한 정보&lt;/b&gt;를 제공하는 React 19의 훅입니다. 특히 &lt;b&gt;폼의 자식 컴포넌트&lt;/b&gt;에서 제출 상태에 따라 UI를 동적으로 제어할 수 있도록 해줍니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제출 중일 때 버튼 비활성화&lt;/li&gt;
&lt;li&gt;로딩 스피너 표시&lt;/li&gt;
&lt;li&gt;상태에 따라 메시지 표시 등&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useFormStatus } from &quot;react&quot;;

function SubmitButton() {
  const { pending } = useFormStatus(); // 현재 폼의 제출 상태

  return (
    &amp;lt;button disabled={pending}&amp;gt;
      {pending ? &quot;Submitting...&quot; : &quot;Submit&quot;}
    &amp;lt;/button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useFormStatus는 &lt;b&gt;&amp;lt;form&amp;gt; 태그 안에 위치한 컴포넌트에서만 사용할 수 있습니다.&lt;/b&gt; &amp;lt;form&amp;gt; 외부에서 호출하면 동작하지 않습니다.&lt;/li&gt;
&lt;li&gt;해당 폼 요소의 &lt;b&gt;직접적인 하위 자식 컴포넌트&lt;/b&gt;여야 합니다. 즉, 같은 컴포넌트 트리 안에 있어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 반환되는 객체 속성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useFormStatus()는 단순히 pending만 제공하는 것이 아니라 &lt;b&gt;여러 상태 정보&lt;/b&gt;를 담고 있는 객체를 반환합니다:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 151px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;속성명&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;pending&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;폼이 &lt;b&gt;제출 중&lt;/b&gt;일 때 true, 그렇지 않으면 false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;data&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;폼 제출 시 함께 전송되는 FormData 객체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;method&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;폼 요청 방식 (예: &quot;post&quot; 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;action&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;폼에 지정된 액션 함수 또는 URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;enctype&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;폼 인코딩 타입 (기본은 application/x-www-form-urlencoded)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 일반적으로 가장 많이 사용하는 속성은 pending이며, 제출 상태에 따라 버튼을 비활성화하거나 메시지를 띄우는 데 활용됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  useOptimistic: 낙관적 UI 업데이트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useOptimistic은 사용자의 액션이 서버에 반영되기 전, &lt;b&gt;예상 결과를 먼저 UI에 반영&lt;/b&gt;하여 즉각적인 피드백을 주는 방식입니다. 예를 들어 유저가 투표 버튼을 클릭하면, 서버에 반영되기 전에 일단 UI 상의 투표 수부터 바꿔 보여줄 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 사용법&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const [optimisticState, updateOptimistically] = useOptimistic(
  initialValue,          
  (previousValue, action) =&amp;gt; newValue 
);

&lt;/code&gt;&lt;/pre&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;예: 현재 투표 수 (votes)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2️⃣ 두 번째 인자: 업데이트 함수&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가 useOptimistic 내부적으로 &lt;b&gt;폼 제출 중에 호출&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;첫 번째 매개변수는 React가 전달하는 이전 상태(previousValue)입니다.&lt;/li&gt;
&lt;li&gt;두 번째 매개변수는 개발자가 updateOptimistically() 호출 시 전달한 사용자 정의 값 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  반환값&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;optimisticState
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;낙관적으로 관리되는 상태입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폼이 제출되는 동안에만&lt;/b&gt; 이 값이 UI에 표시됩니다.&lt;/li&gt;
&lt;li&gt;폼 제출이 끝나면 이 상태는 사라지고, 앱의 원래 상태로 복귀합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;updateOptimistically(action)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;낙관적 UI 상태를 적용하고 싶을 때 사용하는 함수입니다.&lt;/li&gt;
&lt;li&gt;폼 액션 내부에서 호출됩니다.&lt;/li&gt;
&lt;li&gt;인자로 넘긴 값은 useOptimistic()의 두 번째 인자인 함수의 두 번째 매개변수로 전달됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 실제 예시: 투표 수를 즉시 반영하는 컴포넌트&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { OpinionsContext } from &quot;../store/opinions-context.jsx&quot;;
import { use, useActionState, useOptimistic } from &quot;react&quot;;

export function Opinion({ opinion: { id, title, body, userName, votes } }) {
  const { upvoteOpinion, downvoteOpinion } = use(OpinionsContext);

  // 낙관적 상태 설정: 'up'이면 +1, 'down'이면 -1
  const [optimisticVotes, setVotesOptimistically] = useOptimistic(
    votes,
    (prevVotes, mode) =&amp;gt; (mode === &quot;up&quot; ? prevVotes + 1 : prevVotes - 1),
  );

  // 서버 요청 전 낙관적 업데이트 후 실제 서버에 반영
  async function upVoteAction() {
    setVotesOptimistically(&quot;up&quot;);
    await upvoteOpinion(id);
  }

  async function downVoteAction() {
    setVotesOptimistically(&quot;down&quot;);
    await downvoteOpinion(id);
  }

  // 각각의 폼 액션 상태 관리
  const [upVoteFormState, upVoteFormAction, upVotePending] =
    useActionState(upVoteAction);
  const [downVoteFormState, downVoteFormAction, downVotePending] =
    useActionState(downVoteAction);

  return (
    &amp;lt;article&amp;gt;
      &amp;lt;header&amp;gt;
        &amp;lt;h3&amp;gt;{title}&amp;lt;/h3&amp;gt;
        &amp;lt;p&amp;gt;Shared by {userName}&amp;lt;/p&amp;gt;
      &amp;lt;/header&amp;gt;
      &amp;lt;p&amp;gt;{body}&amp;lt;/p&amp;gt;
      &amp;lt;form className=&quot;votes&quot;&amp;gt;
        &amp;lt;buttonformAction={upVoteFormAction}
          disabled={upVotePending || downVotePending}
        &amp;gt;
           
        &amp;lt;/button&amp;gt;

        &amp;lt;span&amp;gt;{optimisticVotes}&amp;lt;/span&amp;gt;

        &amp;lt;buttonformAction={downVoteFormAction}
          disabled={upVotePending || downVotePending}
        &amp;gt;
           
        &amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/article&amp;gt;
  );
}

&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;useOptimistic(초기값, 업데이트 함수)를 사용하여 상태를 낙관적으로 관리합니다.&lt;/li&gt;
&lt;li&gt;실제 비동기 작업이 완료되기 전에 setVotesOptimistically(&quot;up&quot;) 등을 호출해 UI를 즉시 반영합니다.&lt;/li&gt;
&lt;li&gt;실제 서버 작업은 await upvoteOpinion(id)에서 진행됩니다.&lt;/li&gt;
&lt;li&gt;폼 제출 중에는 optimisticVotes 값이 화면에 표시되고, 제출이 완료되면 이 값은 사라지고 실제 상태로 대체 됩니다.&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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  버튼마다 다른 액션 처리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React의 폼은 각 버튼에 &lt;b&gt;formAction 속성&lt;/b&gt;을 지정하면, 같은 &amp;lt;form&amp;gt; 안에 있어도 서로 다른 함수를 실행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;form&amp;gt;
  &amp;lt;button formAction={saveDraft}&amp;gt;임시 저장&amp;lt;/button&amp;gt;
  &amp;lt;button formAction={submitFinal}&amp;gt;최종 제출&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Form Action vs onSubmit&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교 항목 Form Action onSubmit&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;선언 방식&lt;/td&gt;
&lt;td&gt;&amp;lt;form action={function}&amp;gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;form onSubmit={handler}&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기본 동작 방지&lt;/td&gt;
&lt;td&gt;자동&lt;/td&gt;
&lt;td&gt;수동 (event.preventDefault())&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 수집 방식&lt;/td&gt;
&lt;td&gt;자동으로 FormData 제공&lt;/td&gt;
&lt;td&gt;event.target에서 수동 수집&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비동기 처리 지원&lt;/td&gt;
&lt;td&gt;✔ (Promise 지원)&lt;/td&gt;
&lt;td&gt;✔ (직접 핸들링)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상태 관리&lt;/td&gt;
&lt;td&gt;useActionState로 간편하게 처리&lt;/td&gt;
&lt;td&gt;수동으로 useState 등 사용&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;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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/182</guid>
      <comments>https://dksek3050.tistory.com/182#entry182comment</comments>
      <pubDate>Fri, 2 May 2025 16:43:31 +0900</pubDate>
    </item>
    <item>
      <title>[React] React에서 폼(form) 다루기 :: 추출, 유효성검증, 초기화, 제출</title>
      <link>https://dksek3050.tistory.com/181</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9iC8w%2FbtsNubNhm0f%2FHFWwAptJcoF0tjazRHsNK1%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트나 일반 HTML에서 form을 다룰 때, 생각보다 다양한 문제를 마주치게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &quot;입력값 받아서 제출&quot;하는 걸 넘어, 다음과 같은 고민들이 따라오죠:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떻게 &lt;u&gt;입력값&lt;/u&gt;을 깔끔하게 &lt;u&gt;추출&lt;/u&gt;할까?&lt;/li&gt;
&lt;li&gt;&lt;u&gt;유효성 검사&lt;/u&gt;는 언제, 어떻게 해야 깔끔할까?&lt;/li&gt;
&lt;li&gt;&lt;u&gt;입력 후 초기화&lt;/u&gt;는 어떻게 해야 할까?&lt;/li&gt;
&lt;li&gt;리액트에서는 &lt;u&gt;form 제출&lt;/u&gt;을 어떻게 다뤄야 할까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 위의 문제들을 하나씩 정리하고, 실제로 어떤 방식으로 해결하는 게 좋은지 이야기해보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  1. 입력 데이터 추출 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폼에서 입력된 값을 가져오는 방식은 상황에 따라 다르게 선택할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 방법 1: useState로 입력 상태 관리 (양방향 바인딩)&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const [username, setUsername] = useState(&quot;&quot;);

&amp;lt;input value={username} onChange={(e) =&amp;gt; setUsername(e.target.value)} /&amp;gt;
&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;li&gt;실시간으로 상태를 추적하고 활용 가능&lt;/li&gt;
&lt;li&gt;단점: 코드가 조금 길어질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 방법 2: useRef로 DOM 요소 접근&lt;/h3&gt;
&lt;pre class=&quot;verilog&quot;&gt;&lt;code&gt;const inputRef = useRef();

function handleSubmit(event) {
  event.preventDefault();

  const input = inputRef.current.value;
}

...

&amp;lt;input ref={inputRef} /&amp;gt;
&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;li&gt;렌더링을 발생시키지 않음 (퍼포먼스 유리)&lt;/li&gt;
&lt;li&gt;단점: 리액트의 상태 흐름에서 벗어날 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 방법 3: FormData API로 한 번에 추출&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function handleSubmit(e) {
  e.preventDefault();
  const formData = new FormData(e.target);
  const data = Object.fromEntries(formData.entries());
}
&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;form 전체 데이터를 한 번에 가져올 수 있음&lt;/li&gt;
&lt;li&gt;input에 name 속성이 필수&lt;/li&gt;
&lt;li&gt;체크박스처럼 여러 값을 가지는 경우는 formData.getAll() 사용 필요
&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function handleSubmit(e) {
  e.preventDefault();
  const formData = new FormData(e.target);
  const acquisitionChannel = formData.getAll(&quot;acquisition&quot;); // 체크박스 
  const data = Object.fromEntries(formData.entries());
  data.acquisition = acquisitionChannel;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  2. 유효성 검증 (Validation)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폼에서 유효성 검사는 UX에 직접적인 영향을 주는 요소입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 이르거나 너무 늦은 타이밍에 표시되면 사용자가 불편을 느낄 수 있어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 방법 1: 실시간 검증 (onChange)&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;&amp;lt;input value={email} onChange={(e) =&amp;gt; {
  setEmail(e.target.value);
  setError(!validateEmail(e.target.value));
}} /&amp;gt;
&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;li&gt;❌ 문제: 사용자가 입력을 마치기도 전에 에러가 떠버리는 UX&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: 포커스 해제 시 검증 (onBlur)&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;&amp;lt;input onBlur={() =&amp;gt; setTouched(true)} /&amp;gt;
&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;li&gt;✅ UX 관점에서 가장 적절한 타이밍&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;✅ 방법 3: useRef와 입력 완료 시 검증&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;/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;✅ 방법 4: HTML의 내장 속성 사용&lt;/h3&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;input required minLength={5} /&amp;gt;
&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;li&gt;간단한 유효성에는 매우 유용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  3. 폼 초기화 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 완료 후 상태를 &lt;b&gt;초기 상태로 되돌리는 방법&lt;/b&gt;입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 방법 1: useState로 초기값 설정&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;setUsername(&quot;&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;가장 일반적인 리액트 방식&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: useRef 초기화&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;inputRef.current.value = &quot;&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;DOM을 직접 조작하는 방식이라 권장되진 않지만, 간단한 경우 사용 가능&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;✅ 방법 3: event.target.reset() 사용&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function handleSubmit(e) {
  e.preventDefault();
  e.target.reset(); // form 전체 초기화
}
&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;li&gt;상태를 쓰지 않는 경우 특히 유용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  4. 양식 제출 제어 (submit 제어)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ HTML의 기본 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;form&amp;gt; 태그는 원래 &lt;b&gt;서버로 데이터 제출 + 전체 페이지 새로고침&lt;/b&gt;을 유발합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 전통적인 &lt;b&gt;SSR(Server-Side Rendering)&lt;/b&gt; 방식에 맞춘 설계죠.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 리액트에선 왜 문제가 될까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트는 &lt;b&gt;CSR(Client-Side Rendering)&lt;/b&gt; 기반입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 페이지 전체를 새로고침하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리액트가 관리하던 상태가 날아가고&lt;/li&gt;
&lt;li&gt;앱 흐름이 깨지며&lt;/li&gt;
&lt;li&gt;사용자 경험이 떨어지게 됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 반드시 기본 submit 동작을 &lt;b&gt;막아야&lt;/b&gt; 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 해결 방법&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ type=&quot;button&quot; 사용&lt;/h3&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;form&amp;gt;
  &amp;lt;button type=&quot;button&quot;&amp;gt;제출&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&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;lt;button&amp;gt;의 기본값은 type=&quot;submit&quot;이므로 명시적으로 설정해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ onSubmit + preventDefault()&lt;/h3&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;form onSubmit={(e) =&amp;gt; {
  e.preventDefault();
  // 사용자 입력 처리 로직
}}&amp;gt;
  &amp;lt;button type=&quot;submit&quot;&amp;gt;제출&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&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;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ formAction (React 19 이상)&lt;/h3&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;form&amp;gt;
  &amp;lt;button formAction={handleAction}&amp;gt;제출&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&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;li&gt;React 19에서만 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&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;&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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/181</guid>
      <comments>https://dksek3050.tistory.com/181#entry181comment</comments>
      <pubDate>Tue, 29 Apr 2025 14:49:30 +0900</pubDate>
    </item>
    <item>
      <title>[React] 왜 React에서는 form submit을 기본으로 두면 안 될까?</title>
      <link>https://dksek3050.tistory.com/180</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9iC8w%2FbtsNubNhm0f%2FHFWwAptJcoF0tjazRHsNK1%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML에서 &amp;lt;form&amp;gt; 태그는 원래 서버로 데이터를 보내기 위한 용도다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML에서 &amp;lt;form&amp;gt; 태그는 **서버로 데이터를 제출(submit)**하고, &lt;b&gt;새로운 페이지를 받아오기 위한&lt;/b&gt; 구조로 설계됐다. 폼 안에서 버튼을 누르면 브라우저는 기본적으로 다음과 같은 흐름으로 동작한다:&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  브라우저 기본 동작&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;버튼 클릭 &amp;rarr; 폼 제출 (submit 이벤트 발생)&lt;/li&gt;
&lt;li&gt;입력값을 포함해서 &lt;b&gt;서버로 HTTP 요청 전송&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;서버가 요청을 처리하고 &lt;b&gt;새로운 HTML 페이지를 응답&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전체 페이지가 새로고침&lt;/b&gt;되면서 새 화면이 열린다&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  왜 이렇게 설계됐을까? (&amp;rarr; SSR 기반 구조)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 &lt;b&gt;웹 초창기부터 사용되던 전통적인 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;SSR(Server-Side Rendering)&lt;/b&gt; 구조에 최적화되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ SSR(Server-Side Rendering) 방식이란?&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;모든 HTML 페이지를 &lt;b&gt;서버에서 렌더링&lt;/b&gt;해서 생성하고&lt;/li&gt;
&lt;li&gt;사용자의 요청마다 &lt;b&gt;서버가 새 페이지를 보내주는&lt;/b&gt; 구조&lt;/li&gt;
&lt;li&gt;폼 제출도 서버로 요청 &amp;rarr; 서버가 새 HTML을 만들어서 응답 &amp;rarr; 전체 페이지 교체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  그래서 &lt;b&gt;폼 제출 시 전체 페이지 새로고침&lt;/b&gt;은 자연스러운 동작이었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 그런데, 리액트에서는 문제가 된다 (&amp;rarr; CSR과 충돌)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 같은 프론트엔드 프레임워크는 대부분 &lt;b&gt;CSR(Client-Side Rendering)&lt;/b&gt; 방식을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ CSR(Client-Side Rendering) 방식이란?&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;b&gt;처음 로딩 시 HTML 한 번만 받아오고&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &amp;lt;form&amp;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;b&gt;리액트가 관리하던 모든 상태(state)가 초기화&lt;/b&gt;된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예상하지 않은 서버 요청&lt;/b&gt;이 발생해서 의도한 흐름이 깨질 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;CSR의 장점이 완전히 사라져버린다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ React에서 폼 제출 시 주의할 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSR 기반 앱에서는 폼 제출의 기본 동작을 제어해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 아래 3가지 방법을 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ 버튼에 type=&quot;button&quot;을 명시하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML에서는 &amp;lt;button&amp;gt;의 기본 type이 submit이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 폼 안의 버튼은 기본적으로 폼 제출을 유발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 단순한 클릭 이벤트만 처리하고 싶다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;type=&quot;button&quot;을 명시&lt;/b&gt;해서 제출을 막아야 한다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;form&amp;gt;
  &amp;lt;button type=&quot;button&quot;&amp;gt;로그인&amp;lt;/button&amp;gt; {/* 이 버튼은 form 제출 안 함 */}
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: type=&quot;button&quot;은 &quot;폼 제출이 아닌 버튼&quot;임을 명시하는 것이지, 이미 발생한 submit 이벤트를 막는 것은 아니다.&lt;/p&gt;
&lt;/blockquote&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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ onSubmit 핸들러에서 preventDefault() 호출하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폼 제출을 직접 다룰 경우에는,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;onSubmit 이벤트 핸들러를 등록하고 preventDefault()를 호출&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function handleSubmit(e) {
  e.preventDefault(); // ✅ HTTP 요청과 새로고침 방지
  // 여기에 로그인 처리 로직 작성
}

&amp;lt;form onSubmit={handleSubmit}&amp;gt;
  &amp;lt;button type=&quot;submit&quot;&amp;gt;로그인&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은, 폼 자체를 유지하면서 &lt;b&gt;제출 흐름만 제어&lt;/b&gt;하고 싶을 때 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(예: 로그인, 회원가입 API 요청 보내기 등)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ (React 19 이상) formAction 속성 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 19부터는 폼 제출을 더 선언적으로 처리할 수 있는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;formAction 속성&lt;/b&gt;이 새롭게 도입되었다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;form&amp;gt;
  &amp;lt;button formAction={handleLogin}&amp;gt;로그인&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;

&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;formAction은 버튼 클릭 시 &lt;b&gt;지정된 함수가 호출&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;특히 **서버 액션(Server Actions)**과 함께 사용할 때 강력하다.&lt;/li&gt;
&lt;li&gt;클라이언트 액션에서도 사용할 수 있지만,&lt;/li&gt;
&lt;li&gt;주로 &lt;b&gt;React Server Components 환경&lt;/b&gt;에서 많이 활용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: formAction은 React 19 이상에서만 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직은 전통적인 onSubmit + preventDefault() 패턴이 더 보편적이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  참고: CSR vs SSR 한눈에 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분 CSR (Client-Side Rendering) SSR (Server-Side Rendering)&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;렌더링 위치&lt;/td&gt;
&lt;td&gt;브라우저(클라이언트)&lt;/td&gt;
&lt;td&gt;서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최초 로딩 속도&lt;/td&gt;
&lt;td&gt;느릴 수 있음 (JS 다운로드 필요)&lt;/td&gt;
&lt;td&gt;빠름 (HTML 바로 렌더링)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;화면 전환 속도&lt;/td&gt;
&lt;td&gt;빠름 (새로고침 없이 변경)&lt;/td&gt;
&lt;td&gt;느림 (매번 서버 요청)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 예시&lt;/td&gt;
&lt;td&gt;React, Vue, Next.js (CSR모드)&lt;/td&gt;
&lt;td&gt;전통적인 서버 웹사이트, Next.js(SSR모드)&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;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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/180</guid>
      <comments>https://dksek3050.tistory.com/180#entry180comment</comments>
      <pubDate>Tue, 29 Apr 2025 11:03:40 +0900</pubDate>
    </item>
    <item>
      <title>[React] 리액트 컴포넌트 스타일링</title>
      <link>https://dksek3050.tistory.com/179</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9iC8w%2FbtsNubNhm0f%2FHFWwAptJcoF0tjazRHsNK1%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;  바닐라 CSS 스타일 컴포넌트에 스코핑 방법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트에서 바닐라 CSS를 사용하는 경우, CSS가 리액트 컴포넌트에 스코프가 자동으로 적용되지 않습니다. 이는 &lt;b&gt;CSS가 전역적으로 적용&lt;/b&gt;되기 때문에, 컴포넌트가 여러 개 있을 때 스타일이 서로 충돌할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 같은 페이지에 존재하는 A 컴포넌트와 B 컴포넌트가 존재한다고 할때, A 컴포넌트의 스타일에 필요한 css가 B컴포넌트의 스타일에 영향을 미칠 수 있습니다. 이를 해결하기 위해서 리액트 컴포넌트에 CSS를 스코프(scope)하기 위해 몇가지 방법을 소개해드리겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣ style Prop 사용하여 Inline(인라인)으로 css 적용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;style Prop을 사용해서 CSS를 자바스크립트 객체로 작성하여 JSX에서 스타일을 적용할 수 있습니다. 이 방법을 통해 간단한 스타일링이나, 동적인 스타일 변경을 할 때 사용 가능 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;style Prop 기본 사용법&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;CSS 속성 이름은 camelCase로 작성&lt;/li&gt;
&lt;li&gt;값은 문자열 또는 숫자로 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;function App() {

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button 
        style={{
          backgroundColor: 'red',  // 고정된 색상
          color: 'white',
          padding: '10px 20px',
          border: 'none',
          cursor: 'pointer'
        }} 
      &amp;gt;
        Click me!
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;

&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;동적 스타일링 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import React, { useState } from 'react';

function App() {
  // useState 훅을 사용하여 isClicked 상태를 정의합니다. 초기 값은 false입니다.
  const [isClicked, setIsClicked] = useState(false);

  // 버튼 클릭 시 호출되는 함수입니다. 상태를 반전시켜 isClicked 값을 변경합니다.
  const handleClick = () =&amp;gt; {
    setIsClicked(prev =&amp;gt; !prev); // 이전 상태의 반대로 값을 변경합니다.
  };

  // 버튼 스타일을 동적으로 생성합니다. isClicked 상태에 따라 배경색이 바뀝니다.
  const buttonStyle = {
    backgroundColor: isClicked ? 'green' : 'red', // 상태에 따라 배경색을 green 또는 red로 설정
    color: 'white', 
    padding: '10px 20px',
    border: 'none', 
    cursor: 'pointer'
  };

  return (
    &amp;lt;div&amp;gt;
      {/* label 태그는 버튼을 설명하는 텍스트를 포함합니다. 
           isClicked 상태에 따라 'clicked' 클래스를 동적으로 추가 */}
      &amp;lt;label className={`label ${isClicked ? 'clicked' : ''}`}&amp;gt;Button&amp;lt;/label&amp;gt;

      {/* 버튼은 style prop을 사용하여 동적으로 스타일을 적용하고, 
           클릭 시 handleClick 함수가 호출되어 isClicked 상태가 변경됩니다. */}
      &amp;lt;button style={buttonStyle} onClick={handleClick}&amp;gt;
        {/* 버튼 텍스트는 isClicked 상태에 따라 'Clicked' 또는 'Click me!'로 변경 */}
        {isClicked ? 'Clicked' : 'Click me!'}
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;style Prop 사용 시 장점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트 내에서 간편한 스타일링&lt;/li&gt;
&lt;li&gt;동적 스타일 적용&lt;/li&gt;
&lt;li&gt;외부 라이브러리 의존성 없음&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;style Prop 사용 시 단점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스타일 재사용 어려움&lt;/li&gt;
&lt;li&gt;성능 이슈
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매번 새로운 스타일 객체가 생성되어 리렌더링 성능에 영향을 미칠 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;복잡한 스타일링에 한계&lt;/li&gt;
&lt;li&gt;스타일 객채와 jsx의 상태와 강한 의존성&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2️⃣ CSS Module 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS Modules은 CSS 클래스 이름을 자동으로 고유하게 만들어주는 방법입니다. 이를 통해 각 컴포넌트에서 사용하는 CSS가 전역에 영향을 미치지 않게 할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;create-react-app을 사용하면, CSS Modules는 기본적으로 지원됩니다. .module.css 확장자를 사용하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Button.module.css&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;/* Button.module.css */
.button {
  background-color: green;
  color: white;
  padding: 10px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Button.js&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Button.js
import React from 'react';
import styles from './Button.module.css';

function Button() {
  return &amp;lt;button className={styles.button}&amp;gt;Click me!&amp;lt;/button&amp;gt;;
}

export default Button;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서 Button.module.css는 CSS Module을 사용하여 자동으로 고유한 클래스 이름을 생성합니다. styles.button을 사용하여 클래스 이름을 적용하면, 해당 CSS가 다른 컴포넌트의 스타일에 영향을 미치지 않습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&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;3️⃣ Styled Components&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CSS-in-JS&lt;/b&gt; 방식을 사용하는 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 &lt;b&gt;CSS 코드를 자바스크립트 안에 작성&lt;/b&gt;할 수 있게 해줍니다. 그리고 중요한 건, 작성한 스타일이 &lt;b&gt;해당 컴포넌트에만 적용&lt;/b&gt;된다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 스타일이 자동으로 스코프(scope)되어 다른 컴포넌트와 충돌하지 않도록 해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;styled-component 설치&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install styled-components
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 방법 예시&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Button.jsx
import styled from 'styled-components';

const StyledButton = styled.button`
  background-color: #4caf50;
  color: white;
  padding: 10px 16px;
  border: none;
  border-radius: 5px;
  cursor: pointer;

  &amp;amp;:hover {
    background-color: #45a049;
  }
`;

function Button({ children }) {
  return &amp;lt;StyledButton&amp;gt;{children}&amp;lt;/StyledButton&amp;gt;;
}

export default Button;

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 StyledButton은 내부적으로 고유한 클래스명을 생성하고, 이 스타일은 다른 컴포넌트와 절대 충돌하지 않습니다. 마치 컴포넌트 하나하나가 스타일 영역을 가지는 것처럼 작동합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;props로 동적 스타일 처리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;styled-components는 props를 받아서 CSS 값을 동적으로 지정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;const Input = styled.input`
  background-color: ${({ invalid }) =&amp;gt; $invalid ? &quot;#fed2d2&quot; : &quot;#d1d5db&quot;};
  color: ${({ invalid }) =&amp;gt; $invalid ? &quot;#ef4444&quot; : &quot;#374151&quot;};
`

&amp;lt;Input $invalid={emailNotValid} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식으로 유효성 검증에 따라 스타일을 다르게 보여줄 수 있어 사용자 경험을 향상시킬 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; $ 접두사는 왜 쓰는 걸까?&lt;/b&gt; styled-components는 $로 시작하는 props는 자동으로 필터링해서 DOM에 전달하지 않도록 처리해줍니다. $invalid는 오직 스타일링에서만 사용되고, 실제 HTML 속성에는 포함되지 않기 때문에 &lt;b&gt;불필요한 경고 없이 안전하게 스타일을 제어할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중첩 스타일, @media, hover 등의 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;styled-components는 일반 CSS 문법을 거의 그대로 사용할 수 있기 때문에, 다음과 같은 고급 기능들도 자연스럽게 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;중첩 선택자 (&amp;amp;)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;const Header = styled.header`
  padding: 2rem;
  background-color: #f0f0f0;

  &amp;amp; h1 {
    font-size: 2rem;
    margin-bottom: 1rem;
  }

  &amp;amp; p {
    color: #555;
  }
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;amp;는 해당 styled 컴포넌트 자신을 가리킵니다. 위 예제는 Header 안의 h1과 p에 각각 스타일을 주는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ 미디어 쿼리 (@media)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const Header = styled.header`
  margin-bottom: 2rem;

  @media (min-width: 768px) {
    margin-bottom: 4rem;
  }
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크린 크기에 따라 스타일을 다르게 적용할 수 있습니다. 일반 CSS에서 하던 방식 그대로 작성하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ Pseudo 선택자 (예: :hover, :focus)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;hsp&quot;&gt;&lt;code&gt;const Button = styled.button`
  background-color: #4caf50;
  color: white;

  &amp;amp;:hover {
    background-color: #45a049;
  }
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hover 상태도 마찬가지로 &amp;amp;:hover와 같이 작성하면 됩니다. &amp;amp;는 항상 해당 요소 자신을 의미합니다. 빈칸 없이 이어서 써야 pseudo 선택자로 인식됩니다.&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;&lt;b&gt;스타일 충돌 걱정 X&lt;/b&gt;: 자동으로 고유 클래스 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;props로 동적 스타일 처리 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컴포넌트 기반의 스타일링&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기존 CSS 문법 대부분 사용 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구조화된 스타일 관리가 쉬움&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의할 점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;런타임에서 CSS를 처리하므로, 매우 큰 프로젝트에서는 퍼포먼스 이슈 가능성 있음&lt;/li&gt;
&lt;li&gt;CSS가 JS 내부에 있어 초반엔 낯설 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&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;4️⃣ Tailwind&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailwind CSS는 &lt;b&gt;유틸리티 퍼스트(Utility-First)&lt;/b&gt; CSS 프레임워크입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 class=&quot;btn primary&quot; 같은 추상적인 클래스명 대신, class=&quot;bg-blue-500 text-white p-4 rounded&quot;처럼 &lt;b&gt;직관적인 유틸리티 클래스&lt;/b&gt;들을 조합해서 스타일을 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로젝트에 Tailwind 설치 (예: Vite 기준)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설정 파일 (tailwind.config.js) 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;module.exports = {
  content: [&quot;./index.html&quot;, &quot;./src/**/*.{js,ts,jsx,tsx}&quot;],
  theme: {
    extend: {},
  },
  plugins: [],
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전역 CSS 파일 설정 (index.css 등)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@tailwind base;
@tailwind components;
@tailwind utilities;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용법&lt;/h3&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;Tailwind는 미리 정의된 수많은 클래스들로 구성되어 있어서, 따로 .css 파일 없이도 className을 통해 바로 스타일을 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;&amp;lt;button className=&quot;bg-blue-500 text-white font-bold py-2 px-4 rounded&quot;&amp;gt;
  로그인
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시처럼 bg-blue-500, text-white, px-4, rounded 등은 각각의 스타일 속성 하나에 대응하는 클래스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2️⃣ 접두어로 반응형 디자인 구성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sm:, md:, lg: 같은 접두어만 붙이면 화면 크기에 따라 반응형 스타일링이 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;div className=&quot;text-base md:text-xl lg:text-2xl&quot;&amp;gt;
  스크린 크기에 따라 글자 크기 변화
&amp;lt;/div&amp;gt;
&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;기본은 text-base&lt;/li&gt;
&lt;li&gt;&lt;b&gt;768px 이상(md)&lt;/b&gt;: text-xl&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1024px 이상(lg)&lt;/b&gt;: text-2xl&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 미디어 쿼리도 &lt;b&gt;직관적인 접두어&lt;/b&gt;로 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3️⃣ 접두어로 가상 선택자 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마우스를 올렸을 때, 클릭했을 때, 포커스됐을 때 등 가상 선택자도 접두어로 처리합니다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;&amp;lt;button className=&quot;bg-blue-500 hover:bg-blue-600 focus:ring-2 active:scale-95&quot;&amp;gt;
  상호작용 가능한 버튼
&amp;lt;/button&amp;gt;
&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;hover:bg-blue-600: 마우스를 올리면 배경색 변경&lt;/li&gt;
&lt;li&gt;focus:ring-2: 포커스 시 ring 효과&lt;/li&gt;
&lt;li&gt;active:scale-95: 클릭 시 약간 작아지는 애니메이션&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailwind의 진짜 강점은 &lt;b&gt;반응형 접두어 + 상태 접두어&lt;/b&gt;를 조합해서 사용할 수 있다는 점입니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;&amp;lt;button className=&quot;bg-blue-500 hover:bg-blue-600 md:hover:bg-green-500&quot;&amp;gt;
  반응형 hover 버튼
&amp;lt;/button&amp;gt;
&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;작은 화면에서는 hover:bg-blue-600&lt;/li&gt;
&lt;li&gt;&lt;b&gt;md 이상&lt;/b&gt;에서는 hover:bg-green-500&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 화면 크기와 상태를 동시에 고려한 스타일도 아주 간결하게 구현할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 클래스 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적 클래스 예시&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;배경색&lt;/td&gt;
&lt;td&gt;bg-red-500, bg-gray-100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;글자 색&lt;/td&gt;
&lt;td&gt;text-white, text-gray-800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;마진/패딩&lt;/td&gt;
&lt;td&gt;m-4, mt-2, p-6, px-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;폰트 스타일&lt;/td&gt;
&lt;td&gt;font-bold, text-xl, tracking-wide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정렬&lt;/td&gt;
&lt;td&gt;flex, justify-between, items-center&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;버튼 꾸미기&lt;/td&gt;
&lt;td&gt;hover:bg-blue-700, rounded, shadow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;따로 CSS 파일 안 만들어도 돼서 빠름&lt;/li&gt;
&lt;li&gt;⚠️ 하지만 클래스가 너무 많아지면 가독성이 떨어질 수도 있음&lt;/li&gt;
&lt;li&gt;&amp;rarr; clsx, classnames, 컴포넌트 분리로 해결 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  팁: 클래스 너무 길어져 가독성이 떨어져요 &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 땐 Tailwind와 함께 &lt;b&gt;clsx&lt;/b&gt; 또는 &lt;b&gt;classnames&lt;/b&gt; 라이브러리를 활용하거나, 컴포넌트 단위로 분리해서 관리하는 방법도 있어요.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install clsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import clsx from 'clsx';

const buttonClass = clsx(
  'bg-blue-500',
  'text-white',
  'font-bold',
  'py-2',
  'px-4',
  'rounded',
  { 'opacity-50': isDisabled }
);
&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;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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/179</guid>
      <comments>https://dksek3050.tistory.com/179#entry179comment</comments>
      <pubDate>Wed, 23 Apr 2025 13:41:54 +0900</pubDate>
    </item>
    <item>
      <title>[React] Tailwind 사용하여 스타일링 하기</title>
      <link>https://dksek3050.tistory.com/178</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9iC8w%2FbtsNubNhm0f%2FHFWwAptJcoF0tjazRHsNK1%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailwind CSS는 &lt;b&gt;유틸리티 퍼스트(Utility-First)&lt;/b&gt; CSS 프레임워크입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 class=&quot;btn primary&quot; 같은 추상적인 클래스명 대신, class=&quot;bg-blue-500 text-white p-4 rounded&quot;처럼 &lt;b&gt;직관적인 유틸리티 클래스&lt;/b&gt;들을 조합해서 스타일을 만듭니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로젝트에 Tailwind 설치 (예: Vite 기준)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설정 파일 (tailwind.config.js) 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;module.exports = {
  content: [&quot;./index.html&quot;, &quot;./src/**/*.{js,ts,jsx,tsx}&quot;],
  theme: {
    extend: {},
  },
  plugins: [],
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전역 CSS 파일 설정 (index.css 등)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@tailwind base;
@tailwind components;
@tailwind utilities;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용법&lt;/h3&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;Tailwind는 미리 정의된 수많은 클래스들로 구성되어 있어서, 따로 .css 파일 없이도 className을 통해 바로 스타일을 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;&amp;lt;button className=&quot;bg-blue-500 text-white font-bold py-2 px-4 rounded&quot;&amp;gt;
  로그인
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시처럼 bg-blue-500, text-white, px-4, rounded 등은 각각의 스타일 속성 하나에 대응하는 클래스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2️⃣ 접두어로 반응형 디자인 구성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sm:, md:, lg: 같은 접두어만 붙이면 화면 크기에 따라 반응형 스타일링이 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;div className=&quot;text-base md:text-xl lg:text-2xl&quot;&amp;gt;
  스크린 크기에 따라 글자 크기 변화
&amp;lt;/div&amp;gt;
&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;기본은 text-base&lt;/li&gt;
&lt;li&gt;&lt;b&gt;768px 이상(md)&lt;/b&gt;: text-xl&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1024px 이상(lg)&lt;/b&gt;: text-2xl&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 미디어 쿼리도 &lt;b&gt;직관적인 접두어&lt;/b&gt;로 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3️⃣ 접두어로 가상 선택자 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마우스를 올렸을 때, 클릭했을 때, 포커스됐을 때 등 가상 선택자도 접두어로 처리합니다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;&amp;lt;button className=&quot;bg-blue-500 hover:bg-blue-600 focus:ring-2 active:scale-95&quot;&amp;gt;
  상호작용 가능한 버튼
&amp;lt;/button&amp;gt;
&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;hover:bg-blue-600: 마우스를 올리면 배경색 변경&lt;/li&gt;
&lt;li&gt;focus:ring-2: 포커스 시 ring 효과&lt;/li&gt;
&lt;li&gt;active:scale-95: 클릭 시 약간 작아지는 애니메이션&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tailwind의 진짜 강점은 &lt;b&gt;반응형 접두어 + 상태 접두어&lt;/b&gt;를 조합해서 사용할 수 있다는 점입니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;&amp;lt;button className=&quot;bg-blue-500 hover:bg-blue-600 md:hover:bg-green-500&quot;&amp;gt;
  반응형 hover 버튼
&amp;lt;/button&amp;gt;
&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;작은 화면에서는 hover:bg-blue-600&lt;/li&gt;
&lt;li&gt;&lt;b&gt;md 이상&lt;/b&gt;에서는 hover:bg-green-500&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 화면 크기와 상태를 동시에 고려한 스타일도 아주 간결하게 구현할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 클래스 예시&lt;/h3&gt;
&lt;p&gt;목적 클래스 예시&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;배경색&lt;/td&gt;
&lt;td&gt;bg-red-500, bg-gray-100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;글자 색&lt;/td&gt;
&lt;td&gt;text-white, text-gray-800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;마진/패딩&lt;/td&gt;
&lt;td&gt;m-4, mt-2, p-6, px-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;폰트 스타일&lt;/td&gt;
&lt;td&gt;font-bold, text-xl, tracking-wide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정렬&lt;/td&gt;
&lt;td&gt;flex, justify-between, items-center&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;버튼 꾸미기&lt;/td&gt;
&lt;td&gt;hover:bg-blue-700, rounded, shadow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;따로 CSS 파일 안 만들어도 돼서 빠름&lt;/li&gt;
&lt;li&gt;⚠️ 하지만 클래스가 너무 많아지면 가독성이 떨어질 수도 있음&lt;/li&gt;
&lt;li&gt;&amp;rarr; clsx, classnames, 컴포넌트 분리로 해결 가능&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;  팁: 클래스 너무 길어져 가독성이 떨어져요 &lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 땐 Tailwind와 함께 &lt;b&gt;clsx&lt;/b&gt; 또는 &lt;b&gt;classnames&lt;/b&gt; 라이브러리를 활용하거나, 컴포넌트 단위로 분리해서 관리하는 방법도 있어요.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install clsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import clsx from 'clsx';

const buttonClass = clsx(
  'bg-blue-500',
  'text-white',
  'font-bold',
  'py-2',
  'px-4',
  'rounded',
  { 'opacity-50': isDisabled }
);
&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/178</guid>
      <comments>https://dksek3050.tistory.com/178#entry178comment</comments>
      <pubDate>Wed, 23 Apr 2025 13:40:16 +0900</pubDate>
    </item>
    <item>
      <title>[React] Styled Components 사용하기</title>
      <link>https://dksek3050.tistory.com/177</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9iC8w%2FbtsNubNhm0f%2FHFWwAptJcoF0tjazRHsNK1%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;styled-component 는 CSS-in-JS&lt;/b&gt; 방식을 사용하는 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 &lt;b&gt;CSS 코드를 자바스크립트 안에 작성&lt;/b&gt;할 수 있게 해줍니다. 그리고 중요한 건, 작성한 스타일이 &lt;b&gt;해당 컴포넌트에만 적용&lt;/b&gt;된다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 스타일이 자동으로 스코프(scope)되어 다른 컴포넌트와 충돌하지 않도록 해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;styled-component 설치&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install styled-components
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 방법 예시&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Button.jsx
import styled from 'styled-components';

const StyledButton = styled.button`
  background-color: #4caf50;
  color: white;
  padding: 10px 16px;
  border: none;
  border-radius: 5px;
  cursor: pointer;

  &amp;amp;:hover {
    background-color: #45a049;
  }
`;

function Button({ children }) {
  return &amp;lt;StyledButton&amp;gt;{children}&amp;lt;/StyledButton&amp;gt;;
}

export default Button;

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 StyledButton은 내부적으로 고유한 클래스명을 생성하고, 이 스타일은 다른 컴포넌트와 절대 충돌하지 않습니다. 마치 컴포넌트 하나하나가 스타일 영역을 가지는 것처럼 작동합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;props로 동적 스타일 처리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;styled-components는 props를 받아서 CSS 값을 동적으로 지정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;const Input = styled.input`
  background-color: ${({ invalid }) =&amp;gt; $invalid ? &quot;#fed2d2&quot; : &quot;#d1d5db&quot;};
  color: ${({ invalid }) =&amp;gt; $invalid ? &quot;#ef4444&quot; : &quot;#374151&quot;};
`

&amp;lt;Input $invalid={emailNotValid} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식으로 유효성 검증에 따라 스타일을 다르게 보여줄 수 있어 사용자 경험을 향상시킬 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; $ 접두사는 왜 쓰는 걸까?&lt;/b&gt; styled-components는 $로 시작하는 props는 자동으로 필터링해서 DOM에 전달하지 않도록 처리해줍니다. $invalid는 오직 스타일링에서만 사용되고, 실제 HTML 속성에는 포함되지 않기 때문에 &lt;b&gt;불필요한 경고 없이 안전하게 스타일을 제어할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중첩 스타일, @media, hover 등의 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;styled-components는 일반 CSS 문법을 거의 그대로 사용할 수 있기 때문에, 다음과 같은 고급 기능들도 자연스럽게 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;중첩 선택자 (&amp;amp;)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;const Header = styled.header`
  padding: 2rem;
  background-color: #f0f0f0;

  &amp;amp; h1 {
    font-size: 2rem;
    margin-bottom: 1rem;
  }

  &amp;amp; p {
    color: #555;
  }
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;amp;는 해당 styled 컴포넌트 자신을 가리킵니다. 위 예제는 Header 안의 h1과 p에 각각 스타일을 주는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ 미디어 쿼리 (@media)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const Header = styled.header`
  margin-bottom: 2rem;

  @media (min-width: 768px) {
    margin-bottom: 4rem;
  }
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크린 크기에 따라 스타일을 다르게 적용할 수 있습니다. 일반 CSS에서 하던 방식 그대로 작성하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ Pseudo 선택자 (예: :hover, :focus)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;hsp&quot;&gt;&lt;code&gt;const Button = styled.button`
  background-color: #4caf50;
  color: white;

  &amp;amp;:hover {
    background-color: #45a049;
  }
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hover 상태도 마찬가지로 &amp;amp;:hover와 같이 작성하면 됩니다. &amp;amp;는 항상 해당 요소 자신을 의미합니다. 빈칸 없이 이어서 써야 pseudo 선택자로 인식됩니다.&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;&lt;b&gt;스타일 충돌 걱정 X&lt;/b&gt;: 자동으로 고유 클래스 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;props로 동적 스타일 처리 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컴포넌트 기반의 스타일링&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기존 CSS 문법 대부분 사용 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구조화된 스타일 관리가 쉬움&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의할 점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;런타임에서 CSS를 처리하므로, 매우 큰 프로젝트에서는 퍼포먼스 이슈 가능성 있음&lt;/li&gt;
&lt;li&gt;CSS가 JS 내부에 있어 초반엔 낯설 수 있음&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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/177</guid>
      <comments>https://dksek3050.tistory.com/177#entry177comment</comments>
      <pubDate>Wed, 23 Apr 2025 13:38:42 +0900</pubDate>
    </item>
    <item>
      <title>[React]  CSS Module 사용하기</title>
      <link>https://dksek3050.tistory.com/176</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9iC8w%2FbtsNubNhm0f%2FHFWwAptJcoF0tjazRHsNK1%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS Modules은 CSS 클래스 이름을 자동으로 고유하게 만들어주는 방법입니다. 이를 통해 각 컴포넌트에서 사용하는 CSS가 전역에 영향을 미치지 않게 할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;create-react-app을 사용하면, CSS Modules는 기본적으로 지원됩니다. .module.css 확장자를 사용하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Button.module.css&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;/* Button.module.css */
.button {
  background-color: green;
  color: white;
  padding: 10px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Button.js&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Button.js
import React from 'react';
import styles from './Button.module.css';

function Button() {
  return &amp;lt;button className={styles.button}&amp;gt;Click me!&amp;lt;/button&amp;gt;;
}

export default Button;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서 Button.module.css는 CSS Module을 사용하여 자동으로 고유한 클래스 이름을 생성합니다. styles.button을 사용하여 클래스 이름을 적용하면, 해당 CSS가 다른 컴포넌트의 스타일에 영향을 미치지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/176</guid>
      <comments>https://dksek3050.tistory.com/176#entry176comment</comments>
      <pubDate>Wed, 23 Apr 2025 13:37:24 +0900</pubDate>
    </item>
    <item>
      <title>[React] style Prop 사용하여 Inline(인라인)으로 css 적용하기</title>
      <link>https://dksek3050.tistory.com/175</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9iC8w%2FbtsNubNhm0f%2FHFWwAptJcoF0tjazRHsNK1%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;style Prop을 사용해서 CSS를 자바스크립트 객체로 작성하여 JSX에서 스타일을 적용할 수 있습니다. 이 방법을 통해 간단한 스타일링이나, 동적인 스타일 변경을 할 때 사용 가능 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;style Prop 기본 사용법&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;CSS 속성 이름은 camelCase로 작성&lt;/li&gt;
&lt;li&gt;값은 문자열 또는 숫자로 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;function App() {

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button 
        style={{
          backgroundColor: 'red',  // 고정된 색상
          color: 'white',
          padding: '10px 20px',
          border: 'none',
          cursor: 'pointer'
        }} 
      &amp;gt;
        Click me!
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;

&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;동적 스타일링 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import React, { useState } from 'react';

function App() {
  // useState 훅을 사용하여 isClicked 상태를 정의합니다. 초기 값은 false입니다.
  const [isClicked, setIsClicked] = useState(false);

  // 버튼 클릭 시 호출되는 함수입니다. 상태를 반전시켜 isClicked 값을 변경합니다.
  const handleClick = () =&amp;gt; {
    setIsClicked(prev =&amp;gt; !prev); // 이전 상태의 반대로 값을 변경합니다.
  };

  // 버튼 스타일을 동적으로 생성합니다. isClicked 상태에 따라 배경색이 바뀝니다.
  const buttonStyle = {
    backgroundColor: isClicked ? 'green' : 'red', // 상태에 따라 배경색을 green 또는 red로 설정
    color: 'white', 
    padding: '10px 20px',
    border: 'none', 
    cursor: 'pointer'
  };

  return (
    &amp;lt;div&amp;gt;
      {/* label 태그는 버튼을 설명하는 텍스트를 포함합니다. 
           isClicked 상태에 따라 'clicked' 클래스를 동적으로 추가 */}
      &amp;lt;label className={`label ${isClicked ? 'clicked' : ''}`}&amp;gt;Button&amp;lt;/label&amp;gt;

      {/* 버튼은 style prop을 사용하여 동적으로 스타일을 적용하고, 
           클릭 시 handleClick 함수가 호출되어 isClicked 상태가 변경됩니다. */}
      &amp;lt;button style={buttonStyle} onClick={handleClick}&amp;gt;
        {/* 버튼 텍스트는 isClicked 상태에 따라 'Clicked' 또는 'Click me!'로 변경 */}
        {isClicked ? 'Clicked' : 'Click me!'}
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;style Prop 사용 시 장점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트 내에서 간편한 스타일링&lt;/li&gt;
&lt;li&gt;동적 스타일 적용&lt;/li&gt;
&lt;li&gt;외부 라이브러리 의존성 없음&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;style Prop 사용 시 단점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스타일 재사용 어려움&lt;/li&gt;
&lt;li&gt;성능 이슈
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매번 새로운 스타일 객체가 생성되어 리렌더링 성능에 영향을 미칠 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;복잡한 스타일링에 한계&lt;/li&gt;
&lt;li&gt;스타일 객채와 jsx의 상태와 강한 의존성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/175</guid>
      <comments>https://dksek3050.tistory.com/175#entry175comment</comments>
      <pubDate>Wed, 23 Apr 2025 13:36:04 +0900</pubDate>
    </item>
    <item>
      <title>[React] 커스텀 훅(Custom Hook)</title>
      <link>https://dksek3050.tistory.com/174</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9iC8w/btsNubNhm0f/HFWwAptJcoF0tjazRHsNK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9iC8w%2FbtsNubNhm0f%2FHFWwAptJcoF0tjazRHsNK1%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React를 사용하다 보면 컴포넌트마다 비슷한 로직이 반복되는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 데이터를 가져오기 위해 useEffect 안에서 fetch 요청을 보내고, loading 상태와 error 상태를 관리하는 코드를 여러 컴포넌트에서 반복하게 되기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 유용하게 사용할 수 있는 것이 바로 &lt;b&gt;&quot;커스텀 훅(Custom Hook)&quot;&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt; &lt;/span&gt; 먼저, 리액트 훅의 두 가지 규칙부터 다시 복습!&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;리액트 훅은 컴포넌트 함수 또는 다른 훅 안에서만 사용 가능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 함수에서는 사용 불가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;훅은 조건문, 반복문, 중첩 함수 안에서 호출하면 안 됨&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt; &lt;/span&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;상태(useState)나 사이드 이펙트(useEffect) 같은 훅을 포함한 로직을 분리하여 깔끔하게 유지&lt;/li&gt;
&lt;li&gt;JSX를 반환하지 않는 순수한 로직은 컴포넌트로 분리할 수 없기 때문에, 커스텀 훅으로 처리&lt;/li&gt;
&lt;li&gt;동일한 훅 로직을 다양한 컴포넌트에서 사용 가능&lt;/li&gt;
&lt;li&gt;설정에 따라 유연하게 동작을 바꿀 수 있도록 설계할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; &lt;span&gt; &lt;/span&gt; &lt;/b&gt;예시: 데이터를 fetch하는 커스텀 훅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 useFetch라는 커스텀 훅의 예시입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 훅은 fetch 함수와 초기값을 인자로 받아 데이터를 가져오고, 상태를 관리합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import { useEffect, useState } from &quot;react&quot;;

export function useFetch(fetchFn, initialValue) {
  const [isFetching, setIsFetching] = useState();
  const [error, setError] = useState();
  const [fetchedData, setFetchedData] = useState(initialValue);

  useEffect(() =&amp;gt; {
    async function fetchData() {
      setIsFetching(true);
      try {
        const data = await fetchFn();
        setFetchedData(data);
      } catch (error) {
        setError({ message: error.message || &quot;Failed to fetch data.&quot; });
      }

      setIsFetching(false);
    }

    fetchData();
  }, [fetchFn]);

  return {
    isFetching,
    error,
    fetchedData,
    setFetchedData,
  };
}
&lt;/code&gt;&lt;/pre&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;필요하면 setFetchedData로 데이터를 외부에서 업데이트할 수도 있도록 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;nbsp;커스텀 훅 사용의 특징&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useFetch 같은 커스텀 훅은 use로 시작해야 React가 훅으로 인식합니다.&lt;/li&gt;
&lt;li&gt;커스텀 훅은 &lt;b&gt;JSX를 반환하지 않으며&lt;/b&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;&amp;rarr; 각 컴포넌트는 훅을 독립적으로 사용하며, 각각의 상태를 가집니다.&lt;/li&gt;
&lt;li&gt;setFetchedData처럼 상태 업데이트 함수를 반환값에 포함시켜 외부에서 데이터 제어도 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⚠️ 커스텀 훅 만들 때 주의할 점&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;이름은 반드시 use로 시작해야&lt;/b&gt; React가 이를 훅으로 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;내부에서 다른 훅(useState, useEffect 등)을 자유롭게 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;위에서 언급한 훅의 규칙은 커스텀 훅에도 &lt;b&gt;동일하게 적용됩니다&lt;/b&gt;.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조건문 안에서 훅을 사용하면 안 되고, 최상위 레벨에서만 호출해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/174</guid>
      <comments>https://dksek3050.tistory.com/174#entry174comment</comments>
      <pubDate>Tue, 22 Apr 2025 16:20:18 +0900</pubDate>
    </item>
    <item>
      <title>[React] HTTP 통신 : fetch로 데이터 요청하고 로딩/에러 처리까지</title>
      <link>https://dksek3050.tistory.com/172</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s3Fl1/btsNtOFjgwn/kwXndKEKPzJKsxb1IkIyDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s3Fl1/btsNtOFjgwn/kwXndKEKPzJKsxb1IkIyDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s3Fl1/btsNtOFjgwn/kwXndKEKPzJKsxb1IkIyDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs3Fl1%2FbtsNtOFjgwn%2FkwXndKEKPzJKsxb1IkIyDK%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 React에서 HTTP 요청을 다루는 데 사용되는 기본적인 fetch API와 이를 어떻게 효율적으로 처리할 수 있는지에 대해 알아보겠습니다. 특히, &lt;b&gt;로딩 상태&lt;/b&gt;와 &lt;b&gt;에러 처리&lt;/b&gt;를 어떻게 관리하는지를 다룰 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 기본 fetch API 사용법 (GET 요청)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. fetch + .then() 사용&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  fetch('&amp;lt;http://localhost:3000/places&amp;gt;')
    .then((response) =&amp;gt; response.json())
    .then((resData) =&amp;gt; setAvailablePlaces(resData.places));
}, []);

&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;response.json()은 비동기 작업이므로 &lt;b&gt;두 번째 .then()에서 실제 데이터를 처리&lt;/b&gt;하기 위해 then 체인을 두번 사용하게 됩니다.&lt;/li&gt;
&lt;li&gt;useEffect에 &lt;b&gt;빈 의존성 배열 ([])&lt;/b&gt; 을 전달해야, &lt;b&gt;초기 렌더링 시 한 번만 실행&lt;/b&gt;됩니다. (무한루프 방지)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;2. async/await 방식&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  async function fetchPlaces() {
    const response = await fetch('&amp;lt;http://localhost:3000/places&amp;gt;');
    const resData = await response.json();
    setAvailablePlaces(resData.places);
  }
  fetchPlaces();
}, []);

&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async/await 방식은 코드가 간결하고 가독성이 좋으며, 에러 처리에도 유리합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 로딩 상태 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 요청하는 동안 isFetching 상태를 true로 설정하고, 완료되면 다시 false로 변경합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const [isFetching, setIsFetching] = useState(false);

useEffect(() =&amp;gt; {
  async function fetchPlaces() {
    setIsFetching(true); // ✅ 요청 시작 시
    const response = await fetch('&amp;lt;http://localhost:3000/places&amp;gt;');
    const resData = await response.json();
    setAvailablePlaces(resData.places);
    setIsFetching(false); // ✅ 요청 완료 시
  }
  fetchPlaces();
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;렌더링 흐름 요약&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt; ️ &lt;b&gt;초기 렌더링&lt;/b&gt;: isFetching=false, 데이터는 빈 배열&lt;/li&gt;
&lt;li&gt;  &lt;b&gt;로딩 시작&lt;/b&gt;: isFetching=true, 로딩 UI 표시 가능&lt;/li&gt;
&lt;li&gt;✅ &lt;b&gt;로딩 완료&lt;/b&gt;: isFetching=false, 데이터 반영&lt;/li&gt;
&lt;/ol&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;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;페이지에 처음 접속하면 useEffect가 실행되기 전에 컴포넌트가 렌더링됩니다.&lt;/li&gt;
&lt;li&gt;초기 상태는 &lt;b&gt;isFetching이 false&lt;/b&gt;, &lt;b&gt;availablePlaces는 빈 배열&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;이때, &lt;b&gt;&quot;Fetching...&quot;&lt;/b&gt; 같은 로딩 메시지가 없다면 그냥 빈 UI가 렌더링됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. useEffect 실행&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;useEffect가 실행되면 fetchPlaces 함수가 호출되고, **setIsFetching(true)*가 실행됩니다.&lt;/li&gt;
&lt;li&gt;이때, &lt;b&gt;isFetching&lt;/b&gt; 값이 true로 변경되고, &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;이 리렌더링에서는 isFetching이 true로 변경된 상태이므로, 로딩 UI(예: &quot;Fetching...&quot;)가 나타날 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. fetch 완료 후 상태 업데이트&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;fetch 요청이 끝나면, 응답을 받아서 setAvailablePlaces(resData.places)와 setIsFetching(false)를 호출합니다.&lt;/li&gt;
&lt;li&gt;이때 setAvailablePlaces와 setIsFetching(false)가 두 번 호출됩니다.
&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;이 발생하고, isFetching이 false로 바뀌며 데이터도 업데이트됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ HTTP 에러 처리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 요청 중 실패할 경우를 대비해 try-catch로 에러를 처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const [error, setError] = useState();

useEffect(() =&amp;gt; {
  async function fetchPlaces() {
    setIsFetching(true);

    try {
      const response = await fetch('&amp;lt;http://localhost:3000/places&amp;gt;');
      const resData = await response.json();

      if (!response.ok) {
        throw new Error('Failed to fetch places.');
      }

      setAvailablePlaces(resData.places);
    } catch (error) {
      setError({ message: error.message || 'Could not fetch places.' });
    }

    setIsFetching(false);
  }

  fetchPlaces();
}, []);

&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;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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  위치 정보 수신 전에 로딩이 끝나버리는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치 기반으로 장소 데이터를 정렬하려는 로직에서, &lt;b&gt;위치 정보 수신 전에 isFetching을 false로 설정&lt;/b&gt;해버려서, &lt;b&gt;정렬 전 데이터가 화면에 잠깐 노출되는 UX 버그&lt;/b&gt;가 발생했습니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  async function fetchPlaces() {
    setIsFetching(true);

    try {
      const response = await fetch(&quot;&amp;lt;http://localhost:3000/places&amp;gt;&quot;);
      const resData = await response.json();

      // ✅ 위치 요청 (비동기)
      navigator.geolocation.getCurrentPosition((position) =&amp;gt; {
        const sortedPlaces = sortPlacesByDistance(
          resData.places,
          position.coords.latitude,
          position.coords.longitude
        );
        setAvailablePlaces(sortedPlaces);
      });
    } catch (error) {
      setError({ message: error.message });
    }

    setIsFetching(false); // ❗ 위치 정렬이 끝나기 전에 로딩 상태 종료됨
  }

  fetchPlaces();
}, []);

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 콘솔 로그 흐름 예시&lt;/h3&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;[1] ===   [fetchPlaces] 시작 ===
[2]   [isFetching] true 설정됨
[3]   [fetch] 요청 성공
[4]   [JSON 파싱 완료] {places: Array(18)}
[5]   [위치 요청] 사용자 위치 가져오는 중...
[6] ✅ [isFetching] false 설정됨    // &amp;lt;===   위치 기반 정렬은 아직 안 끝났음
[7] ===   [fetchPlaces] 종료 ===
[8] ✅ [위치 정보] 수신 완료: latitude: 37.4111, longitude: 127.0983
[9]  ️ [정렬 완료] 거리순 장소 목록: (18) [...]
[10]   [setAvailablePlaces] 정렬된 데이터 적용
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제의 원인&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;포인트 설명&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚠️ getCurrentPosition()은 비동기&lt;/td&gt;
&lt;td&gt;위치 정보는 &lt;b&gt;즉시 반환되지 않고&lt;/b&gt;, 나중에 콜백이 실행됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚠️ setIsFetching(false)가 너무 일찍 실행됨&lt;/td&gt;
&lt;td&gt;위치 정보가 &lt;b&gt;도착하기도 전에 로딩을 종료&lt;/b&gt;시켜 버립니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚠️ 사용자 입장에선 이상하게 느껴짐&lt;/td&gt;
&lt;td&gt;처음엔 기본 정렬 데이터가 보이다가, 몇 초 후에 갑자기 재정렬되어 &lt;b&gt;&quot;깜빡이거나 버벅이는 UI&quot;&lt;/b&gt; 가 됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해서는 &lt;b&gt;위치 정보 요청과 로딩 상태 변경을 적절히 관리&lt;/b&gt;해야 합니다. setIsFetching(false)를 &lt;b&gt;위치 정보 수신 후에&lt;/b&gt; 변경되도록 수정하여, 데이터가 제대로 정렬된 후에 로딩 상태가 종료되도록 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수정된 코드&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  async function fetchPlaces() {
    setIsFetching(true);

    try {
      const response = await fetch(&quot;&amp;lt;http://localhost:3000/places&amp;gt;&quot;);
      const resData = await response.json();

      navigator.geolocation.getCurrentPosition((position) =&amp;gt; {
        const sortedPlaces = sortPlacesByDistance(
          resData.places,
          position.coords.latitude,
          position.coords.longitude
        );
        setAvailablePlaces(sortedPlaces);
        setIsFetching(false); // ✅ 위치 기반 정렬이 끝난 후 로딩 종료
      });
    } catch (error) {
      setError({ message: error.message });
      setIsFetching(false); // ⚠️ 에러가 난 경우도 잊지 말고 로딩 종료
    }
  }

  fetchPlaces();
}, []);

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경 후 로그 흐름 예시&lt;/h3&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;[1] ===   [fetchPlaces] 시작 ===  
[2]   [isFetching] true 설정됨  
[3]   [fetch] 요청 성공  
[4]   [JSON 파싱 완료] {places: Array(18)}  
[5]   [위치 요청] 사용자 위치 가져오는 중...  
[6] ===   [fetchPlaces] 종료 ===  
[7] ✅ [위치 정보] 수신 완료: latitude: 37.4111713, longitude: 127.098369  
[8]  ️ [정렬 완료] 거리순 장소 목록: (18) [...]  
[9]   [setAvailablePlaces] 정렬된 데이터 적용  
[10] ✅ [isFetching] false 설정됨  
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 setIsFetching(false)가 &lt;b&gt;위치 정보를 받고, 장소 목록을 정렬한 후&lt;/b&gt; 호출되므로, 로딩 UI가 올바르게 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 기본 fetch API 사용법 (PUT 요청)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;https.js&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export async function updateUserPlaces(places) {
  const response = await fetch(&quot;&amp;lt;http://localhost:3000/user-places&amp;gt;&quot;, {
    method: &quot;PUT&quot;,
    body: JSON.stringify({ places }), // {places: places}와 동일
    headers: {
      &quot;Content-Type&quot;: &quot;application/json&quot;,
    },
  });

  const resData = await response.json();

  if (!response.ok) {
    throw new Error(&quot;Failed to update user places.&quot;);
  }

  return resData.message;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App.jsx&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export async function handleSelectPlace(selectedPlace) {
		// 상태 업데이트
    setUserPlaces((prevPickedPlaces) =&amp;gt; {
      ....
      return [selectedPlace, ...prevPickedPlaces];
    });

    // backend 통신 함수 호출
    try {
      await updateUserPlaces([selectedPlace, ...userPlaces]);
    } catch (error) {
      console.error(&quot;Error updating user places:&quot;, error);
      setUserPlaces(userPlaces); // 백엔드 단에서 에러가 발생했을 때, 원래의 userPlaces로 되돌리기
    }
  }
&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;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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 낙관적 업데이트 vs 비관적 업데이트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;낙관적 업데이트 (Optimistic Update)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;낙관적 업데이트(Optimistic Update)&amp;rdquo;는 UI를 더 빠르게 반응시키기 위한 전략 중 하나로 서버의 응답을 기다리지 않고, 사용자에게 먼저 결과가 적용된 것처럼 보여주는 방식 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 행동에 &lt;b&gt;즉시 반응하여 UI를 먼저 업데이트&lt;/b&gt;한 후, 백엔드 응답 결과에 따라 유지하거나 롤백합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;async function handleSelectPlace(selectedPlace) {
  setUserPlaces((prevPickedPlaces) =&amp;gt; [selectedPlace, ...prevPickedPlaces]);

  try {
    await updateUserPlaces([selectedPlace, ...userPlaces]);
  } catch (error) {
    console.error(&quot;Error updating user places:&quot;, error);
    setUserPlaces(userPlaces); // ❗ 실패 시 원래 상태로 되돌리기
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ 장점&lt;/b&gt;&lt;/p&gt;
&lt;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;UX가 즉각적이고 부드러움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌ 단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실패 가능성을 고려해야 하며&lt;/li&gt;
&lt;li&gt;&lt;b&gt;되돌리기(rollback)&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;비관적 업데이트 (Pessimistic Update)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버의 응답을 &lt;b&gt;받은 후에야&lt;/b&gt; UI를 업데이트합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function handleSelectPlace(selectedPlace) {
  await updateUserPlaces([selectedPlace]);

  setUserPlaces((prevPickedPlaces) =&amp;gt; {
    if (prevPickedPlaces.some((place) =&amp;gt; place.id === selectedPlace.id)) {
      return prevPickedPlaces;
    }
    return [selectedPlace, ...prevPickedPlaces];
  });
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ 특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안정성은 높지만&lt;/li&gt;
&lt;li&gt;사용자 입장에서 &lt;b&gt;반응이 느려 보일 수 있음&lt;/b&gt;&lt;/li&gt;
&lt;/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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/172</guid>
      <comments>https://dksek3050.tistory.com/172#entry172comment</comments>
      <pubDate>Tue, 22 Apr 2025 10:19:02 +0900</pubDate>
    </item>
    <item>
      <title>[React] 리액트 클래스 컴포넌트, 왜 아직도 알아야 할까?</title>
      <link>https://dksek3050.tistory.com/171</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bens2X/btsNo1xSuXP/OiK9nevJAoLxoeKDunTJB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bens2X/btsNo1xSuXP/OiK9nevJAoLxoeKDunTJB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bens2X/btsNo1xSuXP/OiK9nevJAoLxoeKDunTJB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbens2X%2FbtsNo1xSuXP%2FOiK9nevJAoLxoeKDunTJB0%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;500&quot; height=&quot;498&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 클래스 컴포넌트의 구조, 생명주기 메서드, 컨텍스트 사용 방식, 그리고 오류 경계까지 정리해봤습니다. ✍️&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  클래스 컴포넌트 기본 구조&lt;/h2&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;class MyComponent extends React.Component {
  render() {
    return &amp;lt;h1&amp;gt;Hello, {this.props.name}!&amp;lt;/h1&amp;gt;;
  }
}
&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.Component를 상속받아 정의합니다.&lt;/li&gt;
&lt;li&gt;render() 메서드 안에서 JSX를 반환해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  왜 존재할까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React 16.8 이전에는 &lt;b&gt;state나 생명주기 메서드를 사용하려면 클래스 컴포넌트로 작성해야 했습니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이후 &lt;b&gt;Hooks&lt;/b&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  클래스에서 상태(State) 관리&lt;/h2&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;constructor() {
  super(); // 반드시 호출
  this.state = {
    showUsers: true,
    // 다른 상태들도 이 객체 안에 정의
  };
}

toggleUsersHandler() {
  this.setState((prevState) =&amp;gt; {
    return { showUsers: !prevState.showUsers };
  });
}
&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;li&gt;생성자(constructor)에서 this.state = { ... }로 초기화합니다.&lt;/li&gt;
&lt;li&gt;상태 변경은 this.setState()로 수행하며 &lt;b&gt;기존 상태와 병합&lt;/b&gt;되므로 전체를 덮어쓰지 않기 때문에 부분 업데이트가 가능합니다.&lt;/li&gt;
&lt;li&gt;이벤트 핸들러는 반드시 this 바인딩 필요 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;this.toggleUsersHandler.bind(this)
&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  생명주기 메서드 (Lifecycle Methods)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수형 컴포넌트의 useEffect()와 대응되는 클래스 컴포넌트의 고유 메서드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생명주기 메서드 언제 실행되나요? useEffect와 비교&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;componentDidMount()&lt;/td&gt;
&lt;td&gt;컴포넌트가 &lt;b&gt;마운트&lt;/b&gt;된 직후 실행됨&lt;/td&gt;
&lt;td&gt;useEffect(() =&amp;gt; { }, [])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;componentDidUpdate()&lt;/td&gt;
&lt;td&gt;컴포넌트가 &lt;b&gt;업데이트&lt;/b&gt;된 후 실행됨&lt;/td&gt;
&lt;td&gt;useEffect(() =&amp;gt; { }, [value])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;componentWillUnmount()&lt;/td&gt;
&lt;td&gt;컴포넌트가 &lt;b&gt;언마운트&lt;/b&gt;되기 직전에 실행됨&lt;/td&gt;
&lt;td&gt;useEffect(() =&amp;gt; { return () =&amp;gt; {} }, [])&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ componentDidMount는 API 호출이나 초기 설정 작업에 자주 사용됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  클래스 컴포넌트에서 Context 사용하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 방법 1: &amp;lt;Context.Consumer&amp;gt; 사용&lt;/h3&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;UsersContext.Consumer&amp;gt;
  {(ctx) =&amp;gt; &amp;lt;div&amp;gt;{ctx.users[0].name}&amp;lt;/div&amp;gt;}
&amp;lt;/UsersContext.Consumer&amp;gt;
&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;li&gt;단점 : 매번 Consumer로 감싸야 하기 때문에 &lt;b&gt;코드가 길어지고 복잡&lt;/b&gt;해질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 방법 2: static contextType 사용하기 (&lt;b&gt;클래스 컴포넌트 전용&lt;/b&gt;)&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;import UsersContext from &quot;../store/users-context&quot;;

class UserFinder extends Component {
  static contextType = UsersContext;

  componentDidMount() {
    console.log(this.context.users); // 여기서 컨텍스트 데이터 사용 가능
  }

  render() {
    return &amp;lt;Users users={this.context.users} /&amp;gt;;
  }
}
&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;클래스 컴포넌트에서 가장 많이 쓰는 Context 접근 방식입니다.&lt;/li&gt;
&lt;li&gt;컴포넌트에 static contextType = UsersContext를 선언하면, &amp;rarr; &lt;b&gt;React가 내부적으로 해당 Context의 값을 this.context에 자동으로 넣어줍니다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;단점: 한 컴포넌트에 오직 하나의 context만 연결 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  왜 static이어야 할까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;static 속성은 클래스의 인스턴스가 아니라 &lt;b&gt;클래스 자체에 바인딩되는 속성&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;따라서 React는 컴포넌트가 인스턴스화되기 전에 이 정보를 미리 읽고 준비할 수 있습니다.&lt;/li&gt;
&lt;li&gt;즉, 렌더링 전에 &lt;b&gt;&quot;아, 이 컴포넌트는 어떤 Context랑 연결되어 있어야 하구나!&quot;&lt;/b&gt; 를 인지할 수 있는 방식이에요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❓ 그럼 왜 하나만 연결 가능?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;contextType은 &lt;b&gt;하나의 값만 가질 수 있는 정적 속성&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;즉, &lt;b&gt;static이라는 속성은 중복 선언이 불가능&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  오류 경계(Error Boundary)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하위 컴포넌트에서 예기치 못한 오류가 발생하더라도 자동으로 감지하고 처리하여 전체 앱이 죽지 않도록 방지하기 위해 사용합니다.&lt;/li&gt;
&lt;li&gt;단,&lt;b&gt;이 기능은 클래스 컴포넌트에서만 구현할 수 있습니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor() {
    super();
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // 오류 로깅 가능
    this.setState({ hasError: true });
  }

  render() {
    if (this.state.hasError) {
      return &amp;lt;p&amp;gt;문제가 발생했습니다.  &amp;lt;/p&amp;gt;;
    }

    return this.props.children;
  }
}

export default ErrorBoundary;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 예시:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import ErrorBoundary from './ErrorBoundary';
import Users from './Users';

function App() {
  return (
    &amp;lt;ErrorBoundary&amp;gt;
      &amp;lt;Users /&amp;gt;
    &amp;lt;/ErrorBoundary&amp;gt;
  );
}
&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;Users 컴포넌트에서 오류가 발생해도 전체 앱이 중단되지 않고 &quot;문제가 발생했습니다&quot; 메시지를 출력하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 주의할 점&lt;/p&gt;
&lt;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;함수형 컴포넌트에서는 componentDidCatch()를 사용할 수 없습니다.&lt;/li&gt;
&lt;li&gt;여러 오류 처리 방식이 필요하다면 &lt;b&gt;각 컴포넌트에 별도의 오류 경계&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/171</guid>
      <comments>https://dksek3050.tistory.com/171#entry171comment</comments>
      <pubDate>Fri, 18 Apr 2025 22:13:30 +0900</pubDate>
    </item>
    <item>
      <title>[React]⚡️React 성능 최적화 정리: memo, useMemo, 그리고 key의 역할까지!</title>
      <link>https://dksek3050.tistory.com/170</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1R6BU/btsNlDRLp17/TxiyjKw0VUpMS12koVJN40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1R6BU/btsNlDRLp17/TxiyjKw0VUpMS12koVJN40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1R6BU/btsNlDRLp17/TxiyjKw0VUpMS12koVJN40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1R6BU%2FbtsNlDRLp17%2FTxiyjKw0VUpMS12koVJN40%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;500&quot; height=&quot;498&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  memo 함수&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;memo 함수는 불필요한컴포넌트를 다시 렌더링 하지 않도록 방지하는 함수입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import React, { memo } from &quot;react&quot;;

const MyComponent = memo(function MyComponent(props) {
  // ...
});
&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;부모 컴포넌트가 렌더링되면 자식 컴포넌트도 기본적으로 함께 렌더링됩니다. 하지만 자식 컴포넌트의 props가 &lt;b&gt;변하지 않았다면&lt;/b&gt; 굳이 다시 렌더링할 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;이럴 때 memo로 감싸면, &lt;b&gt;props가 이전과 동일할 경우 렌더링을 건너뜁니다. 즉, 컴호넌트 함수가 다시 호출 되지 않게 됩니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;✅ 사용 전: 상태 변경 시 Counter 컴포넌트가 계속 실행됨&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.png&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;339&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZcsdA/btsNpQa9dYS/EaA4RqF95TL9KCm2Aq0L70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZcsdA/btsNpQa9dYS/EaA4RqF95TL9KCm2Aq0L70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZcsdA/btsNpQa9dYS/EaA4RqF95TL9KCm2Aq0L70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZcsdA%2FbtsNpQa9dYS%2FEaA4RqF95TL9KCm2Aq0L70%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;691&quot; height=&quot;339&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;691&quot; data-origin-height=&quot;339&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;✅ 사용 후: memo 적용 시 Counter는 props가 동일하면 재실행되지 않음&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;image2.png&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/orgro/btsNpvxEwph/bre6ImJ9Hyt06nolGvBqGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/orgro/btsNpvxEwph/bre6ImJ9Hyt06nolGvBqGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/orgro/btsNpvxEwph/bre6ImJ9Hyt06nolGvBqGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Forgro%2FbtsNpvxEwph%2Fbre6ImJ9Hyt06nolGvBqGk%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;684&quot; height=&quot;305&quot; data-filename=&quot;image2.png&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;memo는 이전 props와 새로운 props를 &lt;b&gt;얕게(shallow) 비교&lt;/b&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;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⚠️ 모든 컴포넌트에 memo를 쓰는 건 오히려 성능 저하를 불러올 수 있습니다.&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;props가 객체나 함수일 경우 비교가 까다로워 매번 새로 만들어졌다고 판단할 수 있기 때문입니다.&lt;/li&gt;
&lt;li&gt;그래서 &lt;b&gt;렌더링이 자주 발생하지만 props는 거의 바뀌지 않는 컴포넌트&lt;/b&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  useMemo() 훅&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useMemo()는 &lt;b&gt;컴포넌트 내부의 복잡한 연산이 포함된 함수의 재실행을 방지&lt;/b&gt;하는 데 사용됩니다.&lt;/li&gt;
&lt;li&gt;memo가 전체 컴포넌트의 리렌더링을 막는 것과 달리, useMemo는 특정 값의 &lt;b&gt;계산 결과&lt;/b&gt;를 메모이제이션합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;const initialCountIsPrime = useMemo(
  () =&amp;gt; isPrime(initialCount),
  [initialCount]
);
&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;li&gt;의존성 배열 내 값이 바뀌지 않는 한, 이전에 계산한 값을 재사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⚠️ useMemo 또한 모든 연산에 무조건 사용하는 것은 좋지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모이제이션을 위한 연산도 비용이 들기 때문에, 연산이 무겁고 자주 바뀌지 않는 값에만 사용하는 것이 좋습니다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  컴포넌트 함수의 실행 흐름&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트가 실행되면, React는 &lt;b&gt;가상 DOM(Virtual DOM)&lt;/b&gt; 을 생성합니다.&lt;/li&gt;
&lt;li&gt;이 가상 DOM과 이전 렌더의 가상 DOM을 비교하여 변경 사항을 찾아냅니다.&lt;/li&gt;
&lt;li&gt;변경된 부분만 실제 DOM에 적용하여 성능을 최적화합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 DOM을 사용하는 이유는 실제 DOM 조작보다 훨씬 빠르게 메모리에서 연산이 가능하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  State 관리에서 key의 역할&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트는 각자 고유한 state를 가지며, 같은 컴포넌트를 여러 번 사용해도 상태는 서로 독립적입니다.&lt;/li&gt;
&lt;li&gt;배열을 렌더링할 때 key 값을 &lt;b&gt;index로 사용하는 것은 지양&lt;/b&gt;해야 합니다.&lt;/li&gt;
&lt;li&gt;그 이유는 다음과 같습니다:&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배열 앞에 요소가 추가되면 기존 index가 바뀌어, 잘못된 상태가 다른 항목에 적용될 수 있습니다.&lt;/li&gt;
&lt;li&gt;React가 변경 여부를 정확하게 파악하지 못해 전체 항목을 다시 렌더링할 수 있습니다.&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;key는 &lt;b&gt;고유 ID&lt;/b&gt;를 사용하는 것이 바람직합니다.&lt;/li&gt;
&lt;li&gt;이렇게 하면 React가 각 항목을 정확히 추적할 수 있어 &lt;b&gt;불필요한 렌더링을 줄이고 성능도 향상됩니다&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;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  key의 또 다른 역할&lt;/h2&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;Counter initialCount={chosenCount} /&amp;gt;
&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;위 코드에서 chosenCount가 변경되어도, Counter 컴포넌트는 &lt;b&gt;재사용&lt;/b&gt;되며 내부 상태는 유지됩니다.&lt;/li&gt;
&lt;li&gt;이는 useState(initialCount)가 최초 렌더링 시 초기값만 사용하는 특성 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;Counter key={chosenCount} initialCount={chosenCount} /&amp;gt;
&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;반면, key를 설정하면 React는 chosenCount가 바뀔 때 &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;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: key는 props처럼 전달되지만, 자식 컴포넌트에서는 props.key로 접근할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 React 내부에서만 사용하는 식별자입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  상태 업데이트의 스케줄링 &amp;amp; 배칭&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;setState는 &lt;b&gt;즉시 실행되지 않고&lt;/b&gt;, React가 업데이트 일정을 조정합니다 (비동기 처리).&lt;/li&gt;
&lt;li&gt;따라서 상태를 설정한 직후 console.log로 출력하면, &lt;b&gt;이전 값이 출력되는 경우가 많습니다&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안전한 상태 업데이트 방식:&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;setCount(prev =&amp;gt; prev + 1);
&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;li&gt;상태 업데이트가 여러 번 연달아 일어나도 &lt;b&gt;정확하게 처리됩니다&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배칭(Batching)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 개의 상태 업데이트가 동시에 발생하면 React는 이를 &lt;b&gt;하나의 렌더링으로 묶어 처리&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;이렇게 하면 불필요한 렌더링을 줄이고 성능을 최적화할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Million.js로 React 성능 최적화&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://million.dev/&quot;&gt;Million&lt;/a&gt;은 React 앱의 렌더링 성능을 향상시켜주는 경량 라이브러리입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install million

&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// vite.config.js
import MillionLint from &quot;million/compiler&quot;;
import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react&quot;;

export default defineConfig({
  plugins: [MillionLint.vite({ auto: true }), react()],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정만으로 &lt;b&gt;자동 성능 최적화&lt;/b&gt;를 적용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image3.png&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8ceTs/btsNpprXXaB/2Yad0kK8ykGFJrQPhsgrPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8ceTs/btsNpprXXaB/2Yad0kK8ykGFJrQPhsgrPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8ceTs/btsNpprXXaB/2Yad0kK8ykGFJrQPhsgrPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8ceTs%2FbtsNpprXXaB%2F2Yad0kK8ykGFJrQPhsgrPk%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;797&quot; height=&quot;524&quot; data-filename=&quot;image3.png&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;524&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  마무리 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 목적 사용 시기&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;memo&lt;/td&gt;
&lt;td&gt;불필요한 자식 렌더링 방지&lt;/td&gt;
&lt;td&gt;props가 자주 바뀌지 않는 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useMemo()&lt;/td&gt;
&lt;td&gt;복잡한 계산 결과 캐싱&lt;/td&gt;
&lt;td&gt;무거운 연산이 있을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key&lt;/td&gt;
&lt;td&gt;컴포넌트 구분 &amp;amp; 성능 최적화&lt;/td&gt;
&lt;td&gt;리스트 렌더링, 상태 꼬임 방지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;함수형 setState&lt;/td&gt;
&lt;td&gt;상태 안정적 업데이트&lt;/td&gt;
&lt;td&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되셨다면 ❤️ 좋아요와 댓글로 피드백 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점이 있다면 언제든지 질문 환영입니다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 읽어주셔서 감사합니다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/170</guid>
      <comments>https://dksek3050.tistory.com/170#entry170comment</comments>
      <pubDate>Thu, 17 Apr 2025 21:45:27 +0900</pubDate>
    </item>
    <item>
      <title>[React] React로 퀴즈 앱 만들기 | 개발 회고</title>
      <link>https://dksek3050.tistory.com/169</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FArbm/btsNo6xruqY/GRxAizXqsomkAKcxDUpIF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FArbm/btsNo6xruqY/GRxAizXqsomkAKcxDUpIF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FArbm/btsNo6xruqY/GRxAizXqsomkAKcxDUpIF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFArbm%2FbtsNo6xruqY%2FGRxAizXqsomkAKcxDUpIF0%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;500&quot; height=&quot;498&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 강의 일부의 연습 프로젝트 입니다.&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;&lt;b&gt;  React Quiz App&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 사용자가 문제을 읽고 시간 제한 내에 답안을 선택하는 퀴즈 앱 입니다. 주어진 문제를 다 풀면 결과 페이지를 제공합니다. 단순히 문제를 풀기만 하는 UI가 아닌, &lt;u&gt;&lt;b&gt;상태 관리와 퀴즈 흐름&lt;/b&gt;&lt;/u&gt;에 초점을 맞춰 구현했습니다. 이를 통해 React에서 컴포넌트를 어떻게 효율적으로 설계하고, Context API와 상태 관리를 어떻게 활용할 수 있는지에 대한 경험을 쌓을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, 각 문제의 상태(문제 풀이 중, 정답 확인, 시간 초과 등)를 어떻게 처리할지에 대한 고민과, &lt;b&gt;시간 제한&lt;/b&gt; 및 &lt;b&gt;문제 흐름&lt;/b&gt;을 어떻게 자연스럽게 관리할 수 있을지에 대한 구현이 주요 포인트였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 퀴즈 앱의 &lt;b&gt;주요 기능&lt;/b&gt;과 &lt;b&gt;구현 과정&lt;/b&gt;, 그리고&lt;u&gt; 퀴즈 앱을 만들면서 &lt;b&gt;고민했던 부분들&lt;/b&gt;&lt;/u&gt;에 대해 차례대로 설명하려 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;프로젝트 개요&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  배포 링크 &amp;amp; 깃허브&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;  &lt;a href=&quot;https://an0401na.github.io/React_Study/12-React-Quiz/&quot;&gt;배포 링크 바로가기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;  &lt;a href=&quot;https://github.com/An0401na/React_Study/tree/main/12-React-Quiz&quot;&gt;GitHub Repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  프로젝트 소개&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;컴포넌트&lt;/b&gt;: 앱을 작은 독립적인 부분으로 나눠 관리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;useState&lt;/b&gt;: 퀴즈 진행 상태(현재 문제, 선택한 답 등) 관리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;useEffect&lt;/b&gt;: 타이머와 상태 변화 관리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;useContext&lt;/b&gt;: 사용자 답안 상태를 전역적으로 관리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;useReducer&lt;/b&gt;: 복잡한 상태 관리 (답안 추가 등).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조건부 렌더링&lt;/b&gt;: 퀴즈 화면과 결과 화면 전환.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  핵심 기능&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;퀴즈 문제 &amp;amp; 보기 랜덤 셔플&lt;/li&gt;
&lt;li&gt;타이머 기반 자동 진행&lt;/li&gt;
&lt;li&gt;사용자 답안 저장 및 요약&lt;/li&gt;
&lt;li&gt;Context API 기반 상태 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; ️ 구현 과정 및 내부 로직&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ &lt;b&gt;전체적인 흐름&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;퀴즈 시작&lt;/b&gt;: 사용자가 앱을 시작하면, Quiz 컴포넌트가 렌더링되어 문제들이 섞인 상태로 표시됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제 풀기&lt;/b&gt;: 각 문제는 제한 시간이 주어지며, 사용자는 보기를 선택하거나 Skip 버튼을 눌러 문제를 건너뛸 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;답안 제출&lt;/b&gt;: 사용자가 답을 선택하면, userAnswerContext를 통해 답안 상태가 저장되고, 타이머가 종료되면 자동으로 정답이 표시됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다음 문제로 이동&lt;/b&gt;: 정답을 확인한 후, Quiz 컴포넌트는 자동으로 다음 문제로 넘어가며, 마지막 문제까지 진행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과 화면&lt;/b&gt;: 모든 문제를 끝내면 Summary 컴포넌트가 렌더링되어 정답, 오답, 건너뛴 문제 수와 비율을 표시하고, 각 문제에 대한 사용자 선택 및 결과를 출력합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 관리&lt;/b&gt;: UserAnswerContext와 useReducer를 통해 사용자의 답안을 관리하고, 각 컴포넌트 간 상태를 공유합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;2️⃣ 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;폴더 구조&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;  src
├──   assets
├──   components
│   ├──   Answers.jsx
│   ├──   Header.jsx
│   ├──   ProgressBar.jsx
│   ├──   Question.jsx
│   ├──   Quiz.jsx
│   └──   Summary.jsx
├──   constants
│   ├── ❓ nonsense_questions_ko.js
│   ├── ❓ questions.js
│   ├── ❓ questions_ko.js
│   └── ❓ stages.js
├──   store
│   └──   user-answer-context.jsx
├──   App.jsx
├──   index.css
└──   main.jsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  컴포넌트 구조&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image6.png&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTdFGt/btsNnT0DTr0/BYY6GUYDJYhy9Y6F6kVgPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTdFGt/btsNnT0DTr0/BYY6GUYDJYhy9Y6F6kVgPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTdFGt/btsNnT0DTr0/BYY6GUYDJYhy9Y6F6kVgPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTdFGt%2FbtsNnT0DTr0%2FBYY6GUYDJYhy9Y6F6kVgPk%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;408&quot; height=&quot;428&quot; data-filename=&quot;image6.png&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;App
├── Header (상단 로고 + 타이틀)
│ 
├── UserAnswerContextProvider
│   ├── Quiz (퀴즈 진행 중일 때 렌더링)
│   │   ├── Question (질문 + ProgressBar)
│   │   │   └── ProgressBar (타이머 진행 바)
│   │   ├── Answers (선택지 버튼들)
│   │   └── Skip 버튼 영역
│   │
│   └── Summary (퀴즈 종료 후 결과 요약 화면)
│       └── 각 질문에 대한 결과 항목
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3️⃣&amp;nbsp;주요 컴포넌트 및 기능&lt;/b&gt;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;1. App.jsx (메인 컴포넌트)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;퀴즈 앱의 &lt;b&gt;전체 흐름을 제어&lt;/b&gt;하는 루트 컴포넌트&lt;/li&gt;
&lt;li&gt;viewMode 상태로 현재 화면을 관리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;quiz&quot; &amp;rarr; Quiz 컴포넌트 렌더링&lt;/li&gt;
&lt;li&gt;&quot;summary&quot; &amp;rarr; Summary 컴포넌트 렌더링&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;  // 현재 화면 모드를 상태로 관리 (퀴즈 화면 또는 결과 화면)
  const [viewMode, setViewMode] = useState(&quot;quiz&quot;);
  
  // 화면 모드를 변경하는 함수 (퀴즈 화면 -&amp;gt; 결과 화면)
  function handleViewModeChange(mode) {
    setViewMode(mode); // 새로운 화면 모드로 상태 업데이트
  }

  return (
    &amp;lt;&amp;gt;
		    ...
        {/* viewMode 상태에 따라 퀴즈 화면 또는 결과 화면을 렌더링 */}
        {viewMode === &quot;quiz&quot; &amp;amp;&amp;amp; ( &amp;lt;Quiz quizzes={shuffledQuizzes} onQuizEnd={handleViewModeChange} /&amp;gt; )}
        { viewMode === &quot;summary&quot; &amp;amp;&amp;amp; &amp;lt;Summary /&amp;gt;  }
		    ...
    &amp;lt;/&amp;gt;
  );
&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;QUIZS 데이터에서 문제와 보기 순서를 lodash로 셔플하여 출제&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// QUIZS에서 문제를 가져와서 순서를 섞고, 각 문제에 대한 답변 순서도 섞음
  const shuffledQuizzes = _.shuffle(QUIZS).map((quiz) =&amp;gt; ({
    ...quiz,
    answers: _.shuffle(quiz.answers), // 답안 순서 셔플
    correctAnswer: quiz.answers[0], // 첫 번째 답안을 정답으로 설정
  }));
&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;전역 상태 관리용 UserAnswerContextProvider로 감싸, 하위 컴포넌트에서 사용자 답변 데이터를 공유 및 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;return (
  &amp;lt;&amp;gt;&amp;lt;Header /&amp;gt; {/* 상단 고정 헤더 */}
    &amp;lt;UserAnswerContextProvider&amp;gt;
      {currentView}
    &amp;lt;/UserAnswerContextProvider&amp;gt;
  &amp;lt;/&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;2. Quiz.jsx (퀴즈 진행 컴포넌트)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Quiz 컴포넌트는 퀴즈의 전체 흐름을 관리하는 중심 컴포넌트로, 하나의 문제에 대해 퀴즈 문제 표시 &amp;rarr; 사용자 선택 확인 &amp;rarr; 정답 공개라는&lt;b&gt; 3단계 상태 전환 로직&lt;/b&gt;을 갖는다.&lt;/li&gt;
&lt;li&gt;상태는 useState로 관리하며, 각각 다음과 같이 상태가 전환 되어진다.&lt;/li&gt;
&lt;li&gt;QUIZ: 문제 풀이 단계
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간 만료 시: handleTimeOut &amp;rarr; handleSkipClick 호출 &amp;rarr; CORRECT 상태로 이동&lt;/li&gt;
&lt;li&gt;사용자 선택 시: handleAnswerSelect &amp;rarr; SELECTED 상태로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function handleAnswerSelect(answer) {
  setSelectedAnswer(answer);
  setQuizStage(STAGES.SELECTED);
  addUserAnswer({ ... });
}

function handleSkipClick() {
  setQuizStage(STAGES.CORRECT);
  addUserAnswer({ result: &quot;skipped&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;SELECTED: 사용자가 선택한 답안을 보여주는 단계&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간 만료 시: handleTimeOut &amp;rarr; CORRECT 상태로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;**function handleTimeOut() {
  if (quizStage === STAGES.QUIZ) {
    handleSkipClick();
  } else if (quizStage === STAGES.SELECTED) {
    setQuizStage(STAGES.CORRECT);
  } else {
    handleNextQuestion();
  }
}**
&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;CORRECT: 정답을 보여주는 단계
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간 만료 시: handleTimeOut &amp;rarr; handleNextQuestion 호출 &amp;rarr; 다음 문제로 이동, QUIZ 상태 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function handleNextQuestion() {
  if (currentQuizIndex &amp;gt;= quizzes.length - 1) {
    onQuizEnd(&quot;summary&quot;);
    return;
  }
  setSelectedAnswer(&quot;&quot;);
  setCurrentQuizIndex((prev) =&amp;gt; prev + 1);
  setQuizStage(STAGES.QUIZ);
}
&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;Question 컴포넌트에 key를 부여함으로써 상태가 바뀔 때마다 &lt;b&gt;React가 컴포넌트를 새로 마운트하게 하여 타이머가 정상적으로 초기화&lt;/b&gt;되도록 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;Question
  key={`${currentQuizIndex}-${quizStage}`}
  question={quiz.text}
  quizStage={quizStage}
  onTimeOut={handleTimeOut}
/&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자의 선택한 답&lt;/b&gt;은 &lt;b&gt;컨텍스트&lt;/b&gt;를 통해 관리하여 저장 및 추적.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Skip버튼&lt;/b&gt;을 통해 사용자가 문제를 건너뛰고 바로 &lt;b&gt;정답&lt;/b&gt;을 확인할 수 있도록 기능을 추가 QUIZ 상태에서만 표시되도록 조건부 렌더링&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;{quizStage === STAGES.QUIZ &amp;amp;&amp;amp; (
  &amp;lt;button type=&quot;button&quot; onClick={handleSkipClick}&amp;gt;Skip&amp;lt;/button&amp;gt;
)}
​&lt;/code&gt;&lt;/pre&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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;⏳ 3. ProgressBar.jsx (진행 바 컴포넌트)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ProgressBar는 퀴즈 상태에 따라 자동으로 제한 시간을 계산하고, 남은 시간을 &lt;b&gt;10ms 단위로 실시간 표시&lt;/b&gt;하는 컴포넌트&lt;/li&gt;
&lt;li&gt;퀴즈를 푸는 단계(QUIZ)에서는 10초, 선택 확인이나 정답 공개 단계에서는 1초의 시간이 주어지며, 해당 시간은 상태 변화마다 자동 설정&lt;/li&gt;
&lt;li&gt;첫 번째 useEffect에서는 &lt;b&gt;정해진 시간 후 자동 전환을 위한 setTimeout&lt;/b&gt;을 등록하고, 상태 변경이나 언마운트 시 이를 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const timer = setTimeout(() =&amp;gt; {
    onTimeOut();
  }, time);

  return () =&amp;gt; clearTimeout(timer); // 상태 변경 시 타이머 초기화
}, [time]);
&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;두 번째 useEffect에서는 &lt;b&gt;남은 시간을 10ms 단위로 감소시키는 인터벌&lt;/b&gt;을 설정하고, 퀴즈 상태가 바뀔 때마다 남은 시간을 다시 초기화하고 새 인터벌을 시작&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  setRemainingTimer(time); // 상태 전환 시 남은 시간 초기화

  const interval = setInterval(() =&amp;gt; {
    setRemainingTimer((prevTime) =&amp;gt; prevTime - 10);
  }, 10);

  return () =&amp;gt; {
    clearInterval(interval); // 상태 변경 또는 언마운트 시 인터벌 정리
  };
}, [time]);

&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;lt;progress&amp;gt; 태그로 구현되며, value와 max 속성으로 남은 시간의 진행률 표시&lt;/li&gt;
&lt;li&gt;정답이 공개되는 단계에서는 answered 클래스를 조건부로 추가해 스타일을 바꾸어 &lt;b&gt;시각적으로 상태 전환을 표시&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;progress
  className={quizStage !== STAGES.QUIZ ? &quot;answered&quot; : &quot;&quot;}
  value={remainingTimer}
  max={time}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;4. Answers.jsx (선택지 목록 컴포넌트)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;퀴즈에서 &lt;b&gt;사용자가 선택한 답변&lt;/b&gt;과 &lt;b&gt;정답&lt;/b&gt;을 시각적으로 구분하여 보여주는 컴포넌트.&lt;/li&gt;
&lt;li&gt;현재 문제의 &lt;b&gt;답변 리스트를 렌더링&lt;/b&gt;하고, 선택 이벤트 처리&lt;/li&gt;
&lt;li&gt;quizStage에 따라 답변 버튼의 클래스를 동적으로 설정.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;STAGES.SELECTED: 사용자가 답을 선택한 상태에서 해당 답에 &quot;selected&quot; 클래스 추가.&lt;/li&gt;
&lt;li&gt;STAGES.CORRECT: 정답을 표시하는 단계에서 정답에는 &quot;correct&quot;(초록), 틀린 답에는 &quot;wrong&quot; (빨강) 클래스 추가.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;function getClassName(answer) {
  if (quizStage === STAGES.SELECTED) {
    return answer === selectedAnswer ? &quot;selected&quot; : &quot;&quot;;
  }

  if (quizStage === STAGES.CORRECT) {
    if (answer === correctAnswer) return &quot;correct&quot;;
    if (answer === selectedAnswer &amp;amp;&amp;amp; selectedAnswer !== correctAnswer)
      return &quot;wrong&quot;;
  }

  return &quot;&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;quizStage가 &quot;QUIZ&quot;일 때만 선택이 가능하도록 disabled 설정.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;&amp;lt;button
  className={getClassName(answer)} // 동적으로 스타일 클래스 지정
  disabled={quizStage !== STAGES.QUIZ} // 퀴즈 푸는 중에만 클릭 가능
  type=&quot;button&quot;
  onClick={() =&amp;gt; handleAnswerSelect(answer)} // 답안 선택 핸들러
&amp;gt;
  {answer}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  5. Summary.jsx (요약 결과 화면 컴포넌트)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;퀴즈가 끝나면 정답/오답/스킵 비율을 시각적으로 요약&lt;/li&gt;
&lt;li&gt;&lt;b&gt;userAnswer&lt;/b&gt;: 사용자 답안 배열을 UserAnswerContext에서 가져와서 퀴즈 결과를 처리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제 통계 계산&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;총 문제 수, 정답 수, 오답 수, 건너뛴 문제 수 계산.&lt;/li&gt;
&lt;li&gt;각 항목의 비율을 백분율로 계산하여 표시.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const correctAnswers = userAnswer.filter((answer) =&amp;gt; answer.result === &quot;correct&quot;).length;
const wrongAnswers = userAnswer.filter((answer) =&amp;gt; answer.result === &quot;wrong&quot;).length;
const skippedAnswers = userAnswer.filter((answer) =&amp;gt; answer.result === &quot;skipped&quot;).length;

const getPercentage = (count) =&amp;gt; Math.round((count / totalQuestions) * 100);

&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자의 응답 결과를 하나씩 나열하며, &lt;b&gt;정답 여부와 선택한 항목을 강조 표시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;skipped된 문제는 &quot;SKIPPED&quot;로 표시, 선택된 답은 정답/오답에 따라 스타일링.&lt;/li&gt;
&lt;li&gt;context.answers와 questions 데이터를 매핑하여 렌더링&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;ol&amp;gt;
  {userAnswer.map((answer, index) =&amp;gt; {
    const { id, question, selectedAnswer, result } = answer;
    return (
      &amp;lt;li key={id}&amp;gt;
        &amp;lt;h3&amp;gt;{index + 1}&amp;lt;/h3&amp;gt;
        &amp;lt;div className=&quot;question&quot;&amp;gt;{question}&amp;lt;/div&amp;gt;
        {result === &quot;skipped&quot; ? (
          &amp;lt;p className=&quot;user-answer wrong&quot;&amp;gt;SKIPPED&amp;lt;/p&amp;gt;
        ) : (
          &amp;lt;div className=&quot;answer&quot;&amp;gt;
            &amp;lt;p className={`user-answer ${result}`}&amp;gt;{selectedAnswer}&amp;lt;/p&amp;gt;
          &amp;lt;/div&amp;gt;
        )}
      &amp;lt;/li&amp;gt;
    );
  })}
&amp;lt;/ol&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;6. user-answer-context.jsx (Context API + 상태관리)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 답안을 &lt;b&gt;전역 상태&lt;/b&gt;로 관리하고, 이를 &lt;b&gt;다른 컴포넌트&lt;/b&gt;에서 공유할 수 있도록 하는 &lt;b&gt;Context&lt;/b&gt;.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Context&lt;/b&gt;: UserAnswerContext 생성.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Reducer&lt;/b&gt;: useReducer 훅을 이용하여 상태 관리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Provider&lt;/b&gt;: UserAnswerContextProvider 컴포넌트로 자식 컴포넌트에 상태 및 함수를 제공.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 사용자 답안을 Context로 관리했을까?&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;Prop Drilling 없이&lt;/b&gt; 상태를 컴포넌트 내부에서 관리했지만,&lt;/li&gt;
&lt;li&gt;Answer에서 업데이트한 사용자 답안을 Summary에서 사용해야 했음.&lt;/li&gt;
&lt;li&gt;App에서 Quiz와 Summary를 조건부로 렌더링하고,&lt;/li&gt;
&lt;li&gt;Answer &amp;rarr; Quiz &amp;rarr; App &amp;rarr; Summary로 &lt;b&gt;불필요한 props 전달&lt;/b&gt;이 이뤄질 수밖에 없는 구조였음.&lt;/li&gt;
&lt;li&gt;이런 불필요한 **prop 전달(Prop Drilling)**을 피하고, 상태를 &lt;b&gt;일관되게 접근하고 관리&lt;/b&gt;하기 위해 Context API를 도입함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Context 생성&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;사용자 답안을 저장하고 업데이트할 수 있는 Context를 생성.&lt;/li&gt;
&lt;li&gt;userAnswer는 사용자 답안을 배열로 저장하며, 기본값은 빈 배열.&lt;/li&gt;
&lt;li&gt;addUserAnswer는 답안을 추가하는 함수로 초기에는 빈 함수로 설정.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;export const UserAnswerContext = createContext({
  userAnswer: [],
  addUserAnswer: () =&amp;gt; {},
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.&amp;nbsp;Reducer 정의:&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;userAnswerReducer는 사용자 답안을 관리하는 리듀서.&lt;/li&gt;
&lt;li&gt;ADD_USER_ANSWER 액션이 발생하면 payload로 전달된 답안을 기존 상태에 추가하여 새로운 상태를 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;function userAnswerReducer(state, action) {
  switch (action.type) {
    case &quot;ADD_USER_ANSWER&quot;:
      return [...state, action.payload];
    default:
      return state;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3.&amp;nbsp;Provider 정의&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;UserAnswerContextProvider는 이 Context를 제공하는 컴포넌트로, useReducer를 사용하여 상태(userAnswerState)와 상태 업데이트 함수(userAnswerDispatch)를 관리합니다. 이 컴포넌트는 &lt;b&gt;자식 컴포넌트&lt;/b&gt;들에게 userAnswer와 addUserAnswer를 제공하며, 이를 통해 다른 컴포넌트에서 사용자 답안 상태를 조회하고 업데이트할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  주요 단계:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;useReducer&lt;/b&gt;: userAnswerState와 userAnswerDispatch를 관리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;addAnswer 함수&lt;/b&gt;: ADD_USER_ANSWER 액션을 디스패치하여 사용자가 선택한 답안을 상태에 추가.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UserAnswerContext.Provider&lt;/b&gt;: 상태와 함수를 자식 컴포넌트들에 전달.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function UserAnswerContextProvider({ children }) {
  // 상태 관리: useReducer로 상태(userAnswerState) 및 상태 업데이트(userAnswerDispatch) 처리
  const [userAnswerState, userAnswerDispatch] = useReducer(userAnswerReducer, []);

  // 사용자 답안을 추가하는 함수
  function addAnswer(answer) {
    userAnswerDispatch({ type: &quot;ADD_USER_ANSWER&quot;, payload: answer });
  }

  // Context 값 제공
  const ctxValue = {
    userAnswer: userAnswerState,  // 현재 상태 값
    addUserAnswer: addAnswer,     // 답안 추가 함수
  };

  // 자식 컴포넌트들에 Context 값을 제공
  return &amp;lt;UserAnswerContext.Provider value={ctxValue}&amp;gt;{children}&amp;lt;/UserAnswerContext.Provider&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Context 사용 흐름 요약&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 답안을 선택하면 &lt;b&gt;addAnswer 함수&lt;/b&gt;로 답안을 디스패치 (userAnswerDispatch).&lt;/li&gt;
&lt;li&gt;userAnswerReducer는 액션을 처리하고 새로운 배열을 반환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;React&lt;/b&gt;가 상태 변경을 감지하고 컴포넌트를 &lt;b&gt;리렌더링&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;UserAnswerContext.Provider를 통해 &lt;b&gt;최신 상태&lt;/b&gt;를 &lt;b&gt;자식 컴포넌트&lt;/b&gt;들에게 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; &amp;nbsp;결과 예시&lt;/b&gt;&lt;/h2&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;823&quot; data-origin-height=&quot;615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L7uJV/btsNocyElmY/rQ7GIYpBFlNvOvASckzMq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L7uJV/btsNocyElmY/rQ7GIYpBFlNvOvASckzMq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L7uJV/btsNocyElmY/rQ7GIYpBFlNvOvASckzMq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL7uJV%2FbtsNocyElmY%2FrQ7GIYpBFlNvOvASckzMq0%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;823&quot; height=&quot;615&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;823&quot; data-origin-height=&quot;615&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imagde.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLctFj/btsNnWWl8ff/lGTgw2tGudGSm2fnLKm8dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLctFj/btsNnWWl8ff/lGTgw2tGudGSm2fnLKm8dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLctFj/btsNnWWl8ff/lGTgw2tGudGSm2fnLKm8dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLctFj%2FbtsNnWWl8ff%2FlGTgw2tGudGSm2fnLKm8dk%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;760&quot; height=&quot;521&quot; data-filename=&quot;imagde.png&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;521&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imag3e.png&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DODf8/btsNoNSrPXm/ou7j40QNzuJDQFgNyhIEc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DODf8/btsNoNSrPXm/ou7j40QNzuJDQFgNyhIEc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DODf8/btsNoNSrPXm/ou7j40QNzuJDQFgNyhIEc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDODf8%2FbtsNoNSrPXm%2Fou7j40QNzuJDQFgNyhIEc0%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;741&quot; height=&quot;533&quot; data-filename=&quot;imag3e.png&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;533&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;imag4e.png&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4YIp6/btsNoM63guK/aybgxkomskRtDb9WRsXAPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4YIp6/btsNoM63guK/aybgxkomskRtDb9WRsXAPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4YIp6/btsNoM63guK/aybgxkomskRtDb9WRsXAPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4YIp6%2FbtsNoM63guK%2FaybgxkomskRtDb9WRsXAPK%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;694&quot; height=&quot;494&quot; data-filename=&quot;imag4e.png&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;494&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;i5mage.png&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;889&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKeW6p/btsNoOX7QVX/xC9U2SKQGOQSoJ6A1kGc60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKeW6p/btsNoOX7QVX/xC9U2SKQGOQSoJ6A1kGc60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKeW6p/btsNoOX7QVX/xC9U2SKQGOQSoJ6A1kGc60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKeW6p%2FbtsNoOX7QVX%2FxC9U2SKQGOQSoJ6A1kGc60%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;637&quot; height=&quot;889&quot; data-filename=&quot;i5mage.png&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;889&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ &lt;b&gt;트러블슈팅&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 1. 퀴즈가 두 개씩 넘어가는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  문제 원인:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const timer = setTimeout(() =&amp;gt; {
  onTimeExpired();
}, TIMER);
&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;setTimeout이 컴포넌트 렌더링 때마다 &lt;b&gt;중복 실행&lt;/b&gt;되어, onTimeExpired()가 &lt;b&gt;두 번 이상 호출&lt;/b&gt;됨.&lt;/li&gt;
&lt;li&gt;특히 Question 컴포넌트가 상태 변화로 인해 리렌더링될 경우 타이머가 새로 생성되며 중복 타이머 발생.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  해결 방법:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useEffect를 사용해 &lt;b&gt;타이머 생성을 제어&lt;/b&gt;하고, &lt;b&gt;클린업 함수&lt;/b&gt;로 이전 타이머 제거.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const timer = setTimeout(() =&amp;gt; {
    onTimeExpired();
  }, TIMER);

  return () =&amp;gt; clearTimeout(timer);
}, [onTimeExpired]);
&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;이렇게 하면 Question이 리렌더링될 때 &lt;b&gt;기존 타이머 제거 + 새 타이머 생성&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. ProgressBar가 리셋되지 않는 문제&lt;/h3&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;quizStage가 &quot;showSelectedAnswer&quot; &amp;rarr; &quot;showCorrectAnswer&quot;로 바뀌어도&lt;/li&gt;
&lt;li&gt;ProgressBar의 time 값은 변경되지만 &lt;b&gt;컴포넌트 자체는 리렌더링되지 않음&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;React는 같은 컴포넌트에 prop만 바뀐다고 해서 완전히 리셋하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  해결 방법:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key props를 사용해 Question 컴포넌트를 강제로 &lt;b&gt;재마운트&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;key가 바뀌면 React는 해당 컴포넌트를 완전히 제거 후 다시 생성함.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;Question
  key={`${currentQuizIndex}-${quizStage}`} //   이게 핵심!
  question={quiz.text}
  time={time}
  isAnswered={quizStage !== &quot;quiz&quot;}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 이렇게 하면 quizStage가 바뀔 때마다 ProgressBar도 &lt;b&gt;초기화되어 새로 시작&lt;/b&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;❌ 3. 초기에는 질문만 셔플했지만 답안마다 셔플하려다 App 렌더링 시 셔플로 변경&lt;/h3&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;b&gt;렌더링 시마다 답 순서가 바뀌어 혼란&lt;/b&gt; 발생.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  해결 방법:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앱이 &lt;b&gt;최초 렌더링될 때&lt;/b&gt; 질문 + 답안을 모두 셔플해서 &lt;b&gt;일관된 순서 유지&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;lodash.shuffle을 사용하여 초기화 단계에서만 섞이도록 수정.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;배운 점 &amp;amp; 느낀 점&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useReducer와 Context API를 함께 활용해 &lt;b&gt;복잡한 전역 상태를 구조적으로 관리&lt;/b&gt;하는 법을 익혔다.&lt;/li&gt;
&lt;li&gt;상태 흐름을 설계하면서 &lt;b&gt;전역 상태와 지역 상태의 경계&lt;/b&gt;에 대해 고민해볼 수 있었다.&lt;/li&gt;
&lt;li&gt;타이머 기능을 구현하며 useEffect와 setInterval의 &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;b&gt;시도 횟수 제한&lt;/b&gt;, &lt;b&gt;정답 확인&lt;/b&gt; 등 다양한 로직을 React로 유연하게 구현할 수 있었다.&lt;/li&gt;
&lt;li&gt;전역 상태를 기반으로 Answer, Quiz, Summary 컴포넌트 간의 데이터를 주고받으며 &lt;b&gt;prop drilling 없이 컴포넌트 간 소통&lt;/b&gt;하는 구조를 설계해볼 수 있었다.&lt;/li&gt;
&lt;li&gt;작은 기능이라도 &lt;b&gt;커스텀 훅으로 분리&lt;/b&gt;해서 재사용성과 가독성을 높이는 습관을 들이게 되었다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/169</guid>
      <comments>https://dksek3050.tistory.com/169#entry169comment</comments>
      <pubDate>Wed, 16 Apr 2025 22:43:45 +0900</pubDate>
    </item>
    <item>
      <title>[React] Side Effects / useEffect 훅 / useCallback 훅</title>
      <link>https://dksek3050.tistory.com/168</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PQSI5/btsNih0tyZ3/6ynb3II7HkVrflJDIAVoo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PQSI5/btsNih0tyZ3/6ynb3II7HkVrflJDIAVoo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PQSI5/btsNih0tyZ3/6ynb3II7HkVrflJDIAVoo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPQSI5%2FbtsNih0tyZ3%2F6ynb3II7HkVrflJDIAVoo1%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/An0401na/React_Study/tree/main/11-Side-Effects-useEffect&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/An0401na/React_Study/tree/main/11-Side-Effects-useEffec&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1744345651009&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;React_Study/11-Side-Effects-useEffect at main &amp;middot; An0401na/React_Study&quot; data-og-description=&quot;Contribute to An0401na/React_Study development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/An0401na/React_Study/tree/main/11-Side-Effects-useEffect&quot; data-og-url=&quot;https://github.com/An0401na/React_Study/tree/main/11-Side-Effects-useEffect&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zRka8/hyYB9YEKNl/WIuQaCFiC5mb09WCR1oFU1/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_140_1061_233,https://scrap.kakaocdn.net/dn/3fbkV/hyYBf6ewxp/1tlWPvpn4DqvOHXaB41lk1/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_140_1061_233&quot;&gt;&lt;a href=&quot;https://github.com/An0401na/React_Study/tree/main/11-Side-Effects-useEffect&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/An0401na/React_Study/tree/main/11-Side-Effects-useEffect&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zRka8/hyYB9YEKNl/WIuQaCFiC5mb09WCR1oFU1/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_140_1061_233,https://scrap.kakaocdn.net/dn/3fbkV/hyYBf6ewxp/1tlWPvpn4DqvOHXaB41lk1/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_140_1061_233');&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_Study/11-Side-Effects-useEffect at main &amp;middot; An0401na/React_Study&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to An0401na/React_Study development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;  Side Effect(부수 효과)란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Side Effect(부수 효과)&lt;/b&gt;란 컴포넌트가 렌더링되는 과정과는 직접적인 관련이 없지만, 애플리케이션이 정상적으로 작동하기 위해 반드시 수행되어야 하는 작업을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 사용자의 위치 정보를 가져오는 작업은 &lt;b&gt;화면(UI)을 그리는 데 직접 영향을 주지는 않지만&lt;/b&gt;, 앱 기능에는 꼭 필요한 동작이죠. 이런 작업들이 바로 &lt;b&gt;부수 효과&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function App() {
  // 브라우저로부터 사용자 위치 정보를 가져오는 코드 (부수 효과)
  navigator.geolocation.getCurrentPosition((position) =&amp;gt; {
    const sortedPlaces = sortPlacesByDistance(
      AVAILABLE_PLACES,
      position.coords.latitude,
      position.coords.longitude
    );
  });

  return (
    &amp;lt;&amp;gt;&amp;lt;Modal ref={modal}&amp;gt;{/* ... */}&amp;lt;/Modal&amp;gt;
      &amp;lt;header&amp;gt;{/* ... */}&amp;lt;/header&amp;gt;
      &amp;lt;main&amp;gt;{/* ... */}&amp;lt;/main&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드처럼 위치 정보를 가져오는 작업은 화면에 바로 영향을 주지 않기 때문에 &lt;b&gt;Side Effect&lt;/b&gt;라고 할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Side Effect로 인해 발생할 수 있는 문제: 무한 루프&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부수 효과는 꼭 필요하지만, 잘못 사용하면 &lt;b&gt;무한 루프&lt;/b&gt; 같은 문제가 생길 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보면, 컴포넌트가 렌더링될 때마다 getCurrentPosition이 호출되고, 그 결과로 상태가 업데이트되면 다시 렌더링이 발생하게 됩니다. 이게 반복되면서 &lt;b&gt;렌더링 &amp;rarr; 위치 정보 요청 &amp;rarr; 상태 업데이트 &amp;rarr; 또 렌더링&lt;/b&gt;이라는 무한 루프가 생길 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;function App() {
  const [avaliablePlaces, setAvaliablePlaces] = useState();

  // 컴포넌트가 렌더링될 때마다 위치 정보 요청 &amp;rarr; 상태 변경 &amp;rarr; 다시 렌더링
  navigator.geolocation.getCurrentPosition((position) =&amp;gt; {
    const sortedPlaces = sortPlacesByDistance(
      AVAILABLE_PLACES,
      position.coords.latitude,
      position.coords.longitude
    );
    setAvaliablePlaces(sortedPlaces);
  });

  return &amp;lt;&amp;gt;{/* ... */}&amp;lt;/&amp;gt;;
}

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황은 사용자 경험에도 안 좋고, 성능에도 큰 영향을 미칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 문제를 해결하기 위해 사용하는 것이 바로 useEffect입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  useEffect로 Side Effect를 안전하게 처리하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  useEffect란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect는 &lt;b&gt;리액트가 화면을 그린 뒤(DOM 렌더링 이후)&lt;/b&gt; 특정 작업(부수 효과)을 실행할 수 있도록 도와주는 Hook입니다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  // 실행할 부수 효과 코드
}, [의존성 배열]);
&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;li&gt;두 번째 인자: &lt;b&gt;의존성 배열&lt;/b&gt; &amp;mdash; 이 안의 값이 변경될 때만 함수가 다시 실행됨&lt;/li&gt;
&lt;li&gt;&amp;rarr; &lt;b&gt;빈 배열 []을 넣으면 컴포넌트가 최초 렌더링 될 때 한 번만 실행됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예제1 : useEffect로 무한 루프 방지&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function App() {
  const [avaliablePlaces, setAvaliablePlaces] = useState([]);

  // 컴포넌트가 처음 렌더링될 때만 위치 정보를 요청함
  useEffect(() =&amp;gt; {
    navigator.geolocation.getCurrentPosition((position) =&amp;gt; {
      const sortedPlaces = sortPlacesByDistance(
        AVAILABLE_PLACES,
        position.coords.latitude,
        position.coords.longitude
      );
      setAvaliablePlaces(sortedPlaces);
    });
  }, []); // 빈 배열: 의존성 없음 &amp;rarr; 최초 렌더링 때만 실행

  return &amp;lt;&amp;gt;{/* ... */}&amp;lt;/&amp;gt;;
}

export default App;

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 useEffect를 사용하면 위치 정보 요청이 &lt;b&gt;최초 한 번만 실행&lt;/b&gt;되기 때문에 &lt;b&gt;불필요한 재렌더링&lt;/b&gt;이나 &lt;b&gt;무한 루프&lt;/b&gt; 문제를 깔끔하게 방지할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예제2 : useEffect로 브라우저 API 싱크에 활용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect를 활용하면 dialog의 showModal 메소드나 close 메소드와 같은 DOM API와 속성 값 또는 상태 값이 동기화 될 수 있도록 도움을 받을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Modal({ open, children }) {
  const dialog = useRef();

  if (open) {
    dialog.current.showModal();
  } else {
    dialog.current.close();
  }

  return createPortal(
    &amp;lt;dialog className=&quot;modal&quot; ref={dialog} open={open}&amp;gt;
      {children}
    &amp;lt;/dialog&amp;gt;,
    document.getElementById(&quot;modal&quot;),
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 open이라는 상태가 true 일경우 dialog의 참조를 활용해 showModal 메소드를 호출하고 있습니다. 그러나 초기 상태에서는 참조와 jsx 코드의 dialog 연결되지 않은 상태에서 dialog.current.close()를 호출하게 되어 참조가 undefined 상태이기 때문에 호출에 실패하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 useEffect를 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useEffect, useRef } from &quot;react&quot;;

function Modal({ open, children }) {
  const dialog = useRef();

  // 컴포넌트 함수가 실행된 후에 부수효과 함수를 실행하여 dailog 참조가 undefined를 방지
  useEffect(() =&amp;gt; {
    if (open) {
      dialog.current.showModal();
    } else {
      dialog.current.close();
    }
  }, [open]); // open이 변경될 때마다 실행

  return createPortal(
    &amp;lt;dialog className=&quot;modal&quot; ref={dialog}&amp;gt;
      {children}
    &amp;lt;/dialog&amp;gt;,
    document.getElementById(&quot;modal&quot;),
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect로 정의한 effect 함수는 컴포넌트 함수에 바로 이어 실행되어 집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect에 등록된 함수는 컴포넌트 함수보다 먼저, 혹은 같이 실행 되는 것이 아니라 컴포넌트가 렌더링된 후, 즉 화면이 실제로 그려진 뒤에 실행됩니다. 때문에 참조와 dialog간의 연결은 effect 함수가 실행될 시점에서 이미 만들어져있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 의존성 배열에 open 참조를 추가하여 open 값이 변경 될때마다 effect 함수를 실행시켜 모달의 열림여부를 변경할 수 있게 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예제3 : useEffect의 Cleanup 함수로 타이머 제어하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect는 부수 효과를 실행할 수 있을 뿐 아니라, &lt;b&gt;클린업(Cleanup) 함수&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;b&gt;컴포넌트가 화면에서 사라질 때(언마운트) 실행&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다시 렌더링되기 전에&lt;/b&gt; &lt;b&gt;실행&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 개념을 타이머(setTimeout)와 함께 사용하는 예제를 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function DeleteConfirmation({ onConfirm, onCancel }) {
  console.log(&quot;Timer started&quot;);
  setTimeout(() =&amp;gt; {
    onConfirm();
  }, 5000);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={onCancel} className=&quot;button-text&quot;&amp;gt;
        No
      &amp;lt;/button&amp;gt;
      &amp;lt;button onClick={onConfirm} className=&quot;button&quot;&amp;gt;
        Yes
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 컴포넌트는 삭제 확인 모달에서 사용되는 컴포넌트로, 생성된 뒤 &lt;b&gt;5초가 지나면 자동으로 onConfirm()을 호출&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문제는 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 No 버튼을 눌러 모달을 닫으면 이 컴포넌트는 사라지지만,&lt;/li&gt;
&lt;li&gt;타이머는 계속 동작하기 때문에 &lt;b&gt;5초 후 onConfirm()이 여전히 실행&lt;/b&gt;됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 사용자가 취소했음에도 불구하고 삭제가 실행되는 &lt;b&gt;원하지 않는 동작&lt;/b&gt;이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하려면 컴포넌트가 사라질 때 &lt;b&gt;타이머도 함께 정리해줘야&lt;/b&gt; 합니다. 이럴 때 useEffect의 &lt;b&gt;클린업 함수&lt;/b&gt;가 유용하게 쓰입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function DeleteConfirmation({ onConfirm, onCancel }) {
  useEffect(() =&amp;gt; {
    console.log(&quot;Timer started&quot;);

    const timer = setTimeout(() =&amp;gt; {
      onConfirm();
    }, 5000);

    // 클린업 함수: 컴포넌트가 사라지기 전에 실행되어 타이머 정리
    return () =&amp;gt; {
      console.log(&quot;Timer cleared&quot;);
      clearTimeout(timer);
    };
  }, []); 

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;button onClick={onCancel} className=&quot;button-text&quot;&amp;gt;
        No
      &amp;lt;/button&amp;gt;
      &amp;lt;button onClick={onConfirm} className=&quot;button&quot;&amp;gt;
        Yes
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect 안에서 setTimeout을 설정하고, &lt;b&gt;클린업 함수&lt;/b&gt;를 이용해 clearTimeout()으로 타이머를 정리하여 문제를 방지하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;클린업 함수는 부수 효과로 인한 리소스 낭비나 잘못된 동작을 방지하는 데 꼭 필요&lt;/b&gt;합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&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;앞 예제에서 useEffect 내부에서 onConfirm 함수를 사용하고 있었죠. 이럴 경우 onConfirm이 변경되면 effect가 다시 실행되기 때문에, 의존성 배열에 넣어주는 것이 일반적인 패턴입니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function DeleteConfirmation({ onConfirm, onCancel }) {
  useEffect(() =&amp;gt; {
    console.log(&quot;Timer started&quot;);

    const timer = setTimeout(() =&amp;gt; {
      onConfirm();
    }, 5000);

    // 클린업 함수: 컴포넌트가 사라지기 전에 실행되어 타이머 정리
    return () =&amp;gt; {
      console.log(&quot;Timer cleared&quot;);
      clearTimeout(timer);
    };
  }, [ onConfirm ]); 

  return ();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서처럼 onConfirm 함수 안에서 상태를 변경하게 되면, 이 함수는 컴포넌트가 리렌더링될 때마다 &lt;b&gt;새로운 함수로 다시 생성&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과, 리액트는 다음처럼 판단합니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;의존성 배열에 있는 값(onConfirm)이 바뀌었네? &amp;rarr; 그럼 effect 함수를 다시 실행해야지!&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 effect 함수 안에서 상태를 변경하게 되면, 그 상태 변경으로 인해 &lt;b&gt;컴포넌트가 다시 리렌더링&lt;/b&gt;됩니다. 그러면 onConfirm 함수도 다시 생성되고, useEffect는 또다시 그 변화를 감지하고 다시 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름을 정리하면 다음과 같습니다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. effect 실행 &amp;rarr; 상태 변경
2. 상태 변경 &amp;rarr; 리렌더링
3. 리렌더링 &amp;rarr; onConfirm 새로 생성
4. onConfirm 변경 &amp;rarr; effect 다시 실행
5. effect 실행 &amp;rarr; 상태 변경 &amp;rarr; ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하기 위해 우리는 useCallback 훅을 사용할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 왜 이런 문제가 생기는 걸까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트에서는 컴포넌트가 리렌더링될 때마다 컴포넌트 내부에 선언된 &lt;b&gt;함수들도 새로 만들어집니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 함수의 &lt;b&gt;내용이 같더라도 주소값이 다르기 때문에 다른 함수&lt;/b&gt;로 간주됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아래 두 코드의 handleRemovePlace는 매 렌더링마다 새로 만들어져서 주소가 바뀌게 됩니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function App() {
  const handleRemovePlace = () =&amp;gt; {
    // 상태 업데이트
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 이 함수를 useEffect 의존성 배열에 넣었을 때 리렌더링마다 계속해서 effect가 실행되는 부작용이 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;바로 이런 문제를 useCallback이 해결해주는 것!&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  useCallback 란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useCallback은 &lt;b&gt;함수의 재생성을 방지&lt;/b&gt;해주는 React 훅입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트가 리렌더링되더라도, 훅 내부에 지정된 함수를 메모리로서 내부에 저장하기 때문에 의존성 배열에 있는 값들이 바뀌지 않으면 &lt;b&gt;같은 함수(주소)를 재사용&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const memoizedFn = useCallback(() =&amp;gt; {
  // 여기에 재생성하지 않고 유지하고 싶은 함수 로직
}, [의존성]);
&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;li&gt;두 번째 인자: &lt;b&gt;의존성 배열&lt;/b&gt; &amp;mdash; 이 안의 값이 변경될 때만 함수가 다시 생성됨&lt;/li&gt;
&lt;li&gt;&amp;rarr; &lt;b&gt;빈 배열 []을 넣으면 컴포넌트가 최초 렌더링 될 때 한 번만 생성됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 예제: useCallback으로 무한 루프 방지하기&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// App.jsx
import { useEffect, useRef, useState, useCallback } from &quot;react&quot;;

function App() {
  const [modalIsOpen, setModalIsOpen] = useState(false);
  const selectedPlace = useRef(null);

  // useCallback으로 감싸서 함수가 리렌더링될 때마다 재생성되지 않도록 함
  const handleRemovePlace = useCallback(() =&amp;gt; {
    console.log(&quot;Removing place...&quot; + selectedPlace.current);
    setModalIsOpen(false);
  }, []);

  const handleStopRemovePlace = () =&amp;gt; {
    setModalIsOpen(false);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Modal open={modalIsOpen} onClose={handleStopRemovePlace}&amp;gt;
        &amp;lt;DeleteConfirmation
          onCancel={handleStopRemovePlace}
          onConfirm={handleRemovePlace}
        /&amp;gt;
      &amp;lt;/Modal&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 handleRemovePlace는 useCallback으로 감싸져 있기 때문에, App 컴포넌트가 다시 렌더링되더라도 &lt;b&gt;주소가 바뀌지 않습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 따라서 DeleteConfirmation 컴포넌트의 useEffect 안에서 onConfirm이 바뀌었다고 감지되지 않으므로 &lt;b&gt;불필요한 재실행이나 무한 루프를 방지할 수 있게 되는 것입니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&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;  모든 부수 효과에 useEffect를 써야 할까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 꼭 그렇진 않습니다!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모든 Side Effect에 useEffect를 사용할 필요는 없습니다.&lt;/b&gt; 오히려 &lt;b&gt;불필요하게 useEffect를 사용하면 코드가 복잡해지고 성능에도 안 좋을 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 불필요한 useEffect 예시 1.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아래처럼 &lt;b&gt;사용자의 클릭 이벤트에 따라 실행되는 로직&lt;/b&gt;은 useEffect 없이도 전혀 문제가 없습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;  function handleSelectPlace(id) {
    setPickedPlaces((prevPickedPlaces) =&amp;gt; {
      if (prevPickedPlaces.some((place) =&amp;gt; place.id === id)) {
        return prevPickedPlaces;
      }
      const place = AVAILABLE_PLACES.find((place) =&amp;gt; place.id === id);
      return [place, ...prevPickedPlaces];
    });

    // 로컬 스토리지에 저장된 id 목록을 가져옴, 없으면 빈 배열로 초기화
    const storedIds = JSON.parse(localStorage.getItem(&quot;selectedPlaces&quot;) || []);
    // 로컬 스토리지에 저장된 id 목록에 선택한 장소 id가 없으면 추가
    if (storedIds.indexOf(id) === -1) {
      // 로컬 스토리지에 선택한 장소 id를 추가
      localStorage.setItem(
        &quot;selectedPlaces&quot;,
        JSON.stringify([id, ...storedIds]),
      );
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 코드는 &lt;b&gt;사용자가 클릭했을 때만 실행&lt;/b&gt;됩니다.&lt;/li&gt;
&lt;li&gt;무한 루프가 발생하지 않기 때문에 useEffect를 쓸 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;또, useEffect는 &lt;b&gt;컴포넌트 최상단에서만 사용할 수 있기 때문에&lt;/b&gt;, if문이나 중첩 함수 내부에서는 사용할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 useEffect는 다음과 같은 상황에서만 사용하는 것이 적절합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트가 마운트된 이후에 실행되어야 하는 코드일 때&lt;/li&gt;
&lt;li&gt;무한 루프를 방지할 필요가 있을 때&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 불필요한 useEffect 예시 2.&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  const storedIds = JSON.parse(localStorage.getItem(&quot;selectedPlaces&quot;)) || [];
  const storedPlaces = storedIds.map((id) =&amp;gt;
    AVAILABLE_PLACES.find((place) =&amp;gt; place.id === id)
  );
  setPickedPlaces(storedPlaces);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드도 사실 useEffect가 필요하지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;localStorage.getItem()은 &lt;b&gt;동기적이고 즉시 완료&lt;/b&gt;되는 작업입니다.&lt;/li&gt;
&lt;li&gt;콜백 함수도 없고, 브라우저에 요청을 보내는 것도 아닙니다.&lt;/li&gt;
&lt;li&gt;따라서 렌더링 이후에 실행될 이유도 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  이런 경우는 아예 상태 초기화 코드로 빼서 처리하는 게 더 적절합니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const storedIds = JSON.parse(localStorage.getItem(&quot;selectedPlaces&quot;)) || [];
const storedPlaces = storedIds.map((id) =&amp;gt;
  AVAILABLE_PLACES.find((place) =&amp;gt; place.id === id)
);

const [pickedPlaces, setPickedPlaces] = useState(storedPlaces);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 불필요한 useEffect도 줄이고, 코드도 더 간결해집니다.&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/168</guid>
      <comments>https://dksek3050.tistory.com/168#entry168comment</comments>
      <pubDate>Fri, 11 Apr 2025 13:25:30 +0900</pubDate>
    </item>
    <item>
      <title>[React] Prop Drilling / Context API / Provider Pattern / userReducer</title>
      <link>https://dksek3050.tistory.com/167</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTIiJy/btsNe6SNpJj/du2l7RU7WRk5skEM9Jxme0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTIiJy/btsNe6SNpJj/du2l7RU7WRk5skEM9Jxme0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTIiJy/btsNe6SNpJj/du2l7RU7WRk5skEM9Jxme0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTIiJy%2FbtsNe6SNpJj%2Fdu2l7RU7WRk5skEM9Jxme0%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/An0401na/React_Study/tree/main/10-Advanced-State-Managemen-with-Context-useReducer&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/An0401na/React_Study/tree/main/10-Advanced-State-Managemen-with-Context-useReducer&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1744190258147&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;React_Study/10-Advanced-State-Managemen-with-Context-useReducer at main &amp;middot; An0401na/React_Study&quot; data-og-description=&quot;Contribute to An0401na/React_Study development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/An0401na/React_Study/tree/main/10-Advanced-State-Managemen-with-Context-useReducer&quot; data-og-url=&quot;https://github.com/An0401na/React_Study/tree/main/10-Advanced-State-Managemen-with-Context-useReducer&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cZ7W9o/hyYEuAja8I/TrfIKkRYxyMjaR6iAMRFGk/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_140_1061_233,https://scrap.kakaocdn.net/dn/C6XZw/hyYCb2SnPJ/ZKyeRmbzAXxF3BNn8sdtWk/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_140_1061_233&quot;&gt;&lt;a href=&quot;https://github.com/An0401na/React_Study/tree/main/10-Advanced-State-Managemen-with-Context-useReducer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/An0401na/React_Study/tree/main/10-Advanced-State-Managemen-with-Context-useReducer&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cZ7W9o/hyYEuAja8I/TrfIKkRYxyMjaR6iAMRFGk/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_140_1061_233,https://scrap.kakaocdn.net/dn/C6XZw/hyYCb2SnPJ/ZKyeRmbzAXxF3BNn8sdtWk/img.png?width=1200&amp;amp;height=600&amp;amp;face=975_140_1061_233');&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_Study/10-Advanced-State-Managemen-with-Context-useReducer at main &amp;middot; An0401na/React_Study&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to An0401na/React_Study development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; Prop Drilling&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 상태나 함수를 하위 컴포넌트로 넘기다 보면 어느새 중간 컴포넌트들이 prop을 '전달만' 하고 있는 걸 발견하게 됩니다. 이게 바로 &lt;b&gt;Prop Drilling&lt;/b&gt; 문제 입니다. 예시를 들어서 설명해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품을 조회하고 장바구니에 담을 수 있는 간단한 쇼핑 앱을 만든다고 가정해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 앱의 최상위 컴포넌트인 App에는 두 가지 주요 영역이 있습니다.&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;Shop&lt;/b&gt;: 여러 개의 상품을 보여주는 영역&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Header&lt;/b&gt;: 장바구니 정보를 보여주는 영역&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Shop 컴포넌트는 각 상품을 나타내는 여러 개의 Product 컴포넌트로 구성되어 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Header 컴포넌트는 장바구니 UI를 담당하는 CartModal, 그리고 장바구니에 담긴 상품 리스트를 보여주는 Cart 컴포넌트로 구성됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면 캡처 2025-04-09 103030.png&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvj0Qc/btsNdYajN22/fv6EDelCCE3wkBGVc5ED30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvj0Qc/btsNdYajN22/fv6EDelCCE3wkBGVc5ED30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvj0Qc/btsNdYajN22/fv6EDelCCE3wkBGVc5ED30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbvj0Qc%2FbtsNdYajN22%2Ffv6EDelCCE3wkBGVc5ED30%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;659&quot; height=&quot;347&quot; data-filename=&quot;화면 캡처 2025-04-09 103030.png&quot; data-origin-width=&quot;513&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품을 장바구니에 담기 위해 필요한 함수인 onUpdateCart는 App 컴포넌트에 정의되어 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 최종적으로 Product에서 사용되지만, App &amp;rarr; Shop &amp;rarr; Product를 거쳐야 전달됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 cart 상태 역시 App &amp;rarr; Header &amp;rarr; CartModal &amp;rarr; Cart 순으로 전파되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 실제로 prop을 사용하지 않는 &lt;b&gt;중간 컴포넌트들까지&lt;/b&gt; prop을 전달해야 하는 구조가 만들어지고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트가 많아질수록 전달 경로는 복잡해지며 가독성과 유지보수성이 떨어지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 바로 &lt;b&gt;Prop Drilling&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; Component Composition(컴포넌트 합성)으로 Prop Drilling 줄이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 Prop Drilling 문제는 컴포넌트 합성(Component Composition)을 통해 어느 정도 완화할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Before&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// App.jsx
&amp;lt;Shop onAddItemToCart={handleAddItemToCart} /&amp;gt;

// Shop.jsx
export default function Shop({ onAddItemToCart }) {
  return (
    &amp;lt;ul&amp;gt;
      {DUMMY_PRODUCTS.map((product) =&amp;gt; (
        &amp;lt;Product {...product} onAddToCart={onAddItemToCart} /&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 onAddItemToCart는 실제로는 Product 컴포넌트에서 사용하는 함수인데, Shop이 그저 전달만 하고 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ After (컴포넌트 합성 활용)&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// App.jsx
&amp;lt;Shop&amp;gt;
  {DUMMY_PRODUCTS.map((product) =&amp;gt; (
    &amp;lt;Product {...product} onAddToCart={handleAddItemToCart} /&amp;gt;
  ))}
&amp;lt;/Shop&amp;gt;

// Shop.jsx
export default function Shop({ children }) {
  return &amp;lt;ul&amp;gt;{children}&amp;lt;/ul&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Shop은 children을 렌더링하는 단순한 래퍼 역할만 하며, Product에서 필요한 prop을 직접 받을 수 있게 되어 불필요한 prop 전달을 줄이며 Prop Drilling 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 위 해결 방법으로 모든 컴포넌트 층에 사용하기는 적합하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 모든 컴포넌트에서 이 방법을 사용해 문제를 해결한다면 결국 모든 컴포넌트가 앱 컴포넌트에 포함될 것이고 나머지 컴포넌트는 감싸는 래퍼 용도로만 사용될 것이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 다시 해결하기 위해서 아래에서 Context 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;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; Context API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context는 React 앱 전체에 전역 상태를 공유할 수 있도록 도와주는 기능입니다. 간단히 말해, 중간 컴포넌트들을 불필요한 prop 전달을 거치지 않고도 필요한 컴포넌트에서 직접 데이터에 접근할 수 있게 해줍니다. 이를 통해 Prop Drilling 문제를 효과적으로 해결할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면 캡처 2025-04-09 133001.png&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;539&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ba2pqQ/btsNfHxWCSj/EL4OOswc3udKNM9x0NsKq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ba2pqQ/btsNfHxWCSj/EL4OOswc3udKNM9x0NsKq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ba2pqQ/btsNfHxWCSj/EL4OOswc3udKNM9x0NsKq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fba2pqQ%2FbtsNfHxWCSj%2FEL4OOswc3udKNM9x0NsKq0%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;735&quot; height=&quot;413&quot; data-filename=&quot;화면 캡처 2025-04-09 133001.png&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;539&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전과 같은 상황에서, 더 이상 onUpdateCart 함수나 Cart 상태를 props를 통해 하나하나 전달할 필요가 없습니다. Shop, Product, Header, CardModal, Cart 등 하위 컴포넌트 모두가 Cart Context에 접근하여 장바구니 상태를 읽거나 업데이트 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context를 사용하기 위해선 보통 3가지 단계로 구성됩니다:&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Context 생성&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { createContext } from 'react';

export const CartContext = createContext({
  items: [],
});
&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;createContext()를 사용해 Context를 생성합니다.&lt;/li&gt;
&lt;li&gt;여기서 지정한 값은 &lt;b&gt;기본값&lt;/b&gt;이며, Provider 없이 Context를 사용할 경우에만 사용됩니다.&lt;/li&gt;
&lt;li&gt;즉, 실질적인 데이터는 다음 단계인 Provider에서 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. State 연결 &amp;amp; Provider로 값 제공&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context를 사용하고자 하는 컴포넌트들을 &lt;b&gt;Provider로 감싸고&lt;/b&gt;, useState로 관리되는 실제 상태 값을 value로 전달합니다. 예를 들어, 장바구니 데이터를 상태로 관리하고자 할 때 다음과 같이 useState를 사용해 상태를 정의할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;const [shoppingCart, setShoppingCart] = useState({
  items: [],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태를 CartContext.Provider의 value로 전달하면, 하위 컴포넌트들은 Context를 통해 이 데이터를 사용할 수 있게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;CartContext.Provider value={shoppingCart}&amp;gt;
  &amp;lt;Header /&amp;gt;
  &amp;lt;Shop&amp;gt;
    &amp;lt;Product {...product} onAddToCart={handleAddItemToCart} /&amp;gt;
  &amp;lt;/Shop&amp;gt;
&amp;lt;/CartContext.Provider&amp;gt;

&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;이렇게 하면 더 이상 shoppingCart를 props로 직접 전달하지 않아도, CartContext에 연결된 모든 컴포넌트에서 장바구니 상태에 접근할 수 있습니다.&lt;/li&gt;
&lt;li&gt;useContext(CartContext)를 사용하는 모든 컴포넌트는 이 value 값에 접근할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;/b&gt; 참고로, createContext() 함수에서 설정하는 초기값은 &lt;b&gt;Provider 없이 Context를 사용할 경우에만&lt;/b&gt; 사용됩니다. 실질적으로 하위 컴포넌트에 전달되는 값은 value={shoppingCart}에서 지정한 값입니다. 또한 초기값을 설정할 경우 자동완성에 도움을 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  React 19 이상에서는 .Provider를 생략할 수 있습니다!&lt;/b&gt; Context 객체를 바로 JSX 태그처럼 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;CartContext value={shoppingCart}&amp;gt;
  &amp;lt;Header /&amp;gt;
  &amp;lt;Shop&amp;gt;
    &amp;lt;Product {...product} onAddToCart={handleAddItemToCart} /&amp;gt;
  &amp;lt;/Shop&amp;gt;
&amp;lt;/CartContext&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. useContext로 값 사용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 컴포넌트에서는 useContext(CartContext)를 사용하여 값에 직접 접근할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useContext } from &quot;react&quot;;
import { CartContext } from &quot;../store/shopping-cart-context.jsx&quot;;

export default function Cart() {
  const cartCtx = useContext(CartContext);

  return (
    &amp;lt;div id=&quot;cart&quot;&amp;gt;
      {cartCtx.items.length === 0 &amp;amp;&amp;amp; &amp;lt;p&amp;gt;No items in cart!&amp;lt;/p&amp;gt;}
      {cartCtx.items.length &amp;gt; 0 &amp;amp;&amp;amp; (/* 생략 */)}
    &amp;lt;/div&amp;gt;
  );
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  React 19에서는 use() 훅도 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 19에서는 새로운 use() 훅을 통해 조건문 안에서도 Context 값을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { use } from 'react';
import { CartContext } from &quot;../store/shopping-cart-context.jsx&quot;;

export default function Cart() {
  if (true) {
    const cartCtx = use(CartContext);
    // 조건 안에서도 훅 사용 가능!
  }

  return (
    &amp;lt;div id=&quot;cart&quot;&amp;gt;
      {/* 렌더링 */}
    &amp;lt;/div&amp;gt;
  );
}

&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;useContext()는 &lt;b&gt;컴포넌트 최상위 레벨에서만&lt;/b&gt; 사용 가능하지만,&lt;/li&gt;
&lt;li&gt;use() 훅은 &lt;b&gt;if문, 반복문 등 어느 곳에서든 자유롭게 사용할 수 있는 장점&lt;/b&gt;이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Context는 단순한 값뿐만 아니라, &lt;b&gt;상태와 함께 동작 함수도 함께 전달&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const ctxValue = {
  items: shoppingCart.items,
  addItemToCart: handleAddItemToCart, //함수 추가
};

return (
  &amp;lt;CartContext.Provider value={ctxValue}&amp;gt;
    {/* 컴포넌트들 */}
  &amp;lt;/CartContext.Provider&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 컴포넌트에서는 이렇게 사용합니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import { useContext } from &quot;react&quot;;
import {CartContext} from &quot;./store/shopping-cart-context.jsx&quot;;

export default function Product({ ...prop }) {
    const { addItemToCart } = useContext(CartContext);
    return (
        &amp;lt;button onClick={() =&amp;gt; addItemToCart(id)}&amp;gt;Add to Cart&amp;lt;/button&amp;gt;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;props 없이도 함수 호출이 가능하니 코드가 훨씬 깔끔해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 컨텍스트 값이 바뀌면 컴포넌트가 재실행 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useContext()로 값을 사용하고 있는 컴포넌트는, &lt;b&gt;Context 값이 변경되면 React에 의해 자동으로 다시 실행됩니다.&lt;/b&gt; Context 값을 사용 중인 모든 컴포넌트가 &lt;b&gt;자동으로 다시 렌더링&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 상태(state)나 props와 마찬가지로, &lt;b&gt;변경된 데이터를 기반으로 최신 UI를 보여주기 위한 동작&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  React에서 Context를 사용하는 더 깔끔한 방법: Context Provider를 별도 컴포넌트로 분리하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Before&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;shopping-cart-context.jsx&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;export const CartContext = createContext({
  items: [],
  addItemToCart: () =&amp;gt; {},
  updateItemQuantity: () =&amp;gt; {},
});

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App.jsx&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;
import { CartContext } from &quot;./store/shopping-cart-context&quot;;
import { DUMMY_PRODUCTS } from &quot;./dummy-products&quot;;
import Header from &quot;./components/Header&quot;;
import Shop from &quot;./components/Shop&quot;;

function App() {
  const [shoppingCart, setShoppingCart] = useState({ items: [] });

  function handleAddItemToCart(id) {
    // 로직 생략
  }

  function handleUpdateCartItemQuantity(productId, amount) {
    // 로직 생략
  }

  const ctxValue = {
    items: shoppingCart.items,
    addItemToCart: handleAddItemToCart,
    updateItemQuantity: handleUpdateCartItemQuantity,
  };

  return (
    &amp;lt;CartContext.Provider value={ctxValue}&amp;gt;
      &amp;lt;Header /&amp;gt;
      &amp;lt;Shop products={DUMMY_PRODUCTS} /&amp;gt;
    &amp;lt;/CartContext.Provider&amp;gt;
  );
}

export default App;

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 CartContext를 따로 파일을 분리해두었지만, 정작 &lt;b&gt;아이템을 추가하거나 수량을 변경하는 상태 관리 코드는 모두 App.jsx 내부에 존재&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 간단한 앱에서는 문제가 없지만, &lt;b&gt;여러 개의 Context가 필요한 복잡한 앱&lt;/b&gt;에서는 문제가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Cart 외에 User, Theme, Auth 등 다른 Context가 추가되면, 각각의 상태 관리 함수들이 모두 App.jsx에 쌓이게 되죠. 결과적으로 App 컴포넌트는 &lt;b&gt;상태 관리 코드의 집합소&lt;/b&gt;가 되고, 이는 &lt;b&gt;유지보수에 취약&lt;/b&gt;하며, &lt;b&gt;컴포넌트 역할 분리 원칙&lt;/b&gt;에도 어긋나게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ After: CartContextProvider로 상태 관리까지 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CartContext만이 아니라, &lt;b&gt;상태와 그에 관련된 모든 로직&lt;/b&gt;을 별도의 Provider 컴포넌트로 추출할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 CartContextProvider를 만들고, 그 안에서 상태 관리와 Context 제공을 함께 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;shopping-cart-context.jsx&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { createContext, useState } from &quot;react&quot;;
import { DUMMY_PRODUCTS } from &quot;../dummy-products.js&quot;;

export const CartContext = createContext({
  items: [],
  addItemToCart: () =&amp;gt; {},
  updateItemQuantity: () =&amp;gt; {},
});

export default function CartContextProvider({ children }) {
  const [shoppingCart, setShoppingCart] = useState({ items: [] });

  function handleAddItemToCart(id) {
    // 로직 생략
  }

  function handleUpdateCartItemQuantity(productId, amount) {
    // 로직 생략
  }

  const ctxValue = {
    items: shoppingCart.items,
    addItemToCart: handleAddItemToCart,
    updateItemQuantity: handleUpdateCartItemQuantity,
  };

  return (
    &amp;lt;CartContext.Provider value={ctxValue}&amp;gt;
      {children}
    &amp;lt;/CartContext.Provider&amp;gt;
  );
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;App.jsx&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import CartContextProvider from &quot;./store/shopping-cart-context&quot;;

function App() {
  return (
    &amp;lt;CartContextProvider&amp;gt;
      &amp;lt;Header /&amp;gt;
      &amp;lt;Shop /&amp;gt;
    &amp;lt;/CartContextProvider&amp;gt;
  );
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 children prop을 활용하여 CartContextProvider가 &lt;b&gt;Context를 사용해야 하는 모든 컴포넌트를 감싸도록&lt;/b&gt; 만든 점입니다. 상태 관리와 핸들러 로직은 모두 CartContextProvider 안으로 옮겨졌고, App 컴포넌트는 &lt;b&gt;필요한 컴포넌트만 렌더링하는 역할만&lt;/b&gt; 하게 됩니다. App 컴포넌트는 &lt;b&gt;View 계층만 담당하는 단순한 구조&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  useReducer 란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useReducer는 상태(state)와 그 상태를 어떻게 바꿀지 정해주는 &lt;b&gt;로직(reducer 함수)을 관리하는 React의 내장&lt;/b&gt; 훅입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  useState와의 차이점은?&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 100px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;useState&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;useReducer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;사용 시기&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;단순한 상태 관리&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;상태가 복잡하거나, 여러 조건에 따라 바뀔 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;상태 타입&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;보통 하나의 값 (예: 숫자, 문자열)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;주로 객체로 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;상태 변경 방식&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;직접 setState 호출&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;dispatch로 액션 전달 후 reducer에서 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;예시&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;setCount(count + 1)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;dispatch({ type: 'INCREMENT' })&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Before: useState로 상태 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 useState를 사용해서 장바구니 상태를 관리했었습니다. 이렇게 되면 상태 변경 로직이 여러 함수에 나누어져 있고 로직이 복잡해질수록 setState로 관리하는 코드가 길어지게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const [shoppingCart, setShoppingCart] = useState({ items: [] });

function handleAddItemToCart(id) {
  // 아이템 추가 로직이 여기
}

function handleUpdateCartItemQuantity(productId, amount) {
  // 수량 업데이트 로직이 또 여기
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 상태를 바꾸는 로직이 &lt;b&gt;여러 함수에 흩어져&lt;/b&gt; 있고, 기능이 많아질수록 setState가 점점 &lt;b&gt;복잡하고 길어집니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ After: useReducer 로 전환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. useReducer 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const [shoppingCartState, shoppingCartDispatch] = useReducer(
  shoppingCartReducer,   // 상태를 어떻게 바꿀지 정의한 reducer 함수
  { items: [] }          // 초기 상태
);
&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;shoppingCartState: 현재 상태&lt;/li&gt;
&lt;li&gt;shoppingCartDispatch: 액션을 발생시키는 함수&lt;/li&gt;
&lt;li&gt;shoppingCartReducer: 상태를 업데이트하는 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Reducer 함수 만들기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;function shoppingCartReducer(state, action) {
  if (action.type === &quot;ADD_ITEM&quot;) {
    // 아이템 추가 로직
    return { // 업데이트 된 상태 반환
      ...state,
      items: updatedItems,
    };
  }

  if (action.type === &quot;UPDATE_ITEM&quot;) {
    // 수량 업데이트 로직
    return { // 업데이트 된 상태 반환
      ...state,
      items: updatedItems,
    };
  }

  return state; // 기본은 이전 상태 그대로 반환
}
&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;state: snapshot으로 찍은 현재 상태 객체 (ex. { items: [...] })&lt;/li&gt;
&lt;li&gt;action: 상태를 어떻게 바꿀지를 담은 객체 (예: { type: 'ADD_ITEM', payload: 1 })&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  우리가 상태 업데이트를 위해 써야 하는 &lt;b&gt;코드는 이전보다 조금 길어질 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 중요한 차이는 그 로직이 &lt;b&gt;React 컴포넌트 바깥&lt;/b&gt;, &lt;b&gt;리듀서 함수 안에 모여있다&lt;/b&gt;는 점이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 useReducer를 사용할 때는 상태를 직접 다루는 특별한 setState 함수를 기억하지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그저 dispatch로 &quot;이런 행동을 하겠다!&quot;라고 의도(action)만 전달하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React가 자동으로 &lt;b&gt;가장 최신 상태의 스냅샷을 reducer에 전달&lt;/b&gt;해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  즉, &lt;b&gt;이전 상태 참조 문제 없이&lt;/b&gt; 안정적으로 상태를 변경할 수 있죠!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Reducer 함수 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;function handleAddItemToCart(id) {
  shoppingCartDispatch({ type: &quot;ADD_ITEM&quot;, payload: id });
}

function handleUpdateCartItemQuantity(productId, amount) {
  shoppingCartDispatch({
    type: &quot;UPDATE_ITEM&quot;,
    payload: { productId, amount },
  });
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 App이나 Context Provider 컴포넌트는 &lt;b&gt;dispatch만 호출하면 되고&lt;/b&gt;,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 상태 업데이트 로직은 모두 &lt;b&gt;reducer 안에서 깔끔하게 처리&lt;/b&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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  useReducer로 상태가 바뀌는 흐름, 어떻게 작동할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 흔히 useState로 상태를 관리할 때는 상태를 바꾸고자 할 때 setState를 직접 호출하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 useReducer를 쓰면 그 흐름이 달라집니다. 대신 훨씬 더 구조적이고 예측 가능한 방식으로 동작합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;shoppingCartDispatch({ type: &quot;ADD_ITEM&quot;, payload: itemId });
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 dispatch 함수를 호출하면 무슨 일이 일어날까요?&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;dispatch()가 액션을 발생시킴&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 예시에서는 &quot;ADD_ITEM&quot;이라는 &lt;b&gt;행동의 의도(type)&lt;/b&gt;와, 어떤 아이템을 추가할지에 대한 &lt;b&gt;정보(payload)&lt;/b&gt;를 보냈어요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리듀서 함수가 호출됨&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useReducer는 이 dispatch를 받자마자, 우리가 정의해둔 shoppingCartReducer() 함수에 현재 상태(state)와 보낸 action을 넘겨줘요.&lt;/li&gt;
&lt;li&gt;이 리듀서 함수는 &amp;ldquo;어떤 액션을 어떻게 처리할지&amp;rdquo;에 대한 규칙을 정해둔 곳이죠.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;function shoppingCartReducer(state, action) {
  if (action.type === &quot;ADD_ITEM&quot;) {
    // 새로운 상태 계산
    return {
      ...state,
      items: [...state.items, action.payload],
    };
  }
  return state; // 기본은 상태 그대로 반환
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리듀서에서 반환한 새 상태로 업데이트&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리듀서 함수는 바뀐 상태(예: items가 추가된 배열)를 &lt;b&gt;새로운 객체로 만들어 반환&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;useReducer는 이 새 상태를 shoppingCartState에 저장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태를 제공하는 컨텍스트의 value가 변경됨&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 ShoppingCartContext.Provider의 value={{ shoppingCartState, shoppingCartDispatch }}가 있었다면, 이 중 shoppingCartState가 바뀌면서 전체 value 객체가 새로 생성됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그 컨텍스트를 구독 중인 컴포넌트들이 리렌더링됨&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useContext(ShoppingCartContext)로 해당 값을 쓰고 있는 컴포넌트들이 자동으로 리렌더링되며, 최신 shoppingCartState 값을 받아 UI를 업데이트해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  왜 이 흐름이 좋을까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태 변경 로직이 하나의 리듀서 함수 안에 깔끔하게 정리됨&lt;/li&gt;
&lt;li&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;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/167</guid>
      <comments>https://dksek3050.tistory.com/167#entry167comment</comments>
      <pubDate>Wed, 9 Apr 2025 18:09:28 +0900</pubDate>
    </item>
    <item>
      <title>IntelliJ에서 React prettier 설정</title>
      <link>https://dksek3050.tistory.com/166</link>
      <description>&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;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M11ul/btsNbRbDUpL/y9c7sa0IQJzh1FBgi4UDg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M11ul/btsNbRbDUpL/y9c7sa0IQJzh1FBgi4UDg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M11ul/btsNbRbDUpL/y9c7sa0IQJzh1FBgi4UDg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM11ul%2FbtsNbRbDUpL%2Fy9c7sa0IQJzh1FBgi4UDg0%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BackEnd 공부를 하다가 최근 React 공부를 시작하게 되어 IntelliJ에서 React 환경을 구축하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩하는 과정에서 저장시 코드 정렬이 되지 않아 Prettier 플러그인 재설치 하였으나 그래도 안되더라 &amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어를 사용해 해당 프로젝트에 프리티어를 설치한다.&lt;/p&gt;
&lt;pre class=&quot;q&quot;&gt;&lt;code&gt;npm i --save-dev prettier
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 File &amp;gt; Setting &amp;gt; Languages &amp;amp; Frameworks &amp;gt; JavaScript &amp;gt; Prettier 들어간다. Automatic Prettier configuration 체크 후 Run on Save 까지 체크하고 적용하면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IuVe2/btsNce5wNiK/0n8xKZHUCAGrj3b6GmKxDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IuVe2/btsNce5wNiK/0n8xKZHUCAGrj3b6GmKxDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IuVe2/btsNce5wNiK/0n8xKZHUCAGrj3b6GmKxDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIuVe2%2FbtsNce5wNiK%2F0n8xKZHUCAGrj3b6GmKxDk%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;953&quot; height=&quot;706&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;706&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;만약 이렇게 해도 적용이 안된다면 Manual Prettier configuration을 선택하여 Prettier package를 찾아서 선택해주자.&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;946&quot; data-origin-height=&quot;699&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcRnQ2/btsNc8DsZn8/CGhJIvWYK5UqBsddfmIxYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcRnQ2/btsNc8DsZn8/CGhJIvWYK5UqBsddfmIxYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcRnQ2/btsNc8DsZn8/CGhJIvWYK5UqBsddfmIxYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcRnQ2%2FbtsNc8DsZn8%2FCGhJIvWYK5UqBsddfmIxYk%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;946&quot; height=&quot;699&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;699&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/166</guid>
      <comments>https://dksek3050.tistory.com/166#entry166comment</comments>
      <pubDate>Tue, 8 Apr 2025 18:13:03 +0900</pubDate>
    </item>
    <item>
      <title>[React] React 완벽 가이드 - React로 프로젝트 관리 앱 만들기 | 개발 회고</title>
      <link>https://dksek3050.tistory.com/165</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eBc5nM/btsNdk4FNHw/5nOsu2dKdN1zjUzgoKmoDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eBc5nM/btsNdk4FNHw/5nOsu2dKdN1zjUzgoKmoDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eBc5nM/btsNdk4FNHw/5nOsu2dKdN1zjUzgoKmoDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeBc5nM%2FbtsNdk4FNHw%2F5nOsu2dKdN1zjUzgoKmoDK%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;500&quot; height=&quot;498&quot; data-filename=&quot;다운로드.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Project Manage&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://an0401na.github.io/React_Study/09-Practice-Project-Project_Manage/&quot;&gt;React 기반 프로젝트 관리 애플리케이션  &lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;프로젝트 개요&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 강의 일부의 연습 프로젝트 입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  프로젝트 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 사용자가 여러 프로젝트를 생성하고, 각 프로젝트에 대한 작업(Task)을 추가하고 관리할 수 있는 &lt;b&gt;프로젝트 관리 애플리케이션&lt;/b&gt;입니다. 각 프로젝트에는 제목, 설명, 마감일을 설정할 수 있고, 작업(Task)을 추가하여 관리할 수 있습니다. 또한, 사이드바를 통해 프로젝트를 선택하고 선택한 프로젝트의 &lt;b&gt;세부 정보 확인&lt;/b&gt;, &lt;b&gt;작업 입력 및 관리&lt;/b&gt;, &lt;b&gt;중복 방지&lt;/b&gt;, &lt;b&gt;모달 경고&lt;/b&gt; 등의 기능이 포함되어 있습니다. 프로젝트가 선택되지 않았을 때는 적절한 메시지가 화면에 표시됩니다.&lt;/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;React&lt;/b&gt;: 컴포넌트 기반 UI 개발&lt;/li&gt;
&lt;li&gt;&lt;b&gt;useState&lt;/b&gt;: 상태 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;useRef&lt;/b&gt;: 비동기적인 상태 변화 없이 DOM을 직접 참조&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✅ 프로젝트 생성&lt;/td&gt;
&lt;td&gt;제목, 설명, 마감일 입력 필드가 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;❌ 중복 방지&lt;/td&gt;
&lt;td&gt;동일한 프로젝트 제목을 가진 항목은 생성 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚠️ 유효성 검사&lt;/td&gt;
&lt;td&gt;입력 필드가 비어 있으면 경고 모달 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;  작업(Task) 추가&lt;/td&gt;
&lt;td&gt;각 프로젝트에 작업을 개별적으로 추가 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt; ️ 작업(Task) 삭제&lt;/td&gt;
&lt;td&gt;각 작업 삭제 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;  프로젝트 삭제&lt;/td&gt;
&lt;td&gt;현재 보고 있는 프로젝트를 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;  선택 안 됨&lt;/td&gt;
&lt;td&gt;프로젝트가 선택되지 않았을 때 별도 안내 UI 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; ️ 구현 과정 및 내부 로직&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  &lt;a href=&quot;https://github.com/An0401na/React_Study/tree/main/09-Practice-Project-Project_Manage&quot;&gt;&lt;b&gt;github repo&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ 전체적인 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 + Add Project 버튼을 클릭하면 CreateProject 컴포넌트가 렌더링되어 &lt;b&gt;제목, 설명, 마감일&lt;/b&gt;을 입력받습니다.&lt;/li&gt;
&lt;li&gt;프로젝트가 생성되면 사이드바(SideBar)에 추가되고, 클릭 시 해당 프로젝트가 선택됩니다.&lt;/li&gt;
&lt;li&gt;선택된 프로젝트의 세부 정보는 ProjectDetail에서 확인할 수 있으며, 작업(Task)을 추가/삭제할 수 있습니다.&lt;/li&gt;
&lt;li&gt;새로운 작업은 ProjectTask 컴포넌트를 통해 입력받아 addTaskFromProject 함수로 프로젝트에 추가됩니다.&lt;/li&gt;
&lt;li&gt;각 작업 옆의 Clear 버튼을 누르면 handleClearTaskToProject 함수가 호출되어 해당 작업을 삭제합니다.&lt;/li&gt;
&lt;li&gt;아무 프로젝트도 선택되지 않은 상태에서는 NoProjectSelected 컴포넌트가 표시됩니다.&lt;/li&gt;
&lt;li&gt;모든 로직은 useProjects 커스텀 훅을 통해 상태와 함께 관리됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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️⃣ 폴더 구조&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;src/
│
├── assets/                  # 이미지 등 정적 자산
│   └── no-projects.png
│
├── components/              # UI 컴포넌트 모음
│   ├── CreateProject.jsx         # 프로젝트 생성 폼
│   ├── CreateProjectTask.jsx     # 작업(Task) 입력 UI
│   ├── Input.jsx                 # 공통 입력 필드 컴포넌트
│   ├── Modal.jsx                 # 모달 컴포넌트
│   ├── NoProjectSelected.jsx     # 프로젝트 선택되지 않았을 때 UI
│   ├── ProjectDetail.jsx         # 프로젝트 상세 정보 및 Task 관리
│   ├── ProjectTask.jsx           # 작업(Task) 섹션 컨테이너
│   ├── ProjectTaskList.jsx       # 작업(Task) 리스트
│   └── SideBar.jsx               # 프로젝트 목록 및 선택
│
├── hooks/
│   └── useProjects.js        # 상태 및 로직 관리 커스텀 훅
│
├── App.jsx                   # 전체 앱 UI 구성
├── index.jsx                 
└── index.css                 

&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ 주요 컴포넌트 및 기능&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  1. App.jsx (메인 컴포넌트)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 상태와 로직은 useProjects 커스텀 훅에서 관리&lt;/li&gt;
&lt;li&gt;현재 상태에 따라 렌더링할 컴포넌트를 결정하는 currentView 변수를 사용해 동적으로 UI를 전환 :
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 생성 중: CreateProject&lt;/li&gt;
&lt;li&gt;프로젝트 선택됨: ProjectDetail&lt;/li&gt;
&lt;li&gt;아무 프로젝트도 선택되지 않음: NoProjectSelected&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;let currentView;

if (isCreatingProject) {
  currentView = (
    &amp;lt;CreateProject
      onClickSaveProject={saveProject}
      onClickCancleProject={cancleProject}
    /&amp;gt;
  );
} else if (selectedProject) {
  currentView = (
    &amp;lt;ProjectDetail
      project={selectedProject}
      onClickDeleteProject={deleteProject}
      onClickAddTask={addTaskFromProject}
      onClickClearTask={handleClearTaskToProject}
    /&amp;gt;
  );
} else {
  currentView = (
    &amp;lt;NoProjectSelected onClickCreatingProject={startCreatingProject} /&amp;gt;
  );
}
&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;최종적으로 SideBar 컴포넌트와 함께 상태에 맞는 메인 뷰를 렌더링&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;return (
  &amp;lt;main className=&quot;h-screen my-8 flex gap-8&quot;&amp;gt;
    &amp;lt;SideBarprojects={projects}
      selectedProject={selectedProject}
      onClickSelectedProject={selectProject}
      onClickCreatingProject={startCreatingProject}
    /&amp;gt;
    {currentView}
  &amp;lt;/main&amp;gt;
);

&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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. SideBar.jsx (사이드바 컴포넌트)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 등록된 &lt;b&gt;프로젝트 목록을 표시&lt;/b&gt;하고 선택 가능&lt;/li&gt;
&lt;li&gt;선택된 프로젝트는 강조 표시됨&lt;/li&gt;
&lt;li&gt;+ Add Project 버튼으로 프로젝트 생성 뷰 진입 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;button onClick={onClickCreatingProject}&amp;gt;+ Add Project&amp;lt;/button&amp;gt;
{projects.map(project =&amp;gt; (
  &amp;lt;button
    key={project.id}
    className={project.id === selectedProject?.id ? 'active' : ''}
    onClick={() =&amp;gt; onClickSelectedProject(project.id)}
  &amp;gt;
    {project.title}
  &amp;lt;/button&amp;gt;
))}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; ️ 3. CreateProject.jsx (프로젝트 생성 컴포넌트)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자로부터 &lt;b&gt;프로젝트 제목, 설명, 마감일&lt;/b&gt;을 입력받아, App.jsx로 전달하는 역할을&lt;/li&gt;
&lt;li&gt;useRef를 통해 입력 필드의 값을 관리하여 &lt;b&gt;불필요한 리렌더링 없이&lt;/b&gt; 데이터를 추출&lt;/li&gt;
&lt;li&gt;유효성 검사와 중복 검사 기능이 내장되어 있으며, 오류 발생 시 &lt;b&gt;Modal 컴포넌트&lt;/b&gt;를 통해 사용자에게 피드백을 제공&lt;/li&gt;
&lt;li&gt;입력이 유효한 경우 부모로부터 전달받은 onClickSaveProject(title, description, dueDate) 함수를 호출해 프로젝트를 저장&lt;/li&gt;
&lt;li&gt;UI 구성은 Input 컴포넌트를 재사용하여 일관성 있는 입력 필드를 구현, 버튼 클릭 시 취소 또는 저장 이벤트 트리거&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;// 입력값 추적용 useRef
const projectTitleRef = useRef(&quot;&quot;);
const projectDescriptionRef = useRef(&quot;&quot;);
const projectDueDateRef = useRef(&quot;&quot;);

// 저장 버튼 클릭 시 입력값 유효성 검사 및 저장 시도
function handleSaveClick() {
  const title = projectTitleRef.current.value;
  const description = projectDescriptionRef.current.value;
  const dueDate = projectDueDateRef.current.value;

  if (title.trim() === &quot;&quot; || description.trim() === &quot;&quot; || dueDate.trim() === &quot;&quot;) {
    invalidCaseModal.current.open(); // 빈 입력값이 있을 경우 경고 모달
    return;
  }

  const isSaved = onClickSaveProject(title, description, dueDate);
  if (!isSaved) {
    duplicatedCaseModal.current.open(); // 중복된 타이틀인 경우 경고 모달
    return;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  4. Modal.jsx (모달 컴포넌트)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자 피드백&lt;/b&gt;을 위한 모달 창을 생성하는 컴포넌트&lt;/li&gt;
&lt;li&gt;React Portal을 사용하여 모달을 &lt;b&gt;루트 DOM 외부에 렌더링&lt;/b&gt;하고, &lt;b&gt;useImperativeHandle + useRef&lt;/b&gt; 조합으로 외부 컴포넌트에서 &lt;b&gt;모달을 제어&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;부모 컴포넌트에서는 ref.current.open()로 모달 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useImperativeHandle(ref, () =&amp;gt; {
  return {
    open() {
      dialog.current.showModal(); // HTML &amp;lt;dialog&amp;gt; API를 통해 모달 열기
    },
  };
});
&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;createPortal을 통해 #modal-root 요소에 모달을 렌더링하여, 구조적으로 앱의 나머지 UI와 분리되면서도 전역적으로 표시&lt;/li&gt;
&lt;li&gt;buttonCaption으로 버튼 라벨을 커스터마이징 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;&amp;lt;dialog ref={dialog}&amp;gt;
  {children} // 모달 내부 컨텐츠
  &amp;lt;form method=&quot;dialog&quot;&amp;gt;
    &amp;lt;button&amp;gt;{buttonCaption}&amp;lt;/button&amp;gt; 
  &amp;lt;/form&amp;gt;
&amp;lt;/dialog&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  5. ProjectDetail.jsx (프로젝트 상세 보기)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;선택된 프로젝트의 제목, 설명, 마감일, 작업 목록을 보여주는 컴포넌트&lt;/b&gt;입&lt;/li&gt;
&lt;li&gt;작업은 하위 컴포넌트인 ProjectTask를 통해 추가 및 삭제&lt;/li&gt;
&lt;li&gt;마감일은 MM/DD/YYYY 형식으로 포맷팅&lt;/li&gt;
&lt;li&gt;ProjectTask에 작업 배열과 함께 작업 추가/삭제 핸들러를 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 작업 추가 핸들러
function handleAddTaskClick(newTask) {
  if (newTask.trim() !== &quot;&quot;) {
    onClickAddTask(project, newTask); // 부모로 프로젝트와 작업 텍스트 전달
  }
}

// 작업 삭제 핸들러
function handleClearTaskClick(index) {
  onClickClearTask(project, index); // 삭제할 작업의 인덱스를 기준으로 처리
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  6. ProjectTask.jsx (프로젝트 작업 컴포넌트)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선택된 프로젝트의 &lt;b&gt;작업(Task)&lt;/b&gt; 목록을 관리하는 중간 컴포넌트&lt;/li&gt;
&lt;li&gt;하위에 CreateProjectTask(입력창 + 추가 버튼)와 ProjectTaskList(작업 목록 출력)를 포함&lt;/li&gt;
&lt;li&gt;입력된 작업은 유효성 검사를 거쳐 상위 컴포넌트로 전달&lt;/li&gt;
&lt;li&gt;작업이 없을 경우 사용자에게 안내 메시지를 출력.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;h2&amp;gt;Tasks&amp;lt;/h2&amp;gt; // 섹션 제목

&amp;lt;CreateProjectTask handleAddTaskClick={handleAddTaskClick} /&amp;gt;

{tasks.length === 0 ? (
  &amp;lt;p&amp;gt;This Project does not have any task yet.&amp;lt;/p&amp;gt;
) : (
  &amp;lt;ProjectTaskList tasks={tasks} onClickClear={onClickClear} /&amp;gt;
)}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 7. NoProjectSelected.jsx (프로젝트 선택되지 않음 컴포넌트)&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;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function NoProjectSelected({ onClickCreatingProject }) {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h2&amp;gt;No project selected&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;Please select a project or create a new one.&amp;lt;/p&amp;gt;
      &amp;lt;button onClick={onClickCreatingProject}&amp;gt;Create New Project&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  결과 화면&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;431210363-29b8b40d-8cd7-4b28-b20e-9e72ad351cf7.png&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5aNy5/btsNc9oLEIB/cUVBKJnbFYlGFMY0zkOR61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5aNy5/btsNc9oLEIB/cUVBKJnbFYlGFMY0zkOR61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5aNy5/btsNc9oLEIB/cUVBKJnbFYlGFMY0zkOR61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5aNy5%2FbtsNc9oLEIB%2FcUVBKJnbFYlGFMY0zkOR61%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;942&quot; height=&quot;776&quot; data-filename=&quot;431210363-29b8b40d-8cd7-4b28-b20e-9e72ad351cf7.png&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;776&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;431210722-e195a993-b498-4b38-866e-17ff0d10bd84.png&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deYetq/btsNcn8XPpP/CkV8LWTA4b1CfqPtazZaK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deYetq/btsNcn8XPpP/CkV8LWTA4b1CfqPtazZaK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deYetq/btsNcn8XPpP/CkV8LWTA4b1CfqPtazZaK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeYetq%2FbtsNcn8XPpP%2FCkV8LWTA4b1CfqPtazZaK1%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;938&quot; height=&quot;672&quot; data-filename=&quot;431210722-e195a993-b498-4b38-866e-17ff0d10bd84.png&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;431210632-5b6237f3-01c0-46c2-bf53-f30bc925907e.png&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;729&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k8RcS/btsNcifLTQ2/xIZbRXCDK0nI6UGG92IUkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k8RcS/btsNcifLTQ2/xIZbRXCDK0nI6UGG92IUkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k8RcS/btsNcifLTQ2/xIZbRXCDK0nI6UGG92IUkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk8RcS%2FbtsNcifLTQ2%2FxIZbRXCDK0nI6UGG92IUkk%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;918&quot; height=&quot;729&quot; data-filename=&quot;431210632-5b6237f3-01c0-46c2-bf53-f30bc925907e.png&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;729&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;431210609-6f3b415c-a09c-4194-939d-e13356f5c8f7.png&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLP5wg/btsNdk4F192/l37Sfbv1j7rdp6SsOHmD2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLP5wg/btsNdk4F192/l37Sfbv1j7rdp6SsOHmD2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLP5wg/btsNdk4F192/l37Sfbv1j7rdp6SsOHmD2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLP5wg%2FbtsNdk4F192%2Fl37Sfbv1j7rdp6SsOHmD2K%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;925&quot; height=&quot;738&quot; data-filename=&quot;431210609-6f3b415c-a09c-4194-939d-e13356f5c8f7.png&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;738&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;431210660-adf757bf-ea08-4f03-9dce-2037864d4dc0.png&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n4pFh/btsNdyInR69/6fHRUrWAIiT4GxAkIHnpjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n4pFh/btsNdyInR69/6fHRUrWAIiT4GxAkIHnpjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n4pFh/btsNdyInR69/6fHRUrWAIiT4GxAkIHnpjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn4pFh%2FbtsNdyInR69%2F6fHRUrWAIiT4GxAkIHnpjK%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;924&quot; height=&quot;763&quot; data-filename=&quot;431210660-adf757bf-ea08-4f03-9dce-2037864d4dc0.png&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;431210802-2b178ee3-0163-45e1-a0b8-03fadebea34d.png&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;745&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lnVDV/btsNdxJux6Z/2mCdwwz34ioLwIJ5ZHJUaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lnVDV/btsNdxJux6Z/2mCdwwz34ioLwIJ5ZHJUaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lnVDV/btsNdxJux6Z/2mCdwwz34ioLwIJ5ZHJUaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlnVDV%2FbtsNdxJux6Z%2F2mCdwwz34ioLwIJ5ZHJUaK%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;934&quot; height=&quot;745&quot; data-filename=&quot;431210802-2b178ee3-0163-45e1-a0b8-03fadebea34d.png&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;745&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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; ️ 트러블슈팅&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 1. 프로젝트 저장 버튼 클릭 시 프로젝트가 추가되지 않는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  문제 원인: 잘못된 이벤트 처리 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;&amp;lt;button
  className=&quot;...&quot;
  onClick={() =&amp;gt;
    onClickSaveProject(
      projectTitle.current.value,
      projectDescription.current.value,
      projectDueDate.current.value,
    )
  }
&amp;gt;
  Save
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  문제 설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;onClick 이벤트 핸들러 내부에서 바로 onClickSaveProject()를 실행하고 있음.&lt;/li&gt;
&lt;li&gt;이 버튼이 &amp;lt;form&amp;gt; 내부에 있기 때문에, 버튼 클릭 시 **기본적으로 form 제출 이벤트(submit)**가 발생함.&lt;/li&gt;
&lt;li&gt;그러나 onClick 내부에서 이를 preventDefault() 하지 않고 즉시 실행하면서, 페이지가 새로고침되거나 의도한 대로 동작하지 않음.&lt;/li&gt;
&lt;li&gt;이로 인해 프로젝트 저장 함수가 &lt;b&gt;정상적으로 실행되지 않거나&lt;/b&gt;, 실행되어도 곧바로 리렌더링되어 효과가 사라지는 것처럼 보였음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  해결 방법: 폼의 기본 제출 동작 방지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 방법 1 &amp;ndash; e.preventDefault() 사용&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function handleSaveClick(e) {
  e.preventDefault(); // ✅ form 기본 동작 막기
  onClickSaveProject(...);
}

&amp;lt;button onClick={handleSaveClick}&amp;gt;Save&amp;lt;/button&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 방법 2 &amp;ndash; 버튼의 type을 &quot;button&quot;으로 명시&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;button type=&quot;button&quot; onClick={handleSaveClick}&amp;gt;Save&amp;lt;/button&amp;gt;
&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;li&gt;type=&quot;button&quot;을 사용하면 더 간단하고 실수를 줄일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 현재 구조의 문제점: Prop Drilling&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트는 상위 컴포넌트(App.jsx)에서 관리하는 상태와 함수를 여러 하위 컴포넌트에 **props로 계속 전달하는 구조(prop drilling)**를 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 프로젝트 추가 함수(onAddProject)가 App &amp;rarr; SideBar &amp;rarr; CreateProject로 전달되는 식이여서 아래와 같은 문제가 발생된다.&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;필요 없는 props를 전달만&lt;/b&gt; 하는 역할을 하게 됨&lt;/li&gt;
&lt;li&gt;컴포넌트 간 &lt;b&gt;결합도가 높아지고&lt;/b&gt;, &lt;b&gt;가독성 저하&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;규모가 커질수록 &lt;b&gt;상태 관리가 복잡해짐&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 다음시간에 Context 학습해보도록 하겠다!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;배운 점 &amp;amp; 느낀 점&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ref와 status를 적절히 활용해 &lt;b&gt;상태 관리와 DOM 제어&lt;/b&gt;에 익숙해졌다.&lt;/li&gt;
&lt;li&gt;ReactDOM.createPortal을 사용해 &lt;b&gt;모달을 루트 밖에 안전하게 렌더링&lt;/b&gt;하는 법을 배웠다.&lt;/li&gt;
&lt;li&gt;&amp;lt;button type=&quot;button&quot;&amp;gt;을 명시하지 않으면 &lt;b&gt;예상치 못한 폼 제출 이슈&lt;/b&gt;가 생길 수 있다는 걸 알게 되었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Tailwind CSS&lt;/b&gt; 사용법을 익힐 수 있었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커스텀 훅&lt;/b&gt;으로 상태 로직을 분리하면서 관심사 분리(SOC)의 중요성을 체감했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;prop drilling&lt;/b&gt;을 겪으며 전역 상태 관리나 Context API의 필요성을 느꼈다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/165</guid>
      <comments>https://dksek3050.tistory.com/165#entry165comment</comments>
      <pubDate>Tue, 8 Apr 2025 17:51:47 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 빈 스코프</title>
      <link>https://dksek3050.tistory.com/164</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/llCpg/btsMX4ULjK0/Nn5prQjxGGCyMueCBk4wHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/llCpg/btsMX4ULjK0/Nn5prQjxGGCyMueCBk4wHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/llCpg/btsMX4ULjK0/Nn5prQjxGGCyMueCBk4wHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FllCpg%2FbtsMX4ULjK0%2FNn5prQjxGGCyMueCBk4wHk%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;700&quot; height=&quot;482&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 인프런에서 김영한 님의 &quot;스프링 핵심원리 - 기본편&quot;을 수강 후 공부한 내용을 정리한 게시글입니다. 부족한 부분이 있다면 언제든 지적 부탁드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 빈 스코프란?&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스코프는 빈이 존재할 수 있는 범위를 의미한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.&lt;/li&gt;
&lt;li&gt;프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.&lt;/li&gt;
&lt;li&gt;웹 관련 스코프
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.&lt;/li&gt;
&lt;li&gt;session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.&lt;/li&gt;
&lt;li&gt;application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;컴포넌트 스캔 자동 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Scope(&quot;prototype&quot;)
@Component
public class HelloBean {}
&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;pre class=&quot;less&quot;&gt;&lt;code&gt;@Scope(&quot;prototype&quot;)
@Bean
PrototypeBean HelloBean() {
	return new HelloBean();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 프로토타입 스코프&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토타입 스코프는 싱글톤 스코프와 다르게 스프링 컨테이너에서 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.&lt;/li&gt;
&lt;li&gt;스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다&lt;/li&gt;
&lt;li&gt;스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.&lt;/li&gt;
&lt;li&gt;이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징&lt;/p&gt;
&lt;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;빈의 소멸 콜백을 하는 @PreDestory 같은 종료 메소드는 호출 되지 않는다.&lt;/li&gt;
&lt;li&gt;그래서 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package hello.core.scope;
import org.junit.jupiter.api.Test;
import
        org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.*;
public class PrototypeTest {
    @Test
    public void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new
                AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println(&quot;find prototypeBean1&quot;);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        System.out.println(&quot;find prototypeBean2&quot;);
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        System.out.println(&quot;prototypeBean1 = &quot; + prototypeBean1);
        System.out.println(&quot;prototypeBean2 = &quot; + prototypeBean2);
        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
        ac.close(); //종료
    }
    @Scope(&quot;prototype&quot;)
    static class PrototypeBean {
        @PostConstruct
        public void init() {
            System.out.println(&quot;PrototypeBean.init&quot;);
        }
        @PreDestroy
        public void destroy() {
            System.out.println(&quot;PrototypeBean.destroy&quot;);
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토타입 빈을 2번 조회했으므로 완전히 다른 스프링 빈이 생성되고, 초기화도 2번 실행된 것을 확인할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱글톤 빈과 프로토타입 빈을 함께 사용시 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;프로토타입만 사용하는 경우&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class SingletonWithPrototypeTest1 {
    @Test
    void prototypeFind() {
        AnnotationConfigApplicationContext ac = new
                AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);
    }
    @Scope(&quot;prototype&quot;)
    static class PrototypeBean {
        private int count = 0;
        public void addCount() {
            count++;
        }
        public int getCount() {
            return count;
        }
        @PostConstruct
        public void init() {
            System.out.println(&quot;PrototypeBean.init &quot; + this);
        }
        @PreDestroy
        public void destroy() {
            System.out.println(&quot;PrototypeBean.destroy&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;프로토타입 빈만 사용할 경우 클라이언트가 스프링컨테이너에게 프로토타입 빈을 요청하게 되면 새로 생성해여 반환하기 때문에 항상 count 의 값는 초기값 0에서 1증가하여 1을 반환하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 clientBean 이라는 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하는 예를 보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;싱글톤 빈에서 프로토타입 빈을 사용하는 경우&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package hello.core.scope;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.Assertions.*;

public class SingletonWithPrototypeTest1 {

	@Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new
                AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);

    }

    static class ClientBean {

        private final PrototypeBean prototypeBean;

        @Autowired
        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

        public int logic() {
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Scope(&quot;prototype&quot;)
    static class PrototypeBean {

        private int count = 0;

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }

        @PostConstruct
        public void init() {
            System.out.println(&quot;PrototypeBean.init &quot; + this);
        }

        @PreDestroy
        public void destroy() {
            System.out.println(&quot;PrototypeBean.destroy&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clientBean 은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문이 clientBean이 생성될때 의존관계에 있는 프로토타입 빈도 생성되고 주입받게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 프로토타입의 빈의 count 값은 초기값 0이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 count를 증가시키는 메소드를 호출하게 되면 clientBean 을 스프링 컨테이너에 요청해서 받는다.싱글톤이므로 항상 같은 clientBean이 반환되어 의존하고 있는 프로토타입을 접근할 수 있다. 이때 clientBean이 참조하고 있는 프로토타입 빈은 변하지 않기 때문에 동일한 빈의 메소드를 실행하게 되어 count 값이 1 증가하여 1이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다른 클라이언트가 count를 증가시키는 메소드를 호출하게 되면 위와 동일하게 동일한 프로토타입의 빈을 참조하게 되어 1이었던 count 값을 2로 증가하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용 할 때마다 새로 생성되는 것이 아니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다. 그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱글톤 빈과 프로토타입 빈을 함께 사용시 문제점 해결하기 - Provider&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Autowired
private ObjectProvider&amp;lt;PrototypeBean&amp;gt; prototypeBeanProvider;
public int logic() {
    PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}
&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;지정한 빈을 컨테이너에서 대신 찾아주는 DL(Dependency Lookup)서비스를 제공하는 것이 바로&amp;nbsp;ObjectProvider&amp;nbsp;이다.&lt;/li&gt;
&lt;li&gt;ObjectProvider의&amp;nbsp;prototypeBeanProvider.getObject()&amp;nbsp;를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환하고 항상 새로운 프로토타입 빈이 생성된다.&lt;/li&gt;
&lt;li&gt;ObjectProvider:&amp;nbsp;ObjectFactory상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Autowired
private Provider&amp;lt;PrototypeBean&amp;gt; provider;
public int logic() {
    PrototypeBean prototypeBean = provider.get();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}
&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;javax.inject.Provider 라는 JSR-330 자바 표준을 사용한다.(스프링 부트 3.0은 jakarta.inject.Provider 사용 - gradle 에 추가)&lt;/li&gt;
&lt;li&gt;provider 의 get() 을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환하고 항상 새로운 프로토타입 빈이 생성되는 것을 확인 할 수 있다.&lt;/li&gt;
&lt;li&gt;자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 웹 스코프&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&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;동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴때 사용하기 딱 좋은것이 바로 request 스코프이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 스코프는 웹 환경에서만 동작한다.&lt;/li&gt;
&lt;li&gt;스프링이 해당 종료시점까지 관리한다. 따라서 종료 메소드가 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;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;request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.&lt;/li&gt;
&lt;li&gt;session: HTTP Session과 동일한 생명주기를 가지는 스코프&lt;/li&gt;
&lt;li&gt;application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프&lt;/li&gt;
&lt;li&gt;websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ request 스코프 예제&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 다양한 http 요청이 들어오더라도 UUID와 requestURL을 로그에 함께 남겨 요청을 구분해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle 에 web 라이브러리 추가&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-web'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyLogger&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package hello.core.common;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;
@Component
@Scope(value = &quot;request&quot;)
public class MyLogger {

    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message) {
        System.out.println(&quot;[&quot; + uuid + &quot;]&quot; + &quot;[&quot; + requestURL + &quot;] &quot; +message);
    }

    @PostConstruct
    public void init() {
        uuid = UUID.randomUUID().toString();
        System.out.println(&quot;[&quot; + uuid + &quot;] request scope bean create:&quot; + this);
    }

    @PreDestroy
    public void close() {
        System.out.println(&quot;[&quot; + uuid + &quot;] request scope bean close:&quot; + this);
    }
}
&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;@Scope(value = &quot;request&quot;)를 사용해서 request 스코프로 지정했다. 이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.&lt;/li&gt;
&lt;li&gt;이 빈이 생성되는 시점에 자동으로&amp;nbsp;@PostConstruct&amp;nbsp;초기화 메서드를 사용해서 uuid를 생성해서 저장해둔다.&lt;/li&gt;
&lt;li&gt;이 빈은 HTTP 요청 당 하나씩 생성되므로, uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있다.&lt;/li&gt;
&lt;li&gt;이 빈이 소멸되는 시점에&amp;nbsp;@PreDestroy&amp;nbsp;를 사용해서 종료 메시지를 남긴다.&lt;/li&gt;
&lt;li&gt;requestURL 은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 MyLogger를 사용할 테스트 컨트롤러와 서비스 코드를 작성해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LogDemoController&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

		private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping(&quot;log-demo&quot;)
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);
        myLogger.log(&quot;controller test&quot;);
        logDemoService.logic(&quot;testId&quot;);
        return &quot;OK&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LogDemoService&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.core.logdemo;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

		private final MyLogger myLogger;

    public void logic(String id) {
        myLogger.log(&quot;service id = &quot; + id);
    }

}
&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Error creating bean with name 'myLogger': Scope 'request' is not active for the
current thread; consider defining a scoped proxy for this bean if you intend to
refer to it from a singleton;
&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;위와 같은 오류가 나는 이유는 컨트롤러에 MyLogger를 주입받는 부분때문이다.private final MyLogger myLogger;&lt;/li&gt;
&lt;li&gt;스프링 어플리케이션을 실행하는 시점에 싱글톤 빈은 생성되어 주입할 수 있지만 request 스코프 빈은 아직 생성이 되지 않는 상태에서 주입을 하려고 했기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 문제는 Provider와 프록시로 해결할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 스코프와 Provider&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LogDemoController&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final ObjectProvider&amp;lt;MyLogger&amp;gt; myLoggerProvider;

    @RequestMapping(&quot;log-demo&quot;)
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);
        myLogger.log(&quot;controller test&quot;);
        logDemoService.logic(&quot;testId&quot;);
        return &quot;OK&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LogDemoService&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.core.logdemo;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider&amp;lt;MyLogger&amp;gt; myLoggerProvider;

    public void logic(String id) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log(&quot;service id = &quot; + id);
    }

}
&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;ObjectProvider&amp;nbsp;덕분에&amp;nbsp;ObjectProvider.getObject()&amp;nbsp;를 호출하는 시점까지 request scope 빈의생성을 지연할 수 있다.&lt;/li&gt;
&lt;li&gt;ObjectProvider.getObject()&amp;nbsp;를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다.&lt;/li&gt;
&lt;li&gt;ObjectProvider.getObject()를&amp;nbsp;LogDemoController&amp;nbsp;LogDemoService에서 각각 한번씩 따로호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 스코프와 프록시&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LogDemoController&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping(&quot;log-demo&quot;)
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);
        myLogger.log(&quot;controller test&quot;);
        logDemoService.logic(&quot;testId&quot;);
        return &quot;OK&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LogDemoService&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.core.logdemo;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

    public void logic(String id) {
        myLogger.log(&quot;service id = &quot; + id);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyLogger&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@Scope(value = &quot;request&quot;, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
				...

}
&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;proxyMode = ScopedProxyMode.TARGET_CLASS&amp;nbsp;를 추가해주자.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;적용 대상이 인터페이스가 아닌 클래스면&amp;nbsp;TARGET_CLASS&amp;nbsp;를 선택&lt;/li&gt;
&lt;li&gt;적용 대상이 인터페이스면&amp;nbsp;INTERFACES&amp;nbsp;를 선택&lt;/li&gt;
&lt;li&gt;이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;어노테이션에 proxyMode를 지정해줬을 뿐인데 아까는 주입문제로 오류났던 Controller 와 Service가 정상 동작 한다. 그 이유는 CGLIB라는 라이브러리로 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입하기 때문이다!&lt;/li&gt;
&lt;li&gt;@Scope 의 proxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정해줬을 뿐인데 아까는 주입문제로 오류났던 Controller 와 Service가 정상 동작 한다. 그 이유는 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성하여 주입하기 때문이다.&lt;/li&gt;
&lt;li&gt;실제로 request 스코프 빈이 생성 되기 전 MyLogger를 출력하면 아래와 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MyLogger$$EnhancerBySpringCGLIB 이라는 클래스로 만들어진 객체가 대신 등록된 것을 확인할 수 있다. 그리고 스프링 컨테이너에 &quot;myLogger&quot;라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한다. ac.getBean(&quot;myLogger&quot;, MyLogger.class) 로 조회해도 프록시 객체가 조회되는 것을 확인할 수 있다. 그래서 의존관계 주입도 이 가짜 프록시 객체가 주입된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d&lt;/li&gt;
&lt;li&gt;그러면 가짜 프록시 객체가 어떻게 MyLog의 메소드에 접근할 수 있을까 ?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가짜 프록시 객체는 내부에 진짜 myLogger 를 찾는 방법을 알고 있다.&lt;/li&gt;
&lt;li&gt;클라이언트가 myLogger.log() 을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.&lt;/li&gt;
&lt;li&gt;가짜 프록시 객체는 request 스코프의 진짜 myLogger.log() 를 호출한다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;정리&amp;gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.&lt;/li&gt;
&lt;li&gt;가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱글톤 처럼 동작한다.&lt;/li&gt;
&lt;li&gt;프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리한다는 점이다. 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너 가 가진 큰 강점이다.꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Programming/Spring</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/164</guid>
      <comments>https://dksek3050.tistory.com/164#entry164comment</comments>
      <pubDate>Wed, 26 Mar 2025 15:38:23 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 빈 생명주기 콜백</title>
      <link>https://dksek3050.tistory.com/163</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btvvc7/btsMXm9tutP/OLiEhrqF4hVgJAn9iIe5T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btvvc7/btsMXm9tutP/OLiEhrqF4hVgJAn9iIe5T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btvvc7/btsMXm9tutP/OLiEhrqF4hVgJAn9iIe5T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbtvvc7%2FbtsMXm9tutP%2FOLiEhrqF4hVgJAn9iIe5T1%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;700&quot; height=&quot;482&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 인프런에서 김영한 님의 &quot;스프링 핵심원리 - 기본편&quot;을 수강 후 공부한 내용을 정리한 게시글입니다. 부족한 부분이 있다면 언제든 지적 부탁드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 빈 생명 주기 콜백&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 어플리케이션 시작 시점에 필요한 연결을 미리 해두고 어플리케이션 종료 시점에 연결을 모두 종료하는 객체 초기화 작업과 종료 작업이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package hello.core.lifecycle;
public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println(&quot;생성자 호출, url = &quot; + url);
        connect();
        call(&quot;초기화 연결 메시지&quot;);
    }

    public void setUrl(String url) {
        this.url = url;
    }

    //서비스 시작시 호출
    public void connect() {
        System.out.println(&quot;connect: &quot; + url);
    }

    public void call(String message) {
        System.out.println(&quot;call: &quot; + url + &quot; message = &quot; + message);
    }

    //서비스 종료시 호출
    public void disconnect() {
        System.out.println(&quot;close: &quot; + url);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제 코드에서 생성자 호출시&amp;nbsp;connect()&amp;nbsp;메소드를 실행하여 연결을 맺는 초기화 작업을 수행하도록 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 위 코드를 실행해보면 이상한 결과가 나온다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;생성자 호출, url = null
connect: null
call: null message = 초기화 연결 메시지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 결과가 나오는 이유는 객체가 생성된 후에 url을 주입하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 객체를 생성할때 파라미터로 url을 주입해주면 되지 않나 생각했는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 객체는 스프링 컨테이너가 올라갈때 빈으로 생성되야 하기 때문에 url을 사용자가 입력할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 객체의 생성과 초기화를 분리하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는등 무거운 동작을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다. 물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한번에 다 처리하는게 더 나을 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유로 생성자에서 url을 저장하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 스프링 빈은&amp;nbsp;객체 생성 &amp;rarr; 의존 관계 주입&amp;nbsp;라이프 사이클을 가지게 되는데 의존 관계 주입이 끝난 후에 개발자는 객체를 호출 해야 온전하게 의도한 바대로 객체를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 하기 위해서는 의존관계 주입이 끝난 시점을 개발자가 알아야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점을 스프링이 알려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링은 의존관계 주입이 완료되면 스프링빈에게 콜백 메서드를 통해 초기화 시점을 알려주는 다양한 기능을 제공한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백&lt;/b&gt;을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 종료 작업을 안전하게 진행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;스프링 빈의 이벤트 라이프사이클&amp;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;gt; 스프링 빈 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 의존관계 주입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 초기화 콜백&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 소멸 전 콜백&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 스프링 종료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소멸전 콜백: 빈이 소멸되기 직전에 호출&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 빈 생명주기 콜백 방법&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인터페이스(InitializingBean, DisposableBean)&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package hello.core.lifecycle;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class NetworkClient implements InitializingBean, DisposableBean {
    private String url;
    public NetworkClient() {
        System.out.println(&quot;생성자 호출, url = &quot; + url);
    }
    public void setUrl(String url) {
        this.url = url;
    }
    //서비스 시작시 호출
    public void connect() {
        System.out.println(&quot;connect: &quot; + url);
    }
    public void call(String message) {
        System.out.println(&quot;call: &quot; + url + &quot; message = &quot; + message);
    }
    //서비스 종료시 호출
    public void disConnect() {
        System.out.println(&quot;close + &quot; + url);
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call(&quot;초기화 연결 메시지&quot;);
    }
    @Override
    public void destroy() throws Exception {
        disConnect();
    }
}
&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;InitializingBean,&amp;nbsp;DisposableBean&amp;nbsp;인터페이스를 사용한다.&lt;/li&gt;
&lt;li&gt;InitializingBean은&amp;nbsp;afterPropertiesSet()&amp;nbsp;메서드로 초기화를 지원한다.&lt;/li&gt;
&lt;li&gt;DisposableBean은&amp;nbsp;destroy()&amp;nbsp;메서드로 소멸을 지원한다&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 인터페이스는 스프링 전용 인터페이스이다. 따라서 해당 코드가 스프링 전용 인터페이스에 의존한다.&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;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 정보에 초기화 메서드, 종료 메서드 지정&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package hello.core.lifecycle;
public class NetworkClient {
    private String url;
    public NetworkClient() {
        System.out.println(&quot;생성자 호출, url = &quot; + url);
    }
    public void setUrl(String url) {
        this.url = url;
    }
    //서비스 시작시 호출
    public void connect() {
        System.out.println(&quot;connect: &quot; + url);
    }
    public void call(String message) {
        System.out.println(&quot;call: &quot; + url + &quot; message = &quot; + message);
    }
    //서비스 종료시 호출
    public void disConnect() {
        System.out.println(&quot;close + &quot; + url);
    }
    public void init() {
        System.out.println(&quot;NetworkClient.init&quot;);
        connect();
        call(&quot;초기화 연결 메시지&quot;);
    }
    public void close() {
        System.out.println(&quot;NetworkClient.close&quot;);
        disConnect();
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
static class LifeCycleConfig {
    @Bean(initMethod = &quot;init&quot;, destroyMethod = &quot;close&quot;)
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl(&quot;&amp;lt;http://hello-spring.dev&amp;gt;&quot;);
        return networkClient;
    }
}
&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;nbsp;@Bean(initMethod = &quot;init&quot;, destroyMethod = &quot;close&quot;)&amp;nbsp;처럼 초기화, 소멸 메서드를 지정할 수 있다.&lt;/li&gt;
&lt;li&gt;메서드 이름을 자유롭게 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;스프링 빈이 스프링 코드에 의존하지 않는다.&lt;/li&gt;
&lt;li&gt;코드가 아니라 설정정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메소드를 적용할 수 있다.&lt;/li&gt;
&lt;li&gt;destroyMethod 속성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Bean의 destroyMethod 는 기본값이 (inferred) (추론)으로 등록되어 있다. 이 추론 기능은 close , shutdown 라는 이름의 메서드를 자동으로 호출해준다. 이름 그대로 종료 메서드를 추론해서 호출해준다. 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@PostConstruct, @PreDestroy 애노테이션 지원&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package hello.core.lifecycle;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class NetworkClient {
    private String url;
    public NetworkClient() {
        System.out.println(&quot;생성자 호출, url = &quot; + url);
    }
    public void setUrl(String url) {
        this.url = url;
    }
    //서비스 시작시 호출
    public void connect() {
        System.out.println(&quot;connect: &quot; + url);
    }
    public void call(String message) {
        System.out.println(&quot;call: &quot; + url + &quot; message = &quot; + message);
    }
    //서비스 종료시 호출
    public void disConnect() {
        System.out.println(&quot;close + &quot; + url);
    }
    @PostConstruct
    public void init() {
        System.out.println(&quot;NetworkClient.init&quot;);
        connect();
        call(&quot;초기화 연결 메시지&quot;);
    }
    @PreDestroy
    public void close() {
        System.out.println(&quot;NetworkClient.close&quot;);
        disConnect();
    }
}
&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;li&gt;javax.annotation.PostConstruct이다. 스프링에 종속적인 기술이 아니라 JSR-250라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다&lt;/li&gt;
&lt;li&gt;유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.&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;@PostConstruct, @PreDestroy 애노테이션을 사용하자&lt;/li&gt;
&lt;li&gt;코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면 @Bean 의 initMethod , destroyMethod 를 사용하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Programming/Spring</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/163</guid>
      <comments>https://dksek3050.tistory.com/163#entry163comment</comments>
      <pubDate>Wed, 26 Mar 2025 15:36:19 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 의존관계 자동 주입2</title>
      <link>https://dksek3050.tistory.com/162</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uMx7N/btsMW8jlKwz/jnlkiTdEoKr8CLzn0vY7M1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uMx7N/btsMW8jlKwz/jnlkiTdEoKr8CLzn0vY7M1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uMx7N/btsMW8jlKwz/jnlkiTdEoKr8CLzn0vY7M1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuMx7N%2FbtsMW8jlKwz%2FjnlkiTdEoKr8CLzn0vY7M1%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;700&quot; height=&quot;482&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 인프런에서 김영한 님의 &quot;스프링 핵심원리 - 기본편&quot;을 수강 후 공부한 내용을 정리한 게시글입니다. 부족한 부분이 있다면 언제든 지적 부탁드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 의존 관계 주입 방법에 대해서는 &amp;ldquo;이전 게시물&amp;rdquo;을 확인해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 게시물 -&amp;nbsp;&lt;a href=&quot;https://velog.io/@dksek3050/Spring-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%8A%A4%EC%BA%94-%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84-%EC%9E%90%EB%8F%99-%EC%A3%BC%EC%9E%85&quot;&gt;의존관계 자동주입 1&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 자동 주입 대상을 옵션으로 처리&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주입할 스프링 빈이 없어도 동작해야 할 때가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데&amp;nbsp;@Autowired&amp;nbsp;만 사용하면 required 옵션의 기본값이 true 로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다. 이를 해결하기 위해서는 아래와 같이 3가지 방법을 가지고 옵션처리하여 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시에서 Member 는 스프링 빈이 아니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@Autowired(required=false)&amp;nbsp;: 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨&lt;/li&gt;
&lt;li&gt;//호출 안됨 @Autowired(required = false) public void setNoBean1(Member member) { System.out.println(&quot;setNoBean1 = &quot; + member); } //결과 없음&lt;/li&gt;
&lt;li&gt;org.springframework.lang.@Nullable&amp;nbsp;: 자동 주입할 대상이 없으면 null이 입력된다.&lt;/li&gt;
&lt;li&gt;//null 호출 @Autowired public void setNoBean2(@Nullable Member member) { System.out.println(&quot;setNoBean2 = &quot; + member); } //결과 setNoBean2 = null&lt;/li&gt;
&lt;li&gt;Optional&amp;lt;&amp;gt;&amp;nbsp;: 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.&lt;/li&gt;
&lt;li&gt;//Optional.empty 호출 @Autowired(required = false) public void setNoBean3(Optional&amp;lt;Member&amp;gt; member) System.out.println(&quot;setNoBean3 = &quot; + member); } //결과 setNoBean3 = Optional.empty&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 생성자 주입을 선택하자!&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 롬북 Lombok&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 주입을 작성하는 과정에서 비슷한 코드르 여러번 작성하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 형식이 정해져 있는 코드들이기때문에 이를 롬북이라는 라이브러리에서 코드 작성을 간소화 시켜준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@RequiredArgsConstructor&amp;nbsp;기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다. (다음 코드에는 보이지 않지만 실제 호출 가능하다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 특정 빈을 조회할 때 조회된 빈이 2개 이상이면 ?&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ac.getBean(DiscountPolicy.class)&amp;nbsp;으로&amp;nbsp;DiscountPolicy스프링 빈 조회하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 이때&amp;nbsp;DiscountPolicy의 하위 타입인&amp;nbsp;FixDiscountPolicy&amp;nbsp;,&amp;nbsp;RateDiscountPolicy둘다 스프링 빈으로 선언되어 있어&amp;nbsp;ac.getBean(DiscountPolicy.class)&amp;nbsp;으로 조회하게 되면 반환되는 빈이 2개가 되어&amp;nbsp;NoUniqueBeanDefinitionException&amp;nbsp;오류가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;NoUniqueBeanDefinitionException: No qualifying bean of type
'hello.core.discount.DiscountPolicy' available: expected single matching bean
but found 2: fixDiscountPolicy,rateDiscountPolicy
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 하위 타입으로 지정할 수 도 있지만, 하위 타입으로 지정하는 것은 DIP를 위배하고 유연성이 떨어진다. 그리고 이름만 다르고, 완전히 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 안된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Autowired 필드명 매칭&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Autowired 는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Autowired
private DiscountPolicy rateDiscountPolicy;
&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;DiscountPolicy 타입으로 매칭을 시도하고 그 결과 여러 개의 빈이 존재할 경우 rateDiscountPolicy 라는 이름을 가진 빈이 있는지 추가로 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Qualifier @Qualifier끼리 매칭 빈 이름 매칭&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Qualifier 는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@Qualifier(&quot;mainDiscountPolicy&quot;)
public class RateDiscountPolicy implements DiscountPolicy {}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@Qualifier(&quot;fixDiscountPolicy&quot;)
public class FixDiscountPolicy implements DiscountPolicy {}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
                        @Qualifier(&quot;mainDiscountPolicy&quot;) DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}
&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;@Qualifier 가 mainDiscountPolicy 를 찾은 다음 매칭 한다.&lt;/li&gt;
&lt;li&gt;만약 @Qualifier 가 mainDiscountPolicy 인 빈이 없다면 빈 이름이 mainDiscountPolicy 빈을 찾아 매칭 시킨다.&lt;/li&gt;
&lt;li&gt;빈 이름도 동일한 빈이 없다면&amp;nbsp;NoSuchBeanDefinitionException&amp;nbsp;예외를 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Primary 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Primary&amp;nbsp;는 우선순위를 정하는 방법이다.&amp;nbsp;@Autowired&amp;nbsp;시에 여러 빈이 매칭되면&amp;nbsp;@Primary&amp;nbsp;가 우선권을 가진다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;//생성자
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
                        DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}
&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;우선순위가 더 높은 RateDiscountPolicy를 주입힌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 조회한 빈이 모두 필요할 때 List, Map&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도적으로 정말 해당 타입의 스프링 빈이 다 필요한 경우도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, DiscountPolicy 타입의 모든 빈을 다 조회하고 싶은 경우를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 아래와 같이 List나 Map 자료구조로 모두 조회할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;static class DiscountService {

    private final Map&amp;lt;String, DiscountPolicy&amp;gt; policyMap;
    private final List&amp;lt;DiscountPolicy&amp;gt; policies;

    public DiscountService(Map&amp;lt;String, DiscountPolicy&amp;gt; policyMap, List&amp;lt;DiscountPolicy&amp;gt; policies) {
        this.policyMap = policyMap;
        this.policies = policies;

        System.out.println(&quot;policyMap = &quot; + policyMap);
        System.out.println(&quot;policies = &quot; + policies);
    }
}
&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;Map&amp;lt;String, DiscountPolicy&amp;gt; : map의 키에 스프링 빈의 이름을 넣어주고, 그 값을 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.&lt;/li&gt;
&lt;li&gt;List : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 빈 등록 결론&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;편리한 자동 기능을 기본으로 사용하자&lt;/li&gt;
&lt;li&gt;직접 등록하는 기술 지원 객체는 수동 등록하자&lt;/li&gt;
&lt;li&gt;다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Programming/Spring</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/162</guid>
      <comments>https://dksek3050.tistory.com/162#entry162comment</comments>
      <pubDate>Wed, 26 Mar 2025 15:33:50 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 컴포넌트 스캔 &amp;amp; 의존관계 자동 주입</title>
      <link>https://dksek3050.tistory.com/161</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxfAWv/btsMX5F8dj4/jXaA2ckWWNVrhhoQhXgFS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxfAWv/btsMX5F8dj4/jXaA2ckWWNVrhhoQhXgFS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxfAWv/btsMX5F8dj4/jXaA2ckWWNVrhhoQhXgFS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxfAWv%2FbtsMX5F8dj4%2FjXaA2ckWWNVrhhoQhXgFS1%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;700&quot; height=&quot;482&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 인프런에서 김영한 님의 &quot;스프링 핵심원리 - 기본편&quot;을 수강 후 공부한 내용을 정리한 게시글입니다. 부족한 부분이 있다면 언제든 지적 부탁드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 자동으로 스프링 빈을 등록해 &amp;rarr; 컴포넌트 스캔(@ComponentScan, @Component)&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포넌트 스캔이란 ?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금까지의 과정에서 자바 코드의&amp;nbsp;@Bean&amp;nbsp;이나 XML의&amp;nbsp;&amp;lt;bean&amp;gt;&amp;nbsp;를 통해서 직접 스프링 빈을 등록&lt;/li&gt;
&lt;li&gt;상당히 귀찮고 누락할 수 있는 가능성이 높다는 문제점을 지닌다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이를 해결하기 위해 컴포넌트 스캔이라는 기능을 통해서 스프링 빈을 자동으로 등록할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;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;nbsp;@ComponentScan&amp;nbsp;을 설정 클래스에 붙여준다.&lt;/li&gt;
&lt;li&gt;기존의&amp;nbsp;AppConfig와는 다르게&amp;nbsp;@Bean으로 등록한 클래스가 하나도 없다!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AutoAppConfig.java&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package hello.core;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import static org.springframework.context.annotation.ComponentScan.*;
@Configuration
@ComponentScan
public class AutoAppConfig {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;컴포넌트 스캔은&amp;nbsp;@Component&amp;nbsp;어노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그래서 스프링 빈으로 등록하고 싶은 클래스를 컴포넌트 스캔의 대상이 되도록&amp;nbsp;@Component&amp;nbsp;어노테이션을 붙여주자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemoryMemberRepository.java&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Component
public class MemoryMemberRepository implements MemberRepository {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RateDiscountPolicy.java&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Component
public class RateDiscountPolicy implements DiscountPolicy {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberServiceImpl.java&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class MemberServiceImpl implements MemberService {

		private final MemberRepository memberRepository;

		@Autowired
		public MemberServiceImpl(MemberRepository memberRepository) {
				this.memberRepository = memberRepository;
		}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 의존관계 자동 주입(@Autowired)&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Autowired&amp;nbsp;는 의존관계를 자동으로 주입해준다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OrderServiceImpl.java&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class OrderServiceImpl implements OrderService {

		private final MemberRepository memberRepository;
		private final DiscountPolicy discountPolicy;

		@Autowired
		public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
			this.memberRepository = memberRepository;
			this.discountPolicy = discountPolicy;
		}

}
&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;@Autowired&amp;nbsp;를 사용하면 생성자에서 여러 의존관계도 한번에 주입받을 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 컴포넌트 스캔과 자동 의존관계 주입의 동작 과정&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@ComponentScan
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@ComponentScan 은 @Component 가 붙은 모든 클래스를 스프링 빈으로 등록한다.&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;빈 이름 기본 전략: MemberServiceImpl 클래스 memberServiceImpl&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cinxUF/btsMXclFn8B/CcaKpxuwlrZy5PyXeGpL51/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;@Autowired&amp;nbsp;의존관계 자동 주입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.&lt;/li&gt;
&lt;li&gt;기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dr2Oip/btsMX6rxi2r/QceUWXHZGjSAKPTg0mUIcK/img.png&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 탐색 위치와 기본 스캔 대상&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;탐색할 패키지의 시작 위치 지정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@ComponentScan(basePackages = &quot;hello.core&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;basePackages&amp;nbsp;: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다.&lt;/li&gt;
&lt;li&gt;basePackages = {&quot;hello.core&quot;, &quot;hello.service&quot;}&amp;nbsp;이렇게 여러 시작 위치를 지정할 수도 있다.&lt;/li&gt;
&lt;li&gt;basePackageClasses&amp;nbsp;: 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.&lt;/li&gt;
&lt;li&gt;만약 지정하지 않으면&amp;nbsp;@ComponentScan&amp;nbsp;이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.&lt;/li&gt;
&lt;li&gt;설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것을 권장.최근 스프링 부트도 이 방법을 기본으로 제공한다.&lt;/li&gt;
&lt;li&gt;스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인&amp;nbsp;@SpringBootApplication&amp;nbsp;를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다. (그리고 이 설정안에 바로&amp;nbsp;@ComponentScan&amp;nbsp;이 들어있다!)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포넌트 스캔 기본 대상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 스캔은 @Component 뿐만 아니라 다음과 내용도 추가로 대상에 포함한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Controller : 스프링 MVC 컨트롤러로 인식&lt;/li&gt;
&lt;li&gt;@Service : 사실 @Service 는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다&lt;/li&gt;
&lt;li&gt;@Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.&lt;/li&gt;
&lt;li&gt;@Configuration : 앞서 보았듯이 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 애노테이션에는 상속관계라는 것이 없다. 그래서 이렇게 애노테이션이 특정 애노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능은 아니고, 스프링이 지원하는 기능이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 중복 등록과 충돌&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자동 빈 등록 vs 자동 빈 등록&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생시킨다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ConflictingBeanDefinitionException&amp;nbsp;예외 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수동 빈 등록 vs 자동 빈 등록&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수동 빈 등록이 우선
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수동 빈이 자동 빈을 오버라이딩&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 다양한 의존관계 주입 방법&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성자 주입&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성자를 통해서 의존관계를 주입 받음&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;생성자 호출 시점에 딱 1번만 호출됨이 보장&lt;/li&gt;
&lt;li&gt;불변, 필수 의존관계에서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;생성자가 딱 1개만 있으면 @Autowired를 생략 가능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
            discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수정자 주입(setter 주입)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;setter 메소드(수정자 메소드)를 통해서 의존관계를 주입 받음&lt;/li&gt;
&lt;li&gt;특징
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바빈 프로퍼티 규약의 수정자 메소드 방식을 사용하는 방법&lt;/li&gt;
&lt;li&gt;선택, 변경 가능성이 있는 의존관계에서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;@Autowired&amp;nbsp;의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면&amp;nbsp;@Autowired(required = false)&amp;nbsp;로 지정하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필터 주입&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필드에 바로 주입&lt;/li&gt;
&lt;li&gt;특징
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드가 간결하지만 외부에서 변경이 불가능해서 테스트하기 어려움&lt;/li&gt;
&lt;li&gt;DI 프레임워크가 없으면 아무것도 할 수 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용하지 말자!!!!&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어플리케이션의 실제 코드와 관계없는 테스트 코드가 생성됨&lt;/li&gt;
&lt;li&gt;스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;@Component
public class OrderServiceImpl implements OrderService {
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 메소드 주입&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 메소드를 통해서 주입&lt;/li&gt;
&lt;li&gt;특징
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한번에 여러번 필드를 주입받을 수 있다.&lt;/li&gt;
&lt;li&gt;일반적으로 잘 사용하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void init(MemberRepository memberRepository, DiscountPolicy
            discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Programming/Spring</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/161</guid>
      <comments>https://dksek3050.tistory.com/161#entry161comment</comments>
      <pubDate>Wed, 26 Mar 2025 15:31:52 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 싱글톤 컨테이너</title>
      <link>https://dksek3050.tistory.com/160</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 인프런에서 김영한 님의 &quot;스프링 핵심원리 - 기본편&quot;을 수강 후 공부한 내용을 정리한 게시글입니다. 부족한 부분이 있다면 언제든 지적 부탁드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;623&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m2cF7/btsMWuUZy0d/poaC0PcRMUlg1y9yqvRyxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m2cF7/btsMWuUZy0d/poaC0PcRMUlg1y9yqvRyxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m2cF7/btsMWuUZy0d/poaC0PcRMUlg1y9yqvRyxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm2cF7%2FbtsMWuUZy0d%2FpoaC0PcRMUlg1y9yqvRyxk%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;904&quot; height=&quot;623&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;623&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 싱글톤 패턴을 사용하는 이유&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 없는 순수한 DI 컨테이너를 가진 어플리케이션을 사용자에게 배포 했다고 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 어플리케이션이 대박이 나서 동시 접속자수가 만명이 넘는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 만명 사용자가 동시다발적으로 서버에 요청을 보내게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 어플리케이션에서의AppConfig&amp;nbsp;는 아래와 같다&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }

    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때, 로그인을 만 명이 동시에 한다고 생각해보면 로그인이 기능이 있는 memberService() 메소드가 만번 실행되고&amp;nbsp;return new MemberServiceImpl(memberRepository());&amp;nbsp;가 만 번 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면&amp;nbsp;MemberServiceImpl&amp;nbsp;인스턴스가 만 개이 생성되게 된다. 또한 memberRepository()도 호출되면서&amp;nbsp;MemoryMemberRepostiory&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;1033&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wJquD/btsMXPDA1Bo/Tp19vw6FNtEO2Y67qas5Q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wJquD/btsMXPDA1Bo/Tp19vw6FNtEO2Y67qas5Q1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wJquD/btsMXPDA1Bo/Tp19vw6FNtEO2Y67qas5Q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwJquD%2FbtsMXPDA1Bo%2FTp19vw6FNtEO2Y67qas5Q1%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;1033&quot; height=&quot;540&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 메모리 낭비가 엄청나게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해서 해당 객체의 인스턴스들을 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;우리가 자바를 배우면서&amp;nbsp;변수를 공유&amp;nbsp;한다고 하면 떠오르는 키워드가 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로&amp;nbsp;static&amp;nbsp;이를 사용해서 딱 1개만 생성하여 사용하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 싱글톤 패턴 구현&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱글톤 패턴&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴을 말한다.&lt;/li&gt;
&lt;li&gt;객체 인스턴스가 2개 이상 생성하지 못하도록 막아야 한다.&lt;/li&gt;
&lt;li&gt;private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막는다.&lt;/li&gt;
&lt;li&gt;인스턴스를 1개만 생성하여 공유해서 서용하도록 설계한다.&lt;/li&gt;
&lt;li&gt;우리가 자바를 배우면서&amp;nbsp;변수를 공유&amp;nbsp;한다고 하면 떠오르는 키워드가 있을 것이다.&lt;/li&gt;
&lt;li&gt;바로&amp;nbsp;static&amp;nbsp;이를 사용해서 딱 1개만 생성하여 사용하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱글톤 예제&lt;/h3&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;package hello.core.singleton;
public class SingletonService {

	//1. static 영역에 객체를 딱 1개만 생성해둔다.
	private static final SingletonService instance = new SingletonService();

	//2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한
	다.
	public static SingletonService getInstance() {
		return instance;
	}

	//3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
	private SingletonService() {
	}

	public void logic() {
		System.out.println(&quot;싱글톤 객체 로직 호출&quot;);
	}

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.&lt;/li&gt;
&lt;li&gt;이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.&lt;/li&gt;
&lt;li&gt;딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 외부에서 SingletonService.getInstace() 를 통해 서비스를 호출하게 되면 같은 인스턴스를 반환해주는다는 것을 알 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱글톤 패턴의 문제점&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.&lt;/li&gt;
&lt;li&gt;클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.&lt;/li&gt;
&lt;li&gt;테스트하기 어렵다.&lt;/li&gt;
&lt;li&gt;내부 속성을 변경하거나 초기화 하기 어렵다.&lt;/li&gt;
&lt;li&gt;private 생성자로 자식 클래스를 만들기 어렵다.&lt;/li&gt;
&lt;li&gt;결론적으로 유연성이 떨어진다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 스프링에서의 싱글톤 컨테이너&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 싱글톤 패턴의 문제점이 존재해서 스프링에서는 스프링 빈을 싱글톤으로 관리해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱글톤 컨테이너&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.&lt;/li&gt;
&lt;li&gt;스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.&lt;/li&gt;
&lt;li&gt;스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 컨테이너를 사용하는 테스트 코드&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;스프링 컨테이너와 싱글톤&quot;)
void springContainer() {
		ApplicationContext ac = new
		AnnotationConfigApplicationContext(AppConfig.class);
		//1. 조회: 호출할 때 마다 같은 객체를 반환
		MemberService memberService1 = ac.getBean(&quot;memberService&quot;,
		MemberService.class);
		//2. 조회: 호출할 때 마다 같은 객체를 반환
		MemberService memberService2 = ac.getBean(&quot;memberService&quot;,
		MemberService.class);
		//참조값이 같은 것을 확인
		System.out.println(&quot;memberService1 = &quot; + memberService1);
		System.out.println(&quot;memberService2 = &quot; + memberService2);
		//memberService1 == memberService2
		assertThat(memberService1).isSameAs(memberService2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1049&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z4EaH/btsMWjMF7NV/jt1IRDYHoEmWM9fyJ309V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z4EaH/btsMWjMF7NV/jt1IRDYHoEmWM9fyJ309V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z4EaH/btsMWjMF7NV/jt1IRDYHoEmWM9fyJ309V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ4EaH%2FbtsMWjMF7NV%2Fjt1IRDYHoEmWM9fyJ309V0%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;1049&quot; height=&quot;534&quot; data-origin-width=&quot;1049&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SS&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 싱글톤 방식의 주의점&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;무상태(stateless)로 설계해야 한다.&lt;/li&gt;
&lt;li&gt;스프링 빈의 필드에 공유 값을 설정하면 큰 장애가 발생!&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상태를 유지할 경우 발생하는 문제점 예시&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package hello.core.singleton;
public class StatefulService {
	private int price; //상태를 유지하는 필드
	public void order(String name, int price) {
		System.out.println(&quot;name = &quot; + name + &quot; price = &quot; + price);
		this.price = price; //여기가 문제!
	}
	public int getPrice() {
		return price;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import
		org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
	@Test
	void statefulServiceSingleton() {
		ApplicationContext ac = new
				AnnotationConfigApplicationContext(TestConfig.class);
		StatefulService statefulService1 = ac.getBean(&quot;statefulService&quot;,
				StatefulService.class);
		StatefulService statefulService2 = ac.getBean(&quot;statefulService&quot;,
				StatefulService.class);
		//ThreadA: A사용자 10000원 주문
		statefulService1.order(&quot;userA&quot;, 10000);
		//ThreadB: B사용자 20000원 주문
		statefulService2.order(&quot;userB&quot;, 20000);
		//ThreadA: 사용자A 주문 금액 조회
		int price = statefulService1.getPrice();
		//ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
		System.out.println(&quot;price = &quot; + price);
		Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
	}
	static class TestConfig {
		@Bean
		public StatefulService statefulService() {
			return new StatefulService();
		}
	}
}
&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;ThreadA가 사용자A 코드를 호출하고 ThreadB가 사용자B 코드를 호출한다 가정하자.&lt;/li&gt;
&lt;li&gt;StatefulService 의 price 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.&lt;/li&gt;
&lt;li&gt;사용자A의 주문 금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나왔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ @Configuration과 싱글톤&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}

	@Bean
	public OrderService orderService() {
		return new OrderServiceImpl(
				memberRepository(),
				discountPolicy());
	}

	@Bean
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}

 ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memberService 빈을 만드는 코드를 보면 memberRepository() 를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;orderService 빈을 만드는 코드도 동일하게 memberRepository() 를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 각각 다른 2개의 MemoryMemberRepository 가 생성되면서 싱글톤이 깨지는 것 처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 실질적으로는 memberRepository 인스턴스는 1개의 인스터스로 공유되어 사용되어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 비밀은 @Configuration 을 적용한 AppConfig 에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 될 수 있는 이유는 스프링 컨테이너가 싱글톤 레지스트리이기 때문이다. 따라스 스프링 빈이 싱글톤이 되도록 보장 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 스프링 빈이면 무조건 싱글톤일까 ?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것도 아니다. @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용하기 때문에 싱글톤을 보장하는데 CGLIB 기술 없이 빈을 등록하면 순수한 AppConfig로 스프링 빈에 등록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 CGLIB는 무엇일까 ?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈으로 등록될때 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 내가 생성한 클래스를 상속 받은 임의의 다른 클래스를 만들고 그 클래스를 스프링 빈으로 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 등록한 그 클래스가 싱글톤이 되도록 보장해준다. CGLIB 내부에서 해당하는 스프링 빈이 존재하면 존재하는 빈을 반환하고 없으면 스프링빈으로 등록하는 내부 로직이 있기 때문에 싱글톤이 보장 될 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;539&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btw3GR/btsMYqccpKR/uJdUyuPhprmPf73sSVQyJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btw3GR/btsMYqccpKR/uJdUyuPhprmPf73sSVQyJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btw3GR/btsMYqccpKR/uJdUyuPhprmPf73sSVQyJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbtw3GR%2FbtsMYqccpKR%2FuJdUyuPhprmPf73sSVQyJ1%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;1041&quot; height=&quot;539&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;539&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Configuration 을 적용하지 않고, @Bean 만 적용하면 어떻게 될까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만, 만약 @Bean만 적용하면 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된다.&lt;/p&gt;</description>
      <category>Programming/Spring</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/160</guid>
      <comments>https://dksek3050.tistory.com/160#entry160comment</comments>
      <pubDate>Wed, 26 Mar 2025 15:30:40 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 컨테이너와 스프링 빈</title>
      <link>https://dksek3050.tistory.com/159</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwkBoc/btsMWjFUDBZ/2bk3KjlS8wednStkVBYtlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwkBoc/btsMWjFUDBZ/2bk3KjlS8wednStkVBYtlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwkBoc/btsMWjFUDBZ/2bk3KjlS8wednStkVBYtlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwkBoc%2FbtsMWjFUDBZ%2F2bk3KjlS8wednStkVBYtlk%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;700&quot; height=&quot;482&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 인프런에서 김영한 님의 &quot;스프링 핵심원리 - 기본편&quot;을 수강 후 공부한 내용을 정리한 게시글입니다. 부족한 부분이 있다면 언제든 지적 부탁드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 스프링 컨테이너&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 컨테이너 생성&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;//스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
&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;ApplicationContext 는 인터페이스이며&amp;nbsp;스프링 컨테이너라 한다.&lt;/li&gt;
&lt;li&gt;스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.&lt;/li&gt;
&lt;li&gt;직전에 AppConfig 를 사용했던 방식이 애노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것이다.&lt;/li&gt;
&lt;li&gt;AnnotationConfigApplicationContext는 ApplicationContext의 인터페이의 구현체&lt;/li&gt;
&lt;li&gt;스프링 컨테이너를 생성할 때는 구성 정보 (= AppConfig.class (빈 관계 정보))를 지정해주어야 한다.&lt;/li&gt;
&lt;li&gt;스프링 컨테이너의 스프링 빈 저장소에&amp;nbsp;빈 이름을 key로 빈 객체를 value로 하여 스프링 빈을 등록한다&lt;/li&gt;
&lt;li&gt;빈 이름은 메서드 이름을 사용한다. ( 경우에 따라 빈 이름을 직접 부여할 수 있다. )&lt;/li&gt;
&lt;li&gt;빈 이름은 유일해야한다.(유니크 해야한다.)&lt;/li&gt;
&lt;li&gt;스프링 컨테이너는 설정 정보(= AppConfig.class )를 참고해서 의존관계를 주입(DI) 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 스프링 빈 조회&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;package hello.core.beanfind;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
class ApplicationContextInfoTest {

	AnnotationConfigApplicationContext ac = new
			AnnotationConfigApplicationContext(AppConfig.class);

	@Test
	@DisplayName(&quot;모든 빈 출력하기&quot;)
	void findAllBean() {
		String[] beanDefinitionNames = ac.getBeanDefinitionNames();
		for (String beanDefinitionName : beanDefinitionNames) {
			Object bean = ac.getBean(beanDefinitionName);
			System.out.println(&quot;name=&quot; + beanDefinitionName + &quot; object=&quot; +
					bean);
		}
	}

	@Test
	@DisplayName(&quot;애플리케이션 빈 출력하기&quot;)
	void findApplicationBean() {
		String[] beanDefinitionNames = ac.getBeanDefinitionNames();
		for (String beanDefinitionName : beanDefinitionNames) {
			BeanDefinition beanDefinition =
					ac.getBeanDefinition(beanDefinitionName);
			//Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
			//Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
			if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
				Object bean = ac.getBean(beanDefinitionName);
				System.out.println(&quot;name=&quot; + beanDefinitionName + &quot; object=&quot; +
						bean);
			}
		}

	}
}
&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;ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다.&lt;/li&gt;
&lt;li&gt;ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ac.getBean(빈이름, 타입)&lt;/li&gt;
&lt;li&gt;ac.getBean(타입)&lt;/li&gt;
&lt;li&gt;조회 대상 스프링 빈이 없으면 예외 발생NoSuchBeanDefinitionException: No bean named 'xxxxx' available&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스프링이 내부에서 사용하는 빈은 getRole() 로 구분할 수 있다.&lt;/li&gt;
&lt;li&gt;ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈&lt;/li&gt;
&lt;li&gt;ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동일한 타입이 둘 이상&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;MemberRepository memberRepository = ac.getBean(&quot;memberRepository1&quot;, MemberRepository.class);
&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;li&gt;ac.getBeansOfType() 을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.&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;그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면, 모든 스프링 빈을 조회한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ BeanFactory와 ApplicationContext&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BeanFactory&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;getBean() 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ApplicationContext&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BeanFactory 기능을 모두 상속받아서 제공&lt;/li&gt;
&lt;li&gt;빈을 관리하고 조회하는 기능과 같이 수 많은 부가기능을 제공&lt;/li&gt;
&lt;li&gt;BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용&lt;/li&gt;
&lt;li&gt;메시지소스를 활용한 국제화 기능&lt;/li&gt;
&lt;li&gt;환경변수 기능을 제공하여 로컬, 개발, 운영등을 구분해서 처리&lt;/li&gt;
&lt;li&gt;애플리케이션 이벤트 기능을 통해 이벤트를 발행하고 구독하는 모델을 편리하게 지원&lt;/li&gt;
&lt;li&gt;편리한 리소스 조회하는 기능을 제공하여 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 다양한 설정 형식 지원 - 자바 코드, XML&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;new AnnotationConfigApplicationContext(AppConfig.class)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;XML 설정 사용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GenericXmlApplicationContext&amp;nbsp;사용해서 xml 설정 파일을 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.core.xml;
import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import static org.assertj.core.api.Assertions.*;
public class XmlAppContext {
	@Test
	void xmlAppContext() {
		ApplicationContext ac = new
				GenericXmlApplicationContext(&quot;appConfig.xml&quot;);

		MemberService memberService = ac.getBean(&quot;memberService&quot;,
				MemberService.class);
		assertThat(memberService).isInstanceOf(MemberService.class);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;


		

		

		


&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 스프링 빈 설정 메타 정보 - BeanDefinition&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;BeanDefinition 추상화를 이용하여 구현 객체를 다양한 설정 형식에 맞게 바꿔 끼울 수 있다.&lt;/li&gt;
&lt;li&gt;BeanDefinition 을 빈 설정 메타정보라 한다.&lt;/li&gt;
&lt;li&gt;@Bean , 당 각각 하나씩 메타 정보가 생성된다.&lt;/li&gt;
&lt;li&gt;스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.&lt;/li&gt;
&lt;li&gt;AnnotationConfigApplicationContext는&amp;nbsp;AnnotatedBeanDefinitionReader를 사용해서 AppConfig.class 를 읽고 BeanDefinition 을 생성한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Programming/Spring</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/159</guid>
      <comments>https://dksek3050.tistory.com/159#entry159comment</comments>
      <pubDate>Wed, 26 Mar 2025 15:27:51 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링의 핵심 원리 이해2 - 객체 지향 원리 적용</title>
      <link>https://dksek3050.tistory.com/158</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/me99E/btsMXvSQKjA/ea8dYhVRiiC734mt8gR6u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/me99E/btsMXvSQKjA/ea8dYhVRiiC734mt8gR6u1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/me99E/btsMXvSQKjA/ea8dYhVRiiC734mt8gR6u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fme99E%2FbtsMXvSQKjA%2Fea8dYhVRiiC734mt8gR6u1%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;700&quot; height=&quot;482&quot; data-filename=&quot;edited_스프링.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 인프런에서 김영한 님의 &quot;스프링 핵심원리 - 기본편&quot;을 수강 후 공부한 내용을 정리한 게시글입니다. 부족한 부분이 있다면 언제든 지적 부탁드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 비즈니스 설계 Remind&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ DIP, OCP 위반 상황 설명&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&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;변경에 따라 RateDiscountPolicy를 구현하고 할인 정책을 변경하려면 클라이언트인&amp;nbsp;OrderServiceImpl코드를 고쳐야 한다.&lt;/p&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;public class OrderServiceImpl implements OrderService {
		// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
		private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이렇게 될 경우 DIP, OCP 를 위반하게 되는 문제가 발생된다!&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DIP (의존관계 역전 원칙) : 추상화에 의존 해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문서비스 클라이언트(&amp;nbsp;OrderServiceImpl)는&amp;nbsp;DiscountPolicy인터페이스에 의존하면서 DIP를 지킨 것 같아 보이지만 클래스 의존관계를 분석해보면 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다. &amp;rArr;&amp;nbsp;&lt;b&gt;DIP 위반 !&lt;/b&gt;&amp;rarr; 구체(구현) 클래스:&amp;nbsp;FixDiscountPolicy,&amp;nbsp;RateDiscountPolicy&lt;/li&gt;
&lt;li&gt;![image](&amp;lt;&lt;a href=&quot;https://github.com/SpringSync/Spring-Basic/assets/99172832/26a873ff-8b3b-4b1b-a3c3-9718ecfda683&quot;&gt;https://github.com/SpringSync/Spring-Basic/assets/99172832/26a873ff-8b3b-4b1b-a3c3-9718ecfda683&lt;/a&gt;&amp;gt;) ![image](&amp;lt;&lt;a href=&quot;https://github.com/SpringSync/Spring-Basic/assets/99172832/19e4c289-e6c9-4fbf-997f-6f91354fafe9&quot;&gt;https://github.com/SpringSync/Spring-Basic/assets/99172832/19e4c289-e6c9-4fbf-997f-6f91354fafe9&lt;/a&gt;&amp;gt;) 위 그림처럼 의존되어 있다고 생각하지만 실제로는 아래 그림처럼 의존되어있다.&lt;/li&gt;
&lt;li&gt;&amp;rarr; 추상(인터페이스) 의존:&amp;nbsp;DiscountPolicy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OCP (개방폐쇄의 원칙) : 확장에는 열려있으나 변경에는 닫혀 있어야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 즉,&amp;nbsp;FixDiscountPolicy를&amp;nbsp;RateDiscountPolicy로 변경하는 순간&amp;nbsp;OrderServiceImpl의 소스&lt;/li&gt;
&lt;li&gt;코드도 함께 변경해야 한다! &amp;rArr;&amp;nbsp;&lt;b&gt;OCP 위반!&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이를 해결하기 위해서는 ?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터페이스(추상)에만 의존하도록 변경해야한다!&lt;/li&gt;
&lt;li&gt;public class OrderServiceImpl implements OrderService { //private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); private DiscountPolicy discountPolicy; }&lt;/li&gt;
&lt;li&gt;위 코드는 인터페이스에만 의존하도록 코드를 변경하였지만 구현체가 없어서 NullPointException이 발생한다.&lt;/li&gt;
&lt;li&gt;이를 해결하기 위해서는 누군가가 클라이언트인&amp;nbsp;OrderServiceImpl&amp;nbsp;에&amp;nbsp;DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다. 그 역할을 하는 것이 다음에 알아 볼 AppConfig이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 관심사 분리 - AppConfig&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관심사 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문에 대한 서비스와 멤버를 관리하는 서비스가 각각의 역할(인터페이스)를 하듯이 구현 객체를 생성하고 연결하는 역할을 하는 클래스를 별도로 만들어서 관심사를 분리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스인 AppConfig를 만들어보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AppConfig&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진과 같이&amp;nbsp;AppConfig가&amp;nbsp;MemberServiceImpl과&amp;nbsp;MemoryMemberRepository와 같이 구현 객체를 생성하여 주입해줄 수 있도록 만들어 볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 하느냐?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig&amp;nbsp;에서 MemberService 구현 객체를 생성해서 반환하는 메소드를 만들어 둘 것이다. &amp;rarr;&amp;nbsp;memberService()&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(
                new MemoryMemberRepository(),
                new FixDiscountPolicy());
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 memberService() 메소드를 설명하면 MemberServie의 구현 객체인 MemberServiceImpl 를 생성하여 반환하는 메소드 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메소드를 통해서 처음 어플리케이션이 실행되면서&amp;nbsp;MemberService&amp;nbsp;가 생성되어야 할 때 MemberServiceImpl을 생성하고 주입할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;orderService() 메소드 또한 OrderService 구현 객체인 OrderServiceImpl를 생성하여 반환하는 메소드이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MemberServiceImpl - 생성자 주입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Appconfig의 memberService()&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig에서 MemberServiceImpl를 생성하면서 MemberRepository의 구현 객체인&amp;nbsp;MemoryMemberRepository&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;MemberServiceImpl의 생성자 메소드에서 파라미터를&amp;nbsp;MemberRepositoy로 받고 있어야 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.core.member;

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

		. . .

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberRepositroy의 구현객체의 생성은 AppConfig에서만 일어날 뿐 MemberServiceImpl에서는 넣어준 객체를 주입만 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberServiceImpl입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다. 어떤 구현 객체를 주입할지는 오직 외부(&amp;nbsp;AppConfig)에서 결정된다. 그렇기 때문에 MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부(AppConfig)에 맡기고 실행에만 집중하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다&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;MemberServiceImpl인 구현 객체는&amp;nbsp;MemberRepository인 인터페이스에만 의존하게 된다. &amp;rArr;&amp;nbsp;&lt;b&gt;DIP 완성!&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OrderServiceImpl - 생성자 주입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig - orderService()&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public OrderService orderService() {
        return new OrderServiceImpl(
                new MemoryMemberRepository(),
                new FixDiscountPolicy());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
            discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
		. . .
}
&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;설계 변경으로 OrderServiceImpl 은 FixDiscountPolicy 를 의존하지 않는다!&lt;/li&gt;
&lt;li&gt;단지 DiscountPolicy 인터페이스만 의존한다.&lt;/li&gt;
&lt;li&gt;즉 구현객체에 의존하지 않고 인터페이스에만 의존한다.&lt;/li&gt;
&lt;li&gt;OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.&lt;/li&gt;
&lt;li&gt;OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부( AppConfig)에서 결정한다.&lt;/li&gt;
&lt;li&gt;OrderServiceImpl 은 이제부터 실행에만 집중하면 된다.&lt;/li&gt;
&lt;li&gt;OrderServiceImpl 에는 MemoryMemberRepository , FixDiscountPolicy 객체의 의존관계가 주입된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
public class MemberApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        Member member = new Member(1L, &quot;memberA&quot;, Grade.VIP);
        memberService.join(member);
        Member findMember = memberService.findMember(1L);
        System.out.println(&quot;new member = &quot; + member.getName());
        System.out.println(&quot;find Member = &quot; + findMember.getName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
public class OrderApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();
        long memberId = 1L;
        Member member = new Member(memberId, &quot;memberA&quot;, Grade.VIP);
        memberService.join(member);
        Order order = orderService.createOrder(memberId, &quot;itemA&quot;, 10000);
        System.out.println(&quot;order = &quot; + order);
    }
}
&lt;/code&gt;&lt;/pre&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;nbsp;AppConfig가 담당한다.&lt;/li&gt;
&lt;li&gt;AppConfig&amp;nbsp;는 어플리케이션의 실제 동작에 필요한 구현 객체를 &amp;ldquo;생성&amp;rdquo;한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MemberServiceImpl&lt;/li&gt;
&lt;li&gt;MemoryMemberRepository&lt;/li&gt;
&lt;li&gt;OrderServiceImpl&lt;/li&gt;
&lt;li&gt;FixDiscountPolicy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를&amp;nbsp;생성자를 통해서 주입(연결)해준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MemberServiceImpl&amp;nbsp;&amp;rarr;&amp;nbsp;MemoryMemberRepository&lt;/li&gt;
&lt;li&gt;OrderServiceImpl&amp;nbsp;&amp;rarr;&amp;nbsp;MemoryMemberRepository&amp;nbsp;,&amp;nbsp;FixDiscountPolicy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AppConfig 리팩토링&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;new MemoryMemberRepository()&amp;nbsp;이 부분이 중복 제거되었다. 이제&amp;nbsp;MemoryMemberRepository&amp;nbsp;를다른 구현체로 변경할 때 한 부분만 변경하면 된다.&lt;/li&gt;
&lt;li&gt;AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }

    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 새로운 구조&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&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;AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FixDiscountPolicy RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고, 사용 영역은 전혀&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영향을 받지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig의 discountPolicy()&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public DiscountPolicy discountPolicy() {
		// return new FixDiscountPolicy();
		 return new RateDiscountPolicy();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 할인 정책을 변경해도, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다. 클라이언트 코드인 OrderServiceImpl 를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 AppConfig 코드만 변경해주면 주입관계가 위 사진과 같이 변경된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 좋은 객체 지향 설계를 하고 있을까?&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SRP 단일 책임 원칙&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 클래스는 하나의 책임만 가져야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 객체는 실행하는 책임만 담당&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OCP 개방 폐쇄의 원칙&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig가 FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DIP 의존 관계 역전 원칙&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추상화에 의존해야지, 구체화에 의존하면 안된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig가 의존관계를 FixDiscountPolicy &amp;rarr; RateDiscountPolicy 로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제어의 역전 IoC(Inversion of Control)&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; 구현 객체가 프로그램의 제어 흐름을 스스로 조종&lt;/li&gt;
&lt;li&gt;반면에 AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간다. 예를 들어서 OrderServiceImpl 은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모른다.&lt;/li&gt;
&lt;li&gt;프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다. 심지어 OrderServiceImpl 도AppConfig가 생성한다. 그리고 AppConfig는 OrderServiceImpl 이 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수 도 있다. 그런 사실도 모른체 OrderServiceImpl 은 묵묵히 자신의 로직을 실행할 뿐이다.&lt;/li&gt;
&lt;li&gt;이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+) 프레임워크 vs 라이브러리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다. (JUnit)&lt;/li&gt;
&lt;li&gt;반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존관계 주입 DI(Dependency Injection)&lt;/h3&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;클래스가 사용하는 import 코드만 보고 의존관계를 판단&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.&lt;/li&gt;
&lt;li&gt;객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.&lt;/li&gt;
&lt;li&gt;의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.&lt;/li&gt;
&lt;li&gt;의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IoC 컨테이너, DI 컨테이너&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✏️ 스프링으로 전환&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AppConfig 스프링 기반으로 변경&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}
&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;설정을 구성한다는 뜻의 @Configuration 을 붙여준다.&lt;/li&gt;
&lt;li&gt;메서드에 @Bean 을 붙여준다. 이렇게 하면 스프링 컨테이너에 스프링 빈으로 등록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MemberApp에 스프링 컨테이너 적용&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MemberApp {
    public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
        ApplicationContext applicationContext = new
                AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService =
                applicationContext.getBean(&quot;memberService&quot;, MemberService.class);
        Member member = new Member(1L, &quot;memberA&quot;, Grade.VIP);
        memberService.join(member);
        Member findMember = memberService.findMember(1L);
        System.out.println(&quot;new member = &quot; + member.getName());
        System.out.println(&quot;find Member = &quot; + findMember.getName());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OrderApp에 스프링 컨테이너 적용&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class OrderApp {
    public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// OrderService orderService = appConfig.orderService();
        ApplicationContext applicationContext = new
                AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService =
                applicationContext.getBean(&quot;memberService&quot;, MemberService.class);
        OrderService orderService = applicationContext.getBean(&quot;orderService&quot;,
                OrderService.class);
        long memberId = 1L;
        Member member = new Member(memberId, &quot;memberA&quot;, Grade.VIP);
        memberService.join(member);
        Order order = orderService.createOrder(memberId, &quot;itemA&quot;, 10000);
        System.out.println(&quot;order = &quot; + order);
    }
}
&lt;/code&gt;&lt;/pre&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;ApplicationContext&amp;nbsp;를 스프링 컨테이너라 한다.기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.&lt;/li&gt;
&lt;li&gt;스프링 컨테이너는&amp;nbsp;@Configuration이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된객체를 스프링 빈이라 한다.&lt;/li&gt;
&lt;li&gt;스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. ( memberService , orderService )&lt;/li&gt;
&lt;li&gt;이전에는 개발자가 필요한 객체를 AppConfig 를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.&lt;/li&gt;
&lt;li&gt;기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Programming/Spring</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/158</guid>
      <comments>https://dksek3050.tistory.com/158#entry158comment</comments>
      <pubDate>Wed, 26 Mar 2025 15:26:41 +0900</pubDate>
    </item>
    <item>
      <title>[백준 Java] 1407번 2로 몇 번 나누어질까</title>
      <link>https://dksek3050.tistory.com/156</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Algorithm-Problem-Solving.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zsZYe/btsMUoeOaiU/bmcgkxcQztKFezmlUqIfi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zsZYe/btsMUoeOaiU/bmcgkxcQztKFezmlUqIfi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zsZYe/btsMUoeOaiU/bmcgkxcQztKFezmlUqIfi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzsZYe%2FbtsMUoeOaiU%2FbmcgkxcQztKFezmlUqIfi1%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;700&quot; height=&quot;536&quot; data-filename=&quot;edited_Algorithm-Problem-Solving.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; 문제&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Gold IV] 2로 몇 번 나누어질까 - 1407&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1407&quot;&gt;문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 요약&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리: 11552 KB, 시간: 76 ms&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; 풀이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범위가 무려 1&amp;le;A&amp;le;B&amp;le;1015&amp;nbsp;로 엄청나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 A ~ B 까지 순회하면서 더할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 ~ n까지 소수 x의 배수의 개수를 구하는 방식을 이용하였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  1 ~ n까지 수들은 소수 x로 몇번 나누어 떨어질까 ?&lt;/p&gt;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1부터 20까지 순회하며 각 수를 소인수분해 했을때 2가 몇 번씩 들어가는지 적어보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 2가 들어간 횟수만큼 동그라미로 표현하게 되면 아래 사진과 같이 표시되며 오른쪽과 같은 의미를 가질 수 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPk6NV/btsMVgmY73Z/5scbVKQeXP6CHkxFWGZGQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPk6NV/btsMVgmY73Z/5scbVKQeXP6CHkxFWGZGQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPk6NV/btsMVgmY73Z/5scbVKQeXP6CHkxFWGZGQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPk6NV%2FbtsMVgmY73Z%2F5scbVKQeXP6CHkxFWGZGQ0%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;1091&quot; height=&quot;155&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위를 참고하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;|&amp;nbsp;&lt;s&gt;1&lt;/s&gt;&amp;nbsp;| 2 |&amp;nbsp;&lt;s&gt;3&lt;/s&gt;&amp;nbsp;| 4 |&amp;nbsp;&lt;s&gt;5&lt;/s&gt;&amp;nbsp;| 6 |&amp;nbsp;&lt;s&gt;7&lt;/s&gt;&amp;nbsp;| 8 |&amp;nbsp;&lt;s&gt;9&lt;/s&gt;&amp;nbsp;| 10 |&amp;nbsp;&lt;s&gt;11&lt;/s&gt;&amp;nbsp;| 12 |&amp;nbsp;&lt;s&gt;13&lt;/s&gt;&amp;nbsp;| 14 |&amp;nbsp;&lt;s&gt;15&lt;/s&gt;&amp;nbsp;| 16 |&amp;nbsp;&lt;s&gt;17&lt;/s&gt;&amp;nbsp;| 18 |&amp;nbsp;&lt;s&gt;19&lt;/s&gt;&amp;nbsp;| 20 |&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2로 나누어 지지 않는 수(홀수)는&amp;nbsp;20=1인 2의 거듭제곱 꼴 중 가장 큰 약수를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 여기서 홀수의 개수는 어떻게 구할까 ?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ceil(20/2) = 10&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;|&amp;nbsp;&lt;s&gt;2&lt;/s&gt;&amp;nbsp;| 4 |&amp;nbsp;&lt;s&gt;6&lt;/s&gt;&amp;nbsp;| 8 |&amp;nbsp;&lt;s&gt;10&lt;/s&gt;&amp;nbsp;| 12 |&amp;nbsp;&lt;s&gt;14&lt;/s&gt;&amp;nbsp;| 16 |&amp;nbsp;&lt;s&gt;18&lt;/s&gt;&amp;nbsp;| 20 |&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4로 나누어 지지 않는 수는&amp;nbsp;21=2인 2의 거듭제곱 꼴 중 가장 큰 약수를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 여기서 4로 나누어 지지 않는 수의 개수는 어떻게 구할까 ?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ceil(10/2) = 5&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;|&amp;nbsp;&lt;s&gt;4&lt;/s&gt;&amp;nbsp;| 8 |&amp;nbsp;&lt;s&gt;12&lt;/s&gt;&amp;nbsp;| 16 |&amp;nbsp;&lt;s&gt;20&lt;/s&gt;&amp;nbsp;|&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8로 나누어 지지 않는 수는&amp;nbsp;22=4인 2의 거듭제곱 꼴 중 가장 큰 약수를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 여기서 8로 나누어 지지 않는 수의 개수는 어떻게 구할까 ?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ceil(5/2) = 3 (이런 경우 때문에 올림을 해야한다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;|&amp;nbsp;&lt;s&gt;8&lt;/s&gt;&amp;nbsp;| 16 |&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;16로 나누어 지지 않는 수는&amp;nbsp;23=8인 2의 거듭제곱 꼴 중 가장 큰 약수를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 여기서 8로 나누어 지지 않는 수의 개수는 어떻게 구할까 ?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ceil(2/2) = 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;| 16 |&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;32로 나누어 지지 않는 수는&amp;nbsp;24=16인 2의 거듭제곱 꼴 중 가장 큰 약수를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 여기서 16로 나누어 지지 않는 수의 개수는 어떻게 구할까 ?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ceil(1/2) = 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게&amp;nbsp;20&amp;nbsp;~&amp;nbsp;24&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;110 + 25 + 43 + 81 + 16*1 = 56&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이며 이 숫자는 1 ~ 20까지의 2의 거듭제곱큰약수들의 합이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이용하여 1 ~ B 에서 1 ~ A-1을 빼주면 A~B까지의 합을 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;f(B) - f(A-1)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj;  코드&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main_1407 {
    static long A;
    static long B;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        A = Long.parseLong(st.nextToken());
        B = Long.parseLong(st.nextToken());

        // A부터 B까지의 수 중 2의 거듭제곱 꼴이면서 가장 큰 약수를 찾아 더하여 출력
        System.out.println(twoCount(B) - twoCount(A - 1));
    }

    /**
     * A부터 B까지의 수 중 2의 거듭제곱 꼴이면서 가장 큰 약수를 찾아 더하는 메서드
     *
     * @param n 약수를 찾을 대상 숫자
     * @return 2의 거듭제곱 꼴이면서 가장 큰 약수를 찾아 더한 결과
     */
    private static long twoCount(long n) {
        long result = 0;
        long divisor = 1;

        // 2의 거듭제곱 꼴이면서 가장 큰 약수를 찾기 위한 반복문
        while (n &amp;gt; 0) {
            // 현재의 divisor로 나누어지지 않는 숫자의 가장 큰 약수를 찾아 더함
            long indivisibleNumberCnt = n / divisor;

            // 결과에 현재 divisor와 가장 큰 약수를 곱하여 누적
            result += divisor * indivisibleNumberCnt;

            // 다음 거듭제곱으로 이동
            divisor *= 2;
        }
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[&amp;nbsp;&lt;a href=&quot;https://velog.io/@bgg01578/%EB%B0%B1%EC%A4%80-1407-2%EB%A1%9C-%EB%AA%87-%EB%B2%88-%EB%82%98%EB%88%84%EC%96%B4%EC%A7%88%EA%B9%8C&quot;&gt;참고&lt;/a&gt;&amp;nbsp;]&lt;/h3&gt;</description>
      <category>Algorithm/PS - 백준</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/156</guid>
      <comments>https://dksek3050.tistory.com/156#entry156comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:43:38 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] permission denied while trying to connect to the Docker daemon socket at unix 에러 해결</title>
      <link>https://dksek3050.tistory.com/155</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;docker.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chQ1CD/btsMTCrlCP4/XEX4K8vtPkErnokenapk5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chQ1CD/btsMTCrlCP4/XEX4K8vtPkErnokenapk5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chQ1CD/btsMTCrlCP4/XEX4K8vtPkErnokenapk5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchQ1CD%2FbtsMTCrlCP4%2FXEX4K8vtPkErnokenapk5k%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;612&quot; data-filename=&quot;docker.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 컨테이너에서 실행하거나, 호스트의 Docker 소켓(/var/run/docker.sock)을 컨테이너에 마운트하여 사용하는 경우, 예상치 못한 권한 문제로 인해 Docker 명령이 작동하지 않을 때가 있습니다. 이 글에서는 Docker 소켓 접근 권한 문제를 해결하는 방법을 단계별로 안내합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 증상&lt;/h3&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;[runner@ae8762224dd7 /]$ docker ps
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get &quot;http://%2Fvar%2Frun%2Fdocker.sock/v1.45/containers/json&quot;: dial unix /var/run/docker.sock: connect: permission denied
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker가 설치된 것 같지만,&amp;nbsp;docker&amp;nbsp;명령이 제대로 실행되지 않는 경우가 있습니다. 이 문제는 주로 Docker 소켓 파일(/var/run/docker.sock)의 소유권 문제로 발생합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 데몬은 Unix 소켓을 통해 명령을 수신하며,&amp;nbsp;/var/run/docker.sock&amp;nbsp;파일에 대해 읽기 및 쓰기 권한을 가진 사용자나 그룹만 Docker 명령을 사용할 수 있습니다. 이 소켓 파일은 기본적으로&amp;nbsp;docker&amp;nbsp;그룹에 속한 사용자만 접근할 수 있도록 설정되어 있습니다. 따라서, 소켓 파일에 접근하려는 사용자가&amp;nbsp;docker&amp;nbsp;그룹에 속해 있지 않다면 Docker 명령이 작동하지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 해결 방법&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) Docker 소켓의 소유 계정과 그룹 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선,&amp;nbsp;/var/run/docker.sock의 소유자와 그룹을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;ls -l /var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 예시는 다음과 같을 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;srw-rw---- 1 docker docker 0 Nov  4 12:00 /var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서&amp;nbsp;docker:docker는 소켓 파일의 소유자와 소유 그룹입니다. 즉, Docker 소켓에 접근하려면&amp;nbsp;docker&amp;nbsp;그룹에 속해 있어야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 사용자 추가 및 그룹 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 소켓 파일을 사용하려는 계정이&amp;nbsp;docker&amp;nbsp;그룹에 속해 있지 않다면, 동일한 그룹을 가진 계정을 생성하거나, 현재 계정을&amp;nbsp;docker&amp;nbsp;그룹에 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 소켓 파일의 소유주가&amp;nbsp;docker:docker라면, 다음 명령어로&amp;nbsp;docker&amp;nbsp;사용자 계정을 만들고 전환하여 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 현재 사용자에게 docker 그룹 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 현재 사용 중인 계정으로 Docker에 접근하고 싶다면, 현재 계정을&amp;nbsp;docker&amp;nbsp;그룹에 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# 현재 계정이 docker 그룹에 속해 있도록 설정합니다.
usermod -aG docker $(whoami)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그룹 설정을 변경한 후에는 로그아웃했다가 다시 로그인하거나,&amp;nbsp;newgrp docker&amp;nbsp;명령을 사용하여 변경 사항을 적용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) Docker 실행 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 완료되면&amp;nbsp;docker&amp;nbsp;명령어가 정상적으로 작동하는지 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;docker --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 Docker 버전 정보가 출력되면 설정이 완료된 것입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker 소켓 파일(/var/run/docker.sock)의 소유자와 소유 그룹을 확인합니다.&lt;/li&gt;
&lt;li&gt;필요한 경우&amp;nbsp;docker&amp;nbsp;그룹과 동일한 계정을 생성하거나 현재 계정을&amp;nbsp;docker&amp;nbsp;그룹에 추가하여 권한 문제를 해결합니다.&lt;/li&gt;
&lt;li&gt;그룹 변경 후 Docker 명령이 작동하는지 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 단계를 따라 권한 문제를 해결하면 Docker 명령을 정상적으로 사용할 수 있을 것입니다.&lt;/p&gt;</description>
      <category>DevOps/Docker</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/155</guid>
      <comments>https://dksek3050.tistory.com/155#entry155comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:26:32 +0900</pubDate>
    </item>
    <item>
      <title>[참고] centos 버전을 7이 아닌 8을 사용하는 이유 : /actions-runner/externals/node20/bin/node: /lib64/libm.so.6: version GLIBC_2.27' not found (required by /actions-runner/externals/node20/bin/node)</title>
      <link>https://dksek3050.tistory.com/154</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;devops.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boHbnJ/btsMT1qBB8r/WpI4Ouky52YkkAbieK4NM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boHbnJ/btsMT1qBB8r/WpI4Ouky52YkkAbieK4NM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boHbnJ/btsMT1qBB8r/WpI4Ouky52YkkAbieK4NM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboHbnJ%2FbtsMT1qBB8r%2FWpI4Ouky52YkkAbieK4NM1%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;947&quot; height=&quot;594&quot; data-filename=&quot;devops.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Self-hosted runner 를 구축하기 위해서 Centos7을 컨테이너로 띄웠고 ci/cd 파이프라인을 도는 과정에서 오류가 발생하였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  발생 오류&lt;/h2&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;  Run actions/checkout@v2
    with:
      repository:
      token: ***
      ssh-strict: true
      persist-credentials: true
      clean: true
      fetch-depth: 1
      lfs: false
      submodules: false
      set-safe-directory: true
  /actions-runner/externals/node20/bin/node: /lib64/libm.so.6: version GLIBC_2.27' not found (required by /actions-runner/externals/node20/bin/node)
  /actions-runner/externals/node20/bin/node: /lib64/libstdc++.so.6: version GLIBCXX_3.4.20' not found (required by /actions-runner/externals/node20/bin/node)
  /actions-runner/externals/node20/bin/node: /lib64/libstdc++.so.6: version CXXABI_1.3.9' not found (required by /actions-runner/externals/node20/bin/node)
  /actions-runner/externals/node20/bin/node: /lib64/libstdc++.so.6: version GLIBCXX_3.4.21' not found (required by /actions-runner/externals/node20/bin/node)
  /actions-runner/externals/node20/bin/node: /lib64/libc.so.6: version GLIBC_2.28' not found (required by /actions-runner/externals/node20/bin/node)
  /actions-runner/externals/node20/bin/node: /lib64/libc.so.6: version GLIBC_2.25' not found (required by /actions-runner/externals/node20/bin/node)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  발생 원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 메시지는 GitHub Actions 러너가 사용하는 Node.js 실행 파일이 필요로 하는 여러 라이브러리 버전이 현재 CentOS 7 시스템에 설치된 버전보다 낮기 때문에 발생하고 있습니다. 각 라이브러리의 요구사항은 다음과 같습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GLIBC: GNU C Library로, 시스템의 기본 라이브러리입니다. 여러 버전의 프로그램이 이 라이브러리를 필요로 하며, 버전이 낮으면 호환성 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;libstdc++: GNU Standard C++ Library로, C++ 프로그램을 실행하는 데 필요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  해결 방법&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Centos버전을 8로 변경
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>DevOps</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/154</guid>
      <comments>https://dksek3050.tistory.com/154#entry154comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:25:48 +0900</pubDate>
    </item>
    <item>
      <title>[Centos] Failed to download metadata for repo 'appstream' 에러 해결</title>
      <link>https://dksek3050.tistory.com/153</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;devops.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G63JH/btsMUZFKglO/VQOXTMZWYhbeS3yKRqrVWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G63JH/btsMUZFKglO/VQOXTMZWYhbeS3yKRqrVWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G63JH/btsMUZFKglO/VQOXTMZWYhbeS3yKRqrVWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG63JH%2FbtsMUZFKglO%2FVQOXTMZWYhbeS3yKRqrVWK%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;947&quot; height=&quot;594&quot; data-filename=&quot;devops.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  발생 에러&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;[root@e12ebc3453d8 /]# yum update
Failed to set locale, defaulting to C.UTF-8
CentOS Linux 8 - AppStream                                                                     0.0  B/s |   0  B     00:00
Errors during downloading metadata for repository 'appstream':
  - Curl error (6): Couldn't resolve host name for &amp;lt;http://mirrorlist.centos.org/?release=8&amp;amp;arch=x86_64&amp;amp;repo=AppStream&amp;amp;infra=container&amp;gt; [Could not resolve host: mirrorlist.centos.org]
Error: Failed to download metadata for repo 'appstream': Cannot prepare internal mirrorlist: Curl error (6): Couldn't resolve host name for &amp;lt;http://mirrorlist.centos.org/?release=8&amp;amp;arch=x86_64&amp;amp;repo=AppStream&amp;amp;infra=container&amp;gt; [Could not resolve host: mirrorlist.centos.org]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  발생 원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.centos.org/centos-linux-eol/&quot;&gt;CentOS Linux EOL&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CentOS 8이 2021년 12월 31일에 공식적으로 지원이 종료(EOL, End of Life)되면서, 기본 리포지토리(&lt;a href=&quot;http://mirrorlist.centos.org&quot;&gt;mirrorlist.centos.org&lt;/a&gt;)는 더 이상 CentOS 8 패키지에 대한 업데이트나 접근을 제공하지 않습니다. 이로 인해 CentOS 8에서 기본&amp;nbsp;mirrorlist&amp;nbsp;경로를 사용하면 패키지 다운로드에 실패하게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  해결 방법:Vault 저장소로 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/etc/yum.repos.d/CentOS-Linux-AppStream.repo&amp;nbsp;파일에서&amp;nbsp;mirrorlist&amp;nbsp;대신&amp;nbsp;vault.centos.org&amp;nbsp;URL을&amp;nbsp;baseurl로 지정함으로써, EOL 버전 패키지에 접근할 수 있습니다. Vault 저장소는 CentOS 8 패키지를 포함하고 있지만, 더 이상 업데이트나 보안 패치를 제공하지 않으므로 최신 보안이 필요한 환경에서는 CentOS Stream이나 다른 지원되는 운영체제로 업그레이드하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppStream 뿐만 아니라 Baseos, Extra에서도 모두 발생하니 모든 파일을 아래와 같이 바꿔줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;YUM 레포지토리 파일 수정&lt;/b&gt;/etc/yum.repos.d/CentOS-Linux-AppStream.repo&amp;nbsp;파일을 열고, 다음과 같이 수정합니다:&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;# vi CentOS-Linux-AppStream.repo
-----

[appstream]
name=CentOS Linux 8.5.2111 - AppStream
#mirrorlist=http://mirrorlist.centos.org/?release=8.5.2111&amp;amp;arch=$basearch&amp;amp;repo=AppStream&amp;amp;infra=$infra
baseurl=http://vault.centos.org/$contentdir/8.5.2111/AppStream/$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial
~
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;YUM 레포지토리 파일 수정&lt;/b&gt;/etc/yum.repos.d/CentOS-Linux-BaseOS.repo&amp;nbsp;파일을 열고, 다음과 같이 수정합니다:&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;# vi CentOS-Linux-BaseOS.repo
-----

[baseos]
name=CentOS Linux 8.5.2111 - BaseOS
#mirrorlist=http://mirrorlist.centos.org/?release=8.5.2111&amp;amp;arch=$basearch&amp;amp;repo=BaseOS&amp;amp;infra=$infra
baseurl=http://vault.centos.org/$contentdir/8.5.2111/BaseOS/$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;YUM 레포지토리 파일 수정&lt;/b&gt;/etc/yum.repos.d/CentOS-Linux-Extras.repo&amp;nbsp;파일을 열고, 다음과 같이 수정합니다:&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;# vi CentOS-Linux-Extras.repo
-----

[extras]
name=CentOS Linux 8.5.2111 - Extras
#mirrorlist=http://mirrorlist.centos.org/?release=8.5.2111&amp;amp;arch=$basearch&amp;amp;repo=extras&amp;amp;infra=$infra
baseurl=http://vault.centos.org/$contentdir/8.5.2111/extras/$basearch/os/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial
~
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;+) Centos7에서는&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;YUM 레포지토리 파일 수정&lt;/b&gt;:/etc/yum.repos.d/CentOS-Base.repo&amp;nbsp;파일을 열고,&amp;nbsp;base,&amp;nbsp;updates,&amp;nbsp;extras&amp;nbsp;섹션을 다음과 같이 수정합니다:&lt;/li&gt;
&lt;li&gt;[base] name=CentOS-$releasever - Base baseurl=&lt;a href=&quot;http://vault.centos.org/7.9.2009/os/$basearch/&quot;&gt;http://vault.centos.org/7.9.2009/os/$basearch/&lt;/a&gt; gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 [updates] name=CentOS-$releasever - Updates baseurl=&lt;a href=&quot;http://vault.centos.org/7.9.2009/updates/$basearch/&quot;&gt;http://vault.centos.org/7.9.2009/updates/$basearch/&lt;/a&gt; gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 [extras] name=CentOS-$releasever - Extras baseurl=&lt;a href=&quot;http://vault.centos.org/7.9.2009/extras/$basearch/&quot;&gt;http://vault.centos.org/7.9.2009/extras/$basearch/&lt;/a&gt; gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7&lt;/li&gt;
&lt;li&gt;&lt;b&gt;YUM 캐시 청소&lt;/b&gt;:
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;yum clean all
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;변경한 후에는 YUM 캐시를 청소하세요:&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패키지 설치&lt;/b&gt;:&lt;/li&gt;
&lt;li&gt;yum update&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DevOps/Linux</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/153</guid>
      <comments>https://dksek3050.tistory.com/153#entry153comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:24:48 +0900</pubDate>
    </item>
    <item>
      <title>[CI/CD] GitHub-hosted runner 와 Self-hosted runner 란?</title>
      <link>https://dksek3050.tistory.com/152</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;devops.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmd2s8/btsMT8JWwMq/7OlRmMGFUz7kNEagw2IrF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmd2s8/btsMT8JWwMq/7OlRmMGFUz7kNEagw2IrF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmd2s8/btsMT8JWwMq/7OlRmMGFUz7kNEagw2IrF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbmd2s8%2FbtsMT8JWwMq%2F7OlRmMGFUz7kNEagw2IrF0%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;947&quot; height=&quot;594&quot; data-filename=&quot;devops.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIO61p/btsMTa2VONN/uy9xkXaZlusFuDXrpN5i81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIO61p/btsMTa2VONN/uy9xkXaZlusFuDXrpN5i81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIO61p/btsMTa2VONN/uy9xkXaZlusFuDXrpN5i81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIO61p%2FbtsMTa2VONN%2Fuy9xkXaZlusFuDXrpN5i81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;643&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;643&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GitHub-hosted runner&lt;/b&gt;: GitHub에서 제공하는 서버에서 작업을 실행하는 방식. 사용자는 인프라를 관리할 필요 없이 자동으로 제공된 환경에서 빌드, 테스트, 배포 작업을 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Self-hosted runner&lt;/b&gt;: 사용자가 직접 서버를 설정하고 관리하여 GitHub Actions의 작업을 실행하는 방식. 자신의 인프라에서 작업을 처리하므로 사용자 제어가 가능하지만 유지 관리와 리소스 관리는 사용자의 책임이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 GitHub-hosted Runner Self-hosted Runner&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;인프라 제공 및 관리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;GitHub에서 제공하는 인프라에서 자동 실행&lt;/td&gt;
&lt;td&gt;사용자가 runner 서버를 지정하여 직접 서버를 설정 및 관리하여 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;비용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;제한된 무료 실행 시간 제공, 추가 리소스는 유료 플랜 필요&lt;/td&gt;
&lt;td&gt;GitHub Actions 사용 비용 없음, 서버 유지 비용은 사용자 부담&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;커스터마이징&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;제한적, 표준화된 GitHub 환경에서 실행&lt;/td&gt;
&lt;td&gt;사용자가 런너 환경을 자유롭게 설정 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;성능 및 리소스 제한&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;GitHub이 제공하는 기본 리소스로 제한됨&lt;/td&gt;
&lt;td&gt;사용자가 필요에 맞춰 리소스(예: CPU, 메모리) 설정 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;네트워크 접근성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;고정 IP 주소 제공하지 않음, GitHub IP 범위로 설정 필요&lt;/td&gt;
&lt;td&gt;내부 네트워크 자원에 자유롭게 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;운영 체제 선택&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Linux, Windows, macOS 선택 가능&lt;/td&gt;
&lt;td&gt;사용자가 직접 설정한 운영 체제에서 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;유지 보수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;GitHub이 런너 인프라 관리 및 유지 보수&lt;/td&gt;
&lt;td&gt;사용자 직접 인프라 관리 및 유지 보수 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;네트워크 속도 및 성능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;GitHub의 네트워크 및 인프라 성능에 의존&lt;/td&gt;
&lt;td&gt;사용자가 설정한 네트워크 성능에 따라 다름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;IP 주소 고정 여부&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;고정되지 않음, IP 범위는 제공&lt;/td&gt;
&lt;td&gt;사용자가 설정한 고정 IP 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;보안 제어&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;GitHub이 관리하는 보안 정책에 따름&lt;/td&gt;
&lt;td&gt;사용자가 서버의 보안 정책 및 접근 제어 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Self-hosted runner 구축기 보러가기 ⏬&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@dksek3050/CICD-gitHub-Action%EC%9C%BC%EB%A1%9C-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B3%A0-%EC%9D%B8%EA%B0%84%EC%A0%A0%ED%82%A8%EC%8A%A4-%ED%83%88%EC%B6%9C%ED%95%98%EB%8B%A4-feat.-Self-hosted-runner&quot;&gt;[CI/CD] gitHub Action으로 CI/CD 구축하고 인간젠킨스 탈출하다! (feat. Self-hosted runner)&lt;/a&gt;&lt;/p&gt;</description>
      <category>DevOps</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/152</guid>
      <comments>https://dksek3050.tistory.com/152#entry152comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:24:00 +0900</pubDate>
    </item>
    <item>
      <title>[DevOps] Jenkins로 CI/CD 구축하기-1 (Jenkins 설치편)</title>
      <link>https://dksek3050.tistory.com/151</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;devops.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qxqw7/btsMUs2uHLj/UNCfZxotJO35D7O1AwYUlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qxqw7/btsMUs2uHLj/UNCfZxotJO35D7O1AwYUlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qxqw7/btsMUs2uHLj/UNCfZxotJO35D7O1AwYUlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQxqw7%2FbtsMUs2uHLj%2FUNCfZxotJO35D7O1AwYUlk%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;947&quot; height=&quot;594&quot; data-filename=&quot;devops.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 들어가기 전에&lt;/h2&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;807&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RNMyW/btsMTg9Qpka/5gWgx4KJ4MsHbwK80RA1b0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RNMyW/btsMTg9Qpka/5gWgx4KJ4MsHbwK80RA1b0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RNMyW/btsMTg9Qpka/5gWgx4KJ4MsHbwK80RA1b0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRNMyW%2FbtsMTg9Qpka%2F5gWgx4KJ4MsHbwK80RA1b0%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;807&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;807&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 하고 있는 프로젝트에 바로 ci/cd를 구축하면 좋겠다만 ci/cd가 처음인 나는 전에 했던 프로젝트를 가지고 ci/cd를 구축해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플리보따리 라는 프로젝트인데 이 프로젝트는 현재 팀원이 gitAction으로 cicd를 구축해두어서 나는 따로 포크해와 jenkins로 구성해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 구조를 가지도록 목표하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기회가 된다면 Nginx도 추가해보고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 대충 이 정도만 띄워서 되는지만 확인해보는 것이 단기 목표이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가보자고&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Jenkins 컨테이너 생성하기&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젠킨스 도커 컨테이너를 생성할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에는 이미 도커가 설치되어 있었다. 도커를 설치하는 과정은 넘어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;swapfile 도 2기가 설정해두었다. 이 과정도 기록하지는 않는다. 참고링크에서 참고하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 설치하기 전 생성할 컨테이너에 Java17을 설치 하려고 한다. 자바 11을 쓴다면 아래와 같이 간단하게 명령어를 작성하여 컨테이너를 만들 수 있지만 현재로서는 Jenkins 이미지 중에 Java17을 지원하는 것은 없다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;sudo docker run -d --name jenkins -p 8081:8080 jenkins/jenkins:jdk11
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Java17설치하는 Dockerfile을 작성하고 그 Dockerfile을 통해 이미지를 만들고 이미지를 실행시켜 컨테이너를 만들 것이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Dockerfile 작성&lt;/li&gt;
&lt;li&gt;# Dockerfile FROM jenkins/jenkins USER root # OpenJDK 17 설치 RUN apt-get update &amp;amp;&amp;amp; apt-get install -y openjdk-17-jdk # Jenkins 시작 USER jenkins&lt;/li&gt;
&lt;li&gt;Docker 이미지 빌드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dockerfile 이 있는 디렉토리에서 위 명령어를 실행한다.&lt;/li&gt;
&lt;li&gt;여기서&amp;nbsp;t&amp;nbsp;옵션은 빌드된 이미지에 태그를 부여하는 데 사용된다.&amp;nbsp;jenkins는 이미지의 이름이 된다.&lt;/li&gt;
&lt;li&gt;**.**은 Dockerfile이 있는 현재 디렉토리를 나타낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;sudo docker build -t jenkins .&lt;/li&gt;
&lt;li&gt;Jenkins 컨테이너 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호스트의 8081 포트와 컨테이너의 8080 포트를 매핑하여 호스트의 8081 포트를 통해 컨테이너의 Jenkins 서버에 접근할 수 있게 했다.&lt;/li&gt;
&lt;/ul&gt;
(서버ip):8081&amp;nbsp;를 브라우저에 url로 입력해 이동하면 젠킨스 아저씨를 만날 수 있다. 바로 요렇게&lt;/li&gt;
&lt;li&gt;sudo docker run -d --name jenkins -p 8081:8080 jenkins&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;561&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D58C9/btsMUoy7Qdb/TywdLcztjFU6NVBIk0CTc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D58C9/btsMUoy7Qdb/TywdLcztjFU6NVBIk0CTc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D58C9/btsMUoy7Qdb/TywdLcztjFU6NVBIk0CTc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD58C9%2FbtsMUoy7Qdb%2FTywdLcztjFU6NVBIk0CTc1%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;561&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;561&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;(이 아저씨가 나중에 되게 화낸다던데&amp;hellip;. )&lt;/s&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Jenkins 설정&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젠아저씨가 기다리라는 대로 기다리면 아래와 같은 화면이 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;623&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R7kdL/btsMTYAONWl/hBy8OKgmoCCV9Ik2zv1Da0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R7kdL/btsMTYAONWl/hBy8OKgmoCCV9Ik2zv1Da0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R7kdL/btsMTYAONWl/hBy8OKgmoCCV9Ik2zv1Da0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR7kdL%2FbtsMTYAONWl%2FhBy8OKgmoCCV9Ik2zv1Da0%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;623&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;623&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패스워드를 입력해야 하는데 입력하기 위해서는 젠킨스가 설치된 컨테이너에 접속해 위에 빨간 글씨로 적혀있는 디렉토리로 이동하면 초기 관리자 키가 적혀 있다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;// jenkins 컨테이너에 접속
$ sudo docker exec -it jenkins bash
// 초기 관리자 키 확인
$ cat /var/jenkins_home/secrets/initialAdminPassword
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어를 통해 출력된 키를 입력하고 Continue 하면 아래와 같이 초기 플러그인 설정이 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yvkYT/btsMTnVmcfl/BPhubTvjKdSQarOuvtVzGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yvkYT/btsMTnVmcfl/BPhubTvjKdSQarOuvtVzGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yvkYT/btsMTnVmcfl/BPhubTvjKdSQarOuvtVzGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyvkYT%2FbtsMTnVmcfl%2FBPhubTvjKdSQarOuvtVzGK%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;740&quot; height=&quot;433&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;433&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 추천 플러그인들을 설치하고 넘어갔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;953&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/124YV/btsMUvZdSNf/9ltjTyrkrALwTS9xJbIKF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/124YV/btsMUvZdSNf/9ltjTyrkrALwTS9xJbIKF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/124YV/btsMUvZdSNf/9ltjTyrkrALwTS9xJbIKF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F124YV%2FbtsMUvZdSNf%2F9ltjTyrkrALwTS9xJbIKF0%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;953&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;953&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 되면 유저 설정을 해보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgCZvZ/btsMTKindYD/aUxs6xwekrBqIqKzrChWEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgCZvZ/btsMTKindYD/aUxs6xwekrBqIqKzrChWEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgCZvZ/btsMTKindYD/aUxs6xwekrBqIqKzrChWEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgCZvZ%2FbtsMTKindYD%2FaUxs6xwekrBqIqKzrChWEk%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;1135&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1135&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 끝나면 Jenkins에 접속할 URL을 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 아래와 같이 바꿔주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/el4kFQ/btsMSSPabiy/PsdGNQQ7qzJIi9HrYb5kk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/el4kFQ/btsMSSPabiy/PsdGNQQ7qzJIi9HrYb5kk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/el4kFQ/btsMSSPabiy/PsdGNQQ7qzJIi9HrYb5kk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fel4kFQ%2FbtsMSSPabiy%2FPsdGNQQ7qzJIi9HrYb5kk1%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;846&quot; height=&quot;450&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니 근데 왜 저 url로 들어가면 화내는거임 ?&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;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1QElE/btsMVc52AQA/mpKoekB9qwUlG4JZ7PHRE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1QElE/btsMVc52AQA/mpKoekB9qwUlG4JZ7PHRE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1QElE/btsMVc52AQA/mpKoekB9qwUlG4JZ7PHRE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1QElE%2FbtsMVc52AQA%2FmpKoekB9qwUlG4JZ7PHRE1%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;670&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아오&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 설정은 뭔지 모르겠고 그냥 8081로 접속해주자 ㅎㅎ..&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;711&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bA6fGr/btsMThVbdTR/zlnHAV4wGYkBFn6QwYfbz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bA6fGr/btsMThVbdTR/zlnHAV4wGYkBFn6QwYfbz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bA6fGr/btsMThVbdTR/zlnHAV4wGYkBFn6QwYfbz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbA6fGr%2FbtsMThVbdTR%2FzlnHAV4wGYkBFn6QwYfbz0%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;711&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;711&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짜자자잔&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모든 설정은 마쳤으니 CI를 구축해보자 !&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  참고 링크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://seongwon.dev/DevOps/20220713-CICD란/&quot;&gt;https://seongwon.dev/DevOps/20220713-CICD란/&lt;/a&gt;&lt;/p&gt;</description>
      <category>DevOps</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/151</guid>
      <comments>https://dksek3050.tistory.com/151#entry151comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:20:29 +0900</pubDate>
    </item>
    <item>
      <title>[DevOps] CI/CD란..?</title>
      <link>https://dksek3050.tistory.com/150</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;devops.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lCMOt/btsMTnuh3Oh/EXz3dluquzA23xfIMfCNSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lCMOt/btsMTnuh3Oh/EXz3dluquzA23xfIMfCNSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lCMOt/btsMTnuh3Oh/EXz3dluquzA23xfIMfCNSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlCMOt%2FbtsMTnuh3Oh%2FEXz3dluquzA23xfIMfCNSk%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;947&quot; height=&quot;594&quot; data-filename=&quot;devops.png&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 배경&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bogXhF/btsMTghJBGv/f12SOWwUkfDhogkVp2HpqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bogXhF/btsMTghJBGv/f12SOWwUkfDhogkVp2HpqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bogXhF/btsMTghJBGv/f12SOWwUkfDhogkVp2HpqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbogXhF%2FbtsMTghJBGv%2Ff12SOWwUkfDhogkVp2HpqK%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;480&quot; height=&quot;342&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 참여하게 된 프로젝트로 인해 CI/CD를 공부 해야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 나는 A 서비스를 제공하는 웹서버(간단하게 A서버라고하자)를 구축하고 수정해야 했다. ( 물론 내가 다하진 않는다. 나의 기여도는 개미 눈물만큼? )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 A 서비스 서버는 대략 8개 ..? 정도 되는 서비스로 이루어져 있었다.&amp;nbsp;&lt;s&gt;(자세히는 모르지만 이게 MSA?)&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 소스코드를 수정하고 테스트 서버에서 수정사항이 반영됐는지 확인하기 위해서는 1번 서비스에서 war 파일을 생성해서 옮기고&amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 서비스에서는 jar 파일을 생성해서 올리고&amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔진엑스를 다시 실행시키고 ..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쩌구 저쩌구 하는 상당히 복잡한 과정 ,, 워드로 두, 세장이 작성될 만큼에 서버 구축 과정이 다시 수행되어야 한다.. ( 물론 생략하는 부분도 생기겠지.. )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 이걸 설명을 듣고 이게 맞나..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화 되는 게 있을 거 같은데 라는 생각을 하게 되었고 그게 CI/CD 의 과정이라는 것을 깨닫게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+) 아무래도 프로젝트가 너무 커서 프로젝트에는 ci/cd를 적용하지는 못할 듯 싶다..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5y4nt/btsMS8Rzszt/ZzqL43GpSDgDzqqqnJ9ff1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5y4nt/btsMS8Rzszt/ZzqL43GpSDgDzqqqnJ9ff1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5y4nt/btsMS8Rzszt/ZzqL43GpSDgDzqqqnJ9ff1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5y4nt%2FbtsMS8Rzszt%2FZzqL43GpSDgDzqqqnJ9ff1%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;1072&quot; height=&quot;414&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. CI&amp;nbsp;&lt;b&gt;(Continuous Integration)&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드, 테스트의 자동화&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD의 CI 는 &amp;ldquo;지속적인 통합&amp;rdquo;을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에 변경사항이 있을 경우 자동으로 빌드, 테스트 되어 공유 레포지토리에 통합된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마틴 파울러는 CI의 4가지 규칙이 있다고 언급하였다. 규칙은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모든 소스코드가 살아있고(현재 실행되고) 누구든 현재의 소스에 접근할 수 있는 단일 지점을 유지할 것&lt;/li&gt;
&lt;li&gt;빌드 프로세스를 자동화해서 누구든 소스로부터 시스템을 빌드하는 단일 명령어를 사용할 수 있게 할 것&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스팅을 자동화&lt;/b&gt;해서 단일 명령어로 언제든지 시스템에 대한 건전한 테스트 수트를 실행할 수 있게 할 것&lt;/li&gt;
&lt;li&gt;누구나 현재 실행 파일을 얻으면 지금까지 가장 완전한 실행 파일을 얻었다는 확신을 하게 할 것&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI 과정이 자동화로 수행된다면 Git에 push, merge를 하게 될 경우 정상적으로 빌드가 되는 지 테스트 과정을 자동으로 확인하고 빌드가 성공적으로 이루어지면 수정된 코드가 받아들여지게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. CD&amp;nbsp;&lt;b&gt;(Continuous Deployment or Continuous Delivery)&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포의 자동화&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CD는 두가지 의미가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지속적인 제공과 지속적인 배포이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지속적인 제공(Delivery)는 CI 과정을 거치게 되면 레포지토리에 자동으로 업로드 되는 것을 말한다. 그렇게 레포지토리에 업로드된 코드가 문제가 없는지 검증 후에 수동으로 배포해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 더 나아간 게 지속적인 배포 (Deployment)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 바로 내가 하고 싶은 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포지토리에 수정이 반영된 버전으로 서버에 자동으로 배포하는 과정을 말한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RdZNq/btsMUuzdJeU/43A58KClY2GFUd4MKrtRck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RdZNq/btsMUuzdJeU/43A58KClY2GFUd4MKrtRck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RdZNq/btsMUuzdJeU/43A58KClY2GFUd4MKrtRck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRdZNq%2FbtsMUuzdJeU%2F43A58KClY2GFUd4MKrtRck%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;723&quot; height=&quot;304&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. CI/CD의 대표적인 툴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins, Buildkite, Github Actions, GitLab CI/CD, Bitbucket Pipelines, Circleci 등이 있다. 이 중에서 가장 많이 사용되는 것은 Jenkins이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  참고 링크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://seongwon.dev/DevOps/20220713-CICD란/&quot;&gt;https://seongwon.dev/DevOps/20220713-CICD란/&lt;/a&gt;&lt;/p&gt;</description>
      <category>DevOps</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/150</guid>
      <comments>https://dksek3050.tistory.com/150#entry150comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:18:43 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] DockerHub에 이미지 올리기</title>
      <link>https://dksek3050.tistory.com/149</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;docker.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wYAZv/btsMTc0Kt3K/4Xth6SZW9brOsU1HWPkz7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wYAZv/btsMTc0Kt3K/4Xth6SZW9brOsU1HWPkz7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wYAZv/btsMTc0Kt3K/4Xth6SZW9brOsU1HWPkz7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwYAZv%2FbtsMTc0Kt3K%2F4Xth6SZW9brOsU1HWPkz7K%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;612&quot; data-filename=&quot;docker.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 이미지 저장소&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커는 빌드한 이미지를 서버에 배포하기 위해 직접 파일을 복사하는 방법 대신&amp;nbsp;&lt;a href=&quot;https://docs.docker.com/registry/&quot;&gt;도커 레지스트리&lt;/a&gt;(Docker Registr)y라는 이미지 저장소를 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;289&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbv3t0/btsMSFoOpuM/x92YjI2bWJZOIHq40kc420/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbv3t0/btsMSFoOpuM/x92YjI2bWJZOIHq40kc420/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbv3t0/btsMSFoOpuM/x92YjI2bWJZOIHq40kc420/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdbv3t0%2FbtsMSFoOpuM%2Fx92YjI2bWJZOIHq40kc420%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;683&quot; height=&quot;289&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;289&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 명령어를 이용하여 이미지를 레지스트리에 push 하고 다른 서버에서 pull 받아 사용하는 구조로 git과 비슷하다고 생각하면 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 레지스트리는&amp;nbsp;&lt;a href=&quot;https://github.com/docker/distribution&quot;&gt;오픈소스&lt;/a&gt;로 무료로 설치할 수 있고 설치형이 싫다면 도커(Docker Inc.)에서 서비스 중인&amp;nbsp;&lt;a href=&quot;https://hub.docker.com/&quot;&gt;도커 허브&lt;/a&gt;Docker Hub를 사용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; Docker Hub&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 허브는 도커에서 제공하는 기본 이미지 저장소로 ubuntu, centos, debian등의 베이스 이미지와 ruby, golang, java, python 등의 공식 이미지가 저장되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 사용자들이 만든 이미지도 50만 개가 넘게 저장되어 있고 다운로드 횟수는 80억 회를 넘는다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 만들었던 Ruby 웹 어플리케이션 이미지를 docker hub에 저장해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 게시물 &amp;rarr;&amp;nbsp;&lt;a href=&quot;https://velog.io/@dksek3050/Docker-Docker-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A7%8C%EB%93%A4%EA%B3%A0-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0&quot;&gt;[Docker] Docker 이미지 만들고 컨테이너 생성하기&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;app.rb 코드 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이전에 만들었던 웹 어플리케이션 (이름 : app)을 가상머신에서 도커허브로 올린 다음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 할당받은 서버에서 내려 받으려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할당받은 서버에서도 app.rb가 똑같이 작성되어 있고 컨테이너 이미지가 모두 존재하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만일 하나를 대비하여 차이점을 주기 위해 app.rb 의 코드를 수정하여 다시 이미지 빌드를 하였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;app.rb&lt;/h3&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;require 'sinatra'
require 'socket'

get '/' do
  &quot;Hello World! This is #{Socket.gethostname}&quot;
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JEdub/btsMUAe8T3l/h1xKQ6OJRxG5K0XGH00JGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JEdub/btsMUAe8T3l/h1xKQ6OJRxG5K0XGH00JGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JEdub/btsMUAe8T3l/h1xKQ6OJRxG5K0XGH00JGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJEdub%2FbtsMUAe8T3l%2Fh1xKQ6OJRxG5K0XGH00JGk%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;395&quot; height=&quot;124&quot; data-origin-width=&quot;395&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤵INPUT&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;docker build -t app .
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회원가입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음&amp;nbsp;&lt;a href=&quot;https://hub.docker.com/&quot;&gt;DockerHub&lt;/a&gt;&amp;nbsp;도커허브 사이트에 접속해서 회원가입을 먼저 해주자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커에서 도커 허브 계정을 사용하려면 로그인을 해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤵INPUT&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker login
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤴ OUTPUT&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5iW5z/btsMTK3Is1L/tMD1Ia6sFcOcqiDhNFHBTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5iW5z/btsMTK3Is1L/tMD1Ia6sFcOcqiDhNFHBTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5iW5z/btsMTK3Is1L/tMD1Ia6sFcOcqiDhNFHBTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5iW5z%2FbtsMTK3Is1L%2FtMD1Ia6sFcOcqiDhNFHBTK%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;702&quot; height=&quot;326&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;Docker ID 또는 이메일 주소로 로그인하여 Docker Hub에서 이미지를 푸시하고 가져오세요. Docker ID가 없으면 &amp;lt;https://hub.docker.com/으로&amp;gt; 이동하여 ID를 만드세요.

비밀번호나 PAT(Personal Access Token)를 사용하여 로그인할 수 있습니다. 제한된 범위의 PAT를 사용하면 보안이 강화되며 SSO를 사용하는 조직에 필요합니다. &amp;lt;https://docs.docker.com/go/access-tokens/에서&amp;gt; 자세히 알아보세요.

사용자 이름: dksek3050
비밀번호:
경고! 귀하의 비밀번호는 /home/user/.docker/config.json에 암호화되지 않은 상태로 저장됩니다.
이 경고를 제거하려면 자격 증명 도우미를 구성하십시오. 보다
&amp;lt;https://docs.docker.com/engine/reference/commandline/login/#credentials-store&amp;gt;

로그인 성공
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어 울렁증 있는 나는 냅다 번역기 돌려버림&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호가 config.json 파일에 무려 암호화가 되지 않은 상태로 저장된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호를 통합해서 쓰는 나는 간단 비밀번호로 바꿔주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 config.json 당장 들어가버림.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;681&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z0RNY/btsMSSVZCsU/XcgbGtQd0AvEYc7k2F3nG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z0RNY/btsMSSVZCsU/XcgbGtQd0AvEYc7k2F3nG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z0RNY/btsMSSVZCsU/XcgbGtQd0AvEYc7k2F3nG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz0RNY%2FbtsMSSVZCsU%2FXcgbGtQd0AvEYc7k2F3nG1%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;592&quot; height=&quot;681&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;681&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어라 암호화 안되어 있다고 했는데 인코딩만 되어 있는거 같다. 디코딩해보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PHUVy/btsMUu0jnZo/EyHFegkyrGR8cAUHNbsVOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PHUVy/btsMUu0jnZo/EyHFegkyrGR8cAUHNbsVOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PHUVy/btsMUu0jnZo/EyHFegkyrGR8cAUHNbsVOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPHUVy%2FbtsMUu0jnZo%2FEyHFegkyrGR8cAUHNbsVOK%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;449&quot; height=&quot;656&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헉 바로 바로 보인다. 비밀번호 또 바꾸러가야지 총총 ..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 서버를 쓰거나 공용으로 접속할 수 있는 서버에서 실습을 진행하는 사람들은 본인이 자주 쓰는 비밀번호는 사용하지 않으며 비밀번호가 노출되지 않도록 주의해야 할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;config.json 파일에 저장된 정보가 로그아웃하기 전 까지 로그인 정보가 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 로그아웃 하게 되면 config.json에 있는 내용은 지워진다 !&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLum4W/btsMTGUGDBs/tm4DwF4iCUsMh3kKipewck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLum4W/btsMTGUGDBs/tm4DwF4iCUsMh3kKipewck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLum4W/btsMTGUGDBs/tm4DwF4iCUsMh3kKipewck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLum4W%2FbtsMTGUGDBs%2Ftm4DwF4iCUsMh3kKipewck%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;317&quot; height=&quot;166&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 태그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 이미지 이름은 다음과 같은 형태로 구성된다.&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;[Registry URL]/[사용자 ID]/[이미지명]:[tag]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Registry Url은 기본적으로 도커 허브를 바라보고 있고 사용자 ID를 지정하지 않으면 기본 값(library)을 사용한다. 따라서 ubuntu = library/ubuntu =&amp;nbsp;&lt;a href=&quot;http://doker.io/library/ubuntu&quot;&gt;doker.io/library/ubuntu&lt;/a&gt;&amp;nbsp;는 모두 동일한 표현이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커의 tag 명령어를 이용하여 기존에 만든 이미지를 추가로 이름을 지어 줄 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 만든 app 이미지에 계정 정보와 버전 정보를 추가해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤵INPUT&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;docker tag app anna/anna-app:1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;anna 라는 ID를 사용&amp;rdquo;하고 이미지 이름을 anna-app으로 변경하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 버전이므로 태그는 1로 지정하였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤴ OUTPUT&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;807&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWAmyg/btsMSDYVlvu/AylKlO6Zt1zuVwLrgoqDb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWAmyg/btsMSDYVlvu/AylKlO6Zt1zuVwLrgoqDb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWAmyg/btsMSDYVlvu/AylKlO6Zt1zuVwLrgoqDb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWAmyg%2FbtsMSDYVlvu%2FAylKlO6Zt1zuVwLrgoqDb0%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;807&quot; height=&quot;131&quot; data-origin-width=&quot;807&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 목록을출력해보면 app 과 anna/anna-app이 모두 존재함을 알 수 있는데 image를 보니 같은 아이디를 가지고 있는 걸로 보아 같은 이미지를 참조하고 있음을 확인 할 수 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 푸시(push)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제&amp;nbsp;push라는 명령어를 이용하여 도커 허브에 이미지를 올려보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤵INPUT&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker push anna/anna-app:1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤴ OUTPUT&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCPAaY/btsMUZMw1Dl/rV1EiEMnZmNRLn4QqTmIPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCPAaY/btsMUZMw1Dl/rV1EiEMnZmNRLn4QqTmIPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCPAaY/btsMUZMw1Dl/rV1EiEMnZmNRLn4QqTmIPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCPAaY%2FbtsMUZMw1Dl%2FrV1EiEMnZmNRLn4QqTmIPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;327&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어라 ? 거부됐다 왜지 ?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보니&amp;nbsp;**Docker Hub에 로그인한 ID외 이미지의 유저 ID가 달라서**&amp;nbsp;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 태그를 재설정해주자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 다시 태그&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤵INPUT&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;docker tag app dksek3050/anna-app:1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤴ OUTPUT&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7RGIE/btsMT6ekizN/HSKFUtv3sEkgBKZu6FB3kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7RGIE/btsMT6ekizN/HSKFUtv3sEkgBKZu6FB3kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7RGIE/btsMT6ekizN/HSKFUtv3sEkgBKZu6FB3kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7RGIE%2FbtsMT6ekizN%2FHSKFUtv3sEkgBKZu6FB3kk%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;757&quot; height=&quot;136&quot; data-origin-width=&quot;757&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 다시 푸시(push)&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤵INPUT&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker push dksek3050/anna-app:1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤴ OUTPUT&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CxKss/btsMSXCLKGk/1jcochmvBeu9ULh4uD9BS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CxKss/btsMSXCLKGk/1jcochmvBeu9ULh4uD9BS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CxKss/btsMSXCLKGk/1jcochmvBeu9ULh4uD9BS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCxKss%2FbtsMSXCLKGk%2F1jcochmvBeu9ULh4uD9BS1%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;661&quot; height=&quot;357&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;357&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQBtAQ/btsMS3bSS4M/NoOtWb8zDSc4TX59DvR0n0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQBtAQ/btsMS3bSS4M/NoOtWb8zDSc4TX59DvR0n0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQBtAQ/btsMS3bSS4M/NoOtWb8zDSc4TX59DvR0n0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQBtAQ%2FbtsMS3bSS4M%2FNoOtWb8zDSc4TX59DvR0n0%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;753&quot; height=&quot;197&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공 !&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 도커 허브에 저장된 50만 개의 이미지에 새로운 이미지가 하나 추가되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 어디서든&amp;nbsp;dksek3050/anna-app:1이미지를 사용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &lt;b&gt;Private Docker Registry&lt;/b&gt;&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 이미지를 비공개로 저장하려면&amp;nbsp;&lt;a href=&quot;https://www.docker.com/products/docker-cloud&quot;&gt;Docker Cloud&lt;/a&gt;를 유료로 사용하거나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 Registry 서버를 자체적으로 구축해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 도커 레지스트리 서버를 구축해서 그 서버에 이미지를 저장해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 레지스트리 서버도 도커 이미지를 다운받아 컨테이너를 띄어서 구축할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤵INPUT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 레지스트리에 저장된 이미지는 파일로 저장되기 때문에 호스트(로컬)의 디렉토리를 마운트 해야한다. (S3 저장소를 사용할 수도 있다.)&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;docker run -d \\
-v $PWD/registry:/var/lib/registry \\
-p 5000:5000 \\
distribution/registry:2.6.0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;v $PWD/registry:/var/lib/registry&lt;/b&gt;: 호스트 시스템의 현재 디렉토리에 있는&amp;nbsp;&lt;b&gt;registry&lt;/b&gt;&amp;nbsp;디렉토리를 컨테이너 내부의&amp;nbsp;&lt;b&gt;/var/lib/registry&lt;/b&gt;&amp;nbsp;디렉토리와 연결한다.이렇게 함으로써 호스트 시스템의 파일을 컨테이너 내부에서 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p 5000:5000&lt;/b&gt;: 호스트의 포트 5000과 컨테이너의 포트 5000을 매핑이것은 호스트 시스템에서 Docker 레지스트리에 접근하기 위해 사용된다.즉, 호스트의 5000번 포트를 통해 컨테이너의 5000번 포트에 연결된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;distribution/registry:2.6.0&lt;/b&gt;: 사용할 Docker 이미지를 지정한다.여기서는 Docker 레지스트리 이미지의 버전 2.6.0을 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤴ OUTPUT&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;85&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tdx6q/btsMTliX8X3/GJWcTW0bVgKBh3sB4cUIp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tdx6q/btsMTliX8X3/GJWcTW0bVgKBh3sB4cUIp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tdx6q/btsMTliX8X3/GJWcTW0bVgKBh3sB4cUIp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftdx6q%2FbtsMTliX8X3%2FGJWcTW0bVgKBh3sB4cUIp1%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;662&quot; height=&quot;85&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;85&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 레지스트리 서버의 아이피와 포트 정보를 이미지 명에 추가하면 바로 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤵INPUT&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;docker tag app localhost:5000/dksek3050/anna-app:1
&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;nbsp;**app**이라는 이름의 Docker 이미지를&amp;nbsp;**localhost:5000/dksek3050/anna-app:1**이라는 태그로 다시 태그
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;app&lt;/b&gt;: 이미지의 현재 이름이며, 빌드된 Docker 이미지의 이름이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;localhost:5000/dksek3050/anna-app:1&lt;/b&gt;: 새 태그로, 이것은 레지스트리 호스트&amp;nbsp;**localhost**의 포트 5000에 있는 Docker 레지스트리의&amp;nbsp;**dksek3050**네임스페이스 아래에&amp;nbsp;&lt;b&gt;anna-app&lt;/b&gt;&amp;nbsp;리포지토리의 버전 1을 나타낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker push localhost:5000/**dksek3050**/**anna**-app:1
&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;이 명령은 지정된 Docker 이미지를 Docker 레지스트리로 푸시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;localhost:5000/dksek3050/anna-app:1&lt;/b&gt;: 푸시 할 이미지의 목적지이다. 앞서 태그한 이미지인&amp;nbsp;**localhost:5000/dksek3050/anna-app:1**을 로컬 Docker 레지스트리에 푸시한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤴ OUTPUT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 레지스트리 서버에 이미지가 파일로 잘 저장되었는지 마운트 된 디렉토리를 살펴보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤵INPUT&lt;/h3&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;tree registry
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤴ OUTPUT&lt;/h3&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;user@user-VirtualBox:~$ tree registry
registry
└── docker
    └── registry
        └── v2
            ├── blobs
            │   └── sha256
            │       ├── 20
            │       │   └── 20b669cab462d559d98bcf239b5af8845b32ea3b0eade1cf37d1f0a08748c14f
            │       │       └── data
            │       ├── 26
            │       │   └── 262c764cde3bf49fbbe7b12253dfeb14cd14eec4ac7ee11309946c8423777833
            │       │       └── data
            │       ├── 4f
            │       │   └── 4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1
            │       │       └── data
            │       ├── 6c
            │       │   ├── 6ccac03c405d0a8003c6e7681d43ead31346aec0bcf66f2e022e1aab3b7ed1b6
            │       │   │   └── data
            │       │   └── 6cdaa9e5748ae6aae5c76730ad8c34dd0594aa3c1e165ea9970475d817335cee
            │       │       └── data
            │       ├── a0
            │       │   └── a07bfac88e40c4c6a1c139d31f41c8af375e5028c51cdb5cecb86032117d4621
            │       │       └── data
            │       ├── b2
            │       │   └── b2a4edb7bfbd9b5d00fb86ecd4df90e3d723a984320d1818fcb3a6b387131e8c
            │       │       └── data
            │       ├── df
            │       │   └── df2fac849a4581b035132d99e203fd83dc65590ea565435a266cb0e14a508838
            │       │       └── data
            │       ├── f9
            │       │   └── f9841d926cf5e1a20e500f332b6c93dc294f09f6e5b876e3d994946d53b6c40a
            │       │       └── data
            │       └── fb
            │           └── fbfce4c3047e0b27a1007eaa604026ffa41592fe5f3e60582b52345864cb3abd
            │               └── data
            └── repositories
                ├── dksek3050
                │   └── anna-app
                │       ├── _layers
                │       │   └── sha256
                │       │       ├── 20b669cab462d559d98bcf239b5af8845b32ea3b0eade1cf37d1f0a08748c14f
                │       │       │   └── link
                │       │       ├── 262c764cde3bf49fbbe7b12253dfeb14cd14eec4ac7ee11309946c8423777833
                │       │       │   └── link
                │       │       ├── 4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1
                │       │       │   └── link
                │       │       ├── 6ccac03c405d0a8003c6e7681d43ead31346aec0bcf66f2e022e1aab3b7ed1b6
                │       │       │   └── link
                │       │       ├── 6cdaa9e5748ae6aae5c76730ad8c34dd0594aa3c1e165ea9970475d817335cee
                │       │       │   └── link
                │       │       ├── a07bfac88e40c4c6a1c139d31f41c8af375e5028c51cdb5cecb86032117d4621
                │       │       │   └── link
                │       │       ├── b2a4edb7bfbd9b5d00fb86ecd4df90e3d723a984320d1818fcb3a6b387131e8c
                │       │       │   └── link
                │       │       ├── df2fac849a4581b035132d99e203fd83dc65590ea565435a266cb0e14a508838
                │       │       │   └── link
                │       │       └── fbfce4c3047e0b27a1007eaa604026ffa41592fe5f3e60582b52345864cb3abd
                │       │           └── link
                │       ├── _manifests
                │       │   ├── revisions
                │       │   │   └── sha256
                │       │   │       └── f9841d926cf5e1a20e500f332b6c93dc294f09f6e5b876e3d994946d53b6c40a
                │       │   │           └── link
                │       │   └── tags
                │       │       └── 1
                │       │           ├── current
                │       │           │   └── link
                │       │           └── index
                │       │               └── sha256
                │       │                   └── f9841d926cf5e1a20e500f332b6c93dc294f09f6e5b876e3d994946d53b6c40a
                │       │                       └── link
                │       └── _uploads

73 directories, 34 files
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지가 레이어 별로 예쁘게 저장된걸 확인할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보안&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 레지스트리는 일반적인 HTTP 프로토콜을 사용하여 이미지를 전송한다. 따라서 SSL(HTTPS)을 사용하지 않으면 이미지 내용이 유출될 수 있다. 이런 보안 이슈 때문에 도커는 기본적으로 로컬(localhost) 서버를 제외하곤 HTTP 사용을 금지하고 있으며 이런 보안위험을 무시하려면 도커 엔진을 실행할 때 허용 옵션을 넣어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 설정은&amp;nbsp;&lt;a href=&quot;https://docs.docker.com/registry/insecure/&quot;&gt;문서&lt;/a&gt;를 참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 저장소를 만드는 법은 매우 간단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 내부적으로 이미지를 관리하고 여러 서버에 배포 할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 배포하기&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너 배포 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 드디어 서버 관리의 꽃! 배포deploy를 해보겠다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 어플리케이션을 배포할때는 사용하는 언어, 프레임워크, 웹, 리눅스 배포판, 개발자의 취향등에 따라서 각각 다른 방식을 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 서버를 세팅하고 한번에 배포를 성공한다는건 힘든 일이었고 검증하기도 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 컨테이너를사용하면 어떤 언어, 어떤 프레임워크를 쓰든 상관없이 배포 방식이 동일해지고 단순해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 이미지를 받고 컨테이너를 실행하면 끝이다. !&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬에서 도커 허브로 올린 이미지 받아서 컨테이너 띄우기&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤵INPUT&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker pull dksek3050/anna-app:1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤴ OUTPUT&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;755&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIFPeE/btsMS6Gdy6L/lPuvKFn4rULIbmC4iD3PlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIFPeE/btsMS6Gdy6L/lPuvKFn4rULIbmC4iD3PlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIFPeE/btsMS6Gdy6L/lPuvKFn4rULIbmC4iD3PlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIFPeE%2FbtsMS6Gdy6L%2FlPuvKFn4rULIbmC4iD3PlK%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;755&quot; height=&quot;130&quot; data-origin-width=&quot;755&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤵INPUT&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker run -d -p 8080:4567 dksek3050/anna-app:1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⤴ OUTPUT&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;523&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rp5VW/btsMS8RznpF/Yf0oJPoo730tKvZxomsv3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rp5VW/btsMS8RznpF/Yf0oJPoo730tKvZxomsv3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rp5VW/btsMS8RznpF/Yf0oJPoo730tKvZxomsv3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frp5VW%2FbtsMS8RznpF%2FYf0oJPoo730tKvZxomsv3K%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;523&quot; height=&quot;176&quot; data-origin-width=&quot;523&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;821&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIt2Wq/btsMSRJuSSq/dptwYNzWCkKPRDx0lk66UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIt2Wq/btsMSRJuSSq/dptwYNzWCkKPRDx0lk66UK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIt2Wq/btsMSRJuSSq/dptwYNzWCkKPRDx0lk66UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIt2Wq%2FbtsMSRJuSSq%2FdptwYNzWCkKPRDx0lk66UK%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;821&quot; height=&quot;194&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차란 배포 성공 ! 이제 저 ip랑 구매한 도메인이랑 연결 시켜주면 우리가 사용하는 웹서비스가 완성되는것이다. (대충)&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;703&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lAxmB/btsMUvdRNpj/4TTYCuATO8u2Y3RmGE8Cjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lAxmB/btsMUvdRNpj/4TTYCuATO8u2Y3RmGE8Cjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lAxmB/btsMUvdRNpj/4TTYCuATO8u2Y3RmGE8Cjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlAxmB%2FbtsMUvdRNpj%2F4TTYCuATO8u2Y3RmGE8Cjk%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;703&quot; height=&quot;224&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;224&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커를 사용하면 업데이트 방식도 배포와 큰 차이는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 이미지를 기반으로 새 컨테이너를 만들고 이전 컨테이너를 중지 및 삭제를 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 소스를 어떻게 복사할지 서버 프로세스는 어떻게 재시작을 할지 고민할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 통째로 바꾼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식은 매우 단순하지만, 컨테이너가 멈추는 순간 실행 중인 프로세스가&amp;nbsp;&lt;b&gt;종료&lt;/b&gt;되고 프로세스가 종료되면 고객들은 접속이 안 되고 접속이 안 되면 매출이 떨어지고 매출이 떨어지면 월급이 안나오기 때문에 무중단을 고려한&amp;nbsp;&lt;a href=&quot;http://nginx.org/&quot;&gt;nginx&lt;/a&gt;나&amp;nbsp;&lt;a href=&quot;http://www.haproxy.org/&quot;&gt;HAProxy&lt;/a&gt;같은 로드 발란서Load Balancer와 2대 이상의 컨테이너를 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것에 대해서는 다음에 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해서 초보 Docker 입문은 끝이 났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음번에는 프로젝트로 만들었던 spring을 한번 띄워보자..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  참고 링크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://subicura.com/2017/02/10/docker-guide-for-beginners-create-image-and-deploy.html&quot;&gt;https://subicura.com/2017/02/10/docker-guide-for-beginners-create-image-and-deploy.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sjh9708.tistory.com/81&quot;&gt;https://sjh9708.tistory.com/81&lt;/a&gt;&lt;/p&gt;</description>
      <category>DevOps/Docker</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/149</guid>
      <comments>https://dksek3050.tistory.com/149#entry149comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:15:01 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] Docker 권한 설정하기</title>
      <link>https://dksek3050.tistory.com/148</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;docker.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnRUoV/btsMVgmYFM7/gD5pzdivyujkotaKnKEpv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnRUoV/btsMVgmYFM7/gD5pzdivyujkotaKnKEpv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnRUoV/btsMVgmYFM7/gD5pzdivyujkotaKnKEpv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnRUoV%2FbtsMVgmYFM7%2FgD5pzdivyujkotaKnKEpv0%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;612&quot; data-filename=&quot;docker.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;Ubuntu 22.04&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; Docker 권한 설정&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 에서 모든 도커 관련된 작업이 root 사용자에게만 권한이 허용되어 있기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 명령어를 입력하기 위해서는&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 앞에 붙여주어야만 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kKcab/btsMSAON7VW/PLKBt7QJkzaOwpojkE53rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kKcab/btsMSAON7VW/PLKBt7QJkzaOwpojkE53rK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kKcab/btsMSAON7VW/PLKBt7QJkzaOwpojkE53rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkKcab%2FbtsMSAON7VW%2FPLKBt7QJkzaOwpojkE53rK%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;867&quot; height=&quot;197&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 root 계정이 Host의 기본 유저에게 권한을 주도록 바꿔볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어는&amp;nbsp;새로운 터미널을 띄어서 수행하도록 하자!&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sudo usermod -a -G docker [사용자 이름]   # [사용자이름]에게 권한주기
sudo service docker restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTtcre/btsMTKQflUQ/CEWyF4X2oDC9y5IJYpRIj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTtcre/btsMTKQflUQ/CEWyF4X2oDC9y5IJYpRIj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTtcre/btsMTKQflUQ/CEWyF4X2oDC9y5IJYpRIj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTtcre%2FbtsMTKQflUQ%2FCEWyF4X2oDC9y5IJYpRIj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;656&quot; height=&quot;97&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;97&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 처럼 명령어를 입력한 후 vm을 다시 시작해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;51&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CZuBw/btsMSFvANyK/lKqXsld1IRnKl23am6XGOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CZuBw/btsMSFvANyK/lKqXsld1IRnKl23am6XGOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CZuBw/btsMSFvANyK/lKqXsld1IRnKl23am6XGOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCZuBw%2FbtsMSFvANyK%2FlKqXsld1IRnKl23am6XGOK%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;702&quot; height=&quot;51&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;51&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까와 똑같은 명령어를 sudo를 적지 않아도 잘 실행 됨을 확인 할 수 있다.&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;gt;&amp;nbsp;&lt;a href=&quot;https://velog.io/@dksek3050/Docker-Docker-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%8B%A4%ED%96%89%ED%95%98%EA%B8%B0&quot;&gt;[Docker] Docker 컨테이너 실행하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  참고 링크&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://subicura.com/2017/01/19/docker-guide-for-beginners-2.html&quot;&gt;https://subicura.com/2017/01/19/docker-guide-for-beginners-2.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://agsu.tistory.com/9&quot;&gt;https://agsu.tistory.com/9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@anrun/docker1&quot;&gt;https://velog.io/@anrun/docker1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://seulcode.tistory.com/557&quot;&gt;https://seulcode.tistory.com/557&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DevOps/Docker</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/148</guid>
      <comments>https://dksek3050.tistory.com/148#entry148comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:12:27 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 도커 설치하기</title>
      <link>https://dksek3050.tistory.com/147</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;docker.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqtW0M/btsMS8xmhnS/tX7IuFkkprrPKQRkfki8vK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqtW0M/btsMS8xmhnS/tX7IuFkkprrPKQRkfki8vK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqtW0M/btsMS8xmhnS/tX7IuFkkprrPKQRkfki8vK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqtW0M%2FbtsMS8xmhnS%2FtX7IuFkkprrPKQRkfki8vK%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;612&quot; data-filename=&quot;docker.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;Ubuntu 22.04&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; Docker 설치&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⭐&lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository&quot;&gt;공식 문서 링크&lt;/a&gt;&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;리눅스 계열 운영체제에서 쓰이는 패키지 관리 툴인 apt를 최신 버전으로 업데이트 한다&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;sudo apt-get update
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 필요한 패키지 설치&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;apt-transport-https&lt;/b&gt;&amp;nbsp;: https를 통해 데이터나 패키지에 접근할 수 있다. (&lt;a href=&quot;https://packages.debian.org/jessie/apt-transport-https&quot;&gt;링크&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ca-certificates&lt;/b&gt;&amp;nbsp;: Certificate Authority - certificates라는 뜻으로 SSL기반 웹 애플리케이션이 SSL연결의 진위여부를 판별할 수 있게 해준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;curl&lt;/b&gt;&amp;nbsp;: 링크로 데이터를 다운 받을 수 있게 해주는 도구&lt;/li&gt;
&lt;li&gt;&lt;b&gt;software-properties-common&lt;/b&gt;&amp;nbsp;: 우분투에서 PPA를 사용하기 위한 패키지&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PPA : Personal Package Archive.우분투는 우분투 소프트웨어 센터에 등록된 프로그램 및 버전만 다운받을 수 있다. 이 방식이 편리하고 검증된 프로그램만 다운 받을 수 있다는 점에서 안전하지만 새로운 버전이 빨리 업데이트 되지 않는다는 단점이 있다. 업데이트 주기는 우분투가 업데이트 되는 6개월인데, 보다 자유롭게 최신 버전과 등록되지 않은 프로그램을 다운 받을 수 있도록 도와 주는 것이 PPA다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Docker의 공식 GPG키를 추가&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;curl -fsSL &amp;lt;https://download.docker.com/linux/ubuntu/gpg&amp;gt; | sudo apt-key add -
&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;li&gt;&lt;b&gt;sudo apt-key add -&lt;/b&gt;&amp;nbsp;: 패키지 키를 추가한다&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPG키 : GNU Privacy Guardgpg키는 도커에만 존재하는 것은 아니고, 리눅스 패키지 관리 툴이 이 프로그램 패키지가 유효한지 확인하기 위해 설치 전 gpg키로 검증하는 과정을 거친다고 한다. 그래서 curl로 gpg키를 다운받아 apt 키 리스트에 추가해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Docker의 공식 apt 저장소를 추가&lt;/h3&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;sudo add-apt-repository &quot;deb [arch=amd64] &amp;lt;https://download.docker.com/linux/ubuntu&amp;gt; $(lsb_release -cs) stable&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;&lt;b&gt;sudo add-apt-repository [저장소 이름]&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어를 실행하면 apt 리스트에 도커가 추가된다. (저장소 이름은 구글에 프로그램과 ppa로 검색하면 찾을수 있다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 시스템 패키지 업데이트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 도커 버전을 다운받기 위해 패키지를 업데이트 한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;sudo apt-get update
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Docker 설치&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;sudo apt-get install docker-ce docker-ce-cli containerd.io
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 엔진은 유료로 제공되는 기업용 솔루션인 EE(Enterprise Edition)와 무료로 제공되는 CE(Community Edition)로 나뉘는데, CE버전을 설치했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. Docker가 설치 확인&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-1 도커 실행상태 확인&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo systemctl status docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-2 도커 실행&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;sudo docker run hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ni29n/btsMT41ZJOK/MxrOsbC1Z5vEMVwIWUNFCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ni29n/btsMT41ZJOK/MxrOsbC1Z5vEMVwIWUNFCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ni29n/btsMT41ZJOK/MxrOsbC1Z5vEMVwIWUNFCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNi29n%2FbtsMT41ZJOK%2FMxrOsbC1Z5vEMVwIWUNFCk%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;874&quot; height=&quot;570&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 &quot;해당 컴퓨터에는 hello-world 라는 도커 이미지가 없다&quot; 라는 오류가 나면 정상적으로 설치가 완료!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음게시글 -&amp;gt;&amp;nbsp;&lt;a href=&quot;https://velog.io/@dksek3050/Docker-Docker-%EA%B6%8C%ED%95%9C-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0&quot;&gt;[Docker] 권한 설정하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  참고 링크&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://subicura.com/2017/01/19/docker-guide-for-beginners-2.html&quot;&gt;https://subicura.com/2017/01/19/docker-guide-for-beginners-2.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://agsu.tistory.com/9&quot;&gt;https://agsu.tistory.com/9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@anrun/docker1&quot;&gt;https://velog.io/@anrun/docker1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DevOps/Docker</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/147</guid>
      <comments>https://dksek3050.tistory.com/147#entry147comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:11:28 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 버츄얼박스(VirtualBox) &amp;amp; 우분투(Ubuntu) 설치</title>
      <link>https://dksek3050.tistory.com/146</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;docker.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcG76d/btsMSUsxDbF/hqURE906Y0VB2MIePXBj0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcG76d/btsMSUsxDbF/hqURE906Y0VB2MIePXBj0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcG76d/btsMSUsxDbF/hqURE906Y0VB2MIePXBj0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcG76d%2FbtsMSUsxDbF%2FhqURE906Y0VB2MIePXBj0K%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;612&quot; data-filename=&quot;docker.png&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 버츄얼박스(VirtualBox) &amp;amp; 우분투(Ubuntu) 다운로드&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커는 리눅스 컨테이너 기반의 기술이기 때문에 윈도우나 맥에서 실습하기 위해서는 가상의 리눅스 환경을 만들어 주어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 Vitrual Box와 Ubuntu를 사용하여 실습을 진행하려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버츄얼 박스 버전 : VirtualBox-6.1.50-161033-Win&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우분투 버전 : ubuntu-22.04.3-desktop-amd64&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;&lt;a href=&quot;https://ubuntu.com/download/desktop&quot;&gt;Ubuntu 다운로드&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;&lt;a href=&quot;https://www.virtualbox.org/wiki/Download_Old_Builds_6_1&quot;&gt;Virtual Box 다운로드&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;543&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kIKI8/btsMT0rSn3O/poM67DE5KZe2klsrFQOzf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kIKI8/btsMT0rSn3O/poM67DE5KZe2klsrFQOzf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kIKI8/btsMT0rSn3O/poM67DE5KZe2klsrFQOzf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkIKI8%2FbtsMT0rSn3O%2FpoM67DE5KZe2klsrFQOzf1%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;825&quot; height=&quot;543&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;543&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 VirtualBox 7.0을 깔았을때는 아래와 같은 오류가 났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 파일을 실행시켰을 때 아래와 같은 오류가 뜬다면 Microsoft Visual C++ 2019를 설치하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jt9F8/btsMSxRZPv4/Zm5ZPSNBk16Q0NFaA7ey31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jt9F8/btsMSxRZPv4/Zm5ZPSNBk16Q0NFaA7ey31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jt9F8/btsMSxRZPv4/Zm5ZPSNBk16Q0NFaA7ey31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJt9F8%2FbtsMSxRZPv4%2FZm5ZPSNBk16Q0NFaA7ey31%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;496&quot; height=&quot;310&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/cpp/windows/latest-supported-vc-redist?view=msvc-170&quot;&gt;Microsoft Visual C++ 2019 다운로드&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ARM64: 스마트폰, 태블릿PC의 CPU코어 설계 회사&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X86: 32bit 윈도우즈 운영체제가 동작하는 CPU의 총칭.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X64: 64bit 윈도우즈 운영체제가 동작하는 CPU의 총칭.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 가상머신에서 우분투 환경 세팅&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 가상 머신 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운 받은 Virtual Box를 실행한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;[새로만들기] - 종류 : Linux, 버전: Ubuntu 64 bit&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkcL04/btsMS9XekoC/6WcdegOXpckX1QiFrfjxOk/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;메모리크기 2048mb&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Wh4Fa/btsMUztLXhb/iiuDMsYjFbqXSaV4I6jwAK/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;하드 디스크 - 디폴트 설정&lt;img src=&quot;https://blog.kakaocdn.net/dn/O1KIi/btsMSXJy27n/ML0skJJkYCJyVeh1EILczk/img.png&quot; /&gt;이게 원인인지는 확실히 모르겠지만 기본 메모리 크기로 설정했다가 우분투 크기보다 작아서 우분투 설치를 못했다..생각보다 넉넉하게 잡아주자&amp;hellip; !!!&lt;/li&gt;
&lt;li&gt;위 설정대로 하면 제대로 깔려요 !&lt;/li&gt;
&lt;li&gt;그래서 가상머신 다시 설치함 ..&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMbfWT/btsMUn1fy53/UyP9GVuZudAY6EBtahFC7k/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5mj36/btsMVdcN2R3/e9BAv6bxGWjUcin4TZ38B0/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dWNWrX/btsMUYz5CGF/GXTk6rdVPQOkBhKkPsNCbk/img.png&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 가상머신 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작버튼을 눌러 가상머신을 실행하자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfCBXq/btsMTbU1X1a/HB1e1KcxZDv7a0GRYeAiG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfCBXq/btsMTbU1X1a/HB1e1KcxZDv7a0GRYeAiG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfCBXq/btsMTbU1X1a/HB1e1KcxZDv7a0GRYeAiG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfCBXq%2FbtsMTbU1X1a%2FHB1e1KcxZDv7a0GRYeAiG1%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;884&quot; height=&quot;520&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pA86N/btsMT1Yt8w5/9pM0n8P1bsrcYCm5JstBZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pA86N/btsMT1Yt8w5/9pM0n8P1bsrcYCm5JstBZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pA86N/btsMT1Yt8w5/9pM0n8P1bsrcYCm5JstBZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpA86N%2FbtsMT1Yt8w5%2F9pM0n8P1bsrcYCm5JstBZK%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;854&quot; height=&quot;550&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 다운받아 두었던 ubuntu .iso 파일을 선택하여 시작해 준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xtioN/btsMUPiXxcl/KELHw4W4kurErXoeWwohK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xtioN/btsMUPiXxcl/KELHw4W4kurErXoeWwohK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xtioN/btsMUPiXxcl/KELHw4W4kurErXoeWwohK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxtioN%2FbtsMUPiXxcl%2FKELHw4W4kurErXoeWwohK1%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;861&quot; height=&quot;456&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;Try or Install Ubuntu&amp;rdquo; 선택&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 우분투 설치&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;언어 리스트 쭉 내리면 [한국어] 선택 - [Ubuntu 설치] 선택&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J1RwX/btsMTMHe5G1/gPQK6nJHN8vE0dhc9KaIk1/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;키보드 레이아웃 [Korean] - [Korean] - [계속하기]&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3FJui/btsMTBMEQI1/16JpIkRZIIqKGJkLxcjKI1/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;업데이트 및 기타 소프트웨어 - 최소설치나는 최소설치만 해주었다.&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHZPBf/btsMSVSuH8n/Cj9myoEZ6CtD50PDsMREEk/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;설치형식 - 기본 설정에서 바꾸지 않았다.&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tSHzH/btsMTBzb4U7/9KKut33CaN6gONuqTUCrGK/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;사용자 정보 입력&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kB5HG/btsMTYt19kA/tGnQhzUPC0jV2HiQ1s7Do1/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;설치완료&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpiR3v/btsMUpx1V5X/1H2T6KRezTIlM6p4UGKK60/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;다시 시작하자.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;605&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRYrFc/btsMTG1xh84/izCWOnRdqyHuj5XvGc7Sok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRYrFc/btsMTG1xh84/izCWOnRdqyHuj5XvGc7Sok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRYrFc/btsMTG1xh84/izCWOnRdqyHuj5XvGc7Sok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRYrFc%2FbtsMTG1xh84%2FizCWOnRdqyHuj5XvGc7Sok%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;875&quot; height=&quot;605&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;605&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 완 !&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7.. 재부팅 후 password 설정&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blyR6X/btsMUkco9I4/KFtEEYLj1bxdBhjt3le660/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blyR6X/btsMUkco9I4/KFtEEYLj1bxdBhjt3le660/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blyR6X/btsMUkco9I4/KFtEEYLj1bxdBhjt3le660/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblyR6X%2FbtsMUkco9I4%2FKFtEEYLj1bxdBhjt3le660%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;435&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널을 열고&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Efoky/btsMTG1xh7M/H8HkDmuzx7EfQfUZm75ta1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Efoky/btsMTG1xh7M/H8HkDmuzx7EfQfUZm75ta1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Efoky/btsMTG1xh7M/H8HkDmuzx7EfQfUZm75ta1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEfoky%2FbtsMTG1xh7M%2FH8HkDmuzx7EfQfUZm75ta1%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;457&quot; height=&quot;374&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어를 입력하여 비밀번호를 바꿔주자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꽤나 깐깐하다 rootroot 하려다가 퇴짜먹었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dkssk123&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;gt;&amp;nbsp;&lt;a href=&quot;https://velog.io/@dksek3050/Docker-%EB%8F%84%EC%BB%A4-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot;&gt;[Docker] 도커 설치하기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  참고 링크&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://subicura.com/2017/01/19/docker-guide-for-beginners-2.html&quot;&gt;https://subicura.com/2017/01/19/docker-guide-for-beginners-2.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://agsu.tistory.com/9&quot;&gt;https://agsu.tistory.com/9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://olppaemmit.tistory.com/216&quot;&gt;https://olppaemmit.tistory.com/216&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kimhjin.tistory.com/39&quot;&gt;https://kimhjin.tistory.com/39&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DevOps/Docker</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/146</guid>
      <comments>https://dksek3050.tistory.com/146#entry146comment</comments>
      <pubDate>Sun, 23 Mar 2025 22:09:39 +0900</pubDate>
    </item>
    <item>
      <title>[Ubuntu] 우분투 &amp;harr; 윈도우 클립보드 공유 안됨 문제, VirtualBox 가상 광 디스크 삽입 문제</title>
      <link>https://dksek3050.tistory.com/145</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ubuntu.png&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bA6ESd/btsMS2qtRYB/CG12JJBOkXWANKfYXGs2V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bA6ESd/btsMS2qtRYB/CG12JJBOkXWANKfYXGs2V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bA6ESd/btsMS2qtRYB/CG12JJBOkXWANKfYXGs2V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbA6ESd%2FbtsMS2qtRYB%2FCG12JJBOkXWANKfYXGs2V0%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;1203&quot; height=&quot;631&quot; data-filename=&quot;ubuntu.png&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⚙&amp;nbsp;&lt;b&gt;우분투&amp;harr; 윈도우 클립보드 공유 안됨 문제&lt;/b&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⚙ VirtualBox 가상 광 디스크 삽입 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VirturalBox 의 Ubuntu에서 도커를 설치하는 실습을 하던 중&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우분투와 윈도우간의 클립보드가 공유되지 않은 것에 대한 불편함을 느끼게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전에 가상머신을 사용했을때 내 기억으론 복붙이 공유가 되었던거 같은데 왜 그런지 문제를 찾아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로는 [장치] - [클립보드 공유] - [양방향]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전혀 먹지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sNmap/btsMSz99MwS/HFm1fnl5NrKueIKq8kTyd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sNmap/btsMSz99MwS/HFm1fnl5NrKueIKq8kTyd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sNmap/btsMSz99MwS/HFm1fnl5NrKueIKq8kTyd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsNmap%2FbtsMSz99MwS%2FHFm1fnl5NrKueIKq8kTyd0%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;817&quot; height=&quot;393&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 검색해본 결과 이걸 해소하기 위한 게스트 확장을 설치하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[장치] - [게스트 확장 CD 이미지 삽입]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LfaGp/btsMT2bZeOT/uRkTrnN23VUBDODNk37EwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LfaGp/btsMT2bZeOT/uRkTrnN23VUBDODNk37EwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LfaGp/btsMT2bZeOT/uRkTrnN23VUBDODNk37EwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLfaGp%2FbtsMT2bZeOT%2FuRkTrnN23VUBDODNk37EwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;486&quot; height=&quot;310&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;띠용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상태로 나는 우분투 버전도 바꿔서 다시 깔아보고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VBox도 다시 깔아보고&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;VERR_RDM_MEDIA_LOCKED라는 오류였고 이는 이미 삽입이 되어 있어서 락이 걸려 있다는 내용이라고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결해보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;가상머신 종료&lt;/li&gt;
&lt;li&gt;VBox 에서 [가상머신 선택] - [설정] - [저장소] - [VbBoxGuestAddition.iso] - 디스크모양선택 - [가상드라이브에서 디스크 꺼내기]&lt;/li&gt;
&lt;li&gt;최초 VirtualBox 설치될때 기본적으로 자동 삽입이 되어 있어서 또 삽입하려니까 lock이 걸린거 같다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;836&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR3bxC/btsMTcGtNd8/hpkTNsPzsQ4d8haXkzOhI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR3bxC/btsMTcGtNd8/hpkTNsPzsQ4d8haXkzOhI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR3bxC/btsMTcGtNd8/hpkTNsPzsQ4d8haXkzOhI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR3bxC%2FbtsMTcGtNd8%2FhpkTNsPzsQ4d8haXkzOhI0%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;836&quot; height=&quot;557&quot; data-origin-width=&quot;836&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 설정을 해주고 나면 iso가 있던 곳이 비어있음을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;585&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dFR2g8/btsMT15eGSe/Bxy0N5rrUrTgJ7Km4BWhMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dFR2g8/btsMT15eGSe/Bxy0N5rrUrTgJ7Km4BWhMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dFR2g8/btsMT15eGSe/Bxy0N5rrUrTgJ7Km4BWhMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdFR2g8%2FbtsMT15eGSe%2FBxy0N5rrUrTgJ7Km4BWhMK%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;856&quot; height=&quot;585&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;585&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;가상머신을 다시 켜서 [장치] - [게스트 확장 CD 이미지 삽입]을 선택&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctlaSp/btsMUPXxlaD/3D9gkR51RHMVmKxikeLFD1/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;뜨시는 분들은 실행하시면 됩니당&lt;/li&gt;
&lt;li&gt;하면 아래왜 같이 알림창이 뜬다는데 나는 안떠서 강제 실행 해주었다.&lt;/li&gt;
&lt;li&gt;상태바에 저렇게 CD 모양이 생겼고 누르면 이런 파일들이 들어가 있는데 여기서 [프로그램 실행]을 누르면 된다는 사람들도 있었다.&lt;img src=&quot;https://blog.kakaocdn.net/dn/CInwS/btsMTcGtNa4/K6ZEJWU8IqYhyUhCAKuh71/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;나는 눌러도 아무 반응이 없어서 강제실행해야한다.&lt;/li&gt;
&lt;li&gt;해당 파일에서 터미널을 열자&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q1uDY/btsMUkcoX8m/sXSnsHbJSNH7tNbB8bemkk/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;자동 실행 파일을 실행시켜주자&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmWs33/btsMTFuOWLR/gNI6MwK52lMKXzTmirVxJk/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;sh autorun.sh&lt;/li&gt;
&lt;li&gt;인증짜잔 이게 드디어 떴다 ㅠㅠㅠㅠㅠㅠㅠ&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0GesA/btsMSSuVq4Z/U3RSgOkkfYKAYTaDDmVoaK/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;설치 완료! 껐다 켜보자&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mMRsA/btsMTn2aj3o/QQI7xRmyeYp9VRMn0QG9o1/img.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;새 터미널에서 reboot 명령어를 치면 간단하게 재시작 할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4HVb8/btsMSRQkV9S/gGbTvqQ0y5sgZkkRmMBwdK/img.png&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 윈도우에서 우분투로 복붙성공 !!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linux Ubuntu 복사하기 :&amp;nbsp; &amp;lt; Shift + Ctrl + C &amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linux Ubuntu 붙여넣기 :&amp;nbsp; &amp;lt;Shift + Ctrl + V &amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  참고 링크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sidepower.tistory.com/43#google_vignette&quot;&gt;https://sidepower.tistory.com/43#google_vignette&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://arkainoh.blogspot.com/2016/09/virtualbox.guest.additions.html&quot;&gt;https://arkainoh.blogspot.com/2016/09/virtualbox.guest.additions.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>DevOps/Linux</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/145</guid>
      <comments>https://dksek3050.tistory.com/145#entry145comment</comments>
      <pubDate>Sun, 23 Mar 2025 21:59:46 +0900</pubDate>
    </item>
    <item>
      <title>GitHub Pages에 리액트 프로젝트(React + Vite) 배포하기</title>
      <link>https://dksek3050.tistory.com/144</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3gaOb/btsMSFoKeOt/dnJIA35fmWe69zmDEkDVOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3gaOb/btsMSFoKeOt/dnJIA35fmWe69zmDEkDVOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3gaOb/btsMSFoKeOt/dnJIA35fmWe69zmDEkDVOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3gaOb%2FbtsMSFoKeOt%2FdnJIA35fmWe69zmDEkDVOK%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;500&quot; height=&quot;498&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 블로그 글에서는 &lt;b&gt;GitHub Pages&lt;/b&gt;를 사용해 특정 폴더 안에 있는 리액트 프로젝트를 배포하는 방법에 대해 다룹니다. 배포한 프로젝트는 &lt;a href=&quot;https://username.github.io/repository-name/&quot;&gt;https://username.github.io/repository-name/&lt;/a&gt; 형식으로 접근할 수 있게 됩니다. 저는 레포지토리 안의 디렉토리에 배포 파일이 존재하기 때문에, 아래와 같은 URL로 접근할 수 있도록 설정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://an0401na.github.io/React_Study/08-Refs-Portals/&quot;&gt;https://an0401na.github.io/React_Study/08-Refs-Portals/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742720592276&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;Refs &amp;amp; Portals&quot; data-og-description=&quot;The Almost Final Countdown Stop the timer once you estimate that time is (almost) up&quot; data-og-host=&quot;an0401na.github.io&quot; data-og-source-url=&quot;https://an0401na.github.io/React_Study/08-Refs-Portals/&quot; data-og-url=&quot;https://an0401na.github.io/React_Study/08-Refs-Portals/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://an0401na.github.io/React_Study/08-Refs-Portals/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://an0401na.github.io/React_Study/08-Refs-Portals/&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;Refs &amp;amp; Portals&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Almost Final Countdown Stop the timer once you estimate that time is (almost) up&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;an0401na.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;제가 위와 같이 설계했던 이유는 리액트 강의를 들으며 만들었던 페이지를 같은 레포지토리의 다른 디렉토리로 구분하여 배포할 예정이 있기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;React_Study/            # 레포지토리 이름
 ├── 08-Refs-Portals/   # 첫 번째 배포된 리액트 페이지
 │    └── dist/         # 빌드된 파일
 ├── 09-AnotherPage/    # 두 번째 배포될 리액트 페이지
 │    └── dist/         # 빌드된 파일
 ├── 10-AnotherExample/ # 세 번째 배포될 리액트 페이지
 │    └── dist/         # 빌드된 파일
 └── ...                # 추가적인 배포될 페이지들

&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같은 구조로 각 페이지마다 별도의 디렉토리를 두고, 각 디렉토리마다 dist 폴더에 빌드 파일을 생성하여 배포합니다. 각 페이지는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://username.github.io/React_Study/08-Refs-Portals/&quot;&gt;https://username.github.io/React_Study/08-Refs-Portals/&lt;/a&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://username.github.io/React_Study/09-AnotherPage/&quot;&gt;https://username.github.io/React_Study/09-AnotherPage/&lt;/a&gt;와 같은 URL로 접근할 수 있게 됩니다.&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. GitHub 블로그 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub에서 프로젝트를 배포하려면 먼저 GitHub Page를 활성화 하기 위해 깃허브 블로그를 생성해주어야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1267&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpc9RA/btsMSxLhkWg/yUu7mvBA0AhW8eunCbaSRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpc9RA/btsMSxLhkWg/yUu7mvBA0AhW8eunCbaSRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpc9RA/btsMSxLhkWg/yUu7mvBA0AhW8eunCbaSRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcpc9RA%2FbtsMSxLhkWg%2FyUu7mvBA0AhW8eunCbaSRk%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;1267&quot; height=&quot;488&quot; data-origin-width=&quot;1267&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;New 버튼을 눌러 새로운 레포지토를 생성하는데 꼭 &amp;lt;본인계정명&amp;gt;.github.io로 생성해주세요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 프로젝트 올릴 레포 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포할 리액트 프로젝트를 GitHub에 올릴 레포지토리를 생성합니다. 해당 레포지토리에는 main 브랜치에서 소스 코드가 관리되고, gh-pages 브랜치에는 배포 파일이 관리됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 레포지토리의 디렉토리 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 구조는 아래와 같으며, 리액트 프로젝트의 소스 코드가 08-Refs-Portals 폴더 내에 위치한다고 가정합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;507&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmxMj6/btsMUOxvEb0/KJqtGLG55zOkX4daV1Dws0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmxMj6/btsMUOxvEb0/KJqtGLG55zOkX4daV1Dws0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmxMj6/btsMUOxvEb0/KJqtGLG55zOkX4daV1Dws0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmxMj6%2FbtsMUOxvEb0%2FKJqtGLG55zOkX4daV1Dws0%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;507&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gh-pages 브랜치에서 목표로하는 디렉토리 구조는 아래와 같습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;React_Study/       # 레포지토리명 
 └── 08-Refs-Portals/ # 레포지토리 안의 폴더명
      └── dist/    # 빌드된 파일

&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 빌드 준비하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 프로젝트 디렉토리에서 필요한 의존성을 설치합니다. (프로젝트 경로에서 실행)&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;PS C:\\workspace\\React_Study\\08-Refs-Portals&amp;gt; npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&amp;nbsp; gh-pages를 개발 의존성으로 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;PS C:\\workspace\\React_Study\\08-Refs-Portals&amp;gt;  npm install --save-dev gh-pages
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;package.json 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;homepage를 GitHub Pages URL로 설정합니다. 그리고 deploy 스크립트를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;  &quot;homepage&quot;: &quot;https://an0401na.github.io/React_Study/08-Refs-Portals/&quot;,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite&quot;,
    &quot;build&quot;: &quot;vite build&quot;,
    &quot;preview&quot;: &quot;vite preview&quot;,
    &quot;deploy&quot;: &quot;gh-pages -d dist&quot;
  }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vite.config.js 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vite의 base 경로를 설정하여 GitHub Pages에서 서브 폴더로 배포될 수 있도록 합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  base: '/React_Study/08-Refs-Portals/'
})

&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 빌드 실행&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;PS C:\\workspace\\React_Study\\08-Refs-Portals&amp;gt; npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어로 main 브랜치에서 dist 폴더에 빌드된 파일들이 생성됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 배포 파일 커밋&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. dist 폴더에 있는 파일들을 main 브랜치에 강제로 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;PS C:\\workspace\\React_Study\\08-Refs-Portals&amp;gt; git add -f dist/*
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: 소스 코드가 아닌 dist 디렉토리만 추가해야 합니다. 소스 코드와 함께 올라가면, GitHub Pages에서 index.html이 아닌 소스 코드가 보여질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. main 브랜치로 커밋을 진행합니다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;PS C:\\workspace\\React_Study\\08-Refs-Portals&amp;gt;  git commit -m &quot;Deploy updated build to gh-pages&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요: 아직 PUSH는 하지마세요. gh-pages 브랜치에 푸시할 준비가 되지 않았으므로, 푸시 작업은 아직 하지 않습니다 build된 파일은 다른 브랜치(gh-pages)에 올릴테니까요.&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;&lt;b&gt;7. Push&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포지토리 루트 디렉토리로 이동한 후, gh-pages 브랜치로 배포 파일을 푸시합니다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;PS C:\\workspace\\React_Study&amp;gt; git subtree push --prefix 08-Refs-Portals/dist origin gh-pages
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 gh-pages 브랜치가 아직 생성되지 않았다면, 아래 명령어로 새로운 브랜치를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;PS C:\\workspace\\React_Study&amp;gt; git checkout --orphan gh-pages
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. &lt;b&gt;GitHub Pages 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub에서 프로젝트 페이지를 활성화하려면 &lt;b&gt;Settings&lt;/b&gt; &amp;gt; &lt;b&gt;Pages&lt;/b&gt;에서 배포할 브랜치로 gh-pages를 선택합니다.&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;Source&lt;/b&gt;에서 &lt;b&gt;gh-pages&lt;/b&gt;를 선택하고, &lt;b&gt;Save&lt;/b&gt; 버튼을 클릭합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. &lt;b&gt;배포 완료&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포가 완료된 후, 아래 URL을 통해 프로젝트를 확인할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;https://username.github.io/React_Study/08-Refs-Portals/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;gtx-trans&quot; style=&quot;position: absolute; left: 216px; top: 3420.73px;&quot;&gt;
&lt;div class=&quot;gtx-trans-icon&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/144</guid>
      <comments>https://dksek3050.tistory.com/144#entry144comment</comments>
      <pubDate>Sun, 23 Mar 2025 18:05:28 +0900</pubDate>
    </item>
    <item>
      <title>[React] React 완벽 가이드 - Refs(참조) &amp;amp; Potals(포탈) 활용하기</title>
      <link>https://dksek3050.tistory.com/143</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnyF7p/btsMUkDnGL5/GHeRnoVYxKgz3rXU9MvlFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnyF7p/btsMUkDnGL5/GHeRnoVYxKgz3rXU9MvlFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnyF7p/btsMUkDnGL5/GHeRnoVYxKgz3rXU9MvlFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnyF7p%2FbtsMUkDnGL5%2FGHeRnoVYxKgz3rXU9MvlFK%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;500&quot; height=&quot;498&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  useRef를 이용하여 만든 Timer Challenge 방문하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://an0401na.github.io/React_Study/08-Refs-Portals/&quot;&gt;https://an0401na.github.io/React_Study/08-Refs-Portals/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1742718847805&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;Refs &amp;amp; Portals&quot; data-og-description=&quot;The Almost Final Countdown Stop the timer once you estimate that time is (almost) up&quot; data-og-host=&quot;an0401na.github.io&quot; data-og-source-url=&quot;https://an0401na.github.io/React_Study/08-Refs-Portals/&quot; data-og-url=&quot;https://an0401na.github.io/React_Study/08-Refs-Portals/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://an0401na.github.io/React_Study/08-Refs-Portals/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://an0401na.github.io/React_Study/08-Refs-Portals/&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;Refs &amp;amp; Portals&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Almost Final Countdown Stop the timer once you estimate that time is (almost) up&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;an0401na.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; Refs (참조) 란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refs(References)는 React에서 특정 DOM 요소나 클래스형 컴포넌트의 인스턴스에 직접 접근할 수 있도록 도와주는 기능입니다.&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;  상태 vs 참조 (useState vs useRef)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;상태(useState)를 사용해야 할 경우:&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;b&gt;UI에 즉시 반영되어야 하는 값&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;상태 변경 시 &lt;b&gt;컴포넌트를 다시 렌더링해야 하는 경우&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;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;b&gt;UI에 영향을 주지 않는 값&lt;/b&gt; (ex. 타이머 ID, 이전 값 저장 등)&lt;/li&gt;
&lt;li&gt;내부적으로만 관리되며, &lt;b&gt;렌더링과 관계없는 값&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;참조(useRef)를 사용해야 할 경우:&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;b&gt;컴포넌트가 다시 실행되지 않도록 해야 하는 값&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;참조 값 변경 &amp;rarr; 컴포넌트 리렌더링 없음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DOM 요소에 직접 접근&lt;/b&gt;해야 할 때 (ex. focus(), scrollTo() 등)&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;  useRef (참조)를 사용하면 왜 리렌더링 되지 않을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 상태가 변경되면 리렌더링이 트리거됩니다. 이때 useState는 상태를 저장하고 관리하는 데 사용되며, 상태 값이 변경되면 컴포넌트를 리렌더링합니다. 반면, useRef는 상태 값을 저장하지 않고 &lt;b&gt;단순히 값을 참조&lt;/b&gt;하는 용도로 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useRef로 관리되는 값은 &lt;b&gt;렌더링과 관계없는 값&lt;/b&gt;입니다. 예를 들어, layerName.current.value처럼 current는 참조 객체로, 그 안의 value는 값이 됩니다. 여기서 value가 변경되더라도 &lt;b&gt;React는 이를 감지하지 않습니다&lt;/b&gt;. 즉, useRef는 값이 변해도 리렌더링을 트리거하지 않기 때문에 UI에 변화가 반영되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useRef는 주로 &lt;b&gt;DOM 요소에 직접 접근&lt;/b&gt;하거나, &lt;b&gt;렌더링과 관련 없는 값&lt;/b&gt;을 저장하는 데 유용합니다. 예를 들어, 타이머 ID, 폼 입력 값 등과 같이 컴포넌트 리렌더링 없이 유지해야 하는 값들을 useRef로 관리하는 것이 적합합니다.&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; useRef 훅으로 JSX 요소 연결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useRef 훅은 &lt;b&gt;리렌더링 없이&lt;/b&gt; 특정 DOM 요소에 접근할 때 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는 useRef를 사용하여 &amp;lt;input&amp;gt; 요소를 참조하고, 버튼 클릭 시 입력된 값을 가져와 useRef를 업데이트하는 방식으로 동작합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useState, useRef } from &quot;react&quot;;

export default function Player() {
    // useRef를 사용하여 input 요소에 대한 참조 생성
    const playerName = useRef();
    
    // 플레이어 이름을 상태로 관리 (null 초기값)
    const [enteredPlayerName, setEnteredPlayerName] = useState(null);

    function handleClick() {
        // useRef를 통해 input 요소의 현재 값 가져오기
        setEnteredPlayerName(playerName.current.value);
    }

    return (
        &amp;lt;section id=&quot;player&quot;&amp;gt;
            {/* enteredPlayerName이 있으면 표시, 없으면 'unknown entity' 출력 */}
            &amp;lt;h2&amp;gt;Welcome {enteredPlayerName ?? &quot;unknown entity&quot;}&amp;lt;/h2&amp;gt;
            &amp;lt;p&amp;gt;
                {/* input 요소에 ref 속성 연결 */}
                &amp;lt;input ref={playerName} type=&quot;text&quot; /&amp;gt;
                {/* 버튼 클릭 시 handleClick 함수 실행 */}
                &amp;lt;button onClick={handleClick}&amp;gt;Set Name&amp;lt;/button&amp;gt;
            &amp;lt;/p&amp;gt;
        &amp;lt;/section&amp;gt;
    );
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ &lt;b&gt;useRef로 참조 생성&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;const playerName = useRef();&lt;/li&gt;
&lt;li&gt;playerName은 특정 DOM 요소를 참조하는 객체가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ &lt;b&gt;JSX에서 ref 속성을 사용하여 요소와 연결&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;input ref={playerName} type=&quot;text&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 input 요소는 이제 playerName.current를 통해 접근할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ &lt;b&gt;버튼 클릭 시 input 값을 가져와 상태 업데이트&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function handleClick() {
    setEnteredPlayerName(playerName.current.value);
}
&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;playerName.current.value를 읽어 enteredPlayerName 상태로 저장합니다.&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; useRef 으로 타이머 제어하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작한 타이머를 멈추려면 setTimeout의 반환값(타이머 ID)이 필요하기 때문에 clearTimeout(timerId)와 같이 호출하면 해당 타이머가 취소할 수 있습니다. 그러나 이때 변수를 사용하게 되면 문제가 발생하게 되는데 이런 경우를 아래 예제를 통해 알아보도록 합시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 1️⃣ 변수(let timer 사용)의 문제점&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;

let timer; // ❌ 전역 변수 사용 (문제 발생 가능)

function TimerChallenge() {
    const [timerExpired, setTimerExpired] = useState(false);

    function handleStart() {
        timer = setTimeout(() =&amp;gt; {
            console.log(&quot;타이머 끝!&quot;);
            setTimerExpired(true);
        }, 5000);
    }

    function handleStop() {
        clearTimeout(timer);
    }

    return (
        &amp;lt;div&amp;gt;
            &amp;lt;button onClick={handleStart}&amp;gt;Start&amp;lt;/button&amp;gt;
            &amp;lt;button onClick={handleStop}&amp;gt;Stop&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 발생:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;let timer가 전역 변수라 여러 개의 타이머를 관리하기 여렵다&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;TimerChallenge 컴포넌트가 여러 개 생기면, &lt;b&gt;모든 인스턴스가 같은 timer 변수를 공유&lt;/b&gt;됩니다.&lt;/li&gt;
&lt;li&gt;하나의 컴포넌트에서 handleStart를 실행하고, 다른 컴포넌트에서 handleStop을 실행하면 엉뚱한 타이머가 멈출 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;컴포넌트가 재렌더링되면 timer 값이 유지되지 않&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;setTimerExpired(true)가 실행되면 상태가 변경(setState)됨 &amp;rarr; 리렌더링 발생&lt;/li&gt;
&lt;li&gt;리렌더링 시 let timer 변수는 초기화 됩니다.&lt;/li&gt;
&lt;li&gt;handleStop에서 clearTimeout(timer)을 호출해도 이전에 저장한 timer와 다를 수 있습니다. (타이머가 멈추지 않을 가능성이 있음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;2️⃣&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; useRef를 사용한 해결 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useRef는 &lt;b&gt;렌더링을 발생시키지 않고&lt;/b&gt; 값을 유지할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useRef, useState } from &quot;react&quot;;

function TimerChallenge() {
    const timer = useRef(null); // ✅ useRef를 사용하여 유지되는 값 저장
    const [timerExpired, setTimerExpired] = useState(false);

    function handleStart() {
        timer.current = setTimeout(() =&amp;gt; {
            console.log(&quot;타이머 끝!&quot;);
            setTimerExpired(true);
        }, 5000);
    }

    function handleStop() {
        clearTimeout(timer.current);
    }

    return (
        &amp;lt;div&amp;gt;
            &amp;lt;button onClick={handleStart}&amp;gt;Start&amp;lt;/button&amp;gt;
            &amp;lt;button onClick={handleStop}&amp;gt;Stop&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;각 컴포넌트 인스턴스마다 독립적인 timer 값을 가집니다.&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;useRef를 사용하면, &lt;b&gt;컴포넌트별로 개별적인 timer 값을 유지합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;첫 번째 코드처럼 let timer가 전역적으로 공유되지 않음 &amp;rarr; 여러 개의 TimerChallenge 인스턴스가 독립적으로 작동 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;리렌더링이 발생해도 timer.current 값이 유지될 수 있습니다.&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;useRef는 상태 변경(setState)으로 인해 컴포넌트가 리렌더링 되어도 값이 초기화되지 않습니다.&lt;/li&gt;
&lt;li&gt;setTimerExpired(true)로 인해 리렌더링이 일어나더라도 기존 타이머 ID가 유지됨 &amp;rarr; handleStop이 정확한 타이머를 중지할 수 있습니다.&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; 커스텀 컴포넌트로 Refs(참조) 전달&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;	import { useState, useRef } from &quot;react&quot;;

function TimerChallenge() {
    const timer = useRef();
    const dialog = useRef();
    
		const [timerExpired, setTimerExpired] = useState(false);

    function handleStart() {
        timer.current = setTimeout(() =&amp;gt; {
            console.log(&quot;타이머 끝!&quot;);
		        setTimerExpired(true);
            dialog.current.showModal(); // 타이머 끝나면 모달 표시 / 내장된 dialog 요소에 showModal 메소드 존재
        }, 5000);
    }

    function handleStop() {
        clearTimeout(timer.current);
    }

    return (
    	&amp;lt;&amp;gt;
    		{/* ResultModal 컴포넌트에 ref 전달 */}
            {&amp;lt;ResultModal ref={dialog} targetTime={targetTime} result=&quot;lost&quot;/&amp;gt;}
            &amp;lt;div&amp;gt;
                &amp;lt;button onClick={handleStart}&amp;gt;Start&amp;lt;/button&amp;gt;
                &amp;lt;button onClick={handleStop}&amp;gt;Stop&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/&amp;gt;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import {forwardRef} from &quot;react&quot;;

const ResultModal = forwardRef(function ResultModal ({ result, targetTime }, ref){
    return (
        &amp;lt;dialog ref={ref} className=&quot;result-modal&quot;&amp;gt;
            &amp;lt;h2&amp;gt;You {result}&amp;lt;/h2&amp;gt;
            &amp;lt;p&amp;gt;
                target time : {targetTime}
            &amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;
                You stopped the timer with &amp;lt;strong&amp;gt;X seconds left.&amp;lt;/strong&amp;gt;
            &amp;lt;/p&amp;gt;
            &amp;lt;form method=&quot;dialog&quot;&amp;gt;
                &amp;lt;button&amp;gt;Close&amp;lt;/button&amp;gt;
            &amp;lt;/form&amp;gt;
        &amp;lt;/dialog&amp;gt;
    );
});

export default ResultModal;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ResultModal은 forwardRef를 통해 ref를 받을 수 있도록 설정되어 있음.&lt;/li&gt;
&lt;li&gt;부모 컴포넌트 TimerChallenge에서 useRef()를 사용하여 dialog라는 ref를 생성.&lt;/li&gt;
&lt;li&gt;ResultModal ref={dialog}로 전달하면, dialog.current가 &amp;lt;dialog&amp;gt; 요소를 직접 가리킴.&lt;/li&gt;
&lt;li&gt;dialog.current.showModal()을 호출하면 &amp;lt;dialog&amp;gt;이 표시됨.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 19 이후에서는 아래와 같이 forwardRef를 사용하지 않고 간단하게 ref 를 자식 컴포넌트의 props를 통해 전달 받을 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function ResultModal ({ ref, result, targetTime }){
    return (
        &amp;lt;dialog ref={ref} className=&quot;result-modal&quot; open={true}&amp;gt;
            &amp;lt;h2&amp;gt;You {result}&amp;lt;/h2&amp;gt;
            &amp;lt;p&amp;gt;
                target time : {targetTime}
            &amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;
                You stopped the timer with &amp;lt;strong&amp;gt;X seconds left.&amp;lt;/strong&amp;gt;
            &amp;lt;/p&amp;gt;
            &amp;lt;form method=&quot;dialog&quot;&amp;gt;
                &amp;lt;button&amp;gt;Close&amp;lt;/button&amp;gt;
            &amp;lt;/form&amp;gt;
        &amp;lt;/dialog&amp;gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; userImperativeHandle 훅으로 컴포넌트 API 노출&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트에서 useImperativeHandle 훅은 &lt;b&gt;컴포넌트 내부에서 사용할 수 있는 메서드나 프로퍼티를 외부로 노출&lt;/b&gt;할 때 유용하게 사용됩니다. 일반적으로 ref는 DOM 요소나 컴포넌트 인스턴스를 참조하는 데 사용되지만, useImperativeHandle을 사용하면 외부에서 ref를 통해 &lt;b&gt;특정 메서드&lt;/b&gt;를 호출할 수 있도록 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useState, useRef } from &quot;react&quot;;

function TimerChallenge() {
    const timer = useRef();
    const dialog = useRef();
    const [timerExpired, setTimerExpired] = useState(false);

    // 타이머 시작
    function handleStart() {
        timer.current = setTimeout(() =&amp;gt; {
            console.log(&quot;타이머 끝!&quot;);
            setTimerExpired(true);
            dialog.current.open();  // 타이머 끝나면 모달을 열기 위해 open 메서드 호출
        }, 5000);
    }

    // 타이머 멈추기
    function handleStop() {
        clearTimeout(timer.current);
    }

    return (
        &amp;lt;&amp;gt;
            {/* ResultModal에 ref를 전달하여 외부에서 open을 호출할 수 있도록 함 */}
            &amp;lt;ResultModal ref={dialog} targetTime={5} result=&quot;lost&quot; /&amp;gt;
            &amp;lt;div&amp;gt;
                &amp;lt;button onClick={handleStart}&amp;gt;Start&amp;lt;/button&amp;gt;
                &amp;lt;button onClick={handleStop}&amp;gt;Stop&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/&amp;gt;
    );
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { forwardRef, useImperativeHandle, useRef } from &quot;react&quot;;

const ResultModal = forwardRef(function ResultModal({ result, targetTime }, ref) {
    const dialog = useRef();

    // 외부에서 호출할 수 있도록 open 메서드 노출
    useImperativeHandle(ref, () =&amp;gt; {
        return {
            open() {
                dialog.current.showModal();  // dialog를 열기 위한 메서드
            }
        };
    });

    return (
        &amp;lt;dialog ref={dialog} className=&quot;result-modal&quot;&amp;gt;
            &amp;lt;h2&amp;gt;You {result}&amp;lt;/h2&amp;gt;
            &amp;lt;p&amp;gt;Target time: {targetTime}&amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;
                You stopped the timer with &amp;lt;strong&amp;gt;X seconds left.&amp;lt;/strong&amp;gt;
            &amp;lt;/p&amp;gt;
            &amp;lt;form method=&quot;dialog&quot;&amp;gt;
                &amp;lt;button&amp;gt;Close&amp;lt;/button&amp;gt;
            &amp;lt;/form&amp;gt;
        &amp;lt;/dialog&amp;gt;
    );
});

export default ResultModal;

&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;TimerChallenge 컴포넌트&lt;/b&gt;는 버튼 클릭 시 타이머를 시작하고, 타이머가 끝나면 ResultModal을 열기 위해 dialog.current.open()을 호출합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ResultModal 컴포넌트&lt;/b&gt;는 forwardRef와 useImperativeHandle을 사용하여 부모 컴포넌트에서 ref를 통해 접근할 수 있는 open 메서드를 노출합니다. 이 메서드는 dialog DOM 요소를 열기 위해 사용됩니다.&lt;/li&gt;
&lt;li&gt;ResultModal의 ref는 TimerChallenge에서 전달되며, 이 ref는 useImperativeHandle을 통해 외부에서 호출 가능한 메서드를 노출하게 됩니다.&lt;/li&gt;
&lt;/ol&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;b&gt;캡슐화 및 재사용성&lt;/b&gt;: ResultModal 내부 구현에 대한 정보를 숨기고, 외부에서는 open 메서드만을 호출하여 모달을 열 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컴포넌트 독립성&lt;/b&gt;: ResultModal 컴포넌트는 외부에서 open 메서드를 통해 호출되므로, TimerChallenge는 ResultModal의 내부 구조에 의존하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연성&lt;/b&gt;: open 메서드는 언제든지 ResultModal 내부에서 변경할 수 있으며, 외부 코드에는 영향을 주지 않습니다.&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;h2 data-ke-size=&quot;size26&quot;&gt; React의 포털 (Portal)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;포털(Portal)&lt;/b&gt;은 React에서 제공하는 기능 중 하나로, &lt;b&gt;컴포넌트의 렌더링 위치를 DOM 트리 내의 다른 위치로 옮길 수 있는 기능&lt;/b&gt;입니다. React 애플리케이션 내에서 특정 UI 요소(예: 모달, 팝업 등)가 페이지의 다른 위치에 렌더링되어야 할 때 유용하게 사용됩니다. 이를 통해 UI를 더 깔끔하게 관리할 수 있으며, 예를 들어 모달, 드롭다운, 툴팁 등 &lt;b&gt;페이지 상단이나 다른 위치에 띄워야 할 UI&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;b&gt;컴포넌트 렌더링 위치 변경&lt;/b&gt;: 포털을 사용하면, 예를 들어 모달이나 대화 상자처럼 중요한 UI 요소를 페이지 내 다른 위치에 렌더링할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DOM 트리 외부에 렌더링&lt;/b&gt;: 보통 모달 같은 UI는 페이지 내 다른 요소들과 겹치지 않도록 페이지의 &lt;b&gt;최상위&lt;/b&gt;에 렌더링됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;접근성 향상&lt;/b&gt;: 포털을 활용하면 &lt;b&gt;HTML 요소의 위치를 의미적으로 더 적절한 곳으로 옮길 수&lt;/b&gt; 있어 접근성을 높일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  예제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ResultModal 컴포넌트&lt;/b&gt;에서 createPortal을 사용하여 모달을 별도의 DOM 노드로 렌더링합니다.&lt;/li&gt;
&lt;li&gt;TimerChallenge 컴포넌트에서 &lt;b&gt;타이머 만료 시&lt;/b&gt; 모달을 열도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { forwardRef, useImperativeHandle, useRef } from &quot;react&quot;;
import { createPortal } from &quot;react-dom&quot;;  // 포털을 사용하려면 react-dom에서 가져와야 함

const ResultModal = forwardRef(function ResultModal({ result, targetTime }, ref) {
    const dialog = useRef();

    useImperativeHandle(ref, () =&amp;gt; {
        return {
            open() {
                dialog.current.showModal(); 
            }
        };
    });

    // 포털을 사용하여 모달을 'modal'이라는 id를 가진 div로 렌더링
    return createPortal(
        &amp;lt;dialog ref={dialog} className=&quot;result-modal&quot;&amp;gt;
            &amp;lt;h2&amp;gt;You {result}&amp;lt;/h2&amp;gt;
            &amp;lt;p&amp;gt;Target time: {targetTime}&amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;
                You stopped the timer with &amp;lt;strong&amp;gt;X seconds left.&amp;lt;/strong&amp;gt;
            &amp;lt;/p&amp;gt;
            &amp;lt;form method=&quot;dialog&quot;&amp;gt;
                &amp;lt;button&amp;gt;Close&amp;lt;/button&amp;gt;
            &amp;lt;/form&amp;gt;
        &amp;lt;/dialog&amp;gt;,
        document.getElementById('modal')  // 모달을 ID가 'modal'인 DOM 요소로 렌더링
    );
});

export default ResultModal;

&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1742718306391&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/vite.svg&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Refs &amp;amp; Portals&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;modal&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;div id=&quot;content&quot;&amp;gt;
      &amp;lt;header&amp;gt;
        &amp;lt;h1&amp;gt;The &amp;lt;em&amp;gt;Almost&amp;lt;/em&amp;gt; Final Countdown&amp;lt;/h1&amp;gt;
        &amp;lt;p&amp;gt;Stop the timer once you estimate that time is (almost) up&amp;lt;/p&amp;gt;
      &amp;lt;/header&amp;gt;
      &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script type=&quot;module&quot; src=&quot;/src/main.jsx&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포털을 사용하면, #modal이라는 div에 모달을 렌더링합니다.&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/143</guid>
      <comments>https://dksek3050.tistory.com/143#entry143comment</comments>
      <pubDate>Sun, 23 Mar 2025 17:25:10 +0900</pubDate>
    </item>
    <item>
      <title>[React] 컴포넌트 속성에 컴포넌트 전달하기</title>
      <link>https://dksek3050.tistory.com/139</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SGlTn/btsMPmO1DlH/sIcBeWNYyNCn3eTPc46nk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SGlTn/btsMPmO1DlH/sIcBeWNYyNCn3eTPc46nk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SGlTn/btsMPmO1DlH/sIcBeWNYyNCn3eTPc46nk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSGlTn%2FbtsMPmO1DlH%2FsIcBeWNYyNCn3eTPc46nk0%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;500&quot; height=&quot;498&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  React에서 children을 활용한 유연한 컴포넌트 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 컴포넌트를 만들다 보면, 내부에 어떤 내용이 들어올지 모르는 &lt;b&gt;컨테이너 역할의 컴포넌트&lt;/b&gt;를 만들 일이 많다. 예를 들어, Sidebar, Dialog, Card 같은 컴포넌트들은 내부에 들어갈 요소가 정해져 있지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 &lt;b&gt;React의 children prop&lt;/b&gt;을 활용하면 꽤 유연하게 컴포넌트를 만들 수 있다. 하지만 children 외에도 &lt;b&gt;특정 prop으로 컴포넌트를 직접 전달하는 방식&lt;/b&gt;도 있는데, 이 방법을 활용하면 더 구조적으로 UI를 구성할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 children과 특정 prop을 활용해 &lt;b&gt;재사용 가능한 컴포넌트를 어떻게 만들고 활용할 수 있는지&lt;/b&gt; 직접 예제를 통해 정리해 보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;children prop이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;children은 &lt;b&gt;컴포넌트가 감싸고 있는 내용을 그대로 전달해주는 특별한 prop&lt;/b&gt;이다. 부모 컴포넌트가 자식 요소를 렌더링할 때, 해당 요소를 동적으로 전달하는 데 사용된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  예제: 기본적인 children 사용법&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Box = ({ children }) =&amp;gt; {
  return &amp;lt;div className=&quot;box&quot;&amp;gt;{children}&amp;lt;/div&amp;gt;;
};

const App = () =&amp;gt; {
  return (
    &amp;lt;Box&amp;gt;
      &amp;lt;p&amp;gt;이 내용이 children으로 전달됩니다!&amp;lt;/p&amp;gt;
    &amp;lt;/Box&amp;gt;
  );
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Box 컴포넌트 내부에서 {children}을 사용해서, 부모가 전달한 요소를 그대로 렌더링할 수 있도록 했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;children을 활용한 유연한 컴포넌트 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 여러 개의 콘텐츠 블록(이미지, 표, 텍스트, 버튼 등)을 표시해야 한다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 블록은 &lt;b&gt;제목을 가지고 있고&lt;/b&gt;, 내부에는 다양한 요소가 들어갈 수 있다. 이럴 때 Section 컴포넌트를 만들어서 재사용하면 좋다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rYaP4/btsMOXWg0Fg/MZdreGHkSRCcSv5Y9p6eD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rYaP4/btsMOXWg0Fg/MZdreGHkSRCcSv5Y9p6eD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rYaP4/btsMOXWg0Fg/MZdreGHkSRCcSv5Y9p6eD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrYaP4%2FbtsMOXWg0Fg%2FMZdreGHkSRCcSv5Y9p6eD1%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;1024&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Section 컴포넌트 구현&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Section = ({ title, children }) =&amp;gt; {
  return (
    &amp;lt;section className=&quot;box&quot;&amp;gt;
      &amp;lt;h2 className=&quot;title&quot;&amp;gt;{title}&amp;lt;/h2&amp;gt;
      &amp;lt;div&amp;gt;{children}&amp;lt;/div&amp;gt;  {/* children을 활용하여 다양한 요소를 삽입 가능 */}
    &amp;lt;/section&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Section 컴포넌트는 &lt;b&gt;title과 children을 props로 받아&lt;/b&gt;, 제목을 표시하고 내부에 어떤 내용이든 들어갈 수 있도록 {children}을 활용했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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. Section 컴포넌트 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Section을 사용해서 **네 가지 다른 섹션(이미지, 표, 텍스트, 버튼)**을 만들어 보자.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const App = () =&amp;gt; {
  return (
    &amp;lt;div className=&quot;grid&quot;&amp;gt;
      &amp;lt;Section title=&quot;이미지&quot;&amp;gt;
        &amp;lt;img src=&quot;https://via.placeholder.com/150&quot; alt=&quot;샘플 이미지&quot; className=&quot;img&quot; /&amp;gt;
      &amp;lt;/Section&amp;gt;

      &amp;lt;Section title=&quot;표&quot;&amp;gt;
        &amp;lt;table className=&quot;table&quot;&amp;gt;
          &amp;lt;thead&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;th&amp;gt;항목&amp;lt;/th&amp;gt;
              &amp;lt;th&amp;gt;값&amp;lt;/th&amp;gt;
            &amp;lt;/tr&amp;gt;
          &amp;lt;/thead&amp;gt;
          &amp;lt;tbody&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td&amp;gt;A&amp;lt;/td&amp;gt;
              &amp;lt;td&amp;gt;100&amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td&amp;gt;B&amp;lt;/td&amp;gt;
              &amp;lt;td&amp;gt;200&amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
          &amp;lt;/tbody&amp;gt;
        &amp;lt;/table&amp;gt;
      &amp;lt;/Section&amp;gt;

      &amp;lt;Section title=&quot;텍스트&quot;&amp;gt;
        &amp;lt;p className=&quot;text&quot;&amp;gt;이것은 샘플 텍스트입니다.&amp;lt;/p&amp;gt;
      &amp;lt;/Section&amp;gt;

      &amp;lt;Section title=&quot;버튼&quot;&amp;gt;
        &amp;lt;button className=&quot;btn&quot;&amp;gt;클릭&amp;lt;/button&amp;gt;
      &amp;lt;/Section&amp;gt;
    &amp;lt;/&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;section title=&quot;텍스트&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/section&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;!--

--&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. children을 사용하면 어떤 점이 좋을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;재사용 가능&lt;/b&gt; &amp;rarr; Section 컴포넌트는 내부 콘텐츠에 &lt;b&gt;구애받지 않고&lt;/b&gt; 여러 곳에서 사용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;유연성&lt;/b&gt; &amp;rarr; children을 활용하면 다양한 요소(이미지, 표, 버튼 등)를 자유롭게 삽입 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;가독성 향상&lt;/b&gt; &amp;rarr; 코드가 모듈화되어 유지보수가 쉬워짐&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  특정 prop을 활용하여 컴포넌트 전달하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;children 외에도 &lt;b&gt;특정 prop을 사용해서 컴포넌트를 직접 전달할 수 있다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 Vue의 &lt;b&gt;Named Slots&lt;/b&gt;과 비슷하게 활용할 수 있으며, 특정 역할을 명확하게 나눠야 할 때 더 유용하다. 예를 들어, Card 컴포넌트에서 &lt;b&gt;헤더와 푸터를 따로 지정&lt;/b&gt;하고 싶다면 특정 prop을 활용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 특정 prop을 활용한 Card 컴포넌트&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const Card = ({ header, content, footer }) =&amp;gt; {
  return (
    &amp;lt;div className=&quot;card&quot;&amp;gt;
      {header &amp;amp;&amp;amp; &amp;lt;div className=&quot;card-header&quot;&amp;gt;{header}&amp;lt;/div&amp;gt;}
      &amp;lt;div className=&quot;card-content&quot;&amp;gt;{content}&amp;lt;/div&amp;gt;
      {footer &amp;amp;&amp;amp; &amp;lt;div className=&quot;card-footer&quot;&amp;gt;{footer}&amp;lt;/div&amp;gt;}
    &amp;lt;/div&amp;gt;
  );
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  header, content, footer를 prop으로 받아서 &lt;b&gt;명확하게 역할을 분리했다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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. Card 컴포넌트 활용&lt;/h3&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;const App = () =&amp;gt; {
  return (
    &amp;lt;div className=&quot;p-6 space-y-4&quot;&amp;gt;
      &amp;lt;Card
        header={&amp;lt;h2&amp;gt;  카드 제목&amp;lt;/h2&amp;gt;}
        content={&amp;lt;p&amp;gt;이것은 카드의 내용입니다.&amp;lt;/p&amp;gt;}
        footer={&amp;lt;button className=&quot;btn-primary&quot;&amp;gt;확인&amp;lt;/button&amp;gt;}
      /&amp;gt;

      &amp;lt;Card
        header={&amp;lt;h2&amp;gt;  데이터 카드&amp;lt;/h2&amp;gt;}
        content={
          &amp;lt;table className=&quot;table&quot;&amp;gt;
            &amp;lt;thead&amp;gt;
              &amp;lt;tr&amp;gt;
                &amp;lt;th&amp;gt;항목&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;값&amp;lt;/th&amp;gt;
              &amp;lt;/tr&amp;gt;
            &amp;lt;/thead&amp;gt;
            &amp;lt;tbody&amp;gt;
              &amp;lt;tr&amp;gt;
                &amp;lt;td&amp;gt;A&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;100&amp;lt;/td&amp;gt;
              &amp;lt;/tr&amp;gt;
            &amp;lt;/tbody&amp;gt;
          &amp;lt;/table&amp;gt;
        }
        footer={&amp;lt;span&amp;gt;업데이트: 2025-03-18&amp;lt;/span&amp;gt;}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 여기서 핵심은 header, content, footer에 &lt;b&gt;컴포넌트를 직접 전달했다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  children vs. 특정 prop 선택 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택 기준 children 사용 특정 prop 사용&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;단순한 컨텐츠 삽입&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;여러 개의 영역이 필요한 경우&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;명확한 구조 분리가 필요한 경우&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;유연한 콘텐츠 삽입이 필요한 경우&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;b&gt;한 개의 콘텐츠만 받는다면 children이 더 간단&lt;/b&gt;하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;b&gt;여러 개의 영역을 나눠야 한다면 특정 prop을 활용&lt;/b&gt;하는 것이 좋다!&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/139</guid>
      <comments>https://dksek3050.tistory.com/139#entry139comment</comments>
      <pubDate>Tue, 18 Mar 2025 23:41:29 +0900</pubDate>
    </item>
    <item>
      <title>[React] React 완벽 가이드 - 섹션 5: React로 투자 계산기 만들기 | 개발 회고</title>
      <link>https://dksek3050.tistory.com/138</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yvi54/btsMMnoiWSv/jBU9hq0Uz32HY5TNOGrkx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yvi54/btsMMnoiWSv/jBU9hq0Uz32HY5TNOGrkx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yvi54/btsMMnoiWSv/jBU9hq0Uz32HY5TNOGrkx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyvi54%2FbtsMMnoiWSv%2FjBU9hq0Uz32HY5TNOGrkx0%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;500&quot; height=&quot;498&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;프로젝트 개요&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 코드는 여기로   &lt;b&gt;&lt;a href=&quot;https://github.com/An0401na/React_Study/tree/main/01-starting-project&quot;&gt;https://github.com/An0401na/React_Study/tree/main/01-starting-project&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 &lt;b&gt;React를 활용한 투자 계산기&lt;/b&gt;를 개발했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 입력한 데이터를 기반으로 &lt;b&gt;연간 투자 금액, 예상 수익률, 투자 기간&lt;/b&gt;을 계산하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;매년 누적된 투자 금액과 이자&lt;/b&gt;를 시각적으로 보여주는 기능을 구현했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;React&lt;/b&gt;: 컴포넌트 기반 UI 개발&lt;/li&gt;
&lt;li&gt;&lt;b&gt;useState&lt;/b&gt;: 사용자 입력값을 상태로 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;map()&lt;/b&gt;: 배열 데이터를 테이블 형식으로 렌더링&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Intl.NumberFormat&lt;/b&gt;: 숫자를 화폐 단위로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ &lt;b&gt;구현 과정&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ &lt;b&gt;  상태 관리 &amp;amp; 입력값 처리&lt;/b&gt; (App.jsx)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 입력값을 useState로 관리하고, 변경될 때마다 상태를 업데이트하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 duration 값이 &lt;b&gt;1년 이상인지 확인&lt;/b&gt;하여 유효한 경우에만 결과를 출력하도록 처리했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;
const [inputs, setInputs] = useState({
  initialInvestment: 10000, // 초기 투자금
  annualInvestment: 1200,   // 연간 투자금
  expectedReturn: 6,        // 예상 수익률 (%)
  duration: 10              // 투자 기간 (년)
});

const inputIsValid = inputs.duration &amp;gt;= 1;

function handleInputValueChange(inputIdentifier, newValue) {
  setInputs(prevInputs =&amp;gt; ({
    ...prevInputs,
    [inputIdentifier]: +newValue // 문자열 &amp;rarr; 숫자로 변환
  }));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;배운 점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React에서 &lt;b&gt;상태(state)를 객체로 관리&lt;/b&gt;할 때, 기존 데이터를 보존하면서 업데이트하는 패턴을 익혔다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조건부 렌더링 (&amp;amp;&amp;amp;)을 활용&lt;/b&gt;해 특정 조건에서만 UI를 보여주는 방법을 활용했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;+newValue를 사용하여 문자열을 숫자로 변환&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ &lt;b&gt; ️ 입력 폼 컴포넌트&lt;/b&gt; (Input.jsx)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 재사용성을 고려하여 입력 폼을 Input 컴포넌트로 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onChange 이벤트를 활용해 부모 컴포넌트(App.jsx)의 상태를 업데이트한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function Input({ label, inputIdentifier, value, onChangeValue }) {
  function handleChange(event) {
    onChangeValue(inputIdentifier, event.target.value);
  }

  return (
    &amp;lt;p&amp;gt;
      &amp;lt;label&amp;gt;{label}&amp;lt;/label&amp;gt;
      &amp;lt;inputtype=&quot;number&quot;
        required
        value={value}
        onChange={handleChange}
      /&amp;gt;
    &amp;lt;/p&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;배운 점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;props를 활용한 &lt;b&gt;컴포넌트 재사용성&lt;/b&gt; 향상&lt;/li&gt;
&lt;li&gt;onChange 이벤트 핸들러를 부모로 전달하는 &lt;b&gt;리프팅(lifting state up) 패턴&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ &lt;b&gt;  투자 결과 테이블 렌더링&lt;/b&gt; (Result.jsx)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력된 값들을 기반으로 &lt;b&gt;투자 결과를 연도별로 계산하여 테이블로 출력&lt;/b&gt;했&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금액은 Intl.NumberFormat을 사용하여 &lt;b&gt;화폐 형식&lt;/b&gt;으로 변환했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const results = calculateInvestmentResults(input);
let totalInterest = 0;
let investedCapital = input.initialInvestment;

return (
  &amp;lt;table id=&quot;result&quot;&amp;gt;
    &amp;lt;thead&amp;gt;
      &amp;lt;tr&amp;gt;
        &amp;lt;th&amp;gt;Year&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;Investment Value&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;Interest (Year)&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;Total Interest&amp;lt;/th&amp;gt;
        &amp;lt;th&amp;gt;Invested Capital&amp;lt;/th&amp;gt;
      &amp;lt;/tr&amp;gt;
    &amp;lt;/thead&amp;gt;
    &amp;lt;tbody&amp;gt;
      {results.map((result) =&amp;gt; {
        totalInterest += result.interest;
        investedCapital += input.annualInvestment;

        return (
          &amp;lt;tr key={result.year}&amp;gt;
            &amp;lt;td&amp;gt;{result.year}&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;{formatter.format(result.valueEndOfYear)}&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;{formatter.format(result.interest)}&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;{formatter.format(totalInterest)}&amp;lt;/td&amp;gt;
            &amp;lt;td&amp;gt;{formatter.format(investedCapital)}&amp;lt;/td&amp;gt;
          &amp;lt;/tr&amp;gt;
        );
      })}
    &amp;lt;/tbody&amp;gt;
  &amp;lt;/table&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;배운 점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.map()을 사용해 &lt;b&gt;배열 데이터를 동적으로 테이블에 렌더링&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;누적 값 (totalInterest)을 매년 업데이트&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;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ &lt;b&gt;트러블슈팅&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 1. &lt;b&gt;Hydration Error: In HTML, whitespace text nodes cannot be a child of &amp;lt;tr&amp;gt;&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생 원인: JSX에서 &amp;lt;tr&amp;gt; 내부에 {/* 주석 */}을 사용하면서 불필요한 공백이 발생했기 때문&lt;/p&gt;
&lt;/blockquote&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;JSX 내부에서는 주석을 &amp;lt;tr&amp;gt; 내부가 아닌 바깥쪽에 배치해야 한다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;{/* 각 연도의 투자 결과 표시 */}
&amp;lt;tr key={result.year}&amp;gt;
  &amp;lt;td&amp;gt;{result.year}&amp;lt;/td&amp;gt;{/*태그 내(HERE) 주석 작성시 오류*/}
  &amp;lt;td&amp;gt;{formatter.format(result.valueEndOfYear)}&amp;lt;/td&amp;gt;
  &amp;lt;td&amp;gt;{formatter.format(result.interest)}&amp;lt;/td&amp;gt;
  &amp;lt;td&amp;gt;{formatter.format(totalInterest)}&amp;lt;/td&amp;gt;
  &amp;lt;td&amp;gt;{formatter.format(investedCapital)}&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&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;❌ &lt;b&gt;2. useState 상태 업데이트 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  문제 원인: &lt;b&gt;useState의 불변성 위반&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;const [inputs, setInputs] = useState({INPUT});

function handleInputValueChange(input, newValue){
    setInputs(prevInputs =&amp;gt; {
        console.log(&quot;Inputvalue change:&quot;, input, &quot;:&quot;, newValue);
        let current = prevInputs;
        current.INPUT.initialInvestment = newValue; // ❌ 상태 직접 변경 (잘못된 코드)

        return current;
    })
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;문제 설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;prevInputs를 직접 변경하면 React가 상태 변경을 감지하지 못해 컴포넌트가 리렌더링되지 않음.&lt;/li&gt;
&lt;li&gt;React 상태는 &lt;b&gt;불변성(immutable)을 유지해야 함&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;해결 방법:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 객체를 반환해야 함!&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const [inputs, setInputs] = useState(INPUT);

function handleInputValueChange(input, newValue) {
    setInputs(prevInputs =&amp;gt; {
        console.log(&quot;Input value change:&quot;, input, &quot;:&quot;, newValue);
        return {
            ...prevInputs, // 기존 상태 복사
            [input]: newValue, // 변경된 값 적용
        };
    });
}

&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;❌ &lt;b&gt;3. 변경된 값이 console.log에 찍히지 않는 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  문제 원인: &lt;b&gt;잘못된 참조 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;console.log(&quot;initialInvestment : &quot;+INPUT.initialInvestment);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;문제 설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;INPUT은 useState로 관리하는 &lt;b&gt;상태(state)가 아니고 단순한 변수&lt;/b&gt;이므로 상태 변경이 반영되지 않음.&lt;/li&gt;
&lt;li&gt;console.log(&quot;initialInvestment : &quot; + INPUT.initialInvestment);는 상태 업데이트 전 값을 참조함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;해결 방법:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 값인 inputs를 사용해야 변경된 값을 확인할 수 있음.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;console.log(&quot;initialInvestment : &quot; + inputs.initialInvestment);
&lt;/code&gt;&lt;/pre&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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;프로젝트 결과물&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;1345&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgmdLM/btsMNBZ7X1Z/Q7VKSH0Qvw1sz8hnzqmDeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgmdLM/btsMNBZ7X1Z/Q7VKSH0Qvw1sz8hnzqmDeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgmdLM/btsMNBZ7X1Z/Q7VKSH0Qvw1sz8hnzqmDeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgmdLM%2FbtsMNBZ7X1Z%2FQ7VKSH0Qvw1sz8hnzqmDeK%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;1115&quot; height=&quot;1345&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;1345&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8vft1/btsMMH0WC2l/ffM40s0Bpff79AnmJKzdHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8vft1/btsMMH0WC2l/ffM40s0Bpff79AnmJKzdHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8vft1/btsMMH0WC2l/ffM40s0Bpff79AnmJKzdHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8vft1%2FbtsMMH0WC2l%2FffM40s0Bpff79AnmJKzdHK%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;1040&quot; height=&quot;696&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;696&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  &lt;b&gt;배운 점 &amp;amp; 느낀 점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;useState를 활용한 상태 관리&lt;/b&gt;의 중요성을 다시 한 번 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;컴포넌트 재사용성&lt;/b&gt;을 고려해 작은 단위로 분리하는 것이 유지보수에 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ JSX에서 &lt;b&gt;불필요한 공백을 조심해야 한다&lt;/b&gt;는 점을 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ React에서 &lt;b&gt;데이터 흐름을 관리하는 패턴&lt;/b&gt; (props 전달, lifting state up 등)을 실습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ +newValue를 사용하여 문자열을 숫자로 변환하여 상태 관리&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;b&gt;React의 기본 개념을 정리하고, 실제로 적용&lt;/b&gt;할 수 있었다!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나은 사용자 경험을 위해 &lt;b&gt;차트 라이브러리와 애니메이션&lt;/b&gt;을 추가하는 것도 흥미로울 것 같다.  &lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/138</guid>
      <comments>https://dksek3050.tistory.com/138#entry138comment</comments>
      <pubDate>Tue, 18 Mar 2025 02:25:43 +0900</pubDate>
    </item>
    <item>
      <title>[React]스프레드 연산자를 통한 속성 전달 방식- id 전달</title>
      <link>https://dksek3050.tistory.com/137</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lkDwn/btsMHtVymSg/25uzsyd02Hk9bk6lRqKip0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lkDwn/btsMHtVymSg/25uzsyd02Hk9bk6lRqKip0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lkDwn/btsMHtVymSg/25uzsyd02Hk9bk6lRqKip0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlkDwn%2FbtsMHtVymSg%2F25uzsyd02Hk9bk6lRqKip0%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;500&quot; height=&quot;498&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt; &lt;/span&gt; 속성(props) 전달에서 발생하는 문제와 해결 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트에서 커스텀 컴포넌트에 속성을 전달할 때, HTML의 기본 속성인 id나 className 같은 속성이 &lt;b&gt;정상적으로 전달되지 않는 문제&lt;/b&gt;가 발생할 수 있습니다. 이 문제를 해결하는 방법과 함께, Forwarded 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;h3 data-ke-size=&quot;size23&quot;&gt;1. &lt;b&gt;기본 코드 예시&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 아래와 같은 기본 코드를 봅시다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function UserProfile({name, age, children}) {
  return (
    &amp;lt;div &amp;gt;
      &amp;lt;h2&amp;gt;{name}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;{age} years old&amp;lt;/p&amp;gt;
      &amp;lt;div&amp;gt;{children}&amp;lt;/div&amp;gt;  {/* 자식 요소 출력 */}
    &amp;lt;/div&amp;gt;
  );
}

function App() {
  return (
    &amp;lt;UserProfile id=&quot;profile&quot; className=&quot;user&quot; name=&quot;John&quot; age={30}&amp;gt;
      user
    &amp;lt;/UserProfile&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 App 컴포넌트에서 UserProfile 컴포넌트로 id와 className을 전달하고 있습니다. 하지만 커스텀 컴포넌트인 UserProfile은 &lt;b&gt;HTML 기본 속성&lt;/b&gt;을 자동으로 전달하지 않기 때문에, id=&quot;profile&quot;과 className=&quot;user&quot;가 실제로 div 태그에 적용되지 않습니다.&lt;/p&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. &lt;b&gt;문제점: 속성이 전달되지 않음&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트에서 UserProfile 같은 커스텀 컴포넌트는 &lt;b&gt;JSX에서 전달받은 속성&lt;/b&gt;을 &lt;b&gt;기본적으로 DOM 요소로 전달하지 않습니다&lt;/b&gt;. 예를 들어, id와 className은 UserProfile 컴포넌트 내부에서 사용되지 않으며, 이 때문에 CSS가 적용되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. &lt;b&gt;해결 방법 1: 명시적으로 전달하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하려면, UserProfile 컴포넌트에서 &lt;b&gt;id와 className 속성을 명시적으로&lt;/b&gt; div 태그에 전달해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function UserProfile({ name, age, children, id }) {
  return (
    &amp;lt;div id={id}&amp;gt;
      &amp;lt;h2&amp;gt;{name}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;{age} years old&amp;lt;/p&amp;gt;
      &amp;lt;div&amp;gt;{children}&amp;lt;/div&amp;gt;  {/* 자식 요소 출력 */}
    &amp;lt;/div&amp;gt;
  );
}

function App() {
  return (
    &amp;lt;UserProfile id=&quot;profile&quot; className=&quot;user&quot; name=&quot;John&quot; age={30}&amp;gt;
      user
    &amp;lt;/UserProfile&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 UserProfile 컴포넌트 내에서 id={id}와 같은 방식으로 &lt;b&gt;속성들을 명시적으로 전달&lt;/b&gt;하면, id와 className이 div 태그에 적용됩니다. 하지만, 만약 전달할 속성이 많아진다면, &lt;b&gt;코드가 복잡해질 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. &lt;b&gt;해결 방법 2: 구조분해할당과 스프레드 연산자 사용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성 전달을 더욱 간단하게 처리하려면, &lt;b&gt;구조분해 할당&lt;/b&gt;과 &lt;b&gt;스프레드 연산자&lt;/b&gt;(...props)를 사용할 수 있습니다. 이렇게 하면, UserProfile 컴포넌트에서 전달받은 모든 속성들을 &lt;b&gt;자동으로&lt;/b&gt; div 태그에 전달할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function UserProfile({ name, age, children, ...props }) {// 'name', 'age', 'children'을 구조분해하고, 나머지 props는 'div'에 전달
  return (
    &amp;lt;div {...props}&amp;gt; {/* 모든 속성(props)을 div에 전달 */}
      &amp;lt;h2&amp;gt;{name}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;{age} years old&amp;lt;/p&amp;gt;
      &amp;lt;div&amp;gt;{children}&amp;lt;/div&amp;gt;  {/* 자식 요소 출력 */}
    &amp;lt;/div&amp;gt;
  );
}

function App() {
  return (
    &amp;lt;UserProfile id=&quot;profile&quot; className=&quot;user&quot; name=&quot;John&quot; age={30}&amp;gt;
      user
    &amp;lt;/UserProfile&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 &lt;b&gt;스프레드 연산자&lt;/b&gt;(...props)를 사용하면, UserProfile 컴포넌트로 전달된 &lt;b&gt;모든 속성&lt;/b&gt;이 div 태그에 자동으로 적용됩니다. 이제 id, className, 그리고 다른 속성들도 문제없이 div 태그에 전달되어 적용됩니다.&lt;/p&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;5. &lt;b&gt;결과&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 id=&quot;profile&quot;과 className=&quot;user&quot;가 div 태그에 &lt;b&gt;정상적으로 적용&lt;/b&gt;되어, 스타일링이 잘 이루어집니다. 브라우저에서 렌더링된 HTML은 다음과 같을 것입니다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;&amp;lt;div id=&quot;profile&quot; class=&quot;user&quot;&amp;gt;
  &amp;lt;h2&amp;gt;John&amp;lt;/h2&amp;gt;
  &amp;lt;p&amp;gt;30 years old&amp;lt;/p&amp;gt;
  &amp;lt;div&amp;gt;user&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/137</guid>
      <comments>https://dksek3050.tistory.com/137#entry137comment</comments>
      <pubDate>Wed, 12 Mar 2025 01:39:51 +0900</pubDate>
    </item>
    <item>
      <title>[React] 함수형 컴포넌트의 반환 return 값 조건 &amp;amp; Fragment(프래그먼트) &amp;lt;&amp;gt;...&amp;lt;/&amp;gt;</title>
      <link>https://dksek3050.tistory.com/136</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/60Emu/btsMFI0qhmB/eAOEIGRuAWZdkqsKEPCLAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/60Emu/btsMFI0qhmB/eAOEIGRuAWZdkqsKEPCLAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/60Emu/btsMFI0qhmB/eAOEIGRuAWZdkqsKEPCLAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F60Emu%2FbtsMFI0qhmB%2FeAOEIGRuAWZdkqsKEPCLAK%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;500&quot; height=&quot;498&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 함수에서 return 값을 두 개 가질 수 없듯이 함수형 컴포넌트에서도 여러 개의 요소를 반환 할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 컴포넌트는 반드시 하나의 요소를 반환 해야한다. 이와 같이 리액트의 함수형 컴포넌트의 return 값에는 몇가지 조건이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 1.&lt;b&gt; 반드시&lt;/b&gt; &lt;b&gt;하나의 리액트 엘리먼트(React Element)를 반환해야 한다.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌잘못된 예시 (여러 개의 요소를 반환)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Welcome() {
  return &amp;lt;h1&amp;gt;Hello&amp;lt;/h1&amp;gt;
         &amp;lt;p&amp;gt;React!&amp;lt;/p&amp;gt;;  // 오류 발생 ❌
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 화면의 UI를 구성하기 위해서 하나의 엘리먼트로는 부족한 경우가 많다. 이를 해결하기 위해 불필요한 &amp;lt;div&amp;gt; 로 감쌀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⭕ 올바른 예시 (하나의 부모 요소로 감싸기)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Welcome() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Hello&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;React!&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 요소를 사용하지 않기 위해 &lt;b&gt;Fragment (&amp;lt;&amp;gt;...&amp;lt;/&amp;gt;)&lt;/b&gt; 를 사용하여 불필요한 &amp;lt;div&amp;gt; 없이도 감쌀 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1741702490645&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Fragment } from 'react';

function Welcome() {
  return (
    &amp;lt;Fragment&amp;gt;
      &amp;lt;h1&amp;gt;Hello&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;React!&amp;lt;/p&amp;gt;
    &amp;lt;/Fragment&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Welcome() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;Hello&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;React!&amp;lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. 반드시 유효한 JSX 또는 리액트 엘리먼트를 반환 해야한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return 값으로 일반 문자열이나 숫자를 직접 반환하면 오류가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;❌ 잘못된 예제:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function Welcome() {
  return &quot;Hello, React!&quot;; // 오류 발생 ❌
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 컴포넌트는 &lt;b&gt;반드시 JSX 또는 React.createElement()를 반환해야 한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⭕ 올바른 예제 (JSX를 사용):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Welcome() {
  return &amp;lt;h1&amp;gt;Hello, React!&amp;lt;/h1&amp;gt;; // 올바름 ⭕ 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 React.createElement를 사용:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;function Welcome() {
  return React.createElement(&quot;h1&quot;, null, &quot;Hello, React!&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. 배열 반환 가능 (React 16 이상)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 16 이상에서는 배열을 반환하여 여러 개의 요소를 감싸지 않고도 렌더링할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⭕ 배열을 반환하는 예제:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;jsx
복사편집
function Welcome() {
  return [
    &amp;lt;h1 key=&quot;1&quot;&amp;gt;Hello&amp;lt;/h1&amp;gt;,
    &amp;lt;p key=&quot;2&quot;&amp;gt;React!&amp;lt;/p&amp;gt;
  ];
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  배열 반환 시, 각 요소에는 key를 부여하자. 리액트가 리스트를 렌더링할 때 효율적으로 업데이트할 수 있도록 하기 위해&lt;/p&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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 4. null, false, undefined, []도 반환 가능 (렌더링 안 됨)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무것도 렌더링하고 싶지 않다면 null, false, undefined 등을 반환할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function Welcome() {
  return null; // 화면에 아무것도 출력되지 않음
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;function Welcome({ show }) {
  return show ? &amp;lt;h1&amp;gt;Hello, React!&amp;lt;/h1&amp;gt; : null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 show가 true이면 &amp;lt;h1&amp;gt;Hello, React!&amp;lt;/h1&amp;gt;를 렌더링하고, false이면 아무것도 렌더링하지 않는다.&lt;/p&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/136</guid>
      <comments>https://dksek3050.tistory.com/136#entry136comment</comments>
      <pubDate>Tue, 11 Mar 2025 23:10:22 +0900</pubDate>
    </item>
    <item>
      <title>[React] Element와 Componet의 차이</title>
      <link>https://dksek3050.tistory.com/135</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_react_2.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d2wqMI/btsMIEBMcoY/LTqDQMGkwnzqcwQDZPahik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d2wqMI/btsMIEBMcoY/LTqDQMGkwnzqcwQDZPahik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d2wqMI/btsMIEBMcoY/LTqDQMGkwnzqcwQDZPahik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd2wqMI%2FbtsMIEBMcoY%2FLTqDQMGkwnzqcwQDZPahik%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;298&quot; data-filename=&quot;edited_edited_react_2.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; Element&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앱의 UI를 구성하는 가장 단위&lt;/li&gt;
&lt;li&gt;immutable 객체(불변)&lt;/li&gt;
&lt;li&gt;화면에 표시할 내용을 기술하는 단순한 객체이며, 실제 DOM 요소를 직접 만들지는 않는다.&lt;/li&gt;
&lt;li&gt;React.createElement() 함수로 생성되며, JSX를 사용하면 더 쉽게 작성 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const element = &amp;lt;h1&amp;gt;Hello, React!&amp;lt;/h1&amp;gt;;
&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;아래 element는 화면에 &amp;lt;h1&amp;gt;Hello, React!&amp;lt;/h1&amp;gt;를 표현하는 객체일 뿐, 아직 실제 DOM에 반영된 것은 아니다. 리액트는 이러한 엘리먼트를 ReactDOM.render() 혹은 useState, useEffect 등의 업데이트 과정에서 가상 DOM을 통해 실제 DOM으로 변환합니다.&lt;/li&gt;
&lt;/ul&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; Component&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;JSX를 반환하는 함수형 컴포넌트(Function Component) 또는 React.Component를 상속받는 클래스형 컴포넌트(Class Component)로 만들 수 있다.&lt;/li&gt;
&lt;li&gt;컴포넌트는 &lt;b&gt;자체적인 상태(state)와 생명주기(lifecycle)&lt;/b&gt; 를 가질 수 있다(함수형 컴포넌트에서는 useState, useEffect 같은 훅을 사용).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Welcome() {
  return &amp;lt;h1&amp;gt;Hello, React!&amp;lt;/h1&amp;gt;;
}
&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;위의 Welcome 컴포넌트는 &amp;lt;h1&amp;gt;Hello, React!&amp;lt;/h1&amp;gt; 엘리먼트를 반환하는 &lt;b&gt;함수형 컴포넌트이다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;컴포넌트를 사용할 때는 JSX에서 태그처럼 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// jsx 미사용 시
const element = React.createElement(Welcome);

// jsx 사용시
const element = &amp;lt;Welcome /&amp;gt;;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Programming/React</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/135</guid>
      <comments>https://dksek3050.tistory.com/135#entry135comment</comments>
      <pubDate>Tue, 11 Mar 2025 22:40:30 +0900</pubDate>
    </item>
    <item>
      <title>[React] React 완벽 가이드 - 섹션 3: 리액트의 핵심 - 컴포넌트, JSX, 속성, 상태 등</title>
      <link>https://dksek3050.tistory.com/134</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nW096/btsMAuNGq6i/SkZL55fkxMHvqAfkNfXoJ1/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nW096/btsMAuNGq6i/SkZL55fkxMHvqAfkNfXoJ1/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nW096/btsMAuNGq6i/SkZL55fkxMHvqAfkNfXoJ1/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnW096%2FbtsMAuNGq6i%2FSkZL55fkxMHvqAfkNfXoJ1%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;500&quot; height=&quot;498&quot; data-filename=&quot;edited_react_2.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;br /&gt; Componet&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;HTML, CSS, 자바스크립트 로직 등을 포함하고 있다.&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;컴포넌트가 없다면 html 코드가 방대하고 복잡해져 유지보수가 어려워질 수 있다.&lt;/li&gt;
&lt;li&gt;오류 가능성이 줄어든다.&lt;/li&gt;
&lt;li&gt;코드를 재활용 하는 방식이기 때문에 한 부분에서 일괄적으로 수정이 가능하다.&lt;/li&gt;
&lt;li&gt;공통된 기능을 제공하는 유사한 코드가 묶여 작성되어 개발 과정이 단순해진다.&lt;/li&gt;
&lt;li&gt;각 컴포넌트들 마다 각 기능별로 관심사가 분리되어 작업된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;컴포넌트 저장 및 좋은 프로젝트 구조&lt;/li&gt;
&lt;li&gt;src - assets - components - Header - Header.jsx - Header.css - App.jsx&lt;/li&gt;
&lt;/ul&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; JSX&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JavaScript Syntax Extention, JavaScript 의 확장된 문법&lt;/li&gt;
&lt;li&gt;함수를 추가하여 컴포넌트 생성이 가능하다.&lt;/li&gt;
&lt;li&gt;JSX 문법은 브라우저에서는 사용이 불가능하지만 리액트 개발을 위해 작성하는 코드로 브라우저에 도달하기 전에 개발 서버에서 브라우저에서 사용 가능 하도록 변환된다.\&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;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// App.jsx
// Header 컴포넌트 생성
function Header() {
  return ( // 반호나 값이 여러줄인 경우 괄호로 묶기
    &amp;lt;header&amp;gt;
      &amp;lt;img src=&quot;src/assets/react-core-concepts.png&quot; alt=&quot;Stylized atom&quot; /&amp;gt;
      &amp;lt;h1&amp;gt;React Essentials&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;
        Fundamental React concepts you will need for almost any app you are going to build!
      &amp;lt;/p&amp;gt;
    &amp;lt;/header&amp;gt;
  );
}

// Header 컴포넌트 사용
&amp;lt;Header&amp;gt;&amp;lt;/Header&amp;gt;
// or
&amp;lt;Header/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; Element Rendering&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Element 는 React 의 가장 작은 단위를 의미한다. (Element는 Componet의 &amp;ldquo;구성요소&amp;rdquo;) const element = &amp;lt;h1&amp;gt;Hello, world&amp;lt;/h1&amp;gt;;&lt;/li&gt;
&lt;li&gt;리액트로 구현된 어플리케이션은 일반적으로 하나의 루트 DOM 노드가 존재한다. &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/li&gt;
&lt;li&gt;이 루트 DOM 노드에 들어가는 모든 엘리먼트들을 React DOM에서 관리한다.&lt;/li&gt;
&lt;li&gt;리액트 엘리먼트를 렌더링 하기 위해서는 DOM 엘리먼트를 &amp;nbsp;&lt;a href=&quot;https://ko.legacy.reactjs.org/docs/react-dom-client.html#createroot&quot;&gt;ReactDOM.createRoot()&lt;/a&gt;에 전달한 다음, React 엘리먼트를&amp;nbsp;root.render()에 전달해야 한다.
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const root = ReactDOM.createRoot(
  document.getElementById('root')
);
const element = &amp;lt;h1&amp;gt;Hello, world&amp;lt;/h1&amp;gt;;
root.render(element);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;import ReactDOM from &quot;react-dom/client&quot;; import App from &quot;./App&quot;; import &quot;./index.css&quot;; const entryPoint = document.getElementById(&quot;root&quot;); ReactDOM.createRoot(entryPoint).render(&amp;lt;App /&amp;gt;);&lt;/li&gt;
&lt;li&gt;컴포넌트 계층구조를 의미하는 컴포넌트 트리는 리액트를 통해 화면으로 렌더링 될 뿐, 커스텀 컴포넌트는 실제로 렌더링된 DOM에 나타나지 않는다.&lt;/li&gt;
&lt;/ul&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;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jsx 또는 리액트에 중괄호{}를 사용하여 동적인 값을 출력 가능&lt;/li&gt;
&lt;li&gt;요소 태그 사이에 넣을 수도 있고, 속성 값으로서도 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;단, 동적인 값을 표현할때 따옴표를 작성하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;p&amp;gt;{동적인 값의 변수}&amp;lt;/p&amp;gt;
&amp;lt;img src={동적인 값의 변수}/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const reactDescriptions = [&quot;Fundamental&quot;, &quot;Crucial&quot;, &quot;Core&quot;];

function genRandomInt(max) {
  return Math.floor(Math.random() * (max + 1));
}

function Header() {
  return (
    &amp;lt;header&amp;gt;
      &amp;lt;img src=&quot;src/assets/react-core-concepts.png&quot; alt=&quot;Stylized atom&quot; /&amp;gt;
      &amp;lt;h1&amp;gt;React Essentials&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;
        {reactDescriptions[genRandomInt(2)]} React concepts you will need for
        almost any app you are going to build!
      &amp;lt;/p&amp;gt;
    &amp;lt;/header&amp;gt;
  );
}
&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;이미지를 가져오는 최적의 방법 - import
&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import reactImg from './src/assets/react-core-concepts.png';

...
&amp;lt;img src={reactImg} alt=&quot;Stylized atom&quot; /&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&amp;lt;img src=&quot;src/assets/react-core-concepts.png&quot; alt=&quot;Stylized atom&quot; /&amp;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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; Prop(속성)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Prop는 부모 &amp;rarr; 자식 컴포넌트로 데이터를 전달하는 방법 (부모 &amp;rarr; 자식 방향으로만 데이터 전달 가능)&lt;/li&gt;
&lt;li&gt;읽기 전용이므로 자식 컴포넌트에서 prop 값을 변경 불가능하다.&lt;/li&gt;
&lt;li&gt;일반적으로 props 라고 불리는 하나의 매개변수(변수명수정가능)만 사용해서 데이터를 전달할 때는 커스텀 특성은 key로 속성의 값은 value 형태로 객체화 되어 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;// 부모
function App() {
	return (
		...
		&amp;lt;CoreComcept title=&quot;Components&quot; description=&quot;blablabla&quot; image ={Img} /&amp;gt;
		// or
		&amp;lt;CoreComcept {...CORE_CONCEPT[0]} /&amp;gt; // 스프레드 연산자 사용
		...
	);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// 자식
function CoreComcept (props) { 
	return (
		&amp;lt;img src={props.image} /&amp;gt;
		&amp;lt;h3&amp;gt; {props.title}&amp;lt;/h3&amp;gt;
		&amp;lt;p&amp;gt;{props.descriptsion&amp;gt;/&amp;lt;p&amp;gt;
	);
}

// 구조분해 한 방식
function CoreComcept ({image, title, description}) { // 
	return (
		&amp;lt;img src={image} /&amp;gt;
		&amp;lt;h3&amp;gt; {title}&amp;lt;/h3&amp;gt;
		&amp;lt;p&amp;gt;{description}&amp;gt;/&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; Children Prop&lt;/h3&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;prop의 명이 무조건 children이어야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;&amp;lt;TapButton&amp;gt;buttonName&amp;lt;/TapButton&amp;gt;

------------------------------

export default function TapButton(props) {
	return &amp;lt;button&amp;gt;{props.childeren}&amp;lt;/button&amp;gt;; // props.childeren === buttonName
// or
export default function TapButton({childeren}) { // 객체 구조분해
	return &amp;lt;button&amp;gt;{childeren}&amp;lt;/button&amp;gt;; // props.childeren === buttonName
&lt;/code&gt;&lt;/pre&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; &amp;nbsp;Event 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;button 태그의 on ~ 으로 시작하는 prop 추가 (ex)onClick)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커스텀 컴포넌트인 TabButton의 propd으로 생성한 onSelect는 개발자 임의로 명칭을 변경가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;on ~ 으로 시작하는 이벤트 prop(여기서는 onClick)의 값은 함수. (onSelect 속성 값을 onClick으로 넘겨줌으로 여기서는 onSelect 를 예시로 든다.)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;onSelect=handleSelect() 와 같이 소괄호를 붙이지 않는다. 이 경우 handleSelect()의 반환 값이 속성의 값으로 전달되어 렌더링 될 때 바로 실행되기 때문&lt;/li&gt;
&lt;li&gt;onSelect=hadleSelect 와 onSelect={() =&amp;gt; handleSelect('component')} 두 방식은 함수의 참조값을 전달하며 두 방식의 차이는 매개변수를 넘길 수 있다, 없다 의 차이를 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TapButton을 실행하면 handleSelect 함수가 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// App.jsx
function App() {
	
	function handleSelect(selectedButton) {
		// selectedButton =&amp;gt; component, jsx, props, state
		
	}
	return (
		&amp;lt;TapButton onSelect={() =&amp;gt; handleSelect('component')}&amp;gt;component&amp;lt;/TapButton&amp;gt;
		&amp;lt;TapButton onSelect={() =&amp;gt; handleSelect('jsx')}&amp;gt;jsx&amp;lt;/TapButton&amp;gt;
		&amp;lt;TapButton onSelect={() =&amp;gt; handleSelect('props')}&amp;gt;props&amp;lt;/TapButton&amp;gt;
	);
}
------------------------------

export default function TapButton({childeren , onSelect}) {
	return &amp;lt;button onClick={onSelect}&amp;gt;{childeren}&amp;lt;/button&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; UI가 업데이트 되지 않는 이유 - State를 사용하는 이유&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// App.jsx
function App() {
	let tabContent = &quot;hello&quot;;
	
	function handleSelect(selectedButton) {
		tabContent = selectedButton;
		console.log(tabConent)
	}
	
	return (
		&amp;lt;TapButton onSelect={() =&amp;gt; handleSelect('component')}&amp;gt;component&amp;lt;/TapButton&amp;gt;
		{tabContent}
	);
}

------------------------------

export default function TapButton({childeren , onSelect}) {
	return &amp;lt;button onClick={onSelect}&amp;gt;{childeren}&amp;lt;/button&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;App.jsx의 component 버튼을 누르면 handleSelect 함수를 통해 tabContent의 값이 &amp;ldquo;component&amp;rdquo;로 변경 될 것이다. 그리고 {tabContent}의 내용의 hello 에서 component로 변경될 것이라 예상하지만 아무리 버튼을 눌러도 콘솔에 찍히는 로그만 component로 변경되어 찍힐 뿐, 화면의 텍스트는 바뀌지 않는 것을 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;그 이유는 handleSelect 함수가 실행되었어도 변수만 변경될 뿐, App 컴포넌트 함수가 실행되지 않아 UI가 업데이트 되지 않는 것이다.&lt;/li&gt;
&lt;li&gt;그러므로 App 컴포넌트 함수가 재실행 되어야 한다는 것을 리액트에게 알려줄 필요가 있다.&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;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; State(상태) &amp;amp; Hook(훅)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;use ~ 로 시작하는 모든 함수는 리액트 Hook 이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hook은 컴포넌트 함수 또는 다른 리액트 Hook에서만 호출 될 수 있다.&lt;/li&gt;
&lt;li&gt;Hook은 컴포넌트 함수 최상위에서 호출해야하며 내부 함수나 if문 또는 loop문 안에 중첩되어선 안된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;리액트 라이브러리의 useState 라는 함수의 도움을 받아 리액트에게 데이터가 변한 것을 알려주면 리액트가 UI를 업데이트 하도록 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useState Hook은 일부 컴포넌트에 연결된 상태를 관리하는데, 일부 데이터가 변경되면 이 Hook이 자신이 속한 컴포넌트 함수를 활성화 하여 리액트에 의해 재검토하게 된다.&lt;/li&gt;
&lt;li&gt;useState의 반환 값은 배열로, 첫번째 요소는 컴포넌트 실행 주기의 현재 데이터 값이며, 두번째 요소는 이 상태를 업데이트 하기 위해 실행되는 함수이다. 두번째 요소인 함수가 호출되면 리액트에 컴포넌트 함수가 다시 실행되어야 함을 알려준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import {useState} from 'react';

function App() {
	const [selectedTopic, setSeletedTopic] =	useState(&quot;hello&quot;);
	
	function handleSelect(selectedButton) {
		setSeletedTopic(selectedButton);
		console.log(selectedTopic)
	}
	
	return (
		&amp;lt;TapButton onSelect={() =&amp;gt; handleSelect('component')}&amp;gt;component&amp;lt;/TapButton&amp;gt;
		{selectedTopic}
	);
}&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; 조건적 콘텐츠 렌더링&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import {useState} from 'react';

function App() {
	const [selectedTopic, setSeletedTopic] =	useState(&quot;hello&quot;);
	
	function handleSelect(selectedButton) {
		setSeletedTopic(selectedButton);
		console.log(selectedTopic)
	}
	
	return (
		...
		{!selectedTopic ? &amp;lt;p&amp;gt;hello&amp;lt;/p&amp;gt; : &amp;lt;p&amp;gt;{EXAMPLES[selectedTopic].title}&amp;lt;/p&amp;gt;}
		// or
		{!selectedTopic &amp;amp;&amp;amp; &amp;lt;p&amp;gt;hello&amp;lt;/p&amp;gt;}
		{selectedTopic &amp;amp;&amp;amp;  &amp;lt;p&amp;gt;{EXAMPLES[selectedTopic].title}&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; Css 스타일링 및 동적 스타일링&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jsx 에서는 버튼에 클래스를 추가하기 위해서는 className을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// App.jsx
function App() {
	const [selectedTopic, setSeletedTopic] =	useState(&quot;hello&quot;);
	
	function handleSelect(selectedButton) {
		setSeletedTopic(selectedButton);
		console.log(selectedTopic)
	}
	
	...
	return (
		&amp;lt;TapButton isSelected={isSelected==='component'}
							 onSelect={() =&amp;gt; handleSelect('component')}&amp;gt;component&amp;lt;/TapButton&amp;gt;
	);
}

------------------------------

export default function TapButton({childeren , onSelect, isSelected}) {
	return &amp;lt;button className={isSelected ? 'active' :undefined} onClick={onSelect}&amp;gt;{childeren}&amp;lt;/button&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt; List 데이터 동적 츨력&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.map 함수를 이용하여 list 의 수만큼 자동으로 컴포넌트가 출력할 수 있도록 수정&lt;/li&gt;
&lt;li&gt;.map 함수를 이용하여 list 데이터를 출력 할때 각 항목을 식별할 수 있는 key를 부여하면 리액트가 변경을 효율적으로 감지하며 key 값을 기준으로 어떤 요소가 변경, 추가, 삭제 되었는지 판단하기 때문에 불필요한 리렌더링을 방지하고, 렌더링 성능을 최적화 할 수 있다.&lt;/li&gt;
&lt;li&gt;key 속성이 없다면 매번 전체 리스트를 다시 렌더링하여 불필요한 렌더링이 발생하고, 리스트 중간에 요소가 추가, 삭제 되는 경우 모든 항목을 다시 그릴 수 있어 성능 저하에 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;// 부모
function App() {
	return (
		...
		&amp;lt;CoreComcept title=&quot;Components&quot; description=&quot;blablabla&quot; image ={Img} /&amp;gt;
		// or
		&amp;lt;CoreComcept {...CORE_CONCEPT[0]} /&amp;gt; // 스프레드 연산자 사용
		...
		// or
		{CORE_CONCEPT.map((conceptItem) =&amp;gt; &amp;lt;CoreComcept key={conceptItem.title} {...CORE_CONCEPT[conceptItem]} /&amp;gt; ) // map 함수로 list 동적 출력
	);
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Programming/React</category>
      <category>Component</category>
      <category>React</category>
      <category>react 완벽가이드</category>
      <category>State</category>
      <category>useState</category>
      <category>리액트 Hook</category>
      <category>리액트 prop</category>
      <category>리액트 상태</category>
      <category>컴포넌트</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/134</guid>
      <comments>https://dksek3050.tistory.com/134#entry134comment</comments>
      <pubDate>Mon, 3 Mar 2025 18:05:33 +0900</pubDate>
    </item>
    <item>
      <title>[React] React 완벽 가이드 - 섹션 1: 시작하기</title>
      <link>https://dksek3050.tistory.com/132</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;react_2.png&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;817&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o1FfZ/btsMjS9b66d/wpWdq7IpsaYyk6doU0wBr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o1FfZ/btsMjS9b66d/wpWdq7IpsaYyk6doU0wBr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o1FfZ/btsMjS9b66d/wpWdq7IpsaYyk6doU0wBr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo1FfZ%2FbtsMjS9b66d%2FwpWdq7IpsaYyk6doU0wBr0%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;500&quot; height=&quot;498&quot; data-filename=&quot;react_2.png&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;817&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 글은 Udemy의 &lt;a href=&quot;https://www.udemy.com/course/best-react/&quot; data-token-index=&quot;1&quot;&gt;【한글자막】 React 완벽 가이드 2025 with React Router &amp;amp; Redux&lt;/a&gt; 를 수강하고 정리한 내용입니다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리액트는 무엇일까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 인터페이스를 구축하기 위한 자바스크립트 라이브러리&lt;/li&gt;
&lt;li&gt;리액트는 자바 스크립트 라이브러리라서 재 로딩 없이 웹페이지와 페이지에 표시되는 사용자 인터페이스를 업데이트 가능하기 때문에 사용자에게 즉각적인 반응과 부드러운 전환 경험을 제공한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 스크립트는 백그라운드에서 실행되기 때문에 로딩이 완료된 페이지를 재로딩 없이 새로운 데이터로 업데이트가 가능하다.&lt;/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;리액트가 자바스크립트 라이브러리라면 왜 자바스크립트가 아닌 리액트를 사용해야 할까?&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;HTML 코드와 자바스크립트 코드를 섞어 쓸 수 있다.&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;자바스크립트는 명령형 (거쳐야할 단계들을 정의한다.) &amp;rarr; 번거로움&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Programming/React</category>
      <category>React</category>
      <category>React 시작하기</category>
      <category>react 완벽 가이드</category>
      <category>React 입문</category>
      <category>udemy</category>
      <category>유데미</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/132</guid>
      <comments>https://dksek3050.tistory.com/132#entry132comment</comments>
      <pubDate>Sat, 15 Feb 2025 00:49:30 +0900</pubDate>
    </item>
    <item>
      <title>[CI/CD] gitHub Action으로 CI/CD 구축하고 인간젠킨스 탈출하다! (feat. Self-hosted runner)</title>
      <link>https://dksek3050.tistory.com/131</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;0. 들어가기 전&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 환경&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Host 서버
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CentOS Linux 7 (Core)&lt;/li&gt;
&lt;li&gt;Linux 3.10.0-862.el7.x86_64&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Docker 버전
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker version 19.03.4&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Self-hosted runner 컨테이너
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CentOS Linux 8&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;Java 11&lt;/li&gt;
&lt;li&gt;Maven&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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. 배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는야 인간 젠킨스 &amp;hellip; 수동 배포를 맡고 있지.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 프로젝트를 진행하면서 본사 외부에서 본사 개발 서버에 배포 요청이 들어올 때마다 곤란하곤 했다. 또 작업 후에 배포까지 바로 못해드리는 경우가 있어서 수정사항 확인이 늦어지곤 했는데 이를 해결 하기 위해 CI/CD를 구축해보고자 마음먹었다.&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;3. 설계 히스토리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;gitAciton 을 선택하게 된 배경&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;CI/CD 툴은 Jenkins와 gitAction 중에 고민하였다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;하루라도 빨리 CI/CD를 구축하기 위해 러닝커브가 낮다는 gitAction으로 정했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;형상관리도 git으로 하고 있기에 다른 개발자 분들도 배포상태를 확인하는데 더 편리하지 않을까 싶었다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Self-hosted runner 를 선택하게 된 배경&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@dksek3050/CICD-GitHub-hosted-runner-%EC%99%80-Self-hosted-runner-%EB%9E%80&quot;&gt;GitHub-hosted runner 와 Self-hosted runner 란?&lt;/a&gt;&lt;/li&gt;
&lt;li&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;처음에는 GitHub-hosted runner를 사용하여 배포하려고 했으나, 성공하지 못했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 회사의 개발 서버로 배포하는 것이 목표였으며, 이를 위해 runner 서버에서 사내 개발 서버로 배포 파일을 전송하기 위해서는 scp 명령어를 사용하는 ssh 포트 개방이 필요했다. 그러나 GitHub-hosted runner의 경우 고정된 IP 주소나 도메인을 제공하지 않으며, GitHub-hosted runner는 GitHub 인프라에서 동적으로 실행되기 때문에 작업을 처리하는 동안 IP 주소가 계속 변경된다. 이로 인해 방화벽 규칙에 IP 주소를 등록해 포트를 개방하는 방식은 사용할 수 없었다. 결과적으로 빌드된 파일을 개발 서버로 전송할 수 없었으며, 이 문제를 해결하기 위해 설계를 변경해야 했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해, Self-Hosted Runner를 사내 네트워크 내부에 구축하여 개발 서버와 통신할 수 있는 환경을 만들고자 했다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Self-hosted runner 를 어디에 어떻게 구축할 것인가 ?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;배포 서버에 구축할 수도 있었지만, 해당 서버는 다른 프로젝트도 함께 사용하고 있어 서버에 영향을 최소화하기 위해 도커로 구축하기로 했다. 또한, 프로젝트 자체도 도커로 관리되고 있었기 때문에 runner 서버를 도커 컨테이너에서 운영하면 함께 관리하기에 더 적합할 것이라 판단하여 도커 컨테이너에 runner를 띄우기로 했다.&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 ssh 포트 개방 불가 문제를 해결하기 위해 생각하며 작성했던 메모들이다. 기록을 위함이 참고는 말자 ㅎ&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;1.&amp;nbsp;깃&amp;nbsp;액션에&amp;nbsp;vpn&amp;nbsp;설정해서&amp;nbsp;내&amp;nbsp;vpn&amp;nbsp;계정으로&amp;nbsp;사내망&amp;nbsp;접근 &lt;br /&gt;&amp;nbsp;&amp;nbsp;[Access&amp;nbsp;Private&amp;nbsp;Server&amp;nbsp;with&amp;nbsp;GitHub&amp;nbsp;Actions&amp;nbsp;(VPN&amp;nbsp;connection)](&lt;a href=&quot;https://medium.com/@mahmutali.mas/access-private-server-with-github-actions-vpn-connection-cf20313abdd5)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@mahmutali.mas/access-private-server-with-github-actions-vpn-connection-cf20313abdd5)&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;vpn&amp;nbsp;접근울&amp;nbsp;위한&amp;nbsp;인증서&amp;nbsp;필요&amp;nbsp;&amp;rarr;&amp;nbsp;인프라팀에&amp;nbsp;요청해보자. &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;vpn&amp;nbsp;&amp;nbsp;관련하여&amp;nbsp;관리하고&amp;nbsp;있는&amp;nbsp;인증서가&amp;nbsp;없다고함. &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;vpn&amp;nbsp;설정해서&amp;nbsp;사내망&amp;nbsp;접근&amp;nbsp;방법은&amp;nbsp;불가&amp;hellip; &lt;br /&gt;2.&amp;nbsp;github&amp;nbsp;Action&amp;nbsp;IP&amp;nbsp;주소를&amp;nbsp;확인해서&amp;nbsp;인바운드&amp;nbsp;규칙에&amp;nbsp;적용하고&amp;nbsp;배포&amp;nbsp;후에&amp;nbsp;인바운드&amp;nbsp;규칙에서&amp;nbsp;제거 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;고정&amp;nbsp;ip&amp;nbsp;가&amp;nbsp;아니고&amp;nbsp;매번&amp;nbsp;배포&amp;nbsp;할때마다&amp;nbsp;ip&amp;nbsp;주소&amp;nbsp;확인해야함&amp;nbsp;&amp;rarr;&amp;nbsp;자동&amp;nbsp;배포의&amp;nbsp;의미가&amp;nbsp;없음 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;보안&amp;nbsp;취약&amp;nbsp;위험성&amp;nbsp;있음 &lt;br /&gt;3.&amp;nbsp;배포&amp;nbsp;서버에&amp;nbsp;셀프&amp;nbsp;호스티드&amp;nbsp;러너&amp;nbsp;서버를&amp;nbsp;도커&amp;nbsp;컨테이너로&amp;nbsp;띄운&amp;nbsp;다음에&amp;nbsp;거기에&amp;nbsp;java랑&amp;nbsp;빌드&amp;nbsp;도구&amp;nbsp;설치해두고&amp;nbsp;여기서&amp;nbsp;빌드&amp;nbsp;후&amp;nbsp;빌드가&amp;nbsp;다&amp;nbsp;되면&amp;nbsp;배포&amp;nbsp;컨테이너로&amp;nbsp;배포&amp;nbsp;파일&amp;nbsp;옮겨서&amp;nbsp;배포 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;성공! &lt;br /&gt;4.&amp;nbsp;젠킨스&amp;hellip;&amp;hellip;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 목표 설계 아키텍처&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dksek3050/post/fdedad97-a2f6-4b8e-a9b3-9251a0e0c57d/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Git에서 변경 사항을 푸시하면 GitHub Actions가 동작.&lt;/li&gt;
&lt;li&gt;Self-hosted runner 컨테이너가 빌드 작업을 수행&lt;/li&gt;
&lt;li&gt;빌드된 .war 파일을 &lt;b&gt;웹서버 컨테이너&lt;/b&gt;로 이동.&lt;/li&gt;
&lt;li&gt;웹서버 컨테이너를 &lt;b&gt;재시작&lt;/b&gt;하여 새로운 버전의 애플리케이션을 배포.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&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;&lt;b&gt;CentOS 8 컨테이너를 생성&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨테이너 내에 Docker를 설치하여 컨테이너 내부에서 .war 파일을 옮길 수 있도록 구성&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Java 및 Maven을 설치하고 빌드 환경 설정&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub 페이지에서 self-hosted runner 생성 및 컨테이너 내에 action-runner 설치 및 연결&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub Actions 워크플로우 (.yml 파일) 작성하여 CI/CD 파이프라인 구축&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;1. CentOS 8 컨테이너를 생성&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0. Self - hosted runner 로 사용할 컨테이너 생성 - ⭐Dockerfile⭐&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 작업들을 Dockerfile에 작성하여 한번에 해결 할 수 있도록 정리하였다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. CentOS 8 컨테이너를 생성
2. 컨테이너 내에 Docker를 설치하여 컨테이너 내부에서 .war 파일을 옮길 수 있도록 구성
3. Java 및 Maven을 설치하고 빌드 환경 설정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Dockerfile⏬&lt;/b&gt;&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;pre id=&quot;code_1730903731347&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# CentOS 8 이미지를 기반으로 시작
FROM centos:8

# BaseOS, Extras, AppStream 리포지토리 파일 수정 (vault.centos.org로 변경)
RUN sed -i 's|^mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/CentOS-Linux-BaseOS.repo \
    &amp;amp;&amp;amp; sed -i 's|^#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-Linux-BaseOS.repo \
    &amp;amp;&amp;amp; sed -i 's|^mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/CentOS-Linux-Extras.repo \
    &amp;amp;&amp;amp; sed -i 's|^#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-Linux-Extras.repo \
    &amp;amp;&amp;amp; sed -i 's|^mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/CentOS-Linux-AppStream.repo \
    &amp;amp;&amp;amp; sed -i 's|^#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-Linux-AppStream.repo \
    &amp;amp;&amp;amp; sed -i 's|$releasever|8.5.2111|g' /etc/yum.repos.d/CentOS-Linux-BaseOS.repo /etc/yum.repos.d/CentOS-Linux-Extras.repo /etc/yum.repos.d/CentOS-Linux-AppStream.repo

# YUM 캐시를 정리하고 새로고침
RUN yum clean all &amp;amp;&amp;amp; yum makecache

# 시스템의 패키지 목록을 업데이트하여 최신 버전으로 유지
RUN yum update -y

# Docker 설치를 위해 필요한 도구 패키지 설치 (yum-utils)
RUN yum install -y yum-utils

# Docker의 공식 저장소를 시스템에 추가하여 Docker 패키지 다운로드 가능하게 설정
RUN yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# Docker CLI를 포함한 Docker 패키지 설치
RUN yum install -y docker-ce-cli

# Docker 버전 확인 (설치가 제대로 되었는지 검증)
RUN docker --version

# Java 11 OpenJDK 설치
RUN yum install -y java-11-openjdk

# Maven 설치
RUN yum install -y maven

# 기본 쉘로 시작
CMD [&quot;/bin/bash&quot;]

# --------------------------------------
# --------------------------------------
# 빌드 
# docker build -t cicd-docker-image .

# 실행
# docker run -it --name my_cicd_container -v /var/run/docker.sock:/var/run/docker.sock cicd-docker-image /bin/bash&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위 도커파일이 제대로 빌드 되지 못한다면 아래 과정을 손수 경험하자 ^_^&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 실행된다면 4번(&lt;b&gt;GitHub 페이지에서 self-hosted runner 생성 및 컨테이너 내에 action-runner 설치 및 연결&lt;/b&gt;)으로 바로 넘어가자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. centos:8 이미지 받기&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;docker pull centos:8&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@dksek3050/%EC%B0%B8%EA%B3%A0-centos-%EB%B2%84%EC%A0%84%EC%9D%84-7%EC%9D%B4-%EC%95%84%EB%8B%8C-8%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-actions-runnerexternalsnode20binnode-lib64libm.so.6-version-GLIBC2.27-not-found-required-by-actions-runnerexternalsnode20binnode&quot;&gt;[참고] centos 버전을 7이 아닌 8을 사용하는 이유&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1730904054622&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;[참고] centos 버전을 7이 아닌 8을 사용하는 이유 : /actions-runner/externals/node20/bin/node: /lib64/libm.so.6: ve&quot; data-og-description=&quot;Self-hosted runner 를 구축하기 위해서 Centos7을 컨테이너로 띄웠고 ci/cd 파이프라인을 도는 과정에서 오류가 발생하였다.&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@dksek3050/%EC%B0%B8%EA%B3%A0-centos-%EB%B2%84%EC%A0%84%EC%9D%84-7%EC%9D%B4-%EC%95%84%EB%8B%8C-8%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-actions-runnerexternalsnode20binnode-lib64libm.so.6-version-GLIBC2.27-not-found-required-by-actions-runnerexternalsnode20binnode&quot; data-og-url=&quot;https://velog.io/@dksek3050/참고-centos-버전을-7이-아닌-8을-사용하는-이유-actions-runnerexternalsnode20binnode-lib64libm.so.6-version-GLIBC2.27-not-found-required-by-actions-runnerexternalsnode20binnode&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pNUTm/hyXs5iOtNU/A2J97tUFa3Ket3Al0wIih1/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/cu1foa/hyXwkSTryf/oIXwWKyOvo3GX1DXjrsbY1/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/XbOjR/hyXsU2DoXR/HGzOUtGb0i0b7I0V2avZI0/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638&quot;&gt;&lt;a href=&quot;https://velog.io/@dksek3050/%EC%B0%B8%EA%B3%A0-centos-%EB%B2%84%EC%A0%84%EC%9D%84-7%EC%9D%B4-%EC%95%84%EB%8B%8C-8%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-actions-runnerexternalsnode20binnode-lib64libm.so.6-version-GLIBC2.27-not-found-required-by-actions-runnerexternalsnode20binnode&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@dksek3050/%EC%B0%B8%EA%B3%A0-centos-%EB%B2%84%EC%A0%84%EC%9D%84-7%EC%9D%B4-%EC%95%84%EB%8B%8C-8%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-actions-runnerexternalsnode20binnode-lib64libm.so.6-version-GLIBC2.27-not-found-required-by-actions-runnerexternalsnode20binnode&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pNUTm/hyXs5iOtNU/A2J97tUFa3Ket3Al0wIih1/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/cu1foa/hyXwkSTryf/oIXwWKyOvo3GX1DXjrsbY1/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/XbOjR/hyXsU2DoXR/HGzOUtGb0i0b7I0V2avZI0/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638');&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;[참고] centos 버전을 7이 아닌 8을 사용하는 이유 : /actions-runner/externals/node20/bin/node: /lib64/libm.so.6: ve&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Self-hosted runner 를 구축하기 위해서 Centos7을 컨테이너로 띄웠고 ci/cd 파이프라인을 도는 과정에서 오류가 발생하였다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Centos 컨테이너 생성 및 실행&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;docker run -it --name cicd_runner -v /var/run/docker.sock:/var/run/docker.sock centos:8 /bin/bash &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/var/run/docker.sock&lt;/code&gt; 을 마운트 하는 이유 : 호스트 머신의 Docker 엔진에 접근해 &lt;code&gt;docker cp&lt;/code&gt; 명령어를 사용하기 위함.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이 runner 컨테이너에서 .war 파일를 web 서버 컨테이너로 옮겨야한다.&lt;/li&gt;
&lt;li&gt;runner 컨테이너에서 web 서버 컨테이너로 옮기기 위해 docker cp 명령어를 사용해야한다.&lt;/li&gt;
&lt;li&gt;컨테이너 내부에서 호스트의 도커를 컨트롤 하기 위해서는 호스트 머신의 Docker 엔진에 접근할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;그러기 위해서는 &lt;code&gt;docker.sock&lt;/code&gt; 파일을 마운트하여 컨테이너를 생성하고, &lt;b&gt;Docker CLI&lt;/b&gt;를 설치하여 컨테이너 내에서 실행된 Docker 명령어가 호스트의 Docker 환경에 영향을 미치게 된다. 따라서, 이 방법을 사용할 때는 주의가 필요하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 설정을 통해, runner 서버에서 호스트의 Docker 환경을 활용하여 컨테이너를 관리하고 빌드 작업을 수행할 수 있다.&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;3. Centos8 기본 레포지토리 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CentOS 8이 2021년 12월 31일에 공식적으로 지원이 종료되면서, 기본 리포지토리(mirrorlist.centos.org)는 더 이상 CentOS 8 패키지에 대한 업데이트나 접근을 제공하지 않는다. 이로 인해 CentOS 8에서 기본 &lt;code&gt;mirrorlist&lt;/code&gt; 경로를 사용하면 패키지 다운로드에 실패하게 된다. 이를 접근 가능한 리포지토리 url로 변경해주어야 한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;yum update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어를 입력 시 아래와 같이 오류가 발생한다면 아래 링크를 참고하여 해결하자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dksek3050/post/4dbabfb4-b23a-458a-b8da-5f014c04a758/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@dksek3050/Centos-Failed-to-download-metadata-for-repo-appstream-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0&quot;&gt;[Centos]Failed to download metadata for repo 'appstream' 에러 해결&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1730904042204&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;[Centos] Failed to download metadata for repo 'appstream' 에러 해결&quot; data-og-description=&quot;Failed to download metadata for repo 'appstream' 에러 해결&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@dksek3050/Centos-Failed-to-download-metadata-for-repo-appstream-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0&quot; data-og-url=&quot;https://velog.io/@dksek3050/Centos-Failed-to-download-metadata-for-repo-appstream-에러-해결&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cXkQuH/hyXwmJWr3V/3PmqDcQ5VFX3y5C1vvp8CK/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/heZGu/hyXwuVvthy/cys7pEMOJr9DUZLS0cG9j0/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/dDPA2g/hyXwpfAzIa/PXZcPkDZUXcjf11p1CoNKk/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638&quot;&gt;&lt;a href=&quot;https://velog.io/@dksek3050/Centos-Failed-to-download-metadata-for-repo-appstream-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@dksek3050/Centos-Failed-to-download-metadata-for-repo-appstream-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cXkQuH/hyXwmJWr3V/3PmqDcQ5VFX3y5C1vvp8CK/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/heZGu/hyXwuVvthy/cys7pEMOJr9DUZLS0cG9j0/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/dDPA2g/hyXwpfAzIa/PXZcPkDZUXcjf11p1CoNKk/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638');&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;[Centos] Failed to download metadata for repo 'appstream' 에러 해결&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Failed to download metadata for repo 'appstream' 에러 해결&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;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;2. Docker CLI를 포함한 Docker 패키지 설치&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker cp 명령어를 사용하기 위해 컨테이너 내에서 docker CLI를 설치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말했다시피 /var/run/docker.sock 을 마운트 하여 컨테이너를 생성했기 때문에 호스트 머신의 Docker 엔진에 접근이 가능해졌다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 시스템의 패키지 목록을 업데이트하여 최신 버전으로 유지
yum update

# Docker 설치를 위해 필요한 도구 패키지 설치 (yum-utils)
yum install -y yum-utils

# Docker의 공식 저장소를 시스템에 추가하여 Docker 패키지 다운로드 가능하게 설정
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# Docker CLI를 포함한 Docker 패키지 설치
yum install -y docker-ce-cli

# Docker 버전 확인 (설치가 제대로 되었는지 검증)
docker --version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+) 만약 설치가 제대로 된 것 같은데 실행이 안된다면 /var/run/docker.sock 의 소유계정과 그룹을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@dksek3050/Docker-permission-denied-while-trying-to-connect-to-the-Docker-daemon-socket-at-unix-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0&quot;&gt;[Docker] permission denied while trying to connect to the Docker daemon socket at unix 에러 해결&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1730904130565&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;[Docker] permission denied while trying to connect to the Docker daemon socket at unix 에러 해결&quot; data-og-description=&quot;Docker를 컨테이너에서 실행하거나, 호스트의 Docker 소켓(/var/run/docker.sock)을 컨테이너에 마운트하여 사용하는 경우, 예상치 못한 권한 문제로 인해 Docker 명령이 작동하지 않을 때가 있습니다. 이 &quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@dksek3050/Docker-permission-denied-while-trying-to-connect-to-the-Docker-daemon-socket-at-unix-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0&quot; data-og-url=&quot;https://velog.io/@dksek3050/Docker-permission-denied-while-trying-to-connect-to-the-Docker-daemon-socket-at-unix-에러-해결&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nHhNg/hyXwn9UPII/lZPAkSNeNpxEHO0vkLAcsK/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/lawxG/hyXwqyNM8l/l52pB7KXitKCRAfHDTNUQk/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/Djbnt/hyXwkehFY7/JBtjzKTPddmZoYAURVUXM0/img.png?width=259&amp;amp;height=240&amp;amp;face=0_0_259_240&quot;&gt;&lt;a href=&quot;https://velog.io/@dksek3050/Docker-permission-denied-while-trying-to-connect-to-the-Docker-daemon-socket-at-unix-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@dksek3050/Docker-permission-denied-while-trying-to-connect-to-the-Docker-daemon-socket-at-unix-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nHhNg/hyXwn9UPII/lZPAkSNeNpxEHO0vkLAcsK/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/lawxG/hyXwqyNM8l/l52pB7KXitKCRAfHDTNUQk/img.png?width=1203&amp;amp;height=638&amp;amp;face=0_0_1203_638,https://scrap.kakaocdn.net/dn/Djbnt/hyXwkehFY7/JBtjzKTPddmZoYAURVUXM0/img.png?width=259&amp;amp;height=240&amp;amp;face=0_0_259_240');&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;[Docker] permission denied while trying to connect to the Docker daemon socket at unix 에러 해결&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Docker를 컨테이너에서 실행하거나, 호스트의 Docker 소켓(/var/run/docker.sock)을 컨테이너에 마운트하여 사용하는 경우, 예상치 못한 권한 문제로 인해 Docker 명령이 작동하지 않을 때가 있습니다. 이&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;3. Java 및 Maven을 설치하고 빌드 환경 설정&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. java 11 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+) 자바를 설치하긴 했는데 워크플로우 타면서 java 인식이 안되서 빌드가 안되는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 워크플로우에 자바 set 하는 단계를 추가하여 해결하였다. 만약 java 설치에 애를 먹는다면 넘어가도 될 것 같다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 설치
yum install -y java-11-openjdk

# 설치 확인
java --version&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. maven 설치&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 설치
yum install maven -y

# 설치 확인
mvn -v&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;4. self-hosted runner 생성&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. GitHub 설정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Github Actions을 사용하고자 하는 저장소에서 Settings - Actions - Runners로 이동&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dksek3050/post/d1302666-a106-4d23-92cc-8f226534e7a5/image.png&quot; alt=&quot;&quot; width=&quot;642&quot; height=&quot;422&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;New self-hosted runner 를 클릭하여 runner를 생성&lt;/li&gt;
&lt;li&gt;Runner image를 runner 서버 환경에 맞게 설정&lt;/li&gt;
&lt;li&gt;아래 사진과 같이 환경에 맞게 명령어를 제공해주는데 차근차근 서버에 입력해보자&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dksek3050/post/2d3e88a6-312d-4a6b-bdfc-f08b9c4a0ee1/image.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 컨테이너 설정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Download
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt; # 폴더 생성
 $ mkdir actions-runner &amp;amp;&amp;amp; cd actions-runner

 # 최신 러너 패키지 다운로드
 $ curl -o actions-runner-linux-x64-2.320.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.320.0/actions-runner-linux-x64-2.320.0.tar.gz

 # 선택 사항: 해시 검증
 # 해시 검증 패키시 다운
 $ yum install -y perl-Digest-SHA
 # 해시 검증
 $ echo &quot;93ac1b7ce743ee85b5d386f5c1787385ef07b3d7c728ff66ce0d3813d5f46900  actions-runner-linux-x64-2.320.0.tar.gz&quot; | shasum -a 256 -c

 # 설치 프로그램 추출
 $ tar xzf ./actions-runner-linux-x64-2.320.0.tar.gz&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;아래 과정은 GitHub Actions의 Self-hosted Runner를 설치하는 과정이다. 위에서 runner 로 사용할 컨테이너를 생성해두었으니 그 컨테이너 내부에서 아래 명령어를 하나씩 입력하자.&lt;/li&gt;
&lt;li&gt;Configure&lt;br /&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# GitHub Actions Self-Hosted Runner를 GitHub 리포지토리에 등록
$ ./config.sh --url https://github.com/An0401na/CICD-Test --token AXUUDYARWPNHASTYBPWTWPLHFDNX6

# 마지막 단계, 실행하기!
$ ./run.sh&lt;/code&gt;&lt;/pre&gt;
&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⏬ [트러블 슈팅] Must not run with sudo&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;txc-textbox&quot; style=&quot;background-color: #fafafa; padding: 10px; border: 1px dashed #c1c1c1;&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⚽ 발생 오류&lt;/p&gt;
&lt;pre id=&quot;code_1730906546336&quot; class=&quot;autoit&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[root@9ca5fc8c7aa5 actions-runner]# ./config.sh --url https://github.com/An0401na/CICD-Test --token 
Must not run with sudo&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⚽발생 원인&lt;br /&gt;./config.sh 스크립트를 실행할 때 &quot;Must not run with sudo&quot;라는 오류 메시지가 나타나는 것은 스크립트가 sudo로 실행되었기 때문이다. GitHub Actions의 self-hosted runner 설정 시 sudo를 사용하지 않아야 한다.&lt;br /&gt;아래 ./config.sh 명령어를 실행하기 전에 runner를 관리할 사용자 계정을 생성해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⚽ 해결방법&lt;br /&gt;&lt;span&gt;컨&lt;/span&gt;테이너 내부에서 docker.sock을 사용하려면, docker 그룹과 컨테이너 내부의 사용자 설정이 일치해야 한다. 이를 위해 컨테이너 내부에서 docker 그룹 ID를 호스트와 동일하게 맞추고, 해당 사용자 계정을 docker 그룹에 추가하는&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방법이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# 컨테이너 내부에서 실행

# docker.sock 파일의 소유자를 확인하려면 다음 명령어를 사용
$ ls -l /var/run/docker.sock

# -- 아래와 같은 형식으로 출력
srw-rw----. 1 root 1001 0 Oct  2 02:22 /var/run/docker.sock

- root는 파일의 소유자(user)
- 1001는 파일의 소유 그룹(group)ID &lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;docker.sock&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 소유하고 있는 그룹은&lt;span&gt;&amp;nbsp;&lt;/span&gt;docker&lt;span&gt;&amp;nbsp;&lt;/span&gt;이고&lt;span&gt;&amp;nbsp;&lt;/span&gt;docker&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 그룹 ID가 1001임을 기억하자.&lt;br /&gt;runner 컨테이너 내부에서 아래 명령어 실행&lt;/p&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;&lt;b&gt;⏬&amp;nbsp; [트러블 슈팅] ldd: warning: you do not have execution permission for `./bin/libcoreclr.so'&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #fafafa; padding: 10px; border: 1px dashed #c1c1c1;&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⚽발생 오류&lt;/p&gt;
&lt;pre id=&quot;code_1730906603210&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[runner@1fe76c6022dc actions-runner]$ ./config.sh --url https://github.com/An0401na/CICD-Test --token AXUUDYFGWY3AOXFA6SULMMDHFDVIM
ldd: warning: you do not have execution permission for `./bin/libcoreclr.so'
ldd: warning: you do not have execution permission for `./bin/libSystem.Security.Cryptography.Native.OpenSsl.so'
ldd: warning: you do not have execution permission for `./bin/libSystem.IO.Compression.Native.so'
Libicu's dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⚽발생 원인&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;경고 메시지 ldd: warning: you do not have execution permission for는 ./bin/libcoreclr.so, ./bin/libSystem.Security.Cryptography.Native.OpenSsl.so, ./bin/libSystem.IO.Compression.Native.so 파일들이 실행 권한이 없어 의존성 확인에 실패했음을 의미한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래 명령어를 따라가며 해결하자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⚽해결 방법&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# root로 계정 전환
$ exit 

# 모든 바이너리 파일에 대해 실행 권한 부여
$ chmod +x ./bin/*

# 의존성 설치 스크립트 실행
$ ./bin/installdependencies.sh

# /actions-runner 디렉토리에 대한 쓰기 권한 부여
$ chmod -R 777 /actions-runner

# runner로 계정 전환
su runner

# GitHub Actions Self-Hosted Runner를 GitHub 리포지토리에 등록
$ ./config.sh --url https://github.com/An0401na/CICD-Test --token AXUUDYARWPNHASTYBPWTWPLHFDNX6&lt;/code&gt;&lt;/pre&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;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dksek3050/post/d43919d4-5c13-4136-adda-56ffa89ff99a/image.png&quot; alt=&quot;&quot; width=&quot;514&quot; height=&quot;451&quot; /&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Runner를 생성하기 위한 몇가지 정보를 받는다. 나는 아래와 같이 입력했지만 선택사항이므로 잘 해석해서 입력하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1730906770888&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Runner Registration

Enter the name of the runner group to add this runner to: [press Enter for Default] 

Enter the name of runner: [press Enter for 1fe76c6022dc] CICD-Test

This runner will have the following labels: 'self-hosted', 'Linux', 'X64' 
Enter any additional labels (ex. label-1,label-2): [press Enter to skip] self-hosted,centos8,docker

&amp;radic; Runner successfully added
&amp;radic; Runner connection is good

# Runner settings

Enter name of work folder: [press Enter for _work] cicd_work

&amp;radic; Settings Saved.&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dksek3050/post/360ad5b0-ce02-4a01-b8c6-1cf2fa6155a4/image.png&quot; alt=&quot;&quot; width=&quot;528&quot; height=&quot;162&quot; /&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이로써 GitHub Action Self-hosted runner가 정상적으로 생성되었다.&lt;/p&gt;
&lt;pre id=&quot;code_1730906783908&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ ./run.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행하면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;437&quot; data-origin-height=&quot;123&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4r86R/btsKydHTvwQ/o0RLk2fT5TrBVC5xJFEk41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4r86R/btsKydHTvwQ/o0RLk2fT5TrBVC5xJFEk41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4r86R/btsKydHTvwQ/o0RLk2fT5TrBVC5xJFEk41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4r86R%2FbtsKydHTvwQ%2Fo0RLk2fT5TrBVC5xJFEk41%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;437&quot; height=&quot;123&quot; data-origin-width=&quot;437&quot; data-origin-height=&quot;123&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGntyr/btsKyJ7NRDa/br3SSoikFS0ghesGMkBCRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGntyr/btsKyJ7NRDa/br3SSoikFS0ghesGMkBCRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGntyr/btsKyJ7NRDa/br3SSoikFS0ghesGMkBCRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGntyr%2FbtsKyJ7NRDa%2Fbr3SSoikFS0ghesGMkBCRk%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;652&quot; height=&quot;236&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 리스닝 상태임을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;5. GitHub Actions 워크플로우 작성&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github 페이지의 Action 탭을 활용해 자동으로 생성할 수도 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포명/.github/workflows/????.yml 로 생성하여 워크플로우를 작성할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 후자를 택하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CICD-Test/.github/workflows/cicd.yml 로 지정하였다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; data-ke-language=&quot;css&quot;&gt;&lt;code&gt;name: CI/CD Pipeline  # 파이프라인의 이름 정의

on:
  push:  # 'push' 이벤트에 대한 트리거 설정
    branches:
      - main  # 'main' 브랜치에 푸시될 때 트리거 발생
  pull_request:  # 'pull_request' 이벤트에 대한 트리거 설정
    branches:
      - main  # 'main' 브랜치로의 PR 생성 시 트리거 발생

jobs:
  build:
    runs-on: self-hosted  # 셀프 호스티드 러너에서 이 작업이 실행됨 (GitHub 호스티드 대신 로컬 서버에서 실행)

    steps:
      - name: Checkout code  # 코드를 체크 아웃하는 단계
        uses: actions/checkout@v2  # GitHub Actions에서 제공하는 체크아웃 액션 사용
        # 이 단계에서는 레포지토리의 코드를 셀프 호스티드 러너로 가져옵니다.

      - name: Set up JDK 11  # JDK 11을 설정하는 단계
        uses: actions/setup-java@v1  # GitHub Actions에서 제공하는 Java 설정 액션 사용
        with:
          java-version: '11'  # 사용할 Java 버전을 '11'로 지정
        # 이 단계에서는 JDK 11을 설치하여 빌드 환경을 준비합니다.

      - name: Build with Maven  # Maven을 사용하여 프로젝트 빌드
        run: mvn package  # Maven으로 패키지 명령 실행
        # 이 단계에서는 Maven을 사용해 프로젝트를 빌드하여 .war 파일을 생성합니다.
        # Gradle을 사용하는 경우, ./gradlew build 명령을 사용해야 합니다.

      - name: Backup existing ROOT.war and copy new WAR file  # 기존 WAR 파일 백업 및 새 WAR 파일 복사
        run: |
          TIMESTAMP=$(date +%Y%m%d%H%M%S)  # 현재 시간 포맷: YYYYMMDDHHMMSS
          # 현재 시간을 기준으로 타임스탬프를 생성하여 백업 파일 이름에 사용

          # 기존 ROOT.war 파일을 ROOT.war.&amp;lt;현재시간&amp;gt; 으로 백업
          docker exec web_container mv /usr/local/tomcat/webapps/ROOT.war /usr/local/tomcat/webapps/ROOT.war.$TIMESTAMP
          # 기존 웹 컨테이너 내의 ROOT.war 파일을 백업

          # 새로운 WAR 파일을 컨테이너에 복사
          docker cp target/test-0.0.1-SNAPSHOT.war web_container:/usr/local/tomcat/webapps/ROOT.war
          # Maven 빌드로 생성된 새로운 WAR 파일을 웹 컨테이너에 복사
        # 이 단계에서는 기존 Tomcat 서버의 ROOT.war 파일을 백업하고, 새로운 WAR 파일을 덮어씌웁니다.

      - name: Restart web server container  # 웹 서버 컨테이너 재시작
        run: |
          docker restart web_container
        # Tomcat 웹 서버가 실행 중인 Docker 컨테이너를 재시작하여 새로운 WAR 파일이 반영되도록 합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;Checkout code&lt;/code&gt;: 레포지토리의 코드를 셀프 호스티드 러너로 가져오는 단계입니다. 이 작업이 완료되면 로컬에서 코드를 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Set up JDK 11&lt;/code&gt;: 이 단계는 Java 11 버전을 빌드 환경에 설치하고 설정합니다. 이후 Maven을 통해 프로젝트를 빌드할 때 Java 11을 사용할 수 있게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Build with Maven&lt;/code&gt;: Maven을 사용하여 Java 프로젝트를 빌드하고, 패키징된 결과물인 &lt;code&gt;.war&lt;/code&gt; 파일을 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Backup existing ROOT.war and copy new WAR file&lt;/code&gt;: 이 단계에서는 Tomcat 컨테이너 내의 기존 &lt;code&gt;ROOT.war&lt;/code&gt; 파일을 백업하고, 새로 빌드된 &lt;code&gt;.war&lt;/code&gt; 파일을 Tomcat 컨테이너로 복사합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Restart web server container&lt;/code&gt;: Tomcat 웹 서버가 실행되는 Docker 컨테이너를 재시작하여 새 &lt;code&gt;.war&lt;/code&gt; 파일이 반영되도록 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구조로 CI/CD 파이프라인을 구성하면 코드가 푸시되거나 PR이 생성될 때 자동으로 빌드 및 배포가 이루어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dksek3050/post/08c4b38d-21a7-460b-96d7-b0a844513158/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 작성 후 main 브랜치에 커밋하게 되면 아래와 같이 Action 탭에서 요청한 워크플로우가 돌아감을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dksek3050/post/4d528cf3-0a2c-4cca-a779-29560fa099a1/image.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dksek3050/post/dab250b7-8caa-4a55-bfb6-25baccf4ed38/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;6. 짜잔 완성&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;img src=&quot;https://velog.velcdn.com/images/dksek3050/post/0277eed3-59a7-42ca-8cce-c22e8c283558/image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD 워크플로우가 잘 돌아가서 배포가 성공적으로 되었다 !&lt;/p&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;&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;&lt;span style=&quot;background-color: #c1bef9;&quot;&gt;+) 디벨롭 버전&lt;/span&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬랙 알람을 추가 했고 active profile 이 올바르게 지정되었는지 확인하는 작업을 추가하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 빌드와 배포 과정을 나누었고 빌드가 제대로 됐을때만 배포가 이뤄지게 하였다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;name: CI/CD Pipeline

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: self-hosted
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Notify Slack - Build Started
        run: |
          curl -X POST -H 'Content-type: application/json' --data &quot;{\&quot;text\&quot;:\&quot;[CI/CD] Build has started by ${GITHUB_ACTOR}.\&quot;}&quot; ${{ secrets.WEBHOOK_SLACK_URL }}

      - name: Check active profile
        run: |
          if ! grep -q 'spring.profiles.active=main' src/main/resources/application.properties; then
            echo &quot;Error: spring.profiles.active is not set to 'main'.&quot;
            exit 1  # 프로파일이 맞지 않으면 빌드 실패
          fi
      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: '11'
      - name: Notify Slack - Build Status
        run: |
          curl -X POST -H 'Content-type: application/json' --data &quot;{\&quot;text\&quot;:\&quot;[CI/CD] Build Status: Success by ${GITHUB_ACTOR}.\&quot;}&quot; ${{ secrets.WEBHOOK_SLACK_URL }}

  deploy:
    runs-on: self-hosted
    needs: build  # 'build' 작업이 성공해야 이 작업이 실행됨
    if: github.event_name == 'push' &amp;amp;&amp;amp; github.ref == 'refs/heads/main'  # main 브랜치에 푸시될 때 실행됨
    steps:
      - name: Backup existing ROOT.war and copy new WAR file
        run: |
          TIMESTAMP=$(date +%Y%m%d%H%M%S)  # 현재 시간 포맷: YYYYMMDDHHMMSS

          # 기존 ROOT.war 파일을 ROOT.war.&amp;lt;현재시간&amp;gt; 으로 백업
          docker exec web_container mv /usr/local/tomcat/webapps/ROOT.war /usr/local/tomcat/webapps/ROOT.war.$TIMESTAMP

          # 새로운 WAR 파일을 컨테이너에 복사
          docker cp target/test-0.0.1-SNAPSHOT.war web_container:/usr/local/tomcat/webapps/ROOT.war
      - name: Restart web server container
        run: |
          docker restart web_container&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 도커 레포지토리를 통한 빌드와, 젠킨스를 활용한 CI/CD 구축에 도전해봐야겠다.&lt;/p&gt;</description>
      <category>DevOps</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/131</guid>
      <comments>https://dksek3050.tistory.com/131#entry131comment</comments>
      <pubDate>Wed, 6 Nov 2024 22:42:00 +0900</pubDate>
    </item>
    <item>
      <title>[Database-MySQL] Select 쿼리로 테이블 정의서 만들기 (sql to 정의서)</title>
      <link>https://dksek3050.tistory.com/130</link>
      <description>&lt;pre id=&quot;code_1726826235990&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT 
	a.TABLE_NAME '테이블명',
	b.ORDINAL_POSITION '순번', 
	b.COLUMN_NAME '필드명', 
	b.COLUMN_KEY 'KEY', 
	b.COLUMN_TYPE '데이터길이', 
	b.IS_NULLABLE 'NULL값여부', 
	b.COLUMN_DEFAULT '기본값', 
	b.COLUMN_COMMENT '필드설명' 
FROM information_schema.TABLES a 
	JOIN information_schema.COLUMNS b 
	ON a.TABLE_NAME = b.TABLE_NAME 
	AND a.TABLE_SCHEMA = b.TABLE_SCHEMA 
WHERE a.TABLE_SCHEMA = '스키마명(수정하세요)' 
-- AND a.TABLE_NAME = '테이블명(수정하세요)' // 특정 테이블만 조회할때
ORDER BY a.TABLE_NAME, b.ORDINAL_POSITION&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리를 입력하면 아래와 같이 테이블 정의서에 필요한 내용들을 조회할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면 캡처 2024-09-20 185825.png&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eehKZ9/btsJGjvOrfr/HB8B3ovNFRiiWi1jvGabP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eehKZ9/btsJGjvOrfr/HB8B3ovNFRiiWi1jvGabP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eehKZ9/btsJGjvOrfr/HB8B3ovNFRiiWi1jvGabP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeehKZ9%2FbtsJGjvOrfr%2FHB8B3ovNFRiiWi1jvGabP0%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;705&quot; height=&quot;238&quot; data-filename=&quot;화면 캡처 2024-09-20 185825.png&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;238&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;csv로 빼내서 출력도 가능하고 그대로 드래그 해서 복사해서 엑셀에 붙여넣기도 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 2시간, 3시간 걸릴거 한시간만에 뚝딱 해결해버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/lgr0406/222220564883&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://m.blog.naver.com/lgr0406/222220564883&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1726826506413&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;[SQL] Mysql 테이블 정의서 만들기&quot; data-og-description=&quot;결과물 이걸 csv 로 내려받으면 엑셀로 작업할 수도 있다. 참고 https://didalsgur.tistory.com/35&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://m.blog.naver.com/lgr0406/222220564883&quot; data-og-url=&quot;https://blog.naver.com/lgr0406/222220564883&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iWSph/hyW2VH3BUz/Jyp1kBvmZgc8ynVo6CiC7k/img.png?width=743&amp;amp;height=139&amp;amp;face=0_0_743_139&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/lgr0406/222220564883&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://m.blog.naver.com/lgr0406/222220564883&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iWSph/hyW2VH3BUz/Jyp1kBvmZgc8ynVo6CiC7k/img.png?width=743&amp;amp;height=139&amp;amp;face=0_0_743_139');&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;[SQL] Mysql 테이블 정의서 만들기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;결과물 이걸 csv 로 내려받으면 엑셀로 작업할 수도 있다. 참고 https://didalsgur.tistory.com/35&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.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>Programming/DataBase</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/130</guid>
      <comments>https://dksek3050.tistory.com/130#entry130comment</comments>
      <pubDate>Fri, 20 Sep 2024 19:02:28 +0900</pubDate>
    </item>
    <item>
      <title>99클럽 코테 스터디 15일차 TIL /[프로그래머스] 소수찾기</title>
      <link>https://dksek3050.tistory.com/108</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;tistory1_java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/te8tO/btsIV69527d/6vvNcTBYqCjEKEfZilnDBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/te8tO/btsIV69527d/6vvNcTBYqCjEKEfZilnDBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/te8tO/btsIV69527d/6vvNcTBYqCjEKEfZilnDBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fte8tO%2FbtsIV69527d%2F6vvNcTBYqCjEKEfZilnDBk%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;500&quot; height=&quot;500&quot; data-filename=&quot;tistory1_java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 문제&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[level 2] 소수 찾기 - 42839&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42839&quot;&gt;문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;성능 요약&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리: 84.5 MB, 시간: 23.86 ms&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 접근법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 요약&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1개 이상 7개 이하의 자연수들을 개수 상관없이 한번씩만 뽑아서 만들 수 있는 숫자가 소수인지 판별하는 문제&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;1 &amp;le; numbers의 길이 &amp;le; 7&lt;/li&gt;
&lt;li&gt;numbers를 이루고 있는 숫자 들은 0 ~ 9 자연수 이다.&lt;/li&gt;
&lt;li&gt;$O(N^2)$ 보다는 작아야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⭕ &lt;b&gt;접근 방법. 완탐&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;숫자 문자열로부터 가능한 모든 순열 생성:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 자리 숫자를 사용하여 순열을 만들고, 각 순열조합이 소수인지 확인&lt;/li&gt;
&lt;li&gt;자리수의 중복을 방지하기 위해 방문 여부를 체크하는 배열을 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;소수 판별:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 생성된 숫자가 소수인지 확인. 소수 판별은 2부터 해당 숫자의 제곱근까지의 수로 나누어지는지 확인하는 방식으로 수행하여 시간복잡도를 줄인다.&lt;/li&gt;
&lt;li&gt;소수라면 이를 HashSet 저장하여 중복된 소수의 처리를 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ 해당 풀이법의 시간 복잡도 : $O(N! \sqrt{N})$&lt;/p&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; SUCCESS&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;고냥 단박에 성공&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; &amp;zwj;  코드&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {

    // 소수를 저장하는 HashSet, 중복 소수를 자동으로 제거함
    static HashSet&amp;lt;Integer&amp;gt; prime = new HashSet&amp;lt;&amp;gt;(); 
    // 각 종이 조각의 방문 여부를 체크하는 배열
    static boolean visited[]; 
    // 입력된 종이 조각 숫자 문자열
    static String nums; 

    // 주어진 종이 조각 숫자 문자열에서 만들 수 있는 소수의 개수를 반환하는 메서드
    public int solution(String numbers) {
        nums = numbers; // 전역 변수 nums에 입력된 숫자 문자열 할당
        visited = new boolean[numbers.length()]; // 숫자 문자열 길이만큼 방문 여부 배열 초기화

        // 숫자를 조합하여 소수를 찾는 메서드 호출
        makeNumber(0 , &quot;&quot;); 

        // HashSet에 저장된 소수의 개수를 정답으로 반환
        int answer = prime.size(); 
        return answer; // 소수의 개수 반환
    }

    // 주어진 깊이에서 숫자를 조합하여 소수를 찾는 메서드
    private void makeNumber(int depth, String number) {
        // 현재 조합된 숫자가 소수인지 확인
        if(!number.isEmpty()) {
            isPrime(number);
        }

        // 재귀 호출 종료 조건: 현재 깊이가 숫자 문자열 길이 이상일 때
        if(depth &amp;gt;= nums.length()){ 
            return; // 메서드 종료
        }

        // 숫자 문자열의 각 자리수를 순회하며 숫자 조합 생성
        for (int i = 0; i &amp;lt; nums.length(); i++) {
            if(visited[i]) continue; // 이미 방문한 자리수는 건너뜀
            visited[i] = true; // 현재 자리수 방문 처리
            number = number + nums.charAt(i); // 현재 자리수의 숫자를 조합에 추가
            makeNumber(depth+1, number); // 다음 깊이로 재귀 호출
            number = number.substring(0, number.length()-1); // 마지막에 추가한 자리수의 숫자를 제거
            visited[i] = false; // 방문 처리 해제
        }
    }

    // 주어진 문자열이 소수인지 확인하고, 소수라면 HashSet에 추가하는 메서드
    private void isPrime(String number) {
        int n = Integer.parseInt(number); // 문자열을 정수로 변환
        if(n == 0 || n == 1){ // 0이나 1은 소수가 아님
            return; // 메서드 종료
        }

        // 2부터 n의 제곱근까지의 수로 나누어지는지 확인
        for (int i = 2; i &amp;lt;= Math.sqrt(n); i++) {
            if(n % i == 0) { // 나누어 떨어지면 소수가 아님
                return; // 메서드 종료
            }
        }

        // 소수라면 HashSet에 추가
        prime.add(n); 
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm/99클럽 코테 스터디</category>
      <category>99클럽</category>
      <category>til</category>
      <category>개발자 취업</category>
      <category>코딩테스트 준비</category>
      <category>항해99</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/108</guid>
      <comments>https://dksek3050.tistory.com/108#entry108comment</comments>
      <pubDate>Mon, 5 Aug 2024 18:23:12 +0900</pubDate>
    </item>
    <item>
      <title>99클럽 코테 스터디 13일차 TIL / [프로그래머스] 입국심사</title>
      <link>https://dksek3050.tistory.com/106</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;tistory1_java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpl2TM/btsISJ9BqRO/l0220bxzuBYJfM56Xe7Esk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpl2TM/btsISJ9BqRO/l0220bxzuBYJfM56Xe7Esk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpl2TM/btsISJ9BqRO/l0220bxzuBYJfM56Xe7Esk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcpl2TM%2FbtsISJ9BqRO%2Fl0220bxzuBYJfM56Xe7Esk%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;500&quot; height=&quot;500&quot; data-filename=&quot;tistory1_java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&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;[level&amp;nbsp;3]&amp;nbsp;입국심사&amp;nbsp;-&amp;nbsp;43238&amp;nbsp; &lt;br /&gt;&lt;br /&gt;[문제&amp;nbsp;링크](&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43238)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/43238)&lt;/a&gt;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;###&amp;nbsp;성능&amp;nbsp;요약 &lt;br /&gt;&lt;br /&gt;메모리:&amp;nbsp;99.7&amp;nbsp;MB,&amp;nbsp;시간:&amp;nbsp;107.77&amp;nbsp;ms &lt;/p&gt;
&lt;pre id=&quot;code_1722690991481&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public long solution(int n, int[] times) {
        
        long answer = 0;
        Arrays.sort(times);
        // 이분탐색 기준은 모든 인원이 입국심사를 받는데 걸리는 시간
        // 1분 동안 모든 인원이 입국심사를 전부 받았나? -&amp;gt; X
        // 2분 동안 모든 인원이 입국심사를 전부 받았나? -&amp;gt; X
        // 3분 동안 모든 인원이 입국심사를 전부 받았나? -&amp;gt; O
        // 4분 동안 모든 인원이 입국심사를 전부 받았나? -&amp;gt; O
        // 5분 동안 모든 인원이 입국심사를 전부 받았나? -&amp;gt; O
        // 의 형태를 띄기때문에 이분탐색으로 풀이 가능
        long left = 1;
        long right = times[0] * (long)n; // 가장 오래 걸리는 경우 (심사하는데 걸리는 시간이 가장 긴 시간 * 입국신사 기다리는 사람의 수)
        long mid = (left + right)/2;
        while (left &amp;lt;= right){
            if(isAllPass(mid, n , times)){ // mid 시간내에 모든인원이 입국심사를 받았는지 확인
                // 받았다면 더 짧은 시간 범위를 확인
                right = mid -1 ;
                answer = mid;
            }else{
                // 못받았다면 시간을 늘려서 더 긴 시간 범위 확인
                left = mid +1;
            }
            mid = (left + right)/2;            
        }
        return answer;
    }
    public boolean isAllPass(long time, int n, int[] times){
        int pass = 0;
        for(int i = 0; i &amp;lt; times.length; i ++){
            pass += time/times[i];
            if(pass &amp;gt;= n){
                return true;
            }
        }
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm/99클럽 코테 스터디</category>
      <category>99클럽</category>
      <category>til</category>
      <category>개발자 취업</category>
      <category>코딩테스트 준비</category>
      <category>항해99</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/106</guid>
      <comments>https://dksek3050.tistory.com/106#entry106comment</comments>
      <pubDate>Sat, 3 Aug 2024 22:16:52 +0900</pubDate>
    </item>
    <item>
      <title>99클럽 코테 스터디 11일차 TIL /[프로그래머스]가장 큰 수</title>
      <link>https://dksek3050.tistory.com/104</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;tistory1_java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c87nmU/btsIRevjl4D/IBstDFfEkhckA1keBFOkH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c87nmU/btsIRevjl4D/IBstDFfEkhckA1keBFOkH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c87nmU/btsIRevjl4D/IBstDFfEkhckA1keBFOkH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc87nmU%2FbtsIRevjl4D%2FIBstDFfEkhckA1keBFOkH1%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;500&quot; height=&quot;500&quot; data-filename=&quot;tistory1_java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt; 문제&lt;/h2&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;[level 2] 가장 큰 수 - 42746&lt;br /&gt;&lt;br /&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42746#qna&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;문제 링크&lt;/a&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;성능 요약&lt;/h4&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;메모리:&amp;nbsp;124&amp;nbsp;MB,&amp;nbsp;시간:&amp;nbsp;215.48&amp;nbsp;ms&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; 접근법&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;문제 요약&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0&amp;nbsp;또는&amp;nbsp;양의&amp;nbsp;정수가&amp;nbsp;주어졌을&amp;nbsp;때,&amp;nbsp;정수를&amp;nbsp;이어&amp;nbsp;붙여&amp;nbsp;만들&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;가장&amp;nbsp;큰&amp;nbsp;수를&amp;nbsp;알아내는&amp;nbsp;문제&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;범위 체크 및 시간복잡도 예상&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;numbers의 길이는 1 이상 100,000 이하&lt;/li&gt;
&lt;li&gt;numbers의 원소는 0 이상 1,000 이하&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;풀이법&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⭕&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;접근 방법. 우선순위 큐&lt;/b&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  PriorityQueue를 사용하자!&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;static PriorityQueue&amp;lt;Integer&amp;gt; *pq* = new PriorityQueue&amp;lt;&amp;gt;((o1, o2) -&amp;gt; {return o2-o1;});&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 PQ를 사용하여 우선순위 조건을 변경하면 큰 값이 우선이 되도록 qp를 생성할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;pq 는 힙으로 구성되어 있기 때문에 시간복잡도는 O(logN)을 가진다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;➡️ 해당 풀이법의 시간 복잡도 : $O(NlogN)$&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; FAIL&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;답을 봐서 문제를 푼 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 두 문자열을 더해서 길이를 같게 만든 후에 우선순위를 비교한다는 아이디어를 떠올리지 못하였다  &amp;zwj;♀️&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;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt; &amp;zwj;  코드&lt;/h2&gt;
&lt;pre id=&quot;code_1722522097001&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
        public String solution(int[] numbers) {
            String nums[] = new String[numbers.length];
            for (int i = 0; i &amp;lt; numbers.length; i++) {
                nums[i] = String.valueOf(numbers[i]);
            }

            Arrays.sort(nums,(o1, o2) -&amp;gt; {
                String s1 = o1 + o2;
                String s2 = o2 + o1;
                return s2.compareTo(s1);
            });

//            System.out.println(Arrays.toString(nums));

            StringBuilder answer = new StringBuilder();
            for (int i = 0; i &amp;lt; nums.length; i++) {
                if(i == 0 &amp;amp;&amp;amp; nums[0].equals(&quot;0&quot;)) return &quot;0&quot;;
                answer.append(nums[i]);
            }
            return answer.toString();
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;99클럽 코딩테스트 준비 개발자 취업 항해99 TIL&lt;/p&gt;</description>
      <category>Algorithm/99클럽 코테 스터디</category>
      <category>99클럽</category>
      <category>til</category>
      <category>개발자 취업</category>
      <category>코딩테스트 준비</category>
      <category>항해99</category>
      <author>제가안난데여♪(&amp;acute;&amp;epsilon;｀*)</author>
      <guid isPermaLink="true">https://dksek3050.tistory.com/104</guid>
      <comments>https://dksek3050.tistory.com/104#entry104comment</comments>
      <pubDate>Thu, 1 Aug 2024 23:22:48 +0900</pubDate>
    </item>
  </channel>
</rss>