<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>bright jazz music</title>
    <link>https://catnails.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 4 Aug 2026 11:41:47 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>bright jazz music</managingEditor>
    <image>
      <title>bright jazz music</title>
      <url>https://tistory1.daumcdn.net/tistory/4270495/attach/1b25e52b404d4261ae2bf1754aa38c91</url>
      <link>https://catnails.tistory.com</link>
    </image>
    <item>
      <title>[프레임워크 없는 프론트엔드 개발] 1.2. 뷰 함수 분리</title>
      <link>https://catnails.tistory.com/578</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://catnails.tistory.com/577&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://catnails.tistory.com/577&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1748177529233&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;[프레임워크 없는 프론트엔드 개발] 1.1. 순수함수를 이용한 렌더링 구현&quot; data-og-description=&quot;https://github.com/hojuncha997/frameworkless-front-end-dev GitHub - hojuncha997/frameworkless-front-end-dev: 프레임워크 없는 프론트엔드 서적 학습 리포지토리프레임워크 없는 프론트엔드 서적 학습 리포지토리. Contribut&quot; data-og-host=&quot;catnails.tistory.com&quot; data-og-source-url=&quot;https://catnails.tistory.com/577&quot; data-og-url=&quot;https://catnails.tistory.com/577&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/czGchM/hyYYyXYr3C/10DNF2jKAeSgxKcjmpIPg0/img.png?width=800&amp;amp;height=495&amp;amp;face=0_0_800_495,https://scrap.kakaocdn.net/dn/c9uCiD/hyYW1sCUdN/JCYA9qjHbegVICAM2viVCK/img.png?width=800&amp;amp;height=495&amp;amp;face=0_0_800_495,https://scrap.kakaocdn.net/dn/cAfwjR/hyYYu2kvKD/sVdTKwCeaFzoqG58wJp5GK/img.png?width=2422&amp;amp;height=1500&amp;amp;face=0_0_2422_1500&quot;&gt;&lt;a href=&quot;https://catnails.tistory.com/577&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://catnails.tistory.com/577&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/czGchM/hyYYyXYr3C/10DNF2jKAeSgxKcjmpIPg0/img.png?width=800&amp;amp;height=495&amp;amp;face=0_0_800_495,https://scrap.kakaocdn.net/dn/c9uCiD/hyYW1sCUdN/JCYA9qjHbegVICAM2viVCK/img.png?width=800&amp;amp;height=495&amp;amp;face=0_0_800_495,https://scrap.kakaocdn.net/dn/cAfwjR/hyYYu2kvKD/sVdTKwCeaFzoqG58wJp5GK/img.png?width=2422&amp;amp;height=1500&amp;amp;face=0_0_2422_1500');&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;[프레임워크 없는 프론트엔드 개발] 1.1. 순수함수를 이용한 렌더링 구현&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://github.com/hojuncha997/frameworkless-front-end-dev GitHub - hojuncha997/frameworkless-front-end-dev: 프레임워크 없는 프론트엔드 서적 학습 리포지토리프레임워크 없는 프론트엔드 서적 학습 리포지토리. Contribut&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;catnails.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서 다뤘던 view.js의 함수는 DOM 을 조작하는 함수가 하나뿐이었다. 이번에는 해당 함수를 기능별로 나누었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 디렉토리의 루트 경로에 view 디렉토리를 만들고 아래의 파일을 만들어 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. /view 디렉토리 내부의 파일 설명&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- app.js (작은 뷰 함수들로 이루어진 뷰 함수)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 컨트롤러 역할을 하는 index.js로부터 변경해야 할 HTML 요소와 상태(배열과 필터 텍스트)를 넘겨 받는다. 그리고 아래 파일(함수)들에 분배한다. 함수들로부터의 반환값은, 깊은 복사를 한 HTML 요소에 적용하고 이 복제된 요소를 다시 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- todo.js (할일 목록을 렌더링하는 뷰 함수)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: app.js로부터 HTML 요소와 상태를 인자로 받는다. 상태를 사용해 &amp;lt;li&amp;gt; 태그로 이루어진 텍스트를 만들고 그것을 다시 HTML 요소에 적용하여 변경된 HTML 요소를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- counter.js (할 일 개수를 렌더링 하는 뷰 함수)&lt;/b&gt;&lt;br /&gt;: app.js로부터 HTML 요소와 상태를 인자로 받는다. 상태 객체 내부의 배열의 길이를 텍스트로 만들고, 인자로 받은 HTML요소의 textContent 메서드를 사용하여 텍스트 값을 적용한 뒤 반환한다.&lt;/p&gt;
&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;- filter.js ( 필터링을 이후 결과를 렌더링하는 뷰 함수)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: app.js로부터 HTML 요소와 상태를 인자로 받는다. 건네 받은 요소의 하위 요소 가운데 textContent값이 인자로 받은 상태 객체의 currentFilter 속성 값과 같으면 selected라는 css class를 해당 요소에 추가한 뒤 반환한다.&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;2. 파일 내용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1. /index.js &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;:&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;기존 view.js 대신 app.js를 임포트한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748179162398&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// /index.js
// 컨트롤러 역할을 하는 파일

import getTodos from './getTodos.js'
// import view from './view.js'
import view from './view/app.js'  // 기존 view.js 대신 view/app.js 사용. app.js에서는 분리된 뷰 함수들을 사용한다.

const state = {
  todos: getTodos(),
  currentFilter: 'All'
}

const main = document.querySelector('.todoapp')

window.requestAnimationFrame(() =&amp;gt; {
    // 뷰 함수를 호출하여 투두 앱 요소를 반환한다.
  const newMain = view(main, state)
  // DOM에서 기존 요소를 제거하고 새 요소를 삽입한다.
  main.replaceWith(newMain)
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2. /view/app.js&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1748179418401&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// /view/app.js
import todosView from './todos.js'
import counterView from './counter.js'
import filtersView from './filters.js'

export default (targetElement, state) =&amp;gt; {
  const element = targetElement.cloneNode(true)

  const list = element
    .querySelector('.todo-list')
  const counter = element
    .querySelector('.todo-count')
  const filters = element
    .querySelector('.filters')

  list.replaceWith(todosView(list, state))  // 상태(투두 배열과 필터)를 인자로 넘기고, 반환값으로 HTML 요소를 받아서 교체한다.
  counter.replaceWith(counterView(counter, state))  // 상태(투두 배열)를 인자로 넘기고, 반환값으로 HTML 요소를 받아서 교체한다.
  filters.replaceWith(filtersView(filters, state))  // 상태(필터)를 인자로 넘기고, 반환값으로 HTML 요소를 받아서 교체한다.

  return element
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.3. /view/counter.js&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1748179452408&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// /view/counter.js
const getTodoCount = todos =&amp;gt; {
    const notCompleted = todos
      .filter(todo =&amp;gt; !todo.completed)
  
    const { length } = notCompleted
    if (length === 1) {
      return '1 Item left'
    }
  
    return `${length} Items left`
  }
  
  export default (targetElement, { todos }) =&amp;gt; {
    const newCounter = targetElement.cloneNode(true)
    newCounter.textContent = getTodoCount(todos)
    // 새로운 카운터 HTML 요소를 반환한다.
    return newCounter
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.4. /view/filters.js&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1748179483905&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// /view/filters.js

export default (targetElement, { currentFilter }) =&amp;gt; {
    const newCounter = targetElement.cloneNode(true)
    Array
      .from(newCounter.querySelectorAll('li a'))
      .forEach(a =&amp;gt; {
        if (a.textContent === currentFilter) {
          a.classList.add('selected') // 현재 필터와 같은 것에만 css 클래스를 추가한다.
        } else {
          a.classList.remove('selected') // 현재 필터와 다른 것에는 css 클래스를 제거한다.
        }
      })
    // 새로운 필터 HTML 요소를 반환한다.
    return newCounter
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.5. todos.js&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1748179574390&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// /view/todos.js   : 할일을 표시하는 렌더링 함수

const getTodoElement = todo =&amp;gt; {
    const {
      text,
      completed
    } = todo
  
    return `
        &amp;lt;li ${completed ? 'class=&quot;completed&quot;' : ''}&amp;gt;
          &amp;lt;div class=&quot;view&quot;&amp;gt;
            &amp;lt;input 
              ${completed ? 'checked' : ''}
              class=&quot;toggle&quot; 
              type=&quot;checkbox&quot;&amp;gt;
            &amp;lt;label&amp;gt;${text}&amp;lt;/label&amp;gt;
            &amp;lt;button class=&quot;destroy&quot;&amp;gt;&amp;lt;/button&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;input class=&quot;edit&quot; value=&quot;${text}&quot;&amp;gt;
        &amp;lt;/li&amp;gt;`
  }
  
  export default (targetElement, { todos }) =&amp;gt; {    
    const newTodoList = targetElement.cloneNode(true)
    const todosElements = todos
      .map(getTodoElement)  // 상태로 넘어온 배열을 순회하며 각 투두 아이템 요소를 생성하고 이를 문자열로 변환하여 결합한다. &amp;lt;li&amp;gt;...&amp;lt;/li&amp;gt; 여러 개가 한 줄로 만들어진 문자열이다.
      .join('')
    newTodoList.innerHTML = todosElements // 새로운 투두 아이템 HTML 요소를 반환한다.
    return newTodoList
  }&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Books/프레임워크 없는 프론트엔드 개발</category>
      <author>bright jazz music</author>
      <guid isPermaLink="true">https://catnails.tistory.com/578</guid>
      <comments>https://catnails.tistory.com/578#entry578comment</comments>
      <pubDate>Sun, 25 May 2025 22:30:27 +0900</pubDate>
    </item>
    <item>
      <title>[프레임워크 없는 프론트엔드 개발] 1.1. 순수함수를 이용한 렌더링 구현</title>
      <link>https://catnails.tistory.com/577</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/hojuncha997/frameworkless-front-end-dev&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/hojuncha997/frameworkless-front-end-dev&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1748009762420&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - hojuncha997/frameworkless-front-end-dev: 프레임워크 없는 프론트엔드 서적 학습 리포지토리&quot; data-og-description=&quot;프레임워크 없는 프론트엔드 서적 학습 리포지토리. Contribute to hojuncha997/frameworkless-front-end-dev development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/hojuncha997/frameworkless-front-end-dev&quot; data-og-url=&quot;https://github.com/hojuncha997/frameworkless-front-end-dev&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/hojuncha997/frameworkless-front-end-dev&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/hojuncha997/frameworkless-front-end-dev&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;GitHub - hojuncha997/frameworkless-front-end-dev: 프레임워크 없는 프론트엔드 서적 학습 리포지토리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;프레임워크 없는 프론트엔드 서적 학습 리포지토리. Contribute to hojuncha997/frameworkless-front-end-dev 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;&lt;span style=&quot;color: #f89009;&quot;&gt;* css, 라이브러리 등의 파일들은 여기에 기록하지 않았다. 위 리포지토리에는 남아 있으므로 참고할 것&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;* 브라우저에서 file://경로/index.html 로 접근하면 CORS 정책 오류 때문에 자바스크립트 파일을 가져오지 못한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&amp;nbsp; 따라서 npx http-server를 사용하여 노드 서버를 구동하거나&amp;nbsp; VS Code 의 Live Server등의 확장프로그램을 사용할 것.&lt;/span&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;1. 개요&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트와 같은, 프레임워크에 가까운 라이브러리의 내부 방식에 대한 이해가 부족하여 학습을 시작하였다. 이 책에서는 프레임워크를 사용하지 않으면서 프레임워크에 사용되는 방식을 구현하는 내용이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스팅에서는 2장 '렌더링'의 일부를 기록하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- index.html은 화면에 직접적으로 보여지는 파일이다. css 파일이 임포트 돼 있으며, index.js파일을 임포트하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- index.js 파일은 컨트롤러 역할을 하는 파일이다. getTodos.js와 view.js 파일을 임포트하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- getTodos.js은 fake.js 라이브러리를 사용하여 임의의 todo 리스트 배열을 만들어 반환하는 모듈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- view.js은 HTML 요소와 상태를 인자로 받는다. 인자로 받은 HTML 요소를 복제한 요소를 반환한다. 이 때 복제된 요소를 가공하기 위해 상태값을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, index.js에서 getTodos.js 파일를 사용해 임의의 todos 배열을 만들어내고, 해당 배열을 상태 역할을 하는 객체에 포함시켜 view.js 파일 의 view 함수에 인자로 넘긴다. view 함수는 상태 객체 뿐만 아니라 HTML 요소 또한 인자로 받는데, 이는 view 함수가 인자로 받은 요소의 복사된 HTML 요소를 반환하기 때문이다. 상태값은 이 복사된 요소를 가공하는 데 사용된다.&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. 스키마&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 열거한 파일들로 생성한 스키마는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[브라우저 렌더링] -&amp;gt; [다음 렌더링 대기] -&amp;gt; [새 가상 노드] -&amp;gt; [DOM 조작] -&amp;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;&lt;b&gt;4. 코드&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1. /index.html&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2422&quot; data-origin-height=&quot;1500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEtfKQ/btsOcoKSEJk/wIIYu01wK3q3TVrlbFo7wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEtfKQ/btsOcoKSEJk/wIIYu01wK3q3TVrlbFo7wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEtfKQ/btsOcoKSEJk/wIIYu01wK3q3TVrlbFo7wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEtfKQ%2FbtsOcoKSEJk%2FwIIYu01wK3q3TVrlbFo7wK%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;2422&quot; height=&quot;1500&quot; data-origin-width=&quot;2422&quot; data-origin-height=&quot;1500&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;pre id=&quot;code_1748009834458&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;

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

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

&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.2. /index.js&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1748011806325&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 컨트롤러 역할을 하는 파일

import getTodos from './getTodos.js'
import view from './view.js'

const state = {
  todos: getTodos(),
  currentFilter: 'All'
}

const main = document.querySelector('.todoapp')

window.requestAnimationFrame(() =&amp;gt; {
    // 뷰 함수를 호출하여 투두 앱 요소를 반환한다.
  const newMain = view(main, state)
  // DOM에서 기존 요소를 제거하고 새 요소를 삽입한다.
  main.replaceWith(newMain)
})

/*

# innerHTML 대신 replaceWith()를 사용하는 주요 이유:

1. 성능 이점: innerHTML은 문자열을 HTML로 파싱해야 하므로 더 많은 리소스를 사용한다. replaceWith()는 이미 생성된 DOM 노드를 직접 교체하므로 더 효율적이다.
2. 이벤트 리스너 보존: innerHTML을 사용하면 모든 이벤트 리스너가 제거된다. replaceWith()는 새로운 요소에 등록된 이벤트 리스너를 그대로 유지한다.
3. 깔끔한 코드 구조: 복잡한 요소 구조를 문자열로 조합하지 않고 DOM API를 일관되게 사용할 수 있다.
4. 참조 관리: 노드 참조를 직접 다루기 때문에 코드가 더 명확하고 관리하기 쉽다.


# main = view(main, state) 처럼 할당하지 않는 이유:

1. JavaScript 변수 vs DOM: 변수 main에 새 값을 할당하는 것은 JavaScript 메모리 내의 참조만 변경하고, 실제 DOM에는 아무 영향이 없다.
2. 실제 DOM 조작 필요: DOM 요소를 실제로 교체하려면 DOM API 메서드를 사용해야 한다.
3. 불변성 원칙: 이 코드는 원본 요소를 직접 수정하지 않고 새 요소를 만들어 교체하는 패턴을 사용한다. 이는 프레임워크 없는 환경에서도 리액트와 같은 최신 프레임워크의 작동 방식을 모방하고 있다.
4. 명확한 의도: 새 변수를 생성하고 교체하는 방식은 코드의 의도를 더 명확하게 보여준다.


# window.requestAnimationFrame

1. 기본 기능
- 브라우저의 Window 인터페이스에서 제공하는 메서드이다.
- 다음 리페인트(화면 갱신) 직전에 지정된 콜백 함수를 실행. 즉, 화면 갱신 직전에 실행된다.
- 보통 초당 60회(60fps) 실행되지만, 기기 성능과 화면 주사율에 따라 달라질 수 있다.

2. 장점
- 성능 최적화: 브라우저가 적절한 타이밍에 실행하도록 스케줄링
- 배터리 효율: 백그라운드 탭이나 화면 밖의 요소는 실행 빈도가 줄어든다.
- 부드러운 애니메이션: 모니터 주사율에 맞춰 실행되어 부드러운 시각 효과를 제공한다.

3. 구분
- 자바스크립트 엔진의 메서드가 아니다.
- 브라우저가 제공하는 Web API의 일부이다.
- 브라우저의 렌더링 엔진과 연결되어 있다.

4. 작동 과정
- 1. 콜백이 특별한 큐에 등록된다.
- 2. 브라우저는 다음 리페인트 직전에 이 콜백을 실행하도록 스케줄링한다.
- 3. 자바스크립트 메인 스레드의 다른 작업이 완료된 후 실행된다.
- 4. 브라우저의 화면 주사율에 맞춰 실행된다.

5. 일반적인 사용 사례
- 애니메이션 구현
- DOM 업데이트 최적화
- 복잡한 렌더링 작업의 효율적 처리


*/&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.3. /getTodos.js&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1748011846014&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { faker } = window

const createElement = () =&amp;gt; ({
  text: faker.random.words(2), // 2개의 랜덤 단어를 반환한다.
  completed: faker.random.boolean() // 랜덤 불리언 값을 반환한다.
})

// 함수와 숫자를 받아서 해당 함수를 숫자만큼 반복하여 반환된 값을 배열에 추가하고 배열을 반환
const repeat = (elementFactory, number) =&amp;gt; {
  const array = []
  for (let index = 0; index &amp;lt; number; index++) {
    array.push(elementFactory())
  }
  return array
}

// 익명 함수로 export. index.js 에서 임의의 이름으로 import하여 사용할 수 있다.
export default () =&amp;gt; {
  const howMany = faker.random.number(10) // 10개 이하의 랜덤 숫자를 반환한다.
  return repeat(createElement, howMany) // 결국 {text: '랜덤 단어', completed: 랜덤 불리언} 형태의 객체를 10개 이하로 생성하여 배열로 반환한다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.4. view.js&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1748011917175&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 뷰 역할을 하는 함수


// 투두 아이템 요소 생성을 위한 함수. 투두 아이템 객체를 받아서 투두 아이템 요소를 반환한다.
const getTodoElement = todo =&amp;gt; {
    const {
      text,
      completed
    } = todo
  
    return `
    &amp;lt;li ${completed ? 'class=&quot;completed&quot;' : ''}&amp;gt;
      &amp;lt;div class=&quot;view&quot;&amp;gt;
        &amp;lt;input 
          ${completed ? 'checked' : ''}
          class=&quot;toggle&quot; 
          type=&quot;checkbox&quot;&amp;gt;
        &amp;lt;label&amp;gt;${text}&amp;lt;/label&amp;gt;
        &amp;lt;button class=&quot;destroy&quot;&amp;gt;&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;input class=&quot;edit&quot; value=&quot;${text}&quot;&amp;gt;
    &amp;lt;/li&amp;gt;`
  }
  
  // 투두 아이템 개수를 반환하는 함수. 투두 아이템 배열을 받아서 투두 아이템 개수를 반환한다.
  const getTodoCount = todos =&amp;gt; {
    const notCompleted = todos
      .filter(todo =&amp;gt; !todo.completed)
  
    const { length } = notCompleted
    if (length === 1) {
      return '1 Item left'
    }
  
    return `${length} Items left`
  }

  
  
  // 투두 앱 요소를 반환하는 함수. 투두 앱 요소와 상태를 받아서 투두 앱 요소를 반환한다.
  // 익명함수로 export 되기 때문에 사용하는 곳에서 임의의 이름으로 사용될 수 있다. e.g. import view from './view.js'
  export default (targetElement, state) =&amp;gt; {

    console.log('targetElement: ', targetElement)
    console.log('state:', state)
    const {
      currentFilter,
      todos // 투두 아이템 배열
    } = state
  
    // 타겟 DOM 요소를 복사한다. JS의 DOM API가 가진 메서드이다. true 인자를 넘기면 깊은 복사로, 자식까지 전부 복제한다. false는 자식 없이 해당 요소만 복제한다.
    const element = targetElement.cloneNode(true) 
    console.log('cloned element: ', element)
    // 복제된 DOM 요소의 하위 요소 중에서 클래스 이름으로 조회하여 가져온다. 텍스트만 가져오는 것이 아니라 요소 자체를 가져온다.
    const list = element.querySelector('.todo-list')
    const counter = element.querySelector('.todo-count')
    const filters = element.querySelector('.filters')
  
    // 
    list.innerHTML = todos.map(getTodoElement).join('') // 투두 아이템 배열을 순회하며 각 투두 아이템 요소를 생성하고 이를 문자열로 변환하여 결합한다. &amp;lt;li&amp;gt;...&amp;lt;/li&amp;gt; 여러 개가 한 줄로 만들어진 문자열이다.
    counter.textContent = getTodoCount(todos) // (예: &quot;1 Item left&quot; 또는 &quot;5 Items left&quot;). 이 생성된 문자열이 counter 요소의 textContent 속성에 할당된다. 태그로 감싸진 내부의 문자열에만 적용된다.
  
    Array
      .from(filters.querySelectorAll('li a')) // 타겟 DOM 요소의 하위 요소 중에서 li 요소의 하위 요소 중에서 a 요소를 모두 선택한다.
      .forEach(a =&amp;gt; { // 그리고 해당 요소이 가진 텍스트가 현재 인자로 넘어온 필터와 같은 것들에만 css 클래스를 추가한다.
        if (a.textContent === currentFilter) {
          a.classList.add('selected') // add 역시 DOM API가 가진 메서드이다. HTML요소에 css 클래스를 추가한다. 결과적으로 HTML에서 &amp;lt;a class=&quot;selected&quot;&amp;gt; 형태로 변경된다.
        } else {
          a.classList.remove('selected') // HTML에서 &amp;lt;a&amp;gt; 형태로 변경된다.
        }
      })
  
    return element
  }

  /*
  여기서 export 하는 view 함수는 기본으로 사용되는 타겟 DOM 요소를 받는다. state의 형태는 

1. 파라미터:
- targetElement: 타겟 DOM 요소
- state: 상태 객체. 컨트롤러 역할을 하는 index.js 파일에서 전달받은 상태 객체이며 형태는 아래와 같다.

  const state = {
  todos: getTodos(),
  currentFilter: 'All'
}
다시 todos속성을 확인해보면, 이는 {text: '랜덤 단어', completed: 랜덤 불리언}의 객체형태의 요소가 10개 이하로 채워진 배열이다. 

- 이 함수에서는 인자로 받은 타겟 DOM 요소를 복사하고, 
- 인자로 넘어온 배열을 &amp;lt;li&amp;gt; 문자열로 만들어서 복사한 요소의 하위 요소에 입력한다.
- 그리고 현재 필터 상태에 따라서 필터 요소에 css 클래스를 추가하거나 제거한다.

즉, 이 함수는 실제 HTML요소와 JS로 만들어진 객체인 상태를 인자로 받아서,
기존 요소를 복제하고, 상태 값을 가공하여 HTML 요소에 입력하고 css 스타일링까지 완료하여 
실제 HTML 요소를 반환한다. 
그리고 이 함수를 호출한 곳에서 replaceWith 메서드를 사용하여 기존 요소를 반환된 요소로 교체한다.

**** 주의. ****
반환 값은 '&amp;lt;section class=&quot;todoapp&quot;&amp;gt;...&amp;lt;/section&amp;gt;' 이런 형태의 문자열이 아니다.
아래와 같은 DOM 객체이다.

// 콘솔에 출력했을 때의 표현
const 실제반환값 = {
  tagName: &quot;SECTION&quot;,
  className: &quot;todoapp&quot;,
  childNodes: [...], // DOM 노드 배열
  innerHTML: &quot;...&quot;,
  outerHTML: &quot;&amp;lt;section class=\&quot;todoapp\&quot;&amp;gt;...&amp;lt;/section&amp;gt;&quot;,
  querySelector: function() { ... },
  replaceWith: function() { ... },
  // 기타 수많은 DOM 메서드와 속성들...
}

*/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6Pgvt/btsOaOxkG6P/6rAoSL3X5IWhIkmZD5sh61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6Pgvt/btsOaOxkG6P/6rAoSL3X5IWhIkmZD5sh61/img.png&quot; data-alt=&quot;이렇게 보이지만 실제로는 브라우저가 제공하는 특별한 객체로 JS형태를 가지며 JS로 조작이 가능하다. 여기서는 브라우저가 보기 쉽게 태그로 풀어주는 것뿐이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6Pgvt/btsOaOxkG6P/6rAoSL3X5IWhIkmZD5sh61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6Pgvt%2FbtsOaOxkG6P%2F6rAoSL3X5IWhIkmZD5sh61%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;838&quot; height=&quot;638&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이렇게 보이지만 실제로는 브라우저가 제공하는 특별한 객체로 JS형태를 가지며 JS로 조작이 가능하다. 여기서는 브라우저가 보기 쉽게 태그로 풀어주는 것뿐이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;* 저자가 관리하는 리포지토리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Apress/frameworkless-front-end-development/tree/master&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Apress/frameworkless-front-end-development/tree/master&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1748009903371&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - Apress/frameworkless-front-end-development: Source code for 'Frameworkless Front-End Development' by Francesco Strazzul&quot; data-og-description=&quot;Source code for 'Frameworkless Front-End Development' by Francesco Strazzullo - Apress/frameworkless-front-end-development&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Apress/frameworkless-front-end-development/tree/master&quot; data-og-url=&quot;https://github.com/Apress/frameworkless-front-end-development&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/tBhgl/hyYWXpU62f/LSrmR7l8peMWrbYXTEfxw1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cX0iLV/hyYYv0zlpg/DU6muneB2keec6uJvbHFb0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Apress/frameworkless-front-end-development/tree/master&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Apress/frameworkless-front-end-development/tree/master&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/tBhgl/hyYWXpU62f/LSrmR7l8peMWrbYXTEfxw1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cX0iLV/hyYYv0zlpg/DU6muneB2keec6uJvbHFb0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - Apress/frameworkless-front-end-development: Source code for 'Frameworkless Front-End Development' by Francesco Strazzul&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Source code for 'Frameworkless Front-End Development' by Francesco Strazzullo - Apress/frameworkless-front-end-development&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Books/프레임워크 없는 프론트엔드 개발</category>
      <author>bright jazz music</author>
      <guid isPermaLink="true">https://catnails.tistory.com/577</guid>
      <comments>https://catnails.tistory.com/577#entry577comment</comments>
      <pubDate>Sat, 24 May 2025 00:04:06 +0900</pubDate>
    </item>
    <item>
      <title>프로토타입 prototype (4) : 프로토타입의 생성 시점</title>
      <link>https://catnails.tistory.com/576</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로토타입은 생성자 함수가 생성되는 시점에 함께 생성된다. 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재하기 때문이다. 생성자 함수는 사용자가 직접 정의한 사용자 정의 생성자 함수와 자바스크립트가 기본 제공하는 빌트인 생성자 함수로 구분할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용자 정의 생성자 함수와 프로토타입 생성 시점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 함수로 호출할 수 있는 함수, constructor는 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다.&lt;/p&gt;
&lt;pre id=&quot;code_1747054068683&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 생성자 함수가 평가되는 시점에 프로토타입도 더불어 생성됨
console.log(Person.prototype); // {constructor: f} 

// 생성자 함수
function Person(name) {
	this.name = name;
}

/*
함수 선언문은 런타임 이전에 자바스크립트 엔진에 의해 먼저 실행된다.
따라서 위 방식으로 선언된 Person생성자 함수는 어떤 코드보다도 먼저 평가되어 함수 객체가 된다.
이 때 프로토타입도 더불어 생성된다.
생성된 프로토타입은 Person 생성자 함수의 prototype 프로퍼티에 바인딩된다.

이렇게 생성된 프로토타입은 오직 constructor 프로퍼티만을 갖는 객체이다.
프로토타입도 객체이고 모든 객는 프로토타입은 가지므로 프로토타입도 자신의 프로토타입을 갖는다.
생성된 프로토타입의 프로토타입은 Object.prototype이다
*/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 함수로 호출할 수 없는 함수는 이 시점에는 프로토타입이 생성되지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1747054178141&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 화살표 함수는 non-constructor이다
const Person =&amp;gt; (name) =&amp;gt; {
	this.name = name;
};

console.log(Person.prototype); // undefined&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;gt; 사용자 정의 생성자 함수는 그 자신이 평가되어 함수 객체로 생성되는 시점에 프로토타입도 더불어 생성되며, 생성된 프로토타입의 프로토입은 언제나 Object.prototype이다.&lt;/p&gt;
&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;빌트인 생성자 함수와 프로토타입 생성 시점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Object, String, Number, Function, Array, RegExp, Date, Promise 등과 같은 빌트인 생성자 함수도 일반 함수와 마찬가지로 빌트인 생성자 함수가 생성되는 시점에 프로토타입이 생성된다. 생성된 프로토타입은 빌트인 생성자 함수의 prototype 프로토타입으로 바인딩 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, 객체가 생성되기 이전에 빌트인 생성자 함수와 프로토타입은 이미 객체화되어 존재한다. 이후 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면 프로토타입은 생성된 객체의 [[prototype]] 내부 슬롯에 할당된다&lt;/b&gt;. 이로써 생성된 객체는 프로토타입을 상속받는다.&lt;/p&gt;</description>
      <category>Language/Javascript</category>
      <author>bright jazz music</author>
      <guid isPermaLink="true">https://catnails.tistory.com/576</guid>
      <comments>https://catnails.tistory.com/576#entry576comment</comments>
      <pubDate>Mon, 12 May 2025 22:31:04 +0900</pubDate>
    </item>
    <item>
      <title>프로토타입 prototype (3) : 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입</title>
      <link>https://catnails.tistory.com/575</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 함수에 의해 생성된 인스턴스는 프로토타입의&amp;nbsp; constructor 프로퍼티에 의해 생성자 함수와 생성자 함수와 연결된다. 이 때 constructor 프로퍼티가 가리키는 생성자 함수는 인스턴스를 생성한 생성자 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 리터럴 표기법에 의한 객체 생성 방식과 같이 명시적으로 new 연산자와 함께 생성자 함수를 호출하여 인스턴스를 생성하지 않는 객체 생성 방식도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1746972079666&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 객체 리터럴
const obj = {};

// 함수 리터럴
const add = function (a, b) {return a + b};

// 배열 리터럴
const arr = [1,2,3];

// 정규표현식 리터럴
const regexp = /is/ig;&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;리터럴 표기법에 의해 생성된 객체도 프로토타입이 존재한다. 그러나 리터럴 표기법에 의해 생성된 객체의 경우, 프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수가 반드시 객체를 생성한 생성자 함수라고 단정할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746972359853&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 리터럴 방식으로 생성
const obj = {};

// 그러나 obj의 생성자 함수는 Object 생성자 함수다
console.log(obj.constructor === Object); // true

//-----

//생성자로 객체를 만든 경우
function Person(name) {
	this.name = name;
}

const me = new Person('Lee');
console.log(me.constructor === Person); // true&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;Object 생성자 함수에 인수를 전달하지 않거나 undefined, 또는 null을 인수로 전달하면서 호출하면 내부적으로는 추상 연산(abstract operation) OrdinaryObjectCreate를 호출하여 Object.prototype을 프로토타입으로 갖는 객체를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746973291230&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 인수가 전달되지 않았을 때 OrdinaryObjectCreate를 호출하여 빈 객체 생성
let obj = new Object();
console.log(obj); // {}

// new.target이 undefined이거나 Object가 아닌 경우
class Foo extends Object{};
new Foo() // Foo {}

//인수가 전달된 경우에는 인수를 객체로 변환
obj = new Object(123);
console.log(obj); // Number {123}

// String 객체 생성
obj = new Object('123');
console.log(obj); // String {&quot;123&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;Object 생성자 함수 호출과 객체 리터럴의 평가는 추상 연산 OrdinaryObjectCreate를 호출하여 빈 객체를 생성하는 점에서는 동일하다. 그러나 new.target의 확인이나 프로퍼티를 추가하는 처리 등 세부 내용은 다르다. 따라서 객체 리터럴에 의해 생성된 객체를 Object 생성자 함수가 생성한 객체가 아니다.( 엄격하게 본다면! 객체 리터럴로 객체를 생성하면 Object생성자로 생성한 객체와는 다른 객체가 만들어진다는 의미이다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리터럴 표기법에 의해 생성된 객체도 상속을 위해 프로토타입이 필요하다. &lt;b&gt;따라서 리터럴 표기법에 의해 생성된 객체도 가상의 생성자 함수를 갖는다.&lt;/b&gt; 프로토타입은 생성자 함수와 더불러 생성되며 prototype, constructor 프로퍼티에 의해 연결돼 있기 때문이다. 다시 말해 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍(pair)로 존재한다. (객체 리터럴로 만들었다고 해도 프로토타입을 만들 때에는 생성자 함수가 필요하며, 그렇기 때문에 가상의 생서자 함수를 갖게 된다는 의미)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 리터럴 방식으로 만든 객체도 결과적으로 생성자 함수와 연결 되므로 본질적으로는 생성자 함수로 만든 객체와 동일한 특성을 갖는다. 다만 생성 과정이 다르다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Language/Javascript</category>
      <author>bright jazz music</author>
      <guid isPermaLink="true">https://catnails.tistory.com/575</guid>
      <comments>https://catnails.tistory.com/575#entry575comment</comments>
      <pubDate>Sun, 11 May 2025 23:33:23 +0900</pubDate>
    </item>
    <item>
      <title>프로토타입 prototype (2) : prototype 프로퍼티</title>
      <link>https://catnails.tistory.com/574</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;함수 객체의 prototype 프로토타입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prototype 프로퍼티는 함수객체만이 소유하며&amp;nbsp; 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.&lt;/p&gt;
&lt;pre id=&quot;code_1746620014089&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 함수 객체는 prototype 프로퍼티를 소유한다.
(function () {}).hasOwnProperty('prototype'); // true

// 일반 객체는 prototype 프로퍼티를 소유하지 않는다.
({}).hasOwnProperty('prototype'); // false&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;prototype 프로퍼티는 생성자 함수로 호출할 수 없는 함수들&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746622456585&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 화살표 함수는 non-constructor
const Person = name = &amp;gt; {
	this.name = name;
}

// non-constructor는 prototype 프로퍼티를 소유하지 않는다.
console.log(Person.hasOwnProperty('prototype')); // false

// non-constructor는 프로토타입을 생성하지 않는다.
console.log(Person.prototype); // undefined

// ES6의 메서드 축약 표현으로 정의한 메서드는 non-constructor이다.
const obj = {
	foo() {}
}
console.log(obj.foo.hasOwnProperty('prototype')); // false

// non-constructor는 프로토타입을 생성하지 않는다.
console.log(obj.foo.prototype);// undefined&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;Object.prototype으로부터 상속 받아 모든 객체가 가지고 있는 __proto__ 접근자 프로퍼티와 함수 객체만이 가지고 있는 prototype 프로퍼티는 결굴 동일한 프로토 타입을 가리킨다. 하지만 이들 프로퍼티를 사용하는 주체가 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;__proto__&amp;nbsp; 접근자 프로퍼티는 모든 객체가 소유하며, 모든 객체가 자신의 프로토타입에 접근하거나 프로토타입을 교체하기 위해 사용할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;prototype 프로퍼티는 생성자 함수(constructor)가 소유하며 생성자 함수가 자신이 생성할 객체(인스턴스)의 프로토타입을 할당하기 위해 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1746623144411&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 생성자 함수
function Person(name) {
	this.name = name;
}

const me = new Person('Lee');

// 결국 Person.prototype과 me.__proto__는 동일한 프로토타입을 가리킨다.
console.log(Person.prototype = me.__proto__); // true&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;모든 프로토타입은 constructor 프로퍼티를 갖는다. 이 constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다. 이러한 연결은 생성자 함수가 생성될 때, 즉 함수 객체가&amp;nbsp; 생성될 때 이루어진다.&lt;/p&gt;
&lt;pre id=&quot;code_1746884182068&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 생성자 함수
function Person(name) {
	this.name = name;
}

const me = new Person('Lee');

// me 객체의 생성자 함수는 Person이다
console.log(me.constructor === Person); // true&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Language/Javascript</category>
      <author>bright jazz music</author>
      <guid isPermaLink="true">https://catnails.tistory.com/574</guid>
      <comments>https://catnails.tistory.com/574#entry574comment</comments>
      <pubDate>Wed, 7 May 2025 21:56:37 +0900</pubDate>
    </item>
    <item>
      <title>프로토타입 prototype (1) : __proto__ 접근자 프로퍼티</title>
      <link>https://catnails.tistory.com/573</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로토타입 객체 또는 프로토타입은 객체 간 상속을 구현하기 위해 사용된다. 프로토 타입은 어떤 객체의 상위, &lt;b&gt;즉 부모 객체의 역할을 하는 객체&lt;/b&gt;로서 다른 객체에 공유 프로퍼티와 메서드를 를 제공한다. 프로토타입을 상속 받은 하위 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 객체는 [[property]] 라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조이다. null인 경우도 있다. [[Prototype]] 슬롯에 저장되는 프로토타입은 객체 생성 방식에 의해 결정된다. 즉, 객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고 [[Prototype]] 슬롯에 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 객체 리터럴에 의해 생성된 객체의 프로토타입은 Object.prototype이고 생성자 함수에 의해 생성된 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩 되어 있는 객체다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 const person = { name: 'cha'} 라고 선언했다면, 그 프로토타입은 Object.prototype이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 개체는 하나의 프로토타입을 갖는다. 그리고 모든 프로토타입은 생성자 함수와 연결돼 있다. 즉 객체와 프로토타입은 생성자 함수와 연결돼 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Prototype]] 내부 슬롯에는 직접 접근할 수 없지만,&amp;nbsp; __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입, 즉 자신의 [[prototype]] 내부 슬롯이 가리키는 프로토타입에 간접적으로 접근할 수 있다. 그리고 프로토타입은 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근할 수 있고 생성자 함수는 자신의 prototype 프로퍼티를 통해 프로토타입에 접근할 수 있다.&lt;/p&gt;
&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;즉, 모든 객체는 __proto__ 접근자 프로퍼티를 통해 프로토타입을 가리키는 [[prototype]] 내부 슬롯에 접근할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;---&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;__prototype__ 접근자 프로퍼티&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 슬롯은 프로퍼티가 아니다. 따라서 자바스크립트는 원칙적으로 내부 메서드에 직접적으로 접근하거나 호출할 수 있는 방법을 제공하지 않는다. 단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근할 수 있는 수단을 제공한다. 위에서도 언급했듯이 __proto__ 접근자를 사용해 간접적으로 내부 슬롯인 [[Prototype]]에 접근하여 그 안에 들어 있는 값인 프로토타입에 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근자 프로퍼티인 __proto__(Object.는 그 자체로는 값을 갖지 않는다. 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용되는 접근자 함수, 즉 [[Get]], [[Set]] 프로퍼티 어트리뷰트로 구성되어 있다.&lt;br /&gt;&lt;br /&gt;Object.prototype의 접근자 프로퍼티인 __proto__는 getter/setter 함수라고 부르는 접근자 함수를 통해 내부 슬롯의 값인 프로퍼티를 취득하거나 할당한다. 따라서 __proto__를 통해 프로토타입에 접근한다면, 내부적으로는 __proto__가 가진 get함수(get __proto__)를 사용하는 것이다. 새로운 프로토타입을 할당할 때에는 __proto__가 가진 setter함수(set __proto_)를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746531085933&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const obj = {};
const parent = {x:1}

//getter(get __proto__) 사용gkdu obj 객체의 프로토타입을 가져온 상태
obj.__proto__;

//setter(set __proto__) 사용하여 새로운 값을 할당
obj.__proto__ = parent; 

console.log(obj.x); // 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;__proto__ 접근자 프로퍼티는 상속을 통해 사용된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;__proto__ 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티다. 모든 객체는 상속을 통해 Object.prototype.__proto__ 접근자 프로퍼티를 사용할 수 있다.&lt;/p&gt;
&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;Object.prototype&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 객체는 프로토타입의 계층 구조인 프로토타입 체인(prototype chain)에 묶여있다.&amp;nbsp; 자바스크립트 엔진은 객체의 프로퍼티와 메서드에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티가 존재하지 않는다면, __proto__ 접근자 프로퍼티가 가리키는 참조를 따라 자신의 부모역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. 프로토타입 체인의 종점, 즉 프로토타입 체인의 최상위 객체는 Object.prototype이며 이 객체의 프로퍼티와 메서드는 모든 객체에 상속된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;__proto__ 접근자 프로퍼티를 통해 프로토타입에 접근하는 이유: 순환참조를 막기 위해서&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[[Prototype]] 내부 슬롯의 값, 즉 프로토타입에 접근하기 위해 접근자 프로퍼티를 사용하는 이유는 상호 참조에 의해 프로토타입 체인이 새성되는 것을 방지하기 위해서이다. 이를 지키기 위해 프로토타입 체인은 단방향 링크트 리스트로 구현되어야 한다. 만약 __proto__ 접근자를 사용하지 않고도 프로토타입을 교체할 수 있다면 무한루프 발생 가능성이 높아진다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746533339851&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const parent = {};
const child = {};

// child 프로토타입을 parent로 설정
child.__proto__ = parent;

// parent 의 프로토타입을 child로 설정
parent.__proto__ = child; // 순환참조 에러 발생.&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;__proto__ 접근자 프로퍼티를 코드 내에서 사용하는 것은 권장되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 객체가 __proto__프로퍼티를 사용할 수 있는 것은 아니기 때문이다. Object.prototype을 상속받지 않는 객체를 생성할 수도 있기 때문에 __proto__ 접근자 프로퍼티를 사용할 수 없는 경우기 발생한다. 따라서 __proto__ 접근자 프로퍼티 대신 아래 메서드를 사용하는 것이 권장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Object.getPrototypeOf : 프로토타입의 참조를 취득하려는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Object.setPrototypeOf : 프로토타입을 교체하려는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746534935301&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Object.__proto__를 상속받지 못하는 경우
const obj = Object.create(null);

console.log(obj.__proto__) // undefined

console.log(Object.getPrototypeOf(obj)); // null&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746535658170&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const ob = {};
const parent = { x:1 };

Object.getPrototypeOf(obj); // obj.__proto__

Object.setPrototypeOf(obj, parent); // obj.__proto__ = parent ; 

console.log(obj.x); // 1&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Language/Javascript</category>
      <author>bright jazz music</author>
      <guid isPermaLink="true">https://catnails.tistory.com/573</guid>
      <comments>https://catnails.tistory.com/573#entry573comment</comments>
      <pubDate>Tue, 6 May 2025 20:49:17 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트의 실행 컨텍스트 2 (실행 컨텍스트 객체에 담기는 정보)</title>
      <link>https://catnails.tistory.com/564</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;실행 컨텍스트가 활성화될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는 데 필요한 환경 정보들을 수집해서 실행 컨텍스트 '객체'에 저장한다. 이러한 객체는 자바스크립트 엔진이 활용할 목적으로 생성하는 것이며, 개발자가 확인할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 컨텍스트 객체에는 아래와 같은 값들이 담기게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;VariableEnvironment&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LexicalEnvironment&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ThisBinding&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;1. VariableEnvironment&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;:&amp;nbsp; 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보 + '선언 시' LexicalEnvironment의 스냅샷이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LexicalEnvironment를 만든다. 이후에는 LexicalEnvironment를 주로 사용하게 된다. 어쨌든 두 값 모두 environmentRecord와 outerEnvironmentRecord로 구성돼 있다.&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. LexicalEnvironment&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;:&amp;nbsp; VariableEnvironment를 복사해서 사용하는 정보. environmentRecord와 outerEnvironmentRecord로 구성돼 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.3.1. environmentalHoisting&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트 내부 전체를 처음부터 끝까지 훑어나가며 '순서대로' 수집한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 선언한 함수가 있을 경우 그 함수 자체,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- var로 선언된 변수의 식별자 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;</description>
      <category>Language/Javascript</category>
      <author>bright jazz music</author>
      <guid isPermaLink="true">https://catnails.tistory.com/564</guid>
      <comments>https://catnails.tistory.com/564#entry564comment</comments>
      <pubDate>Wed, 12 Mar 2025 19:53:16 +0900</pubDate>
    </item>
    <item>
      <title>1. 젠킨스 컨테이너 구동 및 설정+필요 플러그인 설치</title>
      <link>https://catnails.tistory.com/562</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d46VcB/btsMHUyi1cT/CpkzlVcrAeGN00oK9s3aZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d46VcB/btsMHUyi1cT/CpkzlVcrAeGN00oK9s3aZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d46VcB/btsMHUyi1cT/CpkzlVcrAeGN00oK9s3aZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd46VcB%2FbtsMHUyi1cT%2FCpkzlVcrAeGN00oK9s3aZK%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;1336&quot; height=&quot;1334&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;1334&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;1. ports:&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;50000번 포트는 젠킨스 마스터 노드와 젠킨스 슬레이브 노드가 통신하는 포트이다. 비워 놓으면 포트포워딩이 설정되지 않기 때문에 명시적으로 기본 포트인 50000을 적어준다.&lt;/li&gt;
&lt;li&gt;8080포트는 젠킨스 웹콘솔에 접근하는 포트이다. 기본포트는 8080이지만 여기서는 9090으로 변경해서 적어줬다.&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;b&gt;2. volumes:&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 볼륨을 의미한다. 도커 컨테이너는 기본적으로 휘발성이기 때문에 컨테이너가 삭제되면 내부 데이터도 전부 삭제돼 버린다. 컨테이너가 삭제되더라도 데이터는 남겨 놓기 위해서 호스트의 공간을 도커 컨테이너에게 연결(마운트)해 놓는 것이다. 이 때 이 공간을 도커 볼륨이라고 하며, 도커 컨테이너는 이 공간을 마치 컨테이너 내부의 공간처럼 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.1 volume 1&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우, 도커 컨테이너 내부의 /var/jenkins_home 디렉토리가 호스트(도커 컨테이너가 구동되는 머신)의 /Users/catnail/jenkins-data디렉토리를 사용하게 된다. 호스트 머신의 경로는 임의의 경로이므로 자신의 원하는 디렉토리를 생성하거나 선택해줘도 된다. 참고로 jenkins_home은&amp;nbsp; 젠&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;킨스 컨테이너 내부에서 젠킨스가 모든 데이터와 설정을 저장하는 기본 디렉토리이다. 아래와 같은 중요한 데이터가 저장된다.&lt;/span&gt;&lt;/p&gt;
&lt;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;작업(Job) 설정 및 구성 정보&lt;/li&gt;
&lt;li&gt;빌드 히스토리 및 결과물&lt;/li&gt;
&lt;li&gt;사용자 인증 정보&lt;/li&gt;
&lt;li&gt;자격 증명(credentials) 정보&lt;/li&gt;
&lt;li&gt;워크스페이스 데이터&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.2 volume 2&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;호스트의 프로젝트가 위치한 디렉토리를를 /projects라는 이름으로 연결해주었다. 이 방법을 통해서 젠킨스가 호스트의 프로젝트 디렉토리에서 작업을 수행할 수 있다. 만약 이렇게 해주지 않는다면 기존 프로젝트를 위에서 마운트한 jenkins-data 프로젝트로 옮기거나 링크를 걸어줘야 한다. 나는 그 방법이 번거롭다고 생각했기 때문에 이렇게 프로젝트들이 위치한 디렉토리를 마운트 해준 것이다.&lt;/p&gt;
&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;br /&gt;&lt;b&gt;2.3. volume 3&lt;/b&gt;&lt;br /&gt;/var/run/docker.sock:&lt;br /&gt;이 파일은 Docker 데몬과 통신하기 위한 Unix 소켓 파일이다. 이 소켓 파일을 Jenkins 컨테이너에 마운트함으로써 Jenkins 컨테이너 내부에서 호스트 머신의 Docker 엔진을 직접 사용할 수 있게 된다. 이를 &quot;Docker-outside-of-Docker(DooD)&quot; 또는 &quot;Docker socket mounting&quot; 방식이라고도 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호스트의 Docker 소켓(/var/run/docker.sock)을 컨테이너에 마운트하는 방식&lt;/li&gt;
&lt;li&gt;컨테이너 내부에 Docker CLI만 설치됨&lt;/li&gt;
&lt;li&gt;실제로는 호스트의 Docker 엔진을 사용하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이&amp;nbsp;설정이&amp;nbsp;필요한&amp;nbsp;이유:&lt;br /&gt;1.&amp;nbsp;Jenkins에서&amp;nbsp;Docker&amp;nbsp;파이프라인을&amp;nbsp;통해&amp;nbsp;Docker&amp;nbsp;명령(빌드,&amp;nbsp;푸시,&amp;nbsp;실행&amp;nbsp;등)을&amp;nbsp;실행하기&amp;nbsp;위함&lt;br /&gt;2.&amp;nbsp;Jenkins&amp;nbsp;작업&amp;nbsp;내에서&amp;nbsp;Docker&amp;nbsp;이미지를&amp;nbsp;빌드하기&amp;nbsp;위함&lt;br /&gt;3.&amp;nbsp;컨테이너&amp;nbsp;내에&amp;nbsp;별도의&amp;nbsp;Docker&amp;nbsp;엔진을&amp;nbsp;설치하지&amp;nbsp;않고도&amp;nbsp;호스트의&amp;nbsp;Docker를&amp;nbsp;사용하기&amp;nbsp;위함&lt;br /&gt;4.&amp;nbsp;자원&amp;nbsp;효율성&amp;nbsp;측면에서&amp;nbsp;컨테이너&amp;nbsp;내&amp;nbsp;별도&amp;nbsp;Docker&amp;nbsp;엔진&amp;nbsp;실행보다&amp;nbsp;유리함&lt;br /&gt;&lt;br /&gt;호스트의 /var/run/docker.sock 파일을 컨테이너의 동일한 경로(/var/run/docker.sock)에 마운트하여 젠킨스가 Docker 명령어를 실행할 수 있게 한다. 이 설정이 없으면 Jenkins 컨테이너 내에서 Docker 명령어를 실행할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 도커 컨테이너가 호스트의 도커 엔진을 사용하기 때문에 중복 설치를 방지하고 자원을 절약하는 이점이 있다. 그러나 이 방법은 개별 컨테이너가 외부 환경에 종속되는 단점을 만들기도 한다. 따라서 개별 컨테이너의 완전한 격리를 원한다면 도커 컨테이너 내부에서 다시 도커 엔진을 설치하여 사용할 수도 있다. 이 후자의 방식을 Docker-in-Docker, DinD)라고 부른다. 여기서는 자원을 절약하기 위해 호스트의 도커 엔진을 사용하는&amp;nbsp; DooD 방식을 사용할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2.4 volume 4&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;호스트의 docker cli를 사용하기 위해 마운트&lt;/p&gt;
&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;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 구동 로그가 출력되는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741607306073&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-03-10 20:47:24 
2025-03-10 20:47:24 *************************************************************
2025-03-10 20:47:24 *************************************************************
2025-03-10 20:47:24 *************************************************************
2025-03-10 20:47:24 
2025-03-10 20:47:24 Jenkins initial setup is required. An admin user has been created and a password generated.
2025-03-10 20:47:24 Please use the following password to proceed to installation:
2025-03-10 20:47:24 
2025-03-10 20:47:24 2b4c769c9b5641f1b673075920c2391a
2025-03-10 20:47:24 
2025-03-10 20:47:24 This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
2025-03-10 20:47:24 
2025-03-10 20:47:24 *************************************************************
2025-03-10 20:47:24 *************************************************************
2025-03-10 20:47:24 *************************************************************
2025-03-10 20:47:24 
2025-03-10 20:47:29 2025-03-10 11:47:29.337+0000 [id=42]        INFO    jenkins.InitReactorRunner$1#onAttained: Completed initialization
2025-03-10 20:47:29 2025-03-10 11:47:29.361+0000 [id=25]        INFO    hudson.lifecycle.Lifecycle#onReady: Jenkins is fully up and running
2025-03-10 20:47:30 2025-03-10 11:47:30.888+0000 [id=60]        INFO    h.m.DownloadService$Downloadable#load: Obtained the updated data file for hudson.tasks.Maven.MavenInstaller
2025-03-10 20:47:30 2025-03-10 11:47:30.888+0000 [id=60]        INFO    hudson.util.Retrier#start: Performed the action check updates server successfully at the attempt #1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 최초 비밀번호 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서&amp;nbsp; http://localhost:9090으로 접근하면 웹콘솔 초기 설정 페이지가 뜬다. 최초 비밀번호를 붙여 넣기 해주자.&lt;/p&gt;
&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;2b4c769c9b5641f1b673075920c2391a&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;2208&quot; data-origin-height=&quot;1618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/13FdO/btsMGCkeg8o/i4pSAhYbvSaNnHb3Kk4M4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/13FdO/btsMGCkeg8o/i4pSAhYbvSaNnHb3Kk4M4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/13FdO/btsMGCkeg8o/i4pSAhYbvSaNnHb3Kk4M4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F13FdO%2FbtsMGCkeg8o%2Fi4pSAhYbvSaNnHb3Kk4M4K%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;2208&quot; height=&quot;1618&quot; data-origin-width=&quot;2208&quot; data-origin-height=&quot;1618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 추천 플러그인 설치&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 install suggested plugins 를 눌러 설치해줬다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2192&quot; data-origin-height=&quot;1636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnUU3C/btsMGPcFNCs/EoxmVh99KCfCVSwW79Yiwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnUU3C/btsMGPcFNCs/EoxmVh99KCfCVSwW79Yiwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnUU3C/btsMGPcFNCs/EoxmVh99KCfCVSwW79Yiwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnUU3C%2FbtsMGPcFNCs%2FEoxmVh99KCfCVSwW79Yiwk%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;2192&quot; height=&quot;1636&quot; data-origin-width=&quot;2192&quot; data-origin-height=&quot;1636&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;마운트 해줬던 도커 볼륨에 아래와 같은 파일들이 생겨나는 것을 볼 수 있다.&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;1034&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHWVLi/btsMGrb3miG/M8Ekh4DaWrYKBhvnCLwlvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHWVLi/btsMGrb3miG/M8Ekh4DaWrYKBhvnCLwlvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHWVLi/btsMGrb3miG/M8Ekh4DaWrYKBhvnCLwlvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHWVLi%2FbtsMGrb3miG%2FM8Ekh4DaWrYKBhvnCLwlvk%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;424&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;424&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;5. 최초 관리자 계정 생성 및 구성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 어드민 계정을 설정해준다. 만약 스킵하고 나중에 계정을 만들어 줄 수도 있지만, 그 전까지는 로그인 시마다 초기 비밀번호(2b4c769c9b5641f1b673075920c2391a)를 사용해야하므로 불편하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2154&quot; data-origin-height=&quot;1632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7nswn/btsMEQRP1ik/cxsyhmWAacVdXQCLPvlkKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7nswn/btsMEQRP1ik/cxsyhmWAacVdXQCLPvlkKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7nswn/btsMEQRP1ik/cxsyhmWAacVdXQCLPvlkKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7nswn%2FbtsMEQRP1ik%2FcxsyhmWAacVdXQCLPvlkKK%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;2154&quot; height=&quot;1632&quot; data-origin-width=&quot;2154&quot; data-origin-height=&quot;1632&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;생성하고 다음으로 넘어가면 아래와 같은 인스턴스 구성 페이지가 뜬다.&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;2184&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVkqQa/btsMFhIcwKJ/WtvVTWydeCPNEbfJ2fzn91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVkqQa/btsMFhIcwKJ/WtvVTWydeCPNEbfJ2fzn91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVkqQa/btsMFhIcwKJ/WtvVTWydeCPNEbfJ2fzn91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVkqQa%2FbtsMFhIcwKJ%2FWtvVTWydeCPNEbfJ2fzn91%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;2184&quot; height=&quot;928&quot; data-origin-width=&quot;2184&quot; data-origin-height=&quot;928&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;여기서 설정하는 &quot;Jenkins URL&quot;은 Jenkins가 외부 링크, 이메일 알림, PR 상태 업데이트 등을 생성할 때 사용된다.&amp;nbsp;현재 제안된 값은 http://localhost:9090/로 되어 있는데, 이는 현재 접속한 주소를 기반으로 자동 생성된 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 URL 설정을 건너뛰거나 변경하지 않고 기본값을 사용해도 Jenkins 기본 기능에는 문제가 없다. 하지만 아래와 같은 상황에서는 정확한 URL 설정이 중요하다.&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;GitHub, GitLab 등과 연동하여 PR 상태 업데이트를 보낼 때&lt;/li&gt;
&lt;li&gt;다른 사람들이 원격에서 Jenkins에 접근해야 할 때&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서만 개인적으로 사용할 예정이라면 그냥 기본값을 사용해도 무방하다. 이 설정은 나중에 Jenkins 관리 페이지에서 언제든지 변경할 수 있다. 즉,&amp;nbsp;설정을 건너뛰더라도 Jenkins의 핵심 기능(빌드, 배포 등)은 정상적으로 작동하지만 외부 시스템과의 연동이나 알림 기능에서 링크가 올바르게 생성되지 않는다는 것이다.&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;1160&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3LGZS/btsMFKbVIkL/6Vq9jJ5gtYJ1b9krp3Glt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3LGZS/btsMFKbVIkL/6Vq9jJ5gtYJ1b9krp3Glt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3LGZS/btsMFKbVIkL/6Vq9jJ5gtYJ1b9krp3Glt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3LGZS%2FbtsMFKbVIkL%2F6Vq9jJ5gtYJ1b9krp3Glt1%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;1160&quot; height=&quot;628&quot; data-origin-width=&quot;1160&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xSQYG/btsMGNy96qE/flUI6c3bUI7r4gFCokDPm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xSQYG/btsMGNy96qE/flUI6c3bUI7r4gFCokDPm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xSQYG/btsMGNy96qE/flUI6c3bUI7r4gFCokDPm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxSQYG%2FbtsMGNy96qE%2FflUI6c3bUI7r4gFCokDPm1%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;2856&quot; height=&quot;1334&quot; data-origin-width=&quot;2856&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. 필요 플러그인 설치. 도커 + ssh agent&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1176&quot; data-origin-height=&quot;1634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFrtFt/btsMHepOTJb/RMbgEw1GItZfyIAyR7k060/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFrtFt/btsMHepOTJb/RMbgEw1GItZfyIAyR7k060/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFrtFt/btsMHepOTJb/RMbgEw1GItZfyIAyR7k060/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFrtFt%2FbtsMHepOTJb%2FRMbgEw1GItZfyIAyR7k060%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;1176&quot; height=&quot;1634&quot; data-origin-width=&quot;1176&quot; data-origin-height=&quot;1634&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;Docker Pipeline, Docker Commons Plugin가 필요하다. 그냥 전체 패키지인 docker 플러그인을 설치했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2866&quot; data-origin-height=&quot;886&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qF2wL/btsMFuHh2iY/r0MjIZwYMmWaFouDBovwtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qF2wL/btsMFuHh2iY/r0MjIZwYMmWaFouDBovwtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qF2wL/btsMFuHh2iY/r0MjIZwYMmWaFouDBovwtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqF2wL%2FbtsMFuHh2iY%2Fr0MjIZwYMmWaFouDBovwtK%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;2866&quot; height=&quot;886&quot; data-origin-width=&quot;2866&quot; data-origin-height=&quot;886&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;1620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7TU0v/btsMHoeObuZ/xEuUB9m9ZtQa0yy9BC1gK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7TU0v/btsMHoeObuZ/xEuUB9m9ZtQa0yy9BC1gK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7TU0v/btsMHoeObuZ/xEuUB9m9ZtQa0yy9BC1gK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7TU0v%2FbtsMHoeObuZ%2FxEuUB9m9ZtQa0yy9BC1gK0%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;1632&quot; height=&quot;1620&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;1620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ssh agent 설치&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;2746&quot; data-origin-height=&quot;1048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddy8Ry/btsMENOdl8g/GJwlXpeM5BFeEk48cEUwLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddy8Ry/btsMENOdl8g/GJwlXpeM5BFeEk48cEUwLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddy8Ry/btsMENOdl8g/GJwlXpeM5BFeEk48cEUwLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fddy8Ry%2FbtsMENOdl8g%2FGJwlXpeM5BFeEk48cEUwLK%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;2746&quot; height=&quot;1048&quot; data-origin-width=&quot;2746&quot; data-origin-height=&quot;1048&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;왼쪽의 installed plugin에 들어가서 플러그인이 설치됐는지 확인해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;1248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d2OCnF/btsMGrwlxUD/BgtKn6eKErdKm5ptec6940/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d2OCnF/btsMGrwlxUD/BgtKn6eKErdKm5ptec6940/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d2OCnF/btsMGrwlxUD/BgtKn6eKErdKm5ptec6940/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd2OCnF%2FbtsMGrwlxUD%2FBgtKn6eKErdKm5ptec6940%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;1802&quot; height=&quot;1248&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;1248&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;1248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/musQ5/btsMF1EGVAU/IRiQ1NENTtDKpqztMmjRKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/musQ5/btsMF1EGVAU/IRiQ1NENTtDKpqztMmjRKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/musQ5/btsMF1EGVAU/IRiQ1NENTtDKpqztMmjRKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmusQ5%2FbtsMF1EGVAU%2FIRiQ1NENTtDKpqztMmjRKK%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;1802&quot; height=&quot;1248&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;1248&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;이것으로 기본적인 구성과 필요한 플러그인 설치는 마쳤다.&lt;/p&gt;</description>
      <category>OS 및 기타/docker &amp;amp; kubernetes</category>
      <author>bright jazz music</author>
      <guid isPermaLink="true">https://catnails.tistory.com/562</guid>
      <comments>https://catnails.tistory.com/562#entry562comment</comments>
      <pubDate>Mon, 10 Mar 2025 21:07:53 +0900</pubDate>
    </item>
    <item>
      <title>[nestjs] 인증 기능(auth) 작성하기</title>
      <link>https://catnails.tistory.com/558</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1. 인증기능 구현에 필요한 패키지 설치하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 설치한 패키지도 있을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1735205625750&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// JWT(JSON Web Token) 생성/검증을 위한 NestJS 모듈
pnpm add @nestjs/jwt 

// Passport.js를 NestJS에서 사용하기 위한 어댑터/래퍼
pnpm add @nestjs/passport 

// Node.js 인증 미들웨어 라이브러리
pnpm add passport 

// JWT 기반 인증을 위한 Passport 전략
# Authorization 헤더에서 JWT를 추출/검증
pnpm add passport-jwt 

// username/password 기반 인증을 위한 Passport 전략  
// 로그인 시점의 인증 처리
pnpm add passport-local 

// 패스워드 해싱을 위한 라이브러리
pnpm add bcrypt

?/ 각 라이브러리의 TypeScript 타입 정의들 (-D는 개발 의존성)
pnpm add -D @types/passport-jwt @types/passport-local @types/bcrypt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. auth.module.ts&lt;/p&gt;
&lt;pre id=&quot;code_1735206781716&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { MembersModule } from '../members/members.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';


@Module({
  imports: [
    MembersModule,
    PassportModule,
    JwtModule.register({
      // JWT 토큰을 서명(sign)하고 검증(verify)하는 데 사용되는 비밀 키
      // 이 키를 사용해 토큰이 서버에서 발급된 것이 맞는지, 중간에 변조되지 않았는지 확인
      secret: 'your-secret-key', // 실제 환경에서는 환경변수로 관리 필요
      signOptions: { expiresIn: '1h' },// 토큰 만료 시간
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Framework/NestJS</category>
      <author>bright jazz music</author>
      <guid isPermaLink="true">https://catnails.tistory.com/558</guid>
      <comments>https://catnails.tistory.com/558#entry558comment</comments>
      <pubDate>Thu, 26 Dec 2024 18:53:11 +0900</pubDate>
    </item>
    <item>
      <title>[nestjs] jest를 사용한 테스트 코드 작성(단위, e2e)</title>
      <link>https://catnails.tistory.com/557</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 단위 테스트&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1.&amp;nbsp; members.controller.specs.ts&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1735203796025&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/members/members.controller.spec.ts
// 회원 관리 컨트롤러 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { MembersController } from './members.controller';
import { MembersService } from './members.service';
import { AuthProvider } from '@common/enums';

describe('MembersController', () =&amp;gt; {
  let controller: MembersController;
  let service: MembersService;

  beforeEach(async () =&amp;gt; {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [MembersController],
      providers: [
        {
          provide: MembersService,
          useValue: {
            create: jest.fn(),
            findOne: jest.fn(),
            // ... 다른 메서드들
          },
        },
      ],
    }).compile();

    controller = module.get&amp;lt;MembersController&amp;gt;(MembersController);
    service = module.get&amp;lt;MembersService&amp;gt;(MembersService);
  });

  describe('create', () =&amp;gt; {
    it('should create a member', async () =&amp;gt; {
      const dto = {
        email: 'test@example.com',
        password: 'password123',
        termsAgreed: true,
        privacyAgreed: true,
        marketingAgreed: false,
        provider: AuthProvider.LOCAL,
      };

      await controller.create(dto);
      expect(service.create).toHaveBeenCalledWith(dto);
    });
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2.&amp;nbsp; members.service.specs.ts&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1735203825785&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/members/members.service.spec.ts
// 회원 관리 서비스 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { MembersService } from './members.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Member } from './entities/member.entity';
import { Repository } from 'typeorm';
import { CreateMemberDto } from './dto/create-member.dto';
import { UpdateMemberDto } from './dto/update-member.dto';
import { ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
import { AuthProvider, MemberStatus } from '@common/enums';
import { EmailService } from '@common/services/email.service';

describe('MembersService', () =&amp;gt; {
  let service: MembersService;
  let repository: Repository&amp;lt;Member&amp;gt;;
  let emailService: EmailService;

  const mockRepository = {
    findOne: jest.fn(),
    save: jest.fn(),
    softDelete: jest.fn(),
  };

  const mockEmailService = {
    send: jest.fn().mockResolvedValue(undefined),
  };

  beforeEach(async () =&amp;gt; {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        MembersService,
        {
          provide: getRepositoryToken(Member),
          useValue: mockRepository,
        },
        {
          provide: EmailService,
          useValue: mockEmailService,
        },
      ],
    }).compile();

    service = module.get&amp;lt;MembersService&amp;gt;(MembersService);
    repository = module.get&amp;lt;Repository&amp;lt;Member&amp;gt;&amp;gt;(getRepositoryToken(Member));
    emailService = module.get&amp;lt;EmailService&amp;gt;(EmailService);
  });

  describe('create', () =&amp;gt; {
    const createMemberDto: CreateMemberDto = {
      email: 'test@example.com',
      password: 'password123',
      provider: AuthProvider.LOCAL,
      termsAgreed: true,
      privacyAgreed: true,
      marketingAgreed: false
    };

    it('필수 약관 미동의 시 예외가 발생해야 함', async () =&amp;gt; {
      const invalidDto = {
        ...createMemberDto,
        termsAgreed: false
      };

      await expect(service.create(invalidDto)).rejects.toThrow(BadRequestException);
    });

    it('회원가입 성공 시 이메일이 발송되어야 함', async () =&amp;gt; {
      mockRepository.findOne.mockResolvedValue(null);
      const emailServiceSpy = jest.spyOn(emailService, 'send');

      await service.create(createMemberDto);

      expect(emailServiceSpy).toHaveBeenCalled();
    });

    it('비밀번호가 해시되어 저장되어야 함', async () =&amp;gt; {
      mockRepository.findOne.mockResolvedValue(null);
      const savedMember = jest.fn();
      
      mockRepository.save.mockImplementation((member) =&amp;gt; {
        savedMember(member);
        return member;
      });

      await service.create(createMemberDto);
      
      // save 호출 시 전달된 엔티티의 password를 검증
      expect(savedMember).toHaveBeenCalledWith(
        expect.objectContaining({
          password: expect.stringMatching(/^\$2[aby]\$\d{1,2}\$[./A-Za-z0-9]{53}$/)
        })
      );
    });

    it('약관 동의 시간이 기록되어야 함', async () =&amp;gt; {
      mockRepository.findOne.mockResolvedValue(null);
      mockRepository.save.mockImplementation(member =&amp;gt; member);

      const result = await service.create(createMemberDto);
      
      expect(result.termsAgreedAt).toBeInstanceOf(Date);
      expect(result.privacyAgreedAt).toBeInstanceOf(Date);
      expect(result.marketingAgreedAt).toBeUndefined(); // marketingAgreed가 false이므로
    });
  });

  describe('findOneByUuid', () =&amp;gt; {
    const uuid = 'test-uuid';
    const mockMember = {
      uuid,
      email: 'test@example.com',
      status: MemberStatus.ACTIVE,
    };

    it('UUID로 회원을 찾아야 함', async () =&amp;gt; {
      mockRepository.findOne.mockResolvedValue(mockMember);

      const result = await service.findOneByUuid(uuid);

      expect(result).toBeDefined();
      expect(result.uuid).toBe(uuid);
    });

    it('회원이 없으면 NotFoundException을 발생시켜야 함', async () =&amp;gt; {
      mockRepository.findOne.mockResolvedValue(null);

      await expect(service.findOneByUuid(uuid)).rejects.toThrow(
        NotFoundException,
      );
    });
  });

  describe('update', () =&amp;gt; {
    const uuid = 'test-uuid';
    const updateMemberDto: UpdateMemberDto = {
      // nickname: '새로운닉네임',
    };
    const mockMember = {
      uuid,
      email: 'test@example.com',
      nickname: '기존닉네임',
    };

    it('회원 정보를 업데이트해야 함', async () =&amp;gt; {
      mockRepository.findOne.mockResolvedValue(mockMember);
      mockRepository.save.mockResolvedValue({
        ...mockMember,
        ...updateMemberDto,
      });

      const result = await service.update(uuid, updateMemberDto);

      // expect(result.nickname).toBe(updateMemberDto.nickname);
      expect(mockRepository.save).toHaveBeenCalled();
    });

    it('존재하지 않는 회원이면 NotFoundException을 발생시켜야 함', async () =&amp;gt; {
      mockRepository.findOne.mockResolvedValue(null);

      await expect(service.update(uuid, updateMemberDto)).rejects.toThrow(
        NotFoundException,
      );
    });

    it('마케팅 동의 상태 변경 시 동의 시간이 기록되어야 함', async () =&amp;gt; {
      const uuid = 'test-uuid';
      const member = {
        uuid,
        marketingAgreed: false,
        marketingAgreedAt: null,
      };

      const updateDto = {
        marketingAgreed: true,
      };

      mockRepository.findOne.mockResolvedValue(member);
      mockRepository.save.mockImplementation(m =&amp;gt; ({
        ...m,
        marketingAgreedAt: expect.any(Date),
      }));

      const result = await service.update(uuid, updateDto);
      
      expect(result.marketingAgreed).toBe(true);
      expect(result.marketingAgreedAt).toBeDefined();
    });

    it('비밀번호 변경 시 해시되어 저장되어야 함', async () =&amp;gt; {
      const uuid = 'test-uuid';
      const member = {
        uuid,
        password: 'old-hashed-password',
      };

      const updateDto = {
        password: 'NewPassword123!',
      };

      mockRepository.findOne.mockResolvedValue(member);
      const savedMember = jest.fn();
      
      mockRepository.save.mockImplementation((member) =&amp;gt; {
        savedMember(member);
        return member;
      });

      await service.update(uuid, updateDto);
      
      // save 호출 시 전달된 엔티티의 password를 검증
      expect(savedMember).toHaveBeenCalledWith(
        expect.objectContaining({
          password: expect.stringMatching(/^\$2[aby]\$\d{1,2}\$[./A-Za-z0-9]{53}$/),
          passwordChangedAt: expect.any(Date)
        })
      );
    });
  });

  describe('softDelete', () =&amp;gt; {
    const uuid = 'test-uuid';

    it('회원을 소프트 삭제해야 함', async () =&amp;gt; {
      mockRepository.softDelete.mockResolvedValue({ affected: 1 });

      await service.softDelete(uuid);

      expect(mockRepository.softDelete).toHaveBeenCalledWith({ uuid });
    });

    it('존재하지 않는 회원이면 NotFoundException을 발생시켜야 함', async () =&amp;gt; {
      mockRepository.softDelete.mockResolvedValue({ affected: 0 });

      await expect(service.softDelete(uuid)).rejects.toThrow(NotFoundException);
    });
  });

  describe('email verification', () =&amp;gt; {
    it('이메일 인증 토큰이 유효하면 회원 상태가 ACTIVE로 변경되어야 함', async () =&amp;gt; {
      const token = 'valid-token';
      const member = {
        verificationToken: token,
        status: MemberStatus.PENDING,
        verificationTokenExpiresAt: new Date(Date.now() + 3600000),
      };

      mockRepository.findOne.mockResolvedValue(member);
      const result = await service.verifyEmail(token);

      expect(result.status).toBe(MemberStatus.ACTIVE);
      expect(result.emailVerified).toBe(true);
    });

    it('만료된 토큰으로 인증 시 새로운 토큰이 발급되어야 함', async () =&amp;gt; {
      const token = 'expired-token';
      const member = {
        verificationToken: token,
        status: MemberStatus.PENDING,
        verificationTokenExpiresAt: new Date(Date.now() - 3600000),
      };

      mockRepository.findOne.mockResolvedValue(member);
      const emailServiceSpy = jest.spyOn(emailService, 'send');

      await expect(service.verifyEmail(token)).rejects.toThrow(BadRequestException);
      expect(emailServiceSpy).toHaveBeenCalled();
    });

    it('잘못된 토큰으로 인증 시 NotFoundException이 발생해야 함', async () =&amp;gt; {
      mockRepository.findOne.mockResolvedValue(null);

      await expect(service.verifyEmail('invalid-token')).rejects.toThrow(NotFoundException);
    });

    it('이미 인증된 회원은 BadRequestException이 발생해야 함', async () =&amp;gt; {
      const token = 'valid-token';
      const member = {
        verificationToken: token,
        status: MemberStatus.ACTIVE,
        emailVerified: true,
        verificationTokenExpiresAt: new Date(Date.now() + 3600000),
      };

      mockRepository.findOne.mockResolvedValue(member);

      await expect(service.verifyEmail(token)).rejects.toThrow(BadRequestException);
    });
  });

  describe('password security', () =&amp;gt; {
    it('비밀번호 변경 시 토큰 버전이 증가해야 함', async () =&amp;gt; {
      const uuid = 'test-uuid';
      const member = {
        uuid,
        tokenVersion: 1,
      };

      mockRepository.findOne.mockResolvedValue(member);
      await service.update(uuid, { password: 'newPassword123!' });

      expect(mockRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          tokenVersion: 2,
        }),
      );
    });
  });

  describe('findOneByEmailWithPassword', () =&amp;gt; {
    it('이메일로 회원을 찾을 때 필요한 필드만 조회해야 함', async () =&amp;gt; {
      const email = 'test@example.com';
      mockRepository.findOne.mockResolvedValue({
        id: 1,
        uuid: 'test-uuid',
        email,
        status: MemberStatus.ACTIVE,
        loginAttempts: 0,
      });

      const result = await service.findOneByEmailWithPassword(email);
      
      expect(result).toHaveProperty('id');
      expect(result).toHaveProperty('uuid');
      expect(result).toHaveProperty('email');
      expect(result).toHaveProperty('status');
      expect(result).toHaveProperty('loginAttempts');
      expect(result).not.toHaveProperty('password'); // 비밀번호는 기본적으로 조회되지 않아야 함
    });
  });
});&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;2. e2e 테스트 디렉토리 설정 및 전체 테스트 코드 작성&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1. 테스트 디렉토리 설정 파일 작성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 디렉토리(프로젝트 디렉토리, src의 상위 디렉토리)에 존재하는 test디렉토리에 jest-e2e.json 파일을 만들고 아래의 내용을 적어준다. json파일은 내외로 주석을 달면 에러가 발생하기 때문에 여기서는 달지 않았다.&lt;/p&gt;
&lt;pre id=&quot;code_1735204092052&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;moduleFileExtensions&quot;: [&quot;js&quot;, &quot;json&quot;, &quot;ts&quot;],
  &quot;rootDir&quot;: &quot;.&quot;,
  &quot;testEnvironment&quot;: &quot;node&quot;,
  &quot;testRegex&quot;: &quot;.e2e-spec.ts$&quot;,
  &quot;transform&quot;: {
    &quot;^.+\\.(t|j)s$&quot;: &quot;ts-jest&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2 애플리케이션 전체 테스트 코드 작성&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1735204163927&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// test/app.e2e-spec.ts
// 애플리케이션 전체 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () =&amp;gt; {
  let app: INestApplication;

  beforeEach(async () =&amp;gt; {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  afterAll(async () =&amp;gt; {
    await app.close();
  });

  it('/ (GET)', () =&amp;gt; {
    return request(app.getHttpServer())
      .get('/')
      .expect(200);
  });
});&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;3. members의 e2e 테스트 작성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test/e2e/members 디렉토리 만들고 아래 파일 작성&lt;/p&gt;
&lt;pre id=&quot;code_1735204338370&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// test/e2e/members/members.e2e-spec.ts
// 회원 관리 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../../../src/app.module';

describe('MembersController (e2e)', () =&amp;gt; {
  let app: INestApplication;

  beforeEach(async () =&amp;gt; {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/members (POST)', () =&amp;gt; {
    return request(app.getHttpServer())
      .post('/members')
      .send({
        email: 'test@example.com',
        password: 'password123',
        termsAgreed: true,
        privacyAgreed: true,
        marketingAgreed: false,
      })
      .expect(201)
      .expect((res) =&amp;gt; {
        expect(res.body.email).toBe('test@example.com');
        expect(res.body.uuid).toBeDefined();
      });
  });
});&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Framework/NestJS</category>
      <author>bright jazz music</author>
      <guid isPermaLink="true">https://catnails.tistory.com/557</guid>
      <comments>https://catnails.tistory.com/557#entry557comment</comments>
      <pubDate>Thu, 26 Dec 2024 18:13:00 +0900</pubDate>
    </item>
  </channel>
</rss>