<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발 창고</title>
    <link>https://devpluto.tistory.com/</link>
    <description>안녕하세요, 개발자 플루토입니다.</description>
    <language>ko</language>
    <pubDate>Tue, 26 May 2026 19:59:14 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>pIutos</managingEditor>
    <image>
      <title>개발 창고</title>
      <url>https://tistory1.daumcdn.net/tistory/5464125/attach/ec32a2aee62d4169a347f3d63d23e721</url>
      <link>https://devpluto.tistory.com</link>
    </image>
    <item>
      <title>[토스 PO Session] 지속 가능한 성장을 만드는 방법</title>
      <link>https://devpluto.tistory.com/entry/%ED%86%A0%EC%8A%A4-PO-Session-%EC%A7%80%EC%86%8D-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%84%B1%EC%9E%A5%EC%9D%84-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;바이럴 성장 만들기 (Designing Viral Growth)&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Virality = Payload x Conversion Rate x Frequency&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pay Load
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;바이럴 루프 한번에 &lt;b&gt;몇 명&lt;/b&gt; 에게 그 메세지가 도달하는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Frequency
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 바퀴 안에서 유저가 그 메세지를 얼마나 겪게 되냐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Conversion Rate
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그 메세지를 한번 봤을 때 &lt;b&gt;신규 유저로 전환되는 비율&lt;/b&gt;이 어떻게 되나?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex. Facebook의 경우 처음 회원가입 때 다른 소셜 서비스의 친구들에게 초대기능 =&amp;gt; Payload: High, 나머지는 low(1대 다, 첫 회원가입시 일회성)&lt;br /&gt;Paypal의 경우 친구 초대시 1만원 지급 =&amp;gt; Conversion Rate: Very High, 나머지는 low(일회성, 1대1)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Growth는 MAU가 아니라 CC를 높이는 것&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 목표의 차이점&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;지속가능하지 않은 것은 장기적으로 무의미함&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지속가능하지 않은 것들의 예시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로모션 마케팅 (일시적 마케팅)&lt;/li&gt;
&lt;li&gt;다크 패턴 푸시&lt;/li&gt;
&lt;li&gt;한동안만 가능한 회원가입 퍼널에서의 Tweak&lt;/li&gt;
&lt;li&gt;리텐션이 보장되지 않은 Paid Marketing&lt;/li&gt;
&lt;li&gt;법률적 혹은 PR 이슈로 인해 결국엔 문제가 될 제품 경험이나 퍼널&lt;/li&gt;
&lt;li&gt;잠시만 노출할 예정인 홈 화면 인텔리전스 (ex. toss 메인 탭)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼 어떤 CC 값을 목표로 하는게 맞을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; Usecase의&lt;br /&gt;a. Broadness&lt;br /&gt;b. Frequency&lt;br /&gt;-&amp;gt; Online Penetration (온라인 전환)&lt;br /&gt;-&amp;gt; Competition Divide Opportunities (해당 시장에서 나눠먹기: 초기에 빠르게 선점하는것이 중요)&lt;br /&gt;-&amp;gt; Maximum C.C (=&amp;gt; 당신 회사의 벨류에이션의 최대값 - 이 이상을 넘어갈 수 없음)&lt;br /&gt;-&amp;gt; Current MAU&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;온라인 서비스라면 Frequency는 월 3~4번이 임계점. 그 이하면 가능성이 없다&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CC에게 가장 큰 영향을 주는 것은 어떤 지표들일까?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Carrying Cpacity = # Of New Daily Customers / % Customers you Lost Each Day&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요도순&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Churn&lt;/li&gt;
&lt;li&gt;Retention&lt;/li&gt;
&lt;li&gt;Activation&lt;/li&gt;
&lt;li&gt;Acquisition&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;: 많은 기업들이 반대로 보고있음..!&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cohort Retention Plateau와 WAU 대비 MAU 비율은 차이가 있을까?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;-&amp;gt; 전혀 다른 지표이고, Cohort Retention Plateau를 봐야한다. 매주 일으키는 변화가 좋은 쪽으로 가는지 안좋은 쪽으로 가는지는 WAU/MAU로는 알 수 없음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하고 있는 일이 긍정적인 변화를 주는지 매주 어떻게 알 수 있을까?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;7d Trailing CC&lt;/li&gt;
&lt;li&gt;30d Trailing CC&lt;br /&gt;를 비교하며 추적하면 매주 나의 행동이 미치는 영향을 이해할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30d는 얼마나 빨리 영향을 받냐는 볼 수 없음. 천천히 올라가기 때문.&lt;br /&gt;그렇다면 7d를 보면 매일매일 행동에 따라 1/7로 올라가기 때문에 가파르게 올라감&lt;br /&gt;(7d가 먼저 반응하고, 30d가 따라감)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 요인이 있을 수 있기에 30d도 보조로 같이 보는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;악영향을 끼치면 반대로 내려가겠지&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제품 구조&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Top Funnel (총 노출 규모 - 마케팅, organic, referral 등)&lt;/li&gt;
&lt;li&gt;Wow Factor (입소문의 영향, 바이럴)&lt;/li&gt;
&lt;li&gt;Recurring Value (a.k.a Retention) (이 제품을 계속 사용할 이유 등, 입소문과 상관없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Wow Factor와 Recurring Value는 상관관계가 없음. 하나가 높더라도 다른 하나는 낮을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지의 곱이 제품이 일으키는 사회적 Impact의 총량&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;영상 링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/4gjRPJZk2us&quot;&gt;https://youtu.be/4gjRPJZk2us&lt;/a&gt;&lt;/p&gt;</description>
      <category>etc</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/46</guid>
      <comments>https://devpluto.tistory.com/entry/%ED%86%A0%EC%8A%A4-PO-Session-%EC%A7%80%EC%86%8D-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%84%B1%EC%9E%A5%EC%9D%84-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%B0%A9%EB%B2%95#entry46comment</comments>
      <pubDate>Thu, 31 Jul 2025 06:26:15 +0900</pubDate>
    </item>
    <item>
      <title>Github action으로 npm package 배포 자동화하기</title>
      <link>https://devpluto.tistory.com/entry/Github-action%EC%9C%BC%EB%A1%9C-npm-package-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 필자가 배포한 &lt;a href=&quot;https://github.com/eunbae0/tiny-equal&quot;&gt;tiny-eqaul&lt;/a&gt; 패키지를 npm에 배포하는 과정을 자동화하는 github workflow를 쓰게된 계기와, 코드를 공유하고자 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배포 자동화를 도입하게 된 계기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm 패키지의 새 업데이트를 배포하기 위해서는 다음과 같은 과정을 거쳐야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;package.json&lt;/code&gt;의 version을 업데이트한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;npm publish&lt;/code&gt;로 npm에 해당 version의 업데이트를 배포한다.&lt;/li&gt;
&lt;li&gt;github release에 소스 코드 zip 파일을 업로드 후, 업데이트 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 업데이트마다 반복하게 되면 번거롭기 때문에, 이 과정을 한번에 수행할 수 있는 workflow를 추가하여 github action을 통해 배포 자동화를 진행하게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;workflow file 작성하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트에 &lt;code&gt;.github/workflows&lt;/code&gt; 폴더를 생성한 다음, yaml 파일을 생성한다. 필자는 &lt;code&gt;deploy_and_release_package.yaml&lt;/code&gt; 이름으로 파일을 생성했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;trigger 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 여러 명에서 작업한다면 예를 들어 git flow를 이용하는 경우, release branch에 PR을 올리면 trigger시켜 배포하도록 할 수 있다.&lt;br /&gt;하지만 해당 패키지는 필자 혼자 작업한 패키지이므로, &lt;code&gt;workflow_dispatch&lt;/code&gt;를 이용해서 trigger하면 actions 페이지에서 수동으로 workflow를 trigger 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;on:
  workflow_dispatch:
    inputs:
      tags:
        description: 'Semantic Tag'     
        required: true
        default: 'v1.0.0'
        type: string # 'v[0-9]+.[0-9]+.[0-9]+'
      release_body:
        description: 'Release body'
        default: ''
        type: string&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;input에 sementic tag 버전과 release body, 즉 해당 release에 대한 설명을 추가할 수 있도록 했다. 그러면 아래와 같이 actions 탭에서 workflow를 선택하여 직접 trigger 시킬 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1842&quot; data-origin-height=&quot;1030&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JzL9j/btsOPCpiC75/SLQK7KsDa2BlFqetzd8ClK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JzL9j/btsOPCpiC75/SLQK7KsDa2BlFqetzd8ClK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JzL9j/btsOPCpiC75/SLQK7KsDa2BlFqetzd8ClK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJzL9j%2FbtsOPCpiC75%2FSLQK7KsDa2BlFqetzd8ClK%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;1842&quot; height=&quot;1030&quot; data-origin-width=&quot;1842&quot; data-origin-height=&quot;1030&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;actions build&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 action 환경에서 node 환경 설정을 하고, &lt;code&gt;pnpm build&lt;/code&gt;를 통해 패키지를 빌드하여 asset을 만들도록 한다.&lt;br /&gt;이 때 주의할 점은 &lt;code&gt;permissions&lt;/code&gt;에 actions, contents에 쓰기 권한 (write)를 줘야한다. 그렇게 해야지 해당 action에서 파일을 업로드 하고, commit을 업데이트 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;jobs:
  publish_npm:
    name: Publishing to NPM

    runs-on: ubuntu-latest

    permissions:
      actions: write
      contents: write
      id-token: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        name: Install pnpm
        with:
          run_install: false

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: latest
          cache: 'pnpm'
          registry-url: https://registry.npmjs.org/

      - name: Install dependencies
        run: pnpm install

      - name: Build dist files
        run: pnpm run build&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 앞서 말한 3가지 문제점을 차례 차례 구현해보겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. package.json의 버전 업데이트하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/ramonpaolo/bump-version&quot;&gt;bump-version&lt;/a&gt;을 사용하면 package.json의 버전을 수동으로 업데이트 할 수 있다. 해당 action을 사용해서 package.json의 버전을 앞서 받은 &lt;code&gt;inputs.tag&lt;/code&gt;와 함께 업데이트 시킨다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;  # Deploy package to NPM
      - name: Bump Version of package.json
        uses: ramonpaolo/bump-version@v2.3.1
        with:
          tag: ${{ inputs.tags }}
          commit: true
          branch_to_push: 'main'&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. npm에 패키지 publish 하기&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;# ...
- name: Publish package to NPM
  run: npm publish --provenance --access public
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 NPM_TOKEN을 key로 사용했고, npm에서 발급받은 secret key를 repository에 등록해야한다. 등록하는 방법은 &lt;a href=&quot;https://funveloper.tistory.com/204&quot;&gt;해당 블로그 문서&lt;/a&gt;에 잘 정리 되어있으니 참고 바란다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. github release에 소스 코드 assets을 등록하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 github release를 생성하는 &lt;a href=&quot;https://github.com/actions/create-release&quot;&gt;actions/create-release&lt;/a&gt; action을 이용했다.&lt;br /&gt;생성할 때, body에 앞서 받은 &lt;code&gt;inputs.release_body&lt;/code&gt;를 추가하여 업데이트 사항을 추가할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;  # Release package to Github
  - name: Create release
    id: create_release
    uses: actions/create-release@v1
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    with: 
      tag_name: ${{ inputs.tags }}
      release_name: ${{ inputs.tags }}
      body: ${{ inputs.release_body }}
      draft: false
      prerelease: false&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;release assets을 추가하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;./dist/index.js&lt;/code&gt;폴더 경로에 있는 index.js를 actions/upload-release-asset@v1 action을 이용해 등록한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;  - name: Upload Release Assets
    id: upload_release_assets
    uses: actions/upload-release-asset@v1
    env: 
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    with:
      upload_url: ${{ steps.create_release.outputs.upload_url }}
      asset_path: ./dist/index.js
      asset_name: tiny_equal.js
      asset_content_type: application/javascript&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 github workflow를 이용해 npm 패키지를 배포하는 것을 자동화 하는 과정을 작성해 보았다.&lt;br /&gt;궁금한 점이 있다면 댓글로 남기거나, &lt;a href=&quot;https://docs.github.com/ko/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry&quot;&gt;github workflow 공식 문서&lt;/a&gt;를 참고하길 바란다.&lt;/p&gt;</description>
      <category>etc</category>
      <category>Github Actions</category>
      <category>GitHub workflow</category>
      <category>npm package</category>
      <category>npm 패키지 배포</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/45</guid>
      <comments>https://devpluto.tistory.com/entry/Github-action%EC%9C%BC%EB%A1%9C-npm-package-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%98%EA%B8%B0#entry45comment</comments>
      <pubDate>Tue, 24 Jun 2025 17:55:35 +0900</pubDate>
    </item>
    <item>
      <title>[NPM] Chalk 패키지 살펴보기</title>
      <link>https://devpluto.tistory.com/entry/NPM-%ED%8C%A8%ED%82%A4%EC%A7%80-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0-1-Chalk</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;chalk_logo.svg&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQ6PiX/btsJSbXRvel/i4tjkhe69S81lu1luViy8k/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQ6PiX/btsJSbXRvel/i4tjkhe69S81lu1luViy8k/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQ6PiX/btsJSbXRvel/i4tjkhe69S81lu1luViy8k/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQ6PiX%2FbtsJSbXRvel%2Fi4tjkhe69S81lu1luViy8k%2Ftfile.svg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;230&quot; data-filename=&quot;chalk_logo.svg&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;230&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;chalk 모듈은 터미널 콘솔에 찍히는 로그에 쉽게 색상등의 스타일링을 할 수 있도록 도와주는 패키지이다.&lt;/p&gt;
&lt;pre id=&quot;code_1727768402020&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import chalk from 'chalk';

const log = console.log;

// Combine styled and normal strings
log(chalk.blue('Hello') + ' World' + chalk.red('!'));

// Compose multiple styles using the chainable API
log(chalk.blue.bgRed.bold('Hello world!'));

// Pass in multiple arguments
log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));&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;chalk 패키지는 어떻게 스타일링을 수행하는지 소스 코드를 살펴보자. 중심 동작 설명에 불필요한 코드는 제거하여 설명하겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;살펴보기 전에..&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 스타일링을 하기 위해서는 &lt;a href=&quot;https://en.wikipedia.org/wiki/ANSI_escape_code&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ANSI escape code&lt;/a&gt;를 사용한다. 예를들어 아래와 같이 빨강색에 해당하는 31을 포함한 ANSI code를 원하는 문자열 앞 뒤로 붙여주게 된다면 색이 입혀 출력된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;chalk_console.png&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IQyTW/btsJRbYy3NJ/hkOKWNp1GA3JZIS6zXH09K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IQyTW/btsJRbYy3NJ/hkOKWNp1GA3JZIS6zXH09K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IQyTW/btsJRbYy3NJ/hkOKWNp1GA3JZIS6zXH09K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIQyTW%2FbtsJRbYy3NJ%2FhkOKWNp1GA3JZIS6zXH09K%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;950&quot; height=&quot;106&quot; data-filename=&quot;chalk_console.png&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소스 코드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;package.json&lt;/h3&gt;
&lt;pre id=&quot;code_1727768483361&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;type&quot;: &quot;module&quot;,
&quot;main&quot;: &quot;./source/index.js&quot;,
&quot;exports&quot;: &quot;./source/index.js&quot;,
&quot;imports&quot;: {
  &quot;#ansi-styles&quot;: &quot;./source/vendor/ansi-styles/index.js&quot;,
  &quot;#supports-color&quot;: {
    &quot;node&quot;: &quot;./source/vendor/supports-color/index.js&quot;,
    &quot;default&quot;: &quot;./source/vendor/supports-color/browser.js&quot;
  }
},
&quot;types&quot;: &quot;./source/index.d.ts&quot;,
&quot;scripts&quot;: {
  &quot;test&quot;: &quot;xo &amp;amp;&amp;amp; c8 ava &amp;amp;&amp;amp; tsd&quot;,
  &quot;bench&quot;: &quot;matcha benchmark.js&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 패키지의 package.json부터 살펴보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;type은 module로 ESM 패키지이다. chalk 4까지는 CJS였으나, 버전 5부터는 ESM을 지원한다.&lt;/li&gt;
&lt;li&gt;따로 번들링 도구를 이용하여 빌드하지는 않으며, index.d.ts 파일을 직접 작성하여 export 해주고 있다.&lt;/li&gt;
&lt;li&gt;script로 testing과 benchmarking을 할 수 있으며, 해당 패키지의 testing에 대해선 글 마지막 단락에 설명하겠다.&lt;/li&gt;
&lt;li&gt;한가지 살펴볼만한 것은 &lt;a href=&quot;https://nodejs.org/api/packages.html#subpath-imports&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Subpath imports&lt;/a&gt;를 사용하고 있다는 점이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1727768553615&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ansiStyles from '#ansi-styles';
import supportsColor from '#supports-color';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subpath imports를 이용하면 위와 같이 패키지 내에서 간결하게 모듈을 import 할 수 있다는 이점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typescript의 alias 설정과 비슷하며, 간결하게 package.json에서 지정할 수 있다는 장점이 있다. &lt;span data-token-index=&quot;1&quot;&gt;참고로 &lt;a href=&quot;https://www.webpro.nl/articles/using-subpath-imports-and-path-aliases&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;typescript 5.4&lt;/a&gt;&lt;/span&gt;&lt;a href=&quot;https://www.webpro.nl/articles/using-subpath-imports-and-path-aliases&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;부터 Subpath imports를 공식 지원&lt;/a&gt;하므로 한번 쯤 이용해 봐도 좋을 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소스 코드 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chalk에서 가장 중요한 로직은 크게 source/index.js와 source/vendor/ansi-styles/index.js 두가지 파일에 존재한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bySSBQ/btsJSF42PrE/3gEYYtAh9lHQOoTlvbyetk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bySSBQ/btsJSF42PrE/3gEYYtAh9lHQOoTlvbyetk/img.png&quot; width=&quot;304&quot; height=&quot;408&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;540&quot; data-filename=&quot;chalk_tree.png&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;32.34&quot; style=&quot;width: 31.9597%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bySSBQ/btsJSF42PrE/3gEYYtAh9lHQOoTlvbyetk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbySSBQ%2FbtsJSF42PrE%2F3gEYYtAh9lHQOoTlvbyetk%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;402&quot; height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be6MwM/btsJSgdB5vY/7prOy81rnnOqG6qGIGifaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be6MwM/btsJSgdB5vY/7prOy81rnnOqG6qGIGifaK/img.png&quot; width=&quot;520&quot; height=&quot;334&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;744&quot; data-filename=&quot;chalk_module.png&quot; data-is-animation=&quot;false&quot; style=&quot;width: 66.8775%;&quot; data-widthpercent=&quot;67.66&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be6MwM/btsJSgdB5vY/7prOy81rnnOqG6qGIGifaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe6MwM%2FbtsJSgdB5vY%2F7prOy81rnnOqG6qGIGifaK%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;1159&quot; height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chalk에서 각 스타일을 모으고, 적용하며 중첩 스타일링을 수행하는 동작은 위 다이어그램에 표기된 모듈과 함수를 통해 이루어진다. 우선 anti-styles 파일부터 살펴보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;anti-styles&lt;/h4&gt;
&lt;pre id=&quot;code_1727768705593&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const styles = {
  modifier: {
    reset: [0, 0],
    bold: [1, 22],
    dim: [2, 22],
    italic: [3, 23],
    underline: [4, 24],
    // ...
  },
  color: {
    black: [30, 39],
    red: [31, 39],
    green: [32, 39],
    yellow: [33, 39],
    // ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일에는 위와 같이 styles Object에 &lt;a href=&quot;https://en.wikipedia.org/wiki/ANSI_escape_code#Colors&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ansi escape code에 맞는 color code&lt;/a&gt;가 정의 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 정의된 객체는 &lt;b&gt;assembleStyles()&lt;/b&gt;&amp;nbsp;함수가 각 property를 가공해서 등록하는 과정을 거친다. 각 스타일의 ANSI code에 해당하는 { open, close } 객체를 생성하여 각 styleName에 매핑한 다음, style을 export한다.&lt;/p&gt;
&lt;pre id=&quot;code_1727768760826&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function assembleStyles() {
  for (const [groupName, group] of Object.entries(styles)) {
    for (const [styleName, style] of Object.entries(group)) {
      styles[styleName] = {
        open: `\u001B[${style[0]}m`,
        close: `\u001B[${style[1]}m`,
      };

      group[styleName] = styles[styleName];
    }

    Object.defineProperty(styles, groupName, {
      value: group,
      enumerable: false,
    });
}
// ...
	
const ansiStyles = assembleStyles();

export default ansiStyles;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;index.js&lt;/h4&gt;
&lt;pre id=&quot;code_1727768845129&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ansiStyles from '#ansi-styles';

for (const [styleName, style] of Object.entries(ansiStyles)) {
  styles[styleName] = {
    get() {
      const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
      Object.defineProperty(this, styleName, {value: builder});
      return builder;
    },
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index.js에서는 ansiStyles import 한 다음, 반복문을 통해 styleName 각각에 &lt;b&gt;createBuilder()&lt;/b&gt;를 이용하여 builder를 생성한다. 각 style의 open, close 값을 통해 styler를 생성한 다음 &lt;b&gt;createStyler()&lt;/b&gt;를 통해 builder에 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 createStyler()의 3번째 인자에 &lt;b&gt;this[STYLER]&lt;/b&gt;를 받는데, 잘 기억해 두길 바란다.&lt;/p&gt;
&lt;pre id=&quot;code_1727768879930&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const createBuilder = (self, _styler, _isEmpty) =&amp;gt; {
  const builder = (...arguments_) =&amp;gt; applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' '));	
  // ...
  builder[GENERATOR] = self;
  builder[STYLER] = _styler;
  builder[IS_EMPTY] = _isEmpty;

  return builder;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createBuilder의 동작을 살펴보면, argument의 개수에 따라 applyStyle()함수를 적용한다. arguments_를 받아 색을 입힐 string을 array로 가져와(구조분해할당) 여러개라면 Array.join()문으로 합쳐주는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;b&gt;builder[STYLER] = _styler;&lt;/b&gt; 를 통해 인수로 전달받은 _styler를 builder에 등록하는 과정을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정으로 만약 체이닝을 통해 새로운 중첩 스타일을 등록한다면, 이 경우 앞서 createStyler()에 제공된 styler의 parent에 등록되게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1727768961925&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const createStyler = (open, close, parent) =&amp;gt; {
  let openAll;
  let closeAll;
  if (parent === undefined) {
    openAll = open;
    closeAll = close;
  } else {
    openAll = parent.openAll + open;
    closeAll = close + parent.closeAll;
  }

  return {
    open,
    close,
    openAll,
    closeAll,
    parent,
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;createStyler()&lt;/b&gt;는 parent 여부에 따라 다른 방법으로 openAll, closeAll을 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1727769036662&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const applyStyle = (self, string) =&amp;gt; {
  // ...
  let styler = self[STYLER];

  if (styler === undefined) {
    return string;
  }

  const {openAll, closeAll} = styler;
  if (string.includes('\u001B')) {
    while (styler !== undefined) {
      string = stringReplaceAll(string, styler.close, styler.open);

      styler = styler.parent;
    }
  }

  // for ES2015 template literal
  const lfIndex = string.indexOf('\n');
  if (lfIndex !== -1) {
    string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
  }

  return openAll + string + closeAll;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;builder는 applyStyle()을 통해 styler를 적용한다. 해당 함수를 살펴보면 styler가 중첩되지 않은 경우 바로 string을 반환하는 모습을 확인할 수 있다. 그리고 &lt;b&gt;if (string.includes('\u001B'))&lt;/b&gt; 분기를 통해 중첩 여부를 파악하고, while 반복문을 통해 중첩된 스타일을 적용해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 template literal로 작성된 string이 들어온다면, ('\n') 여부를 통해 따로 처리를 해준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;styles Object 등록&lt;/h4&gt;
&lt;pre id=&quot;code_1727769319023&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const chalkFactory = options =&amp;gt; {
  const chalk = (...strings) =&amp;gt; strings.join(' ');
  applyOptions(chalk, options);

  Object.setPrototypeOf(chalk, createChalk.prototype);

  return chalk;
};

function createChalk(options) {
  return chalkFactory(options);
}

Object.setPrototypeOf(createChalk.prototype, Function.prototype);
// createChalk의 prototype을 Function으로 변경

// 앞서 builder를 styles에 등록하는 로직

Object.defineProperties(createChalk.prototype, styles);


const chalk = createChalk();

// ...

export default chalk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로, style을 만드는 builder 로직이 담긴 styles 객체를 createChalk prototype에 등록해준 다음 default export한다. 따라서 앞선 가장 처음의 예제와 같이 스타일을 적용하고, 중첩해서도 적용할 수 있게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;+ RGB Colors 기능&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chalk는 기본적인 style 이외에도 RGB 컬러를 직접 입력하여 사용할 수 있도록 하는 기능도 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1727769128027&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Use RGB colors in terminal emulators that support it.
log(chalk.rgb(123, 45, 67).underline('Underlined reddish color'));
log(chalk.hex('#DEADED').bold('Bold gray!'));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능은 어떻게 구현되어있는지 간단하게 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1727769174909&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// index.js

const usedModels = ['rgb', 'hex', 'ansi256'];

for (const model of usedModels) {
  styles[model] = {
    get() {
      const {level} = this;
      return function (...arguments_) {
        const styler = createStyler(getModelAnsi(model, levelMapping[level], 'color', ...arguments_), ansiStyles.color.close, this[STYLER]);
        return createBuilder(this, styler, this[IS_EMPTY]);
      };
    },
  };

  const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1);
  styles[bgModel] = {
    get() {
      const {level} = this;
      return function (...arguments_) {
        const styler = createStyler(getModelAnsi(model, levelMapping[level], 'bgColor', ...arguments_), ansiStyles.bgColor.close, this[STYLER]);
        return createBuilder(this, styler, this[IS_EMPTY]);
      };
    },
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 비슷한 방식으로 rgb, hex, ansi256를 styles Object에 builder를 등록해준다. 추가된 부분은 this.level를 이용하여 getModelAnsi() 함수를 통해 ANSI code로 변환한다는 점이다. 자세하게 설명하면 길어지기에, 더 궁금하다면 &lt;a href=&quot;https://github.com/chalk/chalk/blob/main/source/index.js#L74C7-L74C19&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;해당 함수 소스코드&lt;/a&gt;를 살펴보면 좋을 것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스팅&lt;/h2&gt;
&lt;pre id=&quot;code_1727769193209&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;test&quot;: &quot;xo &amp;amp;&amp;amp; c8 ava &amp;amp;&amp;amp; tsd&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chalk는 3가지 패키지를 이용하여 각 영역을 linting 및 testing 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/xo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;xo&lt;/a&gt;: Eslint wrapper. eslintrc 없이 package.json에서 rule을 정의할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/ava&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ava&lt;/a&gt;: Node.js test runner&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/tsd&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;tsd&lt;/a&gt;: type definition 파일(.d.ts)의 테스팅&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1727769228945&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// test/chalk.js

test('support multiple arguments in base function', t =&amp;gt; {
	t.is(chalk('hello', 'there'), 'hello there');
});

test('style string', t =&amp;gt; {
	t.is(chalk.underline('foo'), '\u001B[4mfoo\u001B[24m');
	t.is(chalk.red('foo'), '\u001B[31mfoo\u001B[39m');
	t.is(chalk.bgRed('foo'), '\u001B[41mfoo\u001B[49m');
});

test('support applying multiple styles at once', t =&amp;gt; {
	t.is(chalk.red.bgGreen.underline('foo'), '\u001B[31m\u001B[42m\u001B[4mfoo\u001B[24m\u001B[49m\u001B[39m');
	t.is(chalk.underline.red.bgGreen('foo'), '\u001B[4m\u001B[31m\u001B[42mfoo\u001B[49m\u001B[39m\u001B[24m');
});

test('support nesting styles', t =&amp;gt; {
	t.is(
		chalk.red('foo' + chalk.underline.bgBlue('bar') + '!'),
		'\u001B[31mfoo\u001B[4m\u001B[44mbar\u001B[49m\u001B[24m!\u001B[39m',
	);
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1727769235730&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// source/index.test-d.ts

// -- Properties --
expectType&amp;lt;ColorSupportLevel&amp;gt;(chalk.level);

// -- Color methods --
expectType&amp;lt;ChalkInstance&amp;gt;(chalk.rgb(0, 0, 0));
expectType&amp;lt;ChalkInstance&amp;gt;(chalk.hex('#DEADED'));

// -- Complex --
expectType&amp;lt;string&amp;gt;(chalk.red.bgGreen.underline('foo'));
expectType&amp;lt;string&amp;gt;(chalk.underline.red.bgGreen('foo'));

// -- Complex template literal --
expectType&amp;lt;string&amp;gt;(chalk.underline``);
expectType&amp;lt;string&amp;gt;(chalk.red.bgGreen.bold`Hello {italic.blue ${name}}`);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같이 각 chalk의 동작과 type definition 파일에 대한 테스팅이 작성되어 있다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>chalk</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/44</guid>
      <comments>https://devpluto.tistory.com/entry/NPM-%ED%8C%A8%ED%82%A4%EC%A7%80-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0-1-Chalk#entry44comment</comments>
      <pubDate>Tue, 1 Oct 2024 16:40:14 +0900</pubDate>
    </item>
    <item>
      <title>Rsbuild 기반 Tistory-react 프레임워크 개발기</title>
      <link>https://devpluto.tistory.com/entry/Rsbuild-%EA%B8%B0%EB%B0%98-Tistory-react-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EA%B0%9C%EB%B0%9C%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rsbuild 기반의 tistory-react 프레임워크를 개발하게 된 계기와 기술 스택 선정부터 개발을 하며 겪은 많은 문제들을 소개하려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프레임워크는 아래 깃허브에 오픈소스로 모두 공개되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/eunbae0/tistory-react&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/eunbae0/tistory-react&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1723995777092&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 - eunbae0/tistory-react: Create tistory skin with React.js&quot; data-og-description=&quot;Create tistory skin with React.js. Contribute to eunbae0/tistory-react development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/eunbae0/tistory-react&quot; data-og-url=&quot;https://github.com/eunbae0/tistory-react&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cAjgKa/hyWOrf0eeU/eXg2c3Q6EkEHCmLvkqDiSk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/eunbae0/tistory-react&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/eunbae0/tistory-react&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cAjgKa/hyWOrf0eeU/eXg2c3Q6EkEHCmLvkqDiSk/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 - eunbae0/tistory-react: Create tistory skin with React.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Create tistory skin with React.js. Contribute to eunbae0/tistory-react 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;h2 data-ke-size=&quot;size26&quot;&gt;프레임워크를 개발하게 된 계기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 필자는 typescript + tailwindcss 개발 환경을 구축하여 티스토리 스킨을 개발한 경험이 있다. 이 당시 개발을 진행하며 스킨 정보 파일(xml)구문을 파악하고, 치환자를 하나하나 파악하고 적용 하느라 개발하는데 많이 애를 먹었었다. 또한 생 HTML 파일을 작성해야해서 스킨을 개발 완료한 시점에서는&amp;nbsp; 무려 1150줄이나 되는 HTML 파일을 들여다 본다고 고생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-08-19 오전 12.46.37.png&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;1998&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpQ7bT/btsI6gTUcic/AGLtWpDmYOKiPPkqr5FH5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpQ7bT/btsI6gTUcic/AGLtWpDmYOKiPPkqr5FH5k/img.png&quot; data-alt=&quot;당시 skin.html파일. 무려 1153줄이나 된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpQ7bT/btsI6gTUcic/AGLtWpDmYOKiPPkqr5FH5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpQ7bT%2FbtsI6gTUcic%2FAGLtWpDmYOKiPPkqr5FH5k%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;520&quot; height=&quot;641&quot; data-filename=&quot;edited_스크린샷 2024-08-19 오전 12.46.37.png&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;1998&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;당시 skin.html파일. 무려 1153줄이나 된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그리고 HTML의 live server를 띄우면 아래처럼 티스토리 치환자가 적용되어있고, 메인페이지와 글 상세 페이지가 혼재하는 등 개발하기 불편한 에로사항이 많았다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-19 오전 12.53.59.png&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;1858&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYudv8/btsI7ffzKcb/OAkViqlZfB8XwLgqIYUhdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYudv8/btsI7ffzKcb/OAkViqlZfB8XwLgqIYUhdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYudv8/btsI7ffzKcb/OAkViqlZfB8XwLgqIYUhdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdYudv8%2FbtsI7ffzKcb%2FOAkViqlZfB8XwLgqIYUhdK%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;619&quot; height=&quot;603&quot; data-filename=&quot;스크린샷 2024-08-19 오전 12.53.59.png&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;1858&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;br /&gt;최근에 문득 이 경험이 떠올라 React를 이용하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;개발한 다음 자동으로 HTML, CSS, JS 파일을 빌드해주는 환경을 개발한다면, 그리고&amp;nbsp; 개발 환경에서는 메인 페이지와 글 상세, 태그 페이지 등을 적절히 라우팅 시켜주면 1000줄이 넘어가는 HTML 파일을 작성하지 않아도 되고, 기능을 개발하기 편리한 환경을 만들 수 있지 않을까? 에서 개발을 시작하게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발에 들어가기 전, 계획&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;목표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 크게 3가지 목표를 잡았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. create tistory-react 명령어를 실행하여 쉽게 프로젝트를 시작 가능하도록 할 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. dev환경에서는 라우팅되어 개발하며 HMR 서버가 제공될 것, build환경에서는 이 결과물이 합쳐져서 xml, html, css, js파일을 output으로 내보낼 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 티스토리 치환자를 완전히 몰라도 깔끔한 문서를 제공해 쉽게 개발이 가능하도록 할 것.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술 스택 선정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Lerna: cli, core와 같은 패키지를 제공하기 위해 모노레포는 필수 적용사항이었다. 이전에 Lerna를 이용하여 디자인 시스템을 개발한 경험이 있기도 하고 패키지를 개발하기 가장 무난한 Lerna를 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Rsbuild: 번들러는 여러 선택지가 있었다. Vite는 번들링보다는 웹 프로젝트 개발이 중심이기 때문에 선택하지 않았고, 속도 측면에서 esbuild를 선택하고 싶었지만 HMR을 지원하지 않았다. 따라서 속도도 매우 빠르고 HMR을 지원하는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;rust기반의&lt;/span&gt;&amp;nbsp;rsbuild를 선택했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현 요구사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 스택을 선정한 다음, 구현시 요구사항을 쭉 적어보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모노레포를 이용하여 cli, core, runtime, theme 등의 패키지를 분리하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- pages/ 하위 폴더에 존재하는 파일의 라우팅 처리(a.k.a. Next.js pages router)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 개발 환경에서 치환자를 자동으로 적용해주는 가이드를 표시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- build 명령어 실행시 SSG로 HTML 파일을 생성하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- tistory-react.config 파일의 정의에 따라 XML 파일을 생성하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모든 티스토리 치환자를 React 컴포넌트 형식으로 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- cli를 이용하여 프로젝트 시작시 JS, TS 선택 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 문서를 통해 컴포넌트, 치환자를 상세하게 안내&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등등.. 많은 요구사항을 정의할 수 있었다. 이후 하나하나 단계별로 개발을 진행했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rspress&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://rspress.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Rspress&lt;/a&gt;는 Rsbuild 기반의 mdx document 사이트를 제공하는 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tistory-react의&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;기반이 되는 코드는 대부분 rspress의 코드를 응용하여 작성했다&lt;/span&gt;. 개발을 진행하며 rspress의 코드를 분석하며 동작원리를 파악하고, 원하는 기능을 개발했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본격적으로 개발하기 (with. 트러블 슈팅)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pages 하위 폴더에 존재하는 라우팅 하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구상은 Next.js처럼 pages 하위 폴더에 존재하는 폴더명에 따라 라우팅을 구현하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dev 환경에서는 페이지 경로에 따라 일치하는 컴포넌트가 렌더링 되어야 하고, build 환경에서는 라우팅되는 것이 아니라 한 html 파일에 모든 컴포넌트가 렌더링 되어야 한다. 스킨 등록시 html을 분리해서는 안되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rspress에서는 페이지 경로마다 문서를 SSR로 렌더링하기 때문에 해당 코드를 분석하고 이용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로에 맞는 컴포넌트를 불러오기 위해 단순히 App.tsx에서 타깃 경로에 있는 컴포넌트를 불러오는 구조를 상상했지만, 실제로 바로 렌더링하는 것은 불가능했고 아래와 같은 방식으로 구현되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 우선 &lt;a href=&quot;https://www.npmjs.com/package/globby&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;globby 패키지&lt;/a&gt;를 이용해 타깃 폴더 목록을 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. RouteService에 각 폴더의 정보를 담아 route를 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. RouteService는 path, 컴포넌트의 ReactNode Object 등이 담긴 RoutesCode를 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. routeVMPlugin 함수에 3을 이용하여 RoutesCode를 할당한 후, 이를 커스텀 rsbuild plugin의 bundlerChain에 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 해당 plugin은 rsbuild config에 등록되어 dev, build 등의 함수와 함께 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. &lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/web-infra-dev/rspress/blob/3774818a44e7f7a028af70a2aee8807fc4cdfbe5/packages/runtime/src/Content.tsx&quot; data-token-index=&quot;0&quot;&gt;&lt;span&gt;rspress/runtime/src/Content.tsx&lt;/span&gt;&lt;/a&gt;에서 해당 모듈을 이용해 page path에 맞는 컴포넌트를 렌더링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. App.tsx에서 &amp;lt;Theme.Layout /&amp;gt;을 불러와 이를 렌더링한다. Layout 컴포넌트에는 Content 컴포넌트를 렌더링하는 로직이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한줄 요약하자면 빌드할 때 각 routes에 해당하는 컴포넌트 Object가 담긴 모듈을 생성해, Contents 컴포넌트에서 이를 가져와 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 rspress의 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;모든 route 경로의 문서를&lt;/span&gt;&amp;nbsp;생성하는 방식이므로, 프로젝트에는 코드를 일부 수정하여 반영했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 타깃 폴더를 불러올 때 원하는 article, layout 등의 경로에 해당하는 파일을 불러오도록 필터링 함수 추가&lt;/p&gt;
&lt;pre id=&quot;code_1725417980609&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// @core/RouteService.ts

async init() {
    const globby = (await import('@tistory-react/shared/globby')).globby;

    // 1. Filter page route paths file
    const files = await globby(
      [
        `**/pages/{${this.#pageRoutePaths.join(',')}}/index.{${this.#extensions.join(',')}}`,
        `**/Layout.{${this.#extensions.join(',')}}`,
      ],
      {
        cwd: this.#scanDir,
        absolute: true,
        ignore: [
          '**/node_modules/**',
          `**/.eslintrc.${this.#extensions.join(',')}`,
        ],
      },
    );

    const filesRelativePath = files.sort().map(
      filePath =&amp;gt; normalizePath(path.relative(this.#scanDir, filePath)), // ex. src/pages/article/index.tsx
    );

    // 2. Error handling if required file does not exist
    filesRelativePath.forEach(defaultRoutePath =&amp;gt; {
      if (!isTistoryRouteFile(defaultRoutePath))
        throw new Error(
          `Required file does not include path: /${defaultRoutePath}`,
        );
    });

    // 3. Generate routeInfo
    filesRelativePath.forEach(relativePath =&amp;gt; {
      const absolutePath = path.join(this.#scanDir, relativePath);

      const routeInfo = {
        absolutePath: normalizePath(absolutePath),
        relativePath: relativePath,
        pageName: extractPageName(relativePath),
      };
      this.addRoute(routeInfo);
    });
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Content 컴포넌트에서 dev, build 환경에 따라 route 컴포넌트 렌더링 여부를 결정하고, App.tsx에 추가&lt;/p&gt;
&lt;pre id=&quot;code_1725418154775&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// @runtime/Content.tsx

const { routes } = process.env.__SSR__
  ? (require('virtual-routes-ssr') as typeof import('virtual-routes-ssr'))
  : (require('virtual-routes') as typeof import('virtual-routes'));

export const Content = () =&amp;gt; {
  const isSSR = process.env.__SSR__;

  // layout 컴포넌트 정의
  const layoutElement = routes.find(
    route =&amp;gt; route.pageName === 'layout',
  )!.element;

  if (!isSSR) { // dev 환경에서는 라우팅 처리
    const { pathname } = useLocation();

    layoutElement.props = { 
      children: routes.find(
        route =&amp;gt; route.pageName === removeLeadingSlash(pathname),
      )?.element,
    }; // 현재 route와 일치하는 page를 렌더링 한다.
    
    return layoutElement;
  }

  // build 환경에서는 layout에 감싸진 형태로 컴포넌트 렌더링
  const routesElements = routes
    .filter(route =&amp;gt; route.pageName !== 'layout')
    .map(route =&amp;gt; route.element);

  layoutElement.props = { children: routesElements };

  return layoutElement;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식으로 원하는 요구사항을 구현할 수 있었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;children이 포함된 Layout 컴포넌트를 렌더링하기&lt;/h4&gt;
&lt;pre id=&quot;code_1725417142581&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Layout&amp;gt;
  &amp;lt;MainPage /&amp;gt;
  &amp;lt;ArticlePage /&amp;gt;
  &amp;lt;GuestPage /&amp;gt;
  &amp;lt;TagsPage /&amp;gt;
&amp;lt;/Layout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 Layout 컴포넌트가 하위 페이지를 감싸는 형태로 구상했다.&lt;/p&gt;
&lt;pre id=&quot;code_1725417705551&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const Content = () =&amp;gt; {
  // ...
  const routesElements = routes
    .filter(route =&amp;gt; route.pageName !== 'layout')
    .map(route =&amp;gt; route.element);

  return &amp;lt;LayoutElement&amp;gt;{routesElements}&amp;lt;/LayoutElement&amp;gt;;
  // Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위와 같이 직관적으로 바로 구현했더니 에러가 발생했다. ReactNode Object를 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;직접적으로&lt;span&gt; &lt;/span&gt;&lt;/span&gt;React Element에 할당해서 발생한 오류였다.&lt;/p&gt;
&lt;pre id=&quot;code_1725417273710&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const Content = () =&amp;gt; {
  // ...
  const routesElements = routes
    .filter(route =&amp;gt; route.pageName !== 'layout')
    .map(route =&amp;gt; route.element);

  layoutElement.props = { children: routesElements };

  return layoutElement;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Object를 억지로 Element에 할당하지만 않으면 되므로, children에 routesElements를 주기 위해 layoutElement.props 속성에 children 값을 routesElements로 할당하는 방식으로 문제를 해결했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;onclick 이벤트 표시하기&lt;/h3&gt;
&lt;pre id=&quot;code_1725416058127&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;textarea
  onclick=&quot;&quot;
  // ...
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티스토리 스킨에서 제공하는 온클릭 이벤트를 사용하기 위해서는, 위 코드처럼 onclick 이벤트에 각 이벤트에 해당하는&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;치환자를 string으로 넘겨줘야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하지만 onClick 이벤트에 string을 할당하면 TS 오류와 함께 리액트에서 실제 DOM으로 변환되면서 onclick 이벤트는 표시되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그렇기 때문에 당연히 onClick, onclick에 string을 할당하면 아예 해당 attribute가 사라지며 표시되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;dangerouslySetInnerHTML?&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;onclick을 string 그대로 표시해주기 위해 여러 방법을 찾던 중, dangerouslySetInnerHTML prop을 활용하는 방법이 보여 이를 적용해 보았다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1725416270780&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const InputTextArea = (props: RepTextareaProps) =&amp;gt; {
  return (
    &amp;lt;div
      ref={ref}
      dangerouslySetInnerHTML={{
        __html: `
        &amp;lt;textarea
          onclick=&quot;${COMMENT_INPUT_COMMENT}&quot;
          name=&quot;${COMMENT_INPUT_COMMENT}&quot;
          ${JSON.stringify({ ...props })}
        /&amp;gt;`,
      }}
    /&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 방법은 온전히 textarea를 반환하는 컴포넌트도 아닐 뿐더러, prop을 사용할 때 리액트 props을 innerHTML에 표시만 하지 적용되지는 않고, 그렇다고 감싸는 div에 적용하면 타깃 Node에 적용되지 않기 때문에 사용할 수 없었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;custom html attribute 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;data-onclick이라는 attribute에 티스토리 치환자에 해당하는 string을 할당하고, 해당 attribute를 HTML render 단계에서 변환하는 방식으로 문제를 해결했다.&lt;/p&gt;
&lt;pre id=&quot;code_1725416391043&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const InputSubmit = (props: RepInputProps) =&amp;gt; {
  return (
    &amp;lt;input
      type=&quot;submit&quot;
      value={`${props.value ?? props.label ?? '댓글 달기'}`}
      data-onclick={COMMENT_INPUT_ONCLICK}
    /&amp;gt;
  );
};

// build.ts

export async function renderHtml() {
  // ...
  
  // appHtml (renderToString을 통해 html string으로 변환됨)
  
  htmlTemplate // string
    .replace(APP_HTML_MARKER, () =&amp;gt; appHtml)
    // ...
    .replaceAll(TEMP_ONCLICK_ATTR, 'onclick');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 data-onclick attribute에 치환자를 할당한 다음, 변환된 html string에서 'data-onclick'을 찾아 'onclick'으로 변환시키는 로직을 추가했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-09-04 오전 11.29.08.png&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be6Usp/btsJqI13RJB/rWMHGyVkvfLuBZ8Vhjba8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be6Usp/btsJqI13RJB/rWMHGyVkvfLuBZ8Vhjba8k/img.png&quot; data-alt=&quot;onclick 치환자가 잘 반영되었다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be6Usp/btsJqI13RJB/rWMHGyVkvfLuBZ8Vhjba8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe6Usp%2FbtsJqI13RJB%2FrWMHGyVkvfLuBZ8Vhjba8k%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;329&quot; height=&quot;245&quot; data-filename=&quot;스크린샷 2024-09-04 오전 11.29.08.png&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;onclick 치환자가 잘 반영되었다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Frontend/react</category>
      <category>framework</category>
      <category>react</category>
      <category>rsbuild</category>
      <category>tistory-react</category>
      <category>티스토리 스킨</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/42</guid>
      <comments>https://devpluto.tistory.com/entry/Rsbuild-%EA%B8%B0%EB%B0%98-Tistory-react-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EA%B0%9C%EB%B0%9C%EA%B8%B0#entry42comment</comments>
      <pubDate>Mon, 19 Aug 2024 01:20:50 +0900</pubDate>
    </item>
    <item>
      <title>[React Native] FlashList로 리스트의 렌더링 성능 최적화하기</title>
      <link>https://devpluto.tistory.com/entry/React-Native-FlashList%EB%A1%9C-%EC%9D%B4%EC%B0%A8%EC%9B%90-%EB%B0%B0%EC%97%B4-%EB%A6%AC%EC%8A%A4%ED%8A%B8%EC%9D%98-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시대생 서비스의 도서관 예약하기 서비스는 아래 이미지처럼 도서관 현재 좌석 상태를 표시합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-07-03 오후 8.53.56.png&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;1656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfN6eg/btsInkOItVl/teMJbsKQM6ABQt2RkBlZ41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfN6eg/btsInkOItVl/teMJbsKQM6ABQt2RkBlZ41/img.png&quot; data-alt=&quot;도서관 예약하기 서비스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfN6eg/btsInkOItVl/teMJbsKQM6ABQt2RkBlZ41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfN6eg%2FbtsInkOItVl%2FteMJbsKQM6ABQt2RkBlZ41%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;310&quot; height=&quot;587&quot; data-filename=&quot;스크린샷 2024-07-03 오후 8.53.56.png&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;1656&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;도서관 예약하기 서비스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 서비스의 좌석 item을 표시하기 위해 보통 18 x 30 크기의 이차원 배열을 이중 리스트로 렌더링합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 구현하기 위해 리스트의 렌더링 성능을 최적화하기 위해 &lt;a href=&quot;https://reactnative.dev/docs/performance#listview-initial-rendering-is-too-slow-or-scroll-performance-is-bad-for-large-lists&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에서 제시하는 FlatList를 사용하고, getItemLayout prop을 이용하여 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719821920113&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;FlatList
  data={...}
  renderItem={...}
  ...
  getItemLayout={(_, index) =&amp;gt; ({
    length: itemWidth + 2,
    offset: (itemWidth + 2) * index,
    index,
  })}
&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 해당 방법을 이용하더라도, 아래와 같이 시뮬레이터 및 실 기기에서 여전히 느린 렌더링 성능을 보여주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 예약 페이지가 넘어가기 전에 버튼이 잠깐 멈춘듯하는 현상과, 렌더링이 될 때까지 좌석 일부만 표시되는 현상도 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FlatList (6).gif&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;2556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UUYsP/btsIm5qBH69/ovEclgKtbX9JR8Xpy2geKK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UUYsP/btsIm5qBH69/ovEclgKtbX9JR8Xpy2geKK/img.gif&quot; data-alt=&quot;예약 페이지의 느린 렌더링&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UUYsP/btsIm5qBH69/ovEclgKtbX9JR8Xpy2geKK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/UUYsP/btsIm5qBH69/ovEclgKtbX9JR8Xpy2geKK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;278&quot; height=&quot;603&quot; data-filename=&quot;FlatList (6).gif&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;2556&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예약 페이지의 느린 렌더링&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 현상은 UX를 저하시키는 요소이기 때문에, 개선하기 위해 Shopify/flash-list의 FlashList 컴포넌트를 서비스 코드에 도입하고, 기타 렌더링 성능을 최적화하기 위한 과정을 작성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FlashList 도입&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://shopify.github.io/flash-list/docs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Shopify/flash-list 패키지&lt;/a&gt;는 기존 RN의 FlatList보다 더 나은 렌더링 성능의 FlashList 컴포넌트를 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치&lt;/h3&gt;
&lt;pre id=&quot;code_1720008091888&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ yarn add @shopify/flash-list &amp;amp;&amp;amp; cd ios &amp;amp;&amp;amp; pod install&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적용&lt;/h3&gt;
&lt;pre id=&quot;code_1719831169014&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;FlashList
  data={currentSeat}
  scrollEnabled={false}
  renderItem={row =&amp;gt; {
	// ...
    return (
      &amp;lt;FlashList
        horizontal
        scrollEnabled={false}
        data={rowItem}
        renderItem={({item}) =&amp;gt; {
          return (
            &amp;lt;SeatItem
             // ...
            /&amp;gt;
          );
        }}
        initialScrollIndex={0}
        estimatedFirstItemOffset={itemWidth + 2}
        estimatedItemSize={itemWidth + 2}
        keyExtractor={(_, index) =&amp;gt; index.toString()}
      /&amp;gt;
    );
  }}
  initialScrollIndex={0}
  estimatedFirstItemOffset={itemWidth + 2}
  estimatedItemSize={itemWidth + 2}
  keyExtractor={(_, index) =&amp;gt; index.toString()}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FlashList의 prop으로 initialScrollIndex, estimatedItemSize 등의 속성을 주면 성능을 조금 더 최적화 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 FlatList를 FlashList로 교체하는 것 만으로도 이전보다는 빠르게 렌더링이 되는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 FlashList는 FlatList와 다르게, List를 감싸는 View의 height, width를 지정해줘야 에러가 발생하지 않습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;트러블 슈팅: 레이아웃이 재배치 되는 현상&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 처음 렌더링이 완료된 다음, 가로로 layout이 재정렬되는 현상이 발생했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Flash, 소숫점.gif&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;1358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEOmXT/btsIibZ2P9p/Fpez6Ndzpl6fxZQKmwumXk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEOmXT/btsIibZ2P9p/Fpez6Ndzpl6fxZQKmwumXk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEOmXT/btsIibZ2P9p/Fpez6Ndzpl6fxZQKmwumXk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cEOmXT/btsIibZ2P9p/Fpez6Ndzpl6fxZQKmwumXk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;651&quot; data-filename=&quot;Flash, 소숫점.gif&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;1358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 파악한 결과, list에 들어갈 각 item의 너비(정사각형)를 소숫점으로 주게되면, 렌더링이 일어난 다음 너비를 재계산한다는 것을 알아냈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Math.round() 함수를 이용해 item의 너비를 반올림하여 정수로 설정하여 해당 문제를 해결했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719829663610&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const itemWidth = useMemo(() =&amp;gt; {
  // ...
  return (width - 16) / libraryRowCount - 2; // 기존 
  return Math.round((width - 8) / libraryRowCount - 2); // 변경
  // ...&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;애니메이션 방식 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FlashList를 이용해 렌더링 시간은 이전에 비해 개선되었지만, 여전히 만족스럽지 못한 렌더링 속도를 보여주고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시대생 앱에서는 일관된 애니메이션을 보여주기 위해 reanimatied 기반의 &lt;a href=&quot;https://moti.fyi/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;@moti 패키지&lt;/a&gt;를 이용해, 아래와 같이 클릭 애니메이션을 추상화하여 사용하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719823245552&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const AnimatePress = ({children, variant, ...props}: Props) =&amp;gt; {
  return (
    &amp;lt;MotiPressable {...props} animate={switchVariant()}&amp;gt;
      {children}
    &amp;lt;/MotiPressable&amp;gt;
  );
}

// 사용
&amp;lt;AnimatePress variant=&quot;scale_up_00&quot;&amp;gt;
  &amp;lt;Component /&amp;gt;
&amp;lt;/AnimatePress&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 해당 커스텀 컴포넌트의 사용으로 인해 예약 페이지의 렌더링 성능이 저하되는 점을 발견했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baqBln/btsIiArEv6f/lxvi9JOP7HZyr01rI4RfiK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baqBln/btsIiArEv6f/lxvi9JOP7HZyr01rI4RfiK/img.gif&quot; data-is-animation=&quot;true&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;1124&quot; data-filename=&quot;Flash&amp;amp;amp;#44; animatePressO.gif&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.4167%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baqBln/btsIiArEv6f/lxvi9JOP7HZyr01rI4RfiK/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaqBln%2FbtsIiArEv6f%2Flxvi9JOP7HZyr01rI4RfiK%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;518&quot; height=&quot;1124&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt9GSE/btsIkM45fIx/iYyOTY8xo15K82kTjc11Dk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt9GSE/btsIkM45fIx/iYyOTY8xo15K82kTjc11Dk/img.gif&quot; data-is-animation=&quot;true&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;1662&quot; data-filename=&quot;Flash&amp;amp;amp;#44; animatePressX.gif&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.4206%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt9GSE/btsIkM45fIx/iYyOTY8xo15K82kTjc11Dk/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt9GSE%2FbtsIkM45fIx%2FiYyOTY8xo15K82kTjc11Dk%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;766&quot; height=&quot;1662&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;좌: Moti 컴포넌트가 적용된 상태, Moti 컴포넌트를 제거한 상태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;item마다 AnimatePress 컴포넌트를 감싸 구현이 되어있는데, devtool을 이용해 확인해 본 결과 View 레이어가 3~4중으로 더 렌더링이 되는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 많은 렌더링이 일어나는 예약 페이지에서는 커스텀 컴포넌트의 사용이 적합하지 못하다 판단하여, reanimated를 이용하여 직접 애니메이션을 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719823587345&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const SeatItem = (...) =&amp;gt; {
  const pressed = useSharedValue&amp;lt;boolean&amp;gt;(false);

  const animatedStyles = useAnimatedStyle(() =&amp;gt; ({
    backgroundColor: pressed.value ? colors.grey60 : colors.grey40,
    transform: [{scale: withTiming(pressed.value ? 1.1 : 1, {duration: 250})}],
  }));
  
  // ...

  return isAvailable ? (
    &amp;lt;Pressable
      onPressIn={() =&amp;gt; (pressed.value = true)}
      onPressOut={() =&amp;gt; (pressed.value = false)}
      onPress={handlePressSeatItem}&amp;gt;
      &amp;lt;Animated.View
        style={[
          {
            width: itemWidth,
            height: itemWidth,
            backgroundColor: SeatStatusColorEnum[status],
            justifyContent: 'center',
            alignItems: 'center',
            borderRadius: 3,
            margin: 1,
            paddingLeft: 0.5,
          },
          animatedStyles,
        ]}&amp;gt;
        // ...
      &amp;lt;/Animated.View&amp;gt;
    &amp;lt;/Pressable&amp;gt;
  ) : (
    &amp;lt;View&amp;gt;
     // 그냥 View 표시
    &amp;lt;/View&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pressable 컴포넌트의 onPressIn, Out prop을 통해 useSharedValue로 생성한 pressed 상태를 변경합니다. 그리고 AnimatedStyle을 컴포넌트에 적용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로, 이미 예약이 불가능한 좌석(ex. 사용 중)은 클릭이 되지 않아야 하기 때문에 해당 경우 Animated가 적용되지 않은 그냥 View 컴포넌트를 렌더링하도록 구현했습니다. 이를 통해 리스트를 조금 더 빠르게 렌더링할 수 있을 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;customAnimation.gif&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;1184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t3Dky/btsIlEt5JK6/fJOopU6mTPfZR6nVd6o5P1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t3Dky/btsIlEt5JK6/fJOopU6mTPfZR6nVd6o5P1/img.gif&quot; data-alt=&quot;reanimated를 이용하여 애니메이션을 직접 적용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t3Dky/btsIlEt5JK6/fJOopU6mTPfZR6nVd6o5P1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/t3Dky/btsIlEt5JK6/fJOopU6mTPfZR6nVd6o5P1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;263&quot; height=&quot;570&quot; data-filename=&quot;customAnimation.gif&quot; data-origin-width=&quot;546&quot; data-origin-height=&quot;1184&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;reanimated를 이용하여 애니메이션을 직접 적용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과 및 마무리&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k5ktL/btsImJH8MFB/b6ELlH1v9yL0VMYaZDC0a1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k5ktL/btsImJH8MFB/b6ELlH1v9yL0VMYaZDC0a1/img.gif&quot; data-is-animation=&quot;true&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;2556&quot; data-filename=&quot;FlatList (7).gif&quot; width=&quot;250&quot; height=&quot;542&quot; style=&quot;width: 49.4267%; margin-right: 10px;&quot; data-widthpercent=&quot;50.01&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k5ktL/btsImJH8MFB/b6ELlH1v9yL0VMYaZDC0a1/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk5ktL%2FbtsImJH8MFB%2Fb6ELlH1v9yL0VMYaZDC0a1%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1178&quot; height=&quot;2556&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3ZKi6/btsInj3lAJ8/k98yl22Q2SQkVMxGqIn9l0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3ZKi6/btsInj3lAJ8/k98yl22Q2SQkVMxGqIn9l0/img.gif&quot; data-is-animation=&quot;true&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;1324&quot; data-filename=&quot;Flash&amp;amp;amp;#44; customAnimate.gif&quot; width=&quot;365&quot; height=&quot;792&quot; style=&quot;width: 49.4105%;&quot; data-widthpercent=&quot;49.99&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3ZKi6/btsInj3lAJ8/k98yl22Q2SQkVMxGqIn9l0/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3ZKi6%2FbtsInj3lAJ8%2Fk98yl22Q2SQkVMxGqIn9l0%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;610&quot; height=&quot;1324&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;좌: 렌더링 성능 개선 전, 우: 개선 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서 소개한 방법으로 도서관 예약하기 페이지의 평균 4~5초에서 1~2초로 렌더링 성능을 많이 개선할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend/react native</category>
      <category>flashlist</category>
      <category>FlatList</category>
      <category>렌더링 성능 개선</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/41</guid>
      <comments>https://devpluto.tistory.com/entry/React-Native-FlashList%EB%A1%9C-%EC%9D%B4%EC%B0%A8%EC%9B%90-%EB%B0%B0%EC%97%B4-%EB%A6%AC%EC%8A%A4%ED%8A%B8%EC%9D%98-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0#entry41comment</comments>
      <pubDate>Wed, 3 Jul 2024 21:41:39 +0900</pubDate>
    </item>
    <item>
      <title>[React Native] native module을 이용해 android notification 띄우기</title>
      <link>https://devpluto.tistory.com/entry/React-Native-native-module%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-android-notification-%EB%9D%84%EC%9A%B0%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android 환경에서 커스텀 알림을 직접 구현하게 된 계기와 구현 과정, 개발을 진행하며 겪은 문제들과 해결 과정을 소개하려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도서관 이용시간 알림 기능&lt;/b&gt;은 유저가 앱에서 나간 상태에도 &lt;b&gt;알림으로 현재 이용시간 또는 외출시간을 쉽게 확인&lt;/b&gt;할 수 있도록 하는 기능입니다. 토스의 따릉이를 이용할 때 알림으로 남은 시간이 표시되는 것을 보고, 이 기능을 도서관 이용시간 알림으로 도입해보면 유저에게 편리함을 제공할 수 있겠다 생각하여 iOS를 먼저 구현했고(&lt;a href=&quot;https://devpluto.tistory.com/entry/React-Native-RN%EC%97%90%EC%84%9C-dynamic-island-%EC%9C%84%EC%A0%AF-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;), Android를 구현하는 과정에 대해 작성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요구사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도서관 이용시간 알림은 아래와 같은 기능이 구현되어야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iOS는 ActivitKit으로 구현이 되어있음. Android 환경에서는 이와 비슷하게 동작하기 위해 Notification을 이용&lt;/li&gt;
&lt;li&gt;카운트 다운 / 카운트 업 표시&lt;/li&gt;
&lt;li&gt;알림은 삭제되지 않아야하며, 클릭시 deeplink를 통해 도서관 페이지로 이동해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱에서 알림을 위해 notifee 패키지를 사용하고 있지만, 요구사항을 만족시키기 위해서는 직접 Native 단에서 알림을 띄우도록 구현해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 NativeModule을 이용하여 직접 알림 구현을 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 기능을 만들어놓은&amp;nbsp;&lt;a style=&quot;background-color: #e6f5ff; text-align: start;&quot; href=&quot;https://github.com/nilavanraj/react-native-custom-timer-notification&quot;&gt;react-native-custom-timer-notification&lt;/a&gt; 라이브러리가 존재했지만 이 라이브러리는 요구사항에 해당하는 기능을 완전히 구현하지 못하고, 특히 Android 12+와 react-native의 최신버전인 0.74.x 버전을 제대로 지원하지 않았기 때문에 해당 라이브러리의 코드를 뜯어서 필요한 부분만 커스텀해서 구현을 진행했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 nativeModule 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://reactnative.dev/docs/native-modules-android?android-language=kotlin#create-a-custom-native-module-file&quot;&gt;공식문서&lt;/a&gt;를 참고하여 작성했습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// MainApplication.kt

// ...
override fun getPackages(): List&amp;lt;ReactPackage&amp;gt; =
  PackageList(this).packages.apply {
    add(LibraryStateNotificationPackage()) // 패키지 추가
  }
  // ...&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// LibraryStateNotificationPackage.kt
class LibraryStateNotificationPackage: ReactPackage {
    override fun createViewManagers(
        reactContext: ReactApplicationContext
    ): MutableList&amp;lt;ViewManager&amp;lt;View, ReactShadowNode&amp;lt;*&amp;gt;&amp;gt;&amp;gt; = mutableListOf()

    override fun createNativeModules(
        reactContext: ReactApplicationContext
    ): MutableList&amp;lt;NativeModule&amp;gt; = listOf(LibraryStateNotificationModule(reactContext)).toMutableList()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1718268711339&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LibraryStateNotificationModule.kt
class LibraryStateNotificationModule: ReactContextBaseJavaModule {
  constructor (context: ReactApplicationContext): super(context) {
      this.myContext = context
      this.packageName = this.myContext.packageName
  }

  override fun getName(): String {
    return &quot;LibraryStateNotification&quot;
  }
  
  //...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 package와 module을 생성한 다음, MainApplication.kt에 패키지를 추가합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;알림 Layout 그리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;res/layout 폴더에 Layout Resource File을 이용해 알림 레이아웃을 만듭니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQjsHU/btsHYZjEZli/0Cosb9jNSsHCd0KpeILyc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQjsHU/btsHYZjEZli/0Cosb9jNSsHCd0KpeILyc0/img.png&quot; style=&quot;width: 52.7419%; margin-right: 10px;&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;340&quot; data-filename=&quot;스크린샷 2024-06-13 오후 6.13.20.png&quot; data-widthpercent=&quot;53.36&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQjsHU/btsHYZjEZli/0Cosb9jNSsHCd0KpeILyc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQjsHU%2FbtsHYZjEZli%2F0Cosb9jNSsHCd0KpeILyc0%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;630&quot; height=&quot;340&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czSd0F/btsHXNxSuyZ/RygOykjNCVsRsBUpk18mTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czSd0F/btsHXNxSuyZ/RygOykjNCVsRsBUpk18mTk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;494&quot; data-filename=&quot;스크린샷 2024-06-13 오후 6.13.11.png&quot; data-widthpercent=&quot;46.64&quot; style=&quot;width: 46.0953%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czSd0F/btsHXNxSuyZ/RygOykjNCVsRsBUpk18mTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczSd0F%2FbtsHXNxSuyZ%2FRygOykjNCVsRsBUpk18mTk%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;800&quot; height=&quot;494&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 알림의 Layout과 알림을 확장했을 때의 Layout이 다르기 때문에, 각각 만들어주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 시간 카운터(Chronometer), text, image 순으로 배치했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Notification Builder 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/android/app/Notification.Builder&quot;&gt;Notification.Builder&lt;/a&gt;는 Android Notification을 위한 다양한 field값과 content view를 생성하는 객체입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이용해 원하는대로 알림을 커스텀하여 Notification Builder를 반환하는 함수를 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718269650015&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun buildNotification(objectData:ReadableMap): NotificationCompat.Builder {
    val id = notificationId

    val isUsing = objectData.getBoolean(&quot;isUsing&quot;);
    val dateInterval = objectData.getInt(&quot;dateInterval&quot;);

    val title = //
    val body = //
    val isCountDown = !isUsing

    val startTime = SystemClock.elapsedRealtime()

    val elapsed: Int = if (isUsing) dateInterval * 1000 else dateInterval * 1000 * (-1)
    val remainingTime = startTime - elapsed

    // set notification view layout
    val notificationLayout = RemoteViews(packageName, R.layout.notification_view);
    notificationLayout.setTextViewText(R.id.title, title)
    notificationLayout.setTextViewText(R.id.text, body)

    notificationLayout.setChronometerCountDown(R.id.simpleChronometer, isCountDown);
    notificationLayout.setChronometer(R.id.simpleChronometer, remainingTime, (&quot;%tM:%tS&quot;), true);

    val bigNotificationLayout = RemoteViews(packageName, R.layout.notification_view_big);
    bigNotificationLayout.setTextViewText(R.id.title, title)
    bigNotificationLayout.setTextViewText(R.id.text, body)

    bigNotificationLayout.setChronometerCountDown(R.id.simpleChronometer, isCountDown);
    bigNotificationLayout.setChronometer(R.id.simpleChronometer, remainingTime, (&quot;%tM:%tS&quot;), true);

    val notificationBuilder: NotificationCompat.Builder =
      NotificationCompat.Builder(myContext,channelId)

    notificationBuilder
      .setContentTitle(title)
      .setContentText(body)
      .setSmallIcon(R.drawable.ic_small_icon)
      .setColor(ContextCompat.getColor(reactApplicationContext, R.color.uoslife_primarybrand)) // small icon background color
      .setStyle(NotificationCompat.DecoratedCustomViewStyle())
      .setCustomContentView(notificationLayout)
      .setCustomBigContentView(bigNotificationLayout)
      .setPriority(NotificationCompat.PRIORITY_LOW)
      .setSound(null)
      .setAutoCancel(false) // 알림 클릭시 제거 방지
      .setShowWhen(false) // timestamp 표시하지 않음
      .setOngoing(true); // 알림 제거 방지

    return notificationBuilder
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 코드는 생략했습니다. js에서 전달받은 objectData의 프로퍼티는 getInt, getBoolean 메서드 등을 이용하여 가져올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 순서별로 간단히 소개하면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;js에서 전달받은 title, 여러 상태값 등을 이용하여 알림에 이용하거나 표시하기 위해 변수를 생성합니다.&lt;/li&gt;
&lt;li&gt; 카운트다운에 사용되는 &lt;a href=&quot;https://developer.android.com/reference/android/widget/Chronometer&quot;&gt;Chronometer&lt;/a&gt;는 SystemClock을 기준으로 표시하기 때문에, remainingTime을 계산합니다.&lt;/li&gt;
&lt;li&gt;이전에 만들었던 알림 layout을 선언(notificationLayout)하고, notificationBuilder 필드에 추가합니다.&lt;/li&gt;
&lt;li&gt;Notification.Builder를 이용하여 notificationBuilder를 만든 다음, 여러 필드를 추가합니다.&lt;/li&gt;
&lt;li&gt;DecoratedCustomViewStyle()을 설정해줘야 알림을 자유로운 layout으로 표시할 수 있습니다.&lt;/li&gt;
&lt;li&gt;이후 setCustom(Big)ContentView를 이용하여 선언해두었던 layout을 적용합니다.&lt;/li&gt;
&lt;li&gt;주석에 나와있듯 여러 옵션을 이용해 유저가 표시된 알림을 제거하지 못하도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;notification channel 생성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android O(API 26) 이상부터는 알림을 띄우기 위해 notification channel이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;buildNotification()함수에 채널을 생성하고, 설정하는 함수를 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718270602111&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun buildNotification() {
    // ...
    // set notification channel
    val notificationChannel =
      NotificationChannel(channelId, &quot;도서관 이용시간&quot;, NotificationManager.IMPORTANCE_LOW)
    notificationChannel.description = &quot;도서관 이용시간 및 외출시간을 안내합니다.&quot;
    notificationChannel.enableLights(true)
    notificationChannel.lightColor = R.color.uoslife_primarybrand
    notificationChannel.setShowBadge(false)
    notificationManager = myContext.getSystemService(NotificationManager::class.java)
    notificationManager.createNotificationChannel(notificationChannel)
    // ...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;deeplink를 이용해 알림 클릭 시 동작 구현하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 deeplink는 React Native Navigation을 이용해 구현되어있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718271179585&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- AndroidManifast.xml --&amp;gt;

&amp;lt;application android:usesCleartextTraffic=&quot;true&quot; android:name=&quot;.MainApplication&quot; android:allowBackup=&quot;false&quot; android:icon=&quot;@mipmap/ic_launcher&quot; android:label=&quot;@string/app_name&quot; android:theme=&quot;@style/BootTheme&quot;&amp;gt;
  &amp;lt;activity android:name=&quot;.MainActivity&quot; android:configChanges=&quot;keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode&quot; android:exported=&quot;true&quot; android:launchMode=&quot;singleTask&quot; android:windowSoftInputMode=&quot;adjustPan&quot;&amp;gt;
    &amp;lt;intent-filter&amp;gt;
      &amp;lt;action android:name=&quot;android.intent.action.MAIN&quot;/&amp;gt;
      &amp;lt;category android:name=&quot;android.intent.category.LAUNCHER&quot;/&amp;gt;
    &amp;lt;/intent-filter&amp;gt;
    &amp;lt;intent-filter&amp;gt;
      &amp;lt;action android:name=&quot;android.intent.action.VIEW&quot;/&amp;gt;
      &amp;lt;category android:name=&quot;android.intent.category.DEFAULT&quot;/&amp;gt;
      &amp;lt;category android:name=&quot;android.intent.category.BROWSABLE&quot;/&amp;gt;
      &amp;lt;data android:scheme=&quot;uoslife&quot;/&amp;gt;
    &amp;lt;/intent-filter&amp;gt;
  &amp;lt;/activity&amp;gt;
  ...
&amp;lt;/application&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AndroidManifast파일에 이렇게 설정되어있는데, intent-filter 설정을 보면 VIEW 액션을 이용하여 deeplink를 여는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718271337683&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun buildNotification() {
  // ...
  // mainActivity Intent
  val intent = Intent(Intent.ACTION_VIEW, &quot;uoslife://library&quot;.toUri(), myContext, MainActivity::class.java)
    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)

  // notification PendingIntent
  var pendingIntent = PendingIntent.getActivity(myContext, 0, intent, if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT)
  
  // ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 ACTION_VIEW 액션을 이용하여 MainActivity class를 여는 Intent를 선언한 다음, 이를 pendingIntent에 할당합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718271443645&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ...
notificationBuilder
  // ...
  .setContentIntent(pendingIntent)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 notificationBuilder.setContentIntent()에 지정해주면 알림 클릭시 intent가 실행되며 deeplink와 함께 앱이 열리게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 intent와 pendingIntent의 동작에 대해 알지 못해서 많이 해멨는데, 구현하신다면 관련 문서를 찾아보면 좋을 것 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외출 중인 경우, countDown이 종료되었을 때 알림 삭제하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외출 중인 상태에서 countDown이 종료되면 도서관 좌석이 자동으로 반납됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 알림을 표시할 필요가 없기 때문에 해당 경우, 알림을 종료하는 코드를 작성합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun buildNotification() {
  private lateinit var handler: Handler
  
  // ...
  
  // 외출 시간 종료 후 알림 삭제
  if (!isUsing) {
    val handler = Handler(Looper.getMainLooper())
    handler.postDelayed({
      try {
        removeNotification(id)
      } catch (e:Exception) {
        println(e)
      }
    }, abs(elapsed).toLong())
  }

  return notificationBuilder
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;외출 중인 경우, handler.postDelayed를 이용해 &lt;span style=&quot;text-align: start;&quot;&gt;알림 카운트 시간&lt;/span&gt;(elapsed)이 지나면 removeNotification()함수를 실행하는 동작을 추가합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Native Method 내보내기&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@ReactMethod 키워드를 이용하여 native에서 실행할 함수를 js로 내보낼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 이용하여 알림을 실행하는 함수와 제거하는 함수를 작성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718270147516&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// LibraryStateNotificationModule.kt

//...
private lateinit var notificationManager: NotificationManager
private val notificationId: Int = 255

// ...

private fun startNotification(objectData: ReadableMap) {
	val notificationBuilder:NotificationCompat.Builder = buildNotification(objectData)
	notificationManager.notify(notificationId, notificationBuilder.build())
}

@ReactMethod
	fun startLibraryStateNotification(objectData: ReadableMap) {
	startNotification(objectData)
}

private fun removeNotification(id: Int) {
	val notificationManager = myContext.getSystemService(NotificationManager::class.java)
	notificationManager.cancel(id);
}

@ReactMethod
	fun endLibraryStateNotification() {
	removeNotification(notificationId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;startNotification에서는 buildNotification함수를 이용해 NotificationBuilder를 만들고, 알림을 띄웁니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;removeNotification에서는 알림을 cancle합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;도서관 이용시간 알림은 하나의 notificationId를 가지고있습니다. 여러 알림을 띄우지 않고, 하나의 알림만 표시하면 되기 때문입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React Native에서 모듈 적용하기&lt;/h3&gt;
&lt;pre id=&quot;code_1718271836055&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {NativeModules} from 'react-native';
import {throwLinkingError} from '../throwLinkingError';

interface ILibraryStateNotification {
  startLibraryStateNotification: ({
    seatRoomName,
    seatNumber,
    isUsing,
    dateInterval,
  }: {
    seatRoomName: string;
    seatNumber: string;
    isUsing: boolean;
    dateInterval: number;
  }) =&amp;gt; void;
  endLibraryStateNotification: () =&amp;gt; void;
}

export class LibraryStateNotificationBridge {
  static libraryStateNotificationModule(): ILibraryStateNotification {
    return NativeModules.LibraryStateNotification;
  }

  static validateExistModule(func: keyof ILibraryStateNotification): boolean {
    if (
      this.libraryStateNotificationModule() &amp;amp;&amp;amp;
      typeof this.libraryStateNotificationModule()[func] === 'function'
    )
      return true;

    throwLinkingError('LibraryStateNotification');
    return false;
  }

  static start({
    seatRoomName,
    seatNumber,
    isUsing,
    dateInterval,
  }: {
    seatRoomName: string;
    seatNumber: string;
    isUsing: boolean;
    dateInterval: number;
  }): void {
    if (!this.validateExistModule('startLibraryStateNotification')) return;
    this.libraryStateNotificationModule().startLibraryStateNotification({
      seatRoomName,
      seatNumber,
      isUsing,
      dateInterval,
    });
  }

  static end(): void {
    if (!this.validateExistModule('endLibraryStateNotification')) return;
    this.libraryStateNotificationModule().endLibraryStateNotification();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 만들었던 Module을 검증 / 실행하는 bridge class를 작성 후, 서비스 로직에 적용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행할 때는 위 bridge를 이용해 아래와 같이 작성하기만 하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718272005104&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {LibraryDynamicIslandBridge} from '../../../utils/ios/libraryDynamicIslandBridge';

LibraryStateNotificationBridge.start({
  seatRoomName: &quot;0 데시벨 1&quot;,
  seatNumber: &quot;16&quot;,
  isUsing: true,
  dateInterval: // 초
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과물&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FOzM8/btsH3kvFmBI/MLNtZW8XKFkk5QbwQTu3C1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FOzM8/btsH3kvFmBI/MLNtZW8XKFkk5QbwQTu3C1/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;375&quot; data-filename=&quot;android_library_state_notification_using.jpg&quot; style=&quot;width: 49.1536%; margin-right: 10px;&quot; data-widthpercent=&quot;49.73&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FOzM8/btsH3kvFmBI/MLNtZW8XKFkk5QbwQTu3C1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFOzM8%2FbtsH3kvFmBI%2FMLNtZW8XKFkk5QbwQTu3C1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;375&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baFGnx/btsH4ocQdn7/FHVV7SZtuUa3yvSXLrXfl1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baFGnx/btsH4ocQdn7/FHVV7SZtuUa3yvSXLrXfl1/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;371&quot; data-filename=&quot;android_library_state_notification_not_using.jpg&quot; data-widthpercent=&quot;50.27&quot; style=&quot;width: 49.6836%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baFGnx/btsH4ocQdn7/FHVV7SZtuUa3yvSXLrXfl1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaFGnx%2FbtsH4ocQdn7%2FFHVV7SZtuUa3yvSXLrXfl1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif-3-2568d5ffcd.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1333&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dX2MKk/btsH4GxzKdP/QVW5NPDEf4ZnukK9vtCD31/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dX2MKk/btsH4GxzKdP/QVW5NPDEf4ZnukK9vtCD31/img.gif&quot; data-alt=&quot;테스트로 '더보기'에 startNotification 동작을 달아두었습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dX2MKk/btsH4GxzKdP/QVW5NPDEf4ZnukK9vtCD31/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dX2MKk/btsH4GxzKdP/QVW5NPDEf4ZnukK9vtCD31/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;306&quot; height=&quot;680&quot; data-filename=&quot;ezgif-3-2568d5ffcd.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1333&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트로 '더보기'에 startNotification 동작을 달아두었습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현이 완료된 모습입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트러블 슈팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BoradcastReciever를 이용해 intent를 열 수 없는 문제(Android 12+)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-06-07 오후 7.13.16.png&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ugzzb/btsH4BQKONX/r4jlKBrkIfwLCKLvul6ibK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ugzzb/btsH4BQKONX/r4jlKBrkIfwLCKLvul6ibK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ugzzb/btsH4BQKONX/r4jlKBrkIfwLCKLvul6ibK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fugzzb%2FbtsH4BQKONX%2Fr4jlKBrkIfwLCKLvul6ibK%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;2548&quot; height=&quot;222&quot; data-filename=&quot;스크린샷 2024-06-07 오후 7.13.16.png&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림을 클릭하여 intent를 실행하는 경우, 해당 오류가 발생했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-06-07 오후 9.23.04.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buIs44/btsH2pxIuLN/8pQgw8LeDMsKhAlSjZWwN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buIs44/btsH2pxIuLN/8pQgw8LeDMsKhAlSjZWwN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buIs44/btsH2pxIuLN/8pQgw8LeDMsKhAlSjZWwN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuIs44%2FbtsH2pxIuLN%2F8pQgw8LeDMsKhAlSjZWwN0%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;1326&quot; height=&quot;500&quot; data-filename=&quot;스크린샷 2024-06-07 오후 9.23.04.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-native-custom-timer-notification 패키지는 위 코드처럼 broadcastReciever를 이용해 pendingIntent 동작을 수행하는데, 이 동작 중 activity를 직접 여는 동작은 &lt;b&gt;Android 12+부터 불가&lt;/b&gt;해서 발생하는 문제였습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;after the user taps on a notification, or an action button&amp;nbsp;within the notification, your app cannot call &lt;b&gt;startActivity()&lt;/b&gt;&amp;nbsp;inside of a service or broadcast receiver.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;a href=&quot;https://proandroiddev.com/notification-trampoline-restrictions-android12-7d2a8b15bbe2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;관련 글&lt;/a&gt;을 참고하여 Broadcast를 사용하지 않고 아래와 같이 getActivity를 이용하는 방식으로 pendingIntent를 생성하도록 수정했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718708645836&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var pendingIntent = TaskStackBuilder.create(myContext).run {
  addNextIntentWithParentStack(intent)
  getPendingIntent(0, if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;foreground에서 알림을 클릭했을 때, 새로운 intent를 실행하여 앱을 덮어쓰는 현상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getActivity를 이용하여 intent를 여는 방법으로 동작을 변경했지만, background에서 알림을 클릭했을때는 intent가 실행되어 문제가 없는 반면에 foreground에서 클릭했을때는 새로운 intent를 실행해서 앱을 덮어쓰는 현상이 발생했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif-3-8215a860dc.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjroGF/btsH4JVkosZ/3KswPssvodKNkUZQjbWHKK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjroGF/btsH4JVkosZ/3KswPssvodKNkUZQjbWHKK/img.gif&quot; data-alt=&quot;새 Activity가 실행되며 화면을 덮어씁니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjroGF/btsH4JVkosZ/3KswPssvodKNkUZQjbWHKK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bjroGF/btsH4JVkosZ/3KswPssvodKNkUZQjbWHKK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;269&quot; height=&quot;568&quot; data-filename=&quot;ezgif-3-8215a860dc.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1267&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;새 Activity가 실행되며 화면을 덮어씁니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-06-07 오후 11.20.45.png&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFsJUS/btsH4Axyrqa/0IgaiFJGqgfhWEcWiyUwKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFsJUS/btsH4Axyrqa/0IgaiFJGqgfhWEcWiyUwKK/img.png&quot; data-alt=&quot;react-native-navigation의 MainActivity가 덮어써져 multiple instances 에러가 발생하는 상황&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFsJUS/btsH4Axyrqa/0IgaiFJGqgfhWEcWiyUwKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFsJUS%2FbtsH4Axyrqa%2F0IgaiFJGqgfhWEcWiyUwKK%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;750&quot; height=&quot;224&quot; data-filename=&quot;스크린샷 2024-06-07 오후 11.20.45.png&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;react-native-navigation의 MainActivity가 덮어써져 multiple instances 에러가 발생하는 상황&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;pendingIntent를 정의하는 방법을 수정하고, 실행할 intent의 Activity Flag도 수정했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718709224815&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// mainActivity Intent
val intent = Intent(Intent.ACTION_VIEW, &quot;uoslife://library&quot;.toUri(), myContext, MainActivity::class.java)
  .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)

// notification PendingIntent
var pendingIntent = PendingIntent.getActivity(myContext, 0, intent, if (Build.VERSION.SDK_INT &amp;gt;= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT)&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;SDK 31+ 이상부터는 pendingIntent 생성시 FLAG_IMMUTABLE 옵션을 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;intent의 Activity Flag에는 두가지 속성을 주었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FLAG_ACTIVITY_NEW_TASK: 이미 실행 중인 Activity가 있다면, 새 Activity를 실행되지 않음&lt;/li&gt;
&lt;li&gt;FLAG_ACTIVITY_SINGLE_TOP: 호출한 Activity와 현재 최상위의 Activity가 동일한 경우, 최상위 Activity가 재사용됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif-3-f516638558.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CbrnR/btsH4HiXubj/AyMgk6Uo5PKvPbyKtkYXbk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CbrnR/btsH4HiXubj/AyMgk6Uo5PKvPbyKtkYXbk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CbrnR/btsH4HiXubj/AyMgk6Uo5PKvPbyKtkYXbk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/CbrnR/btsH4HiXubj/AyMgk6Uo5PKvPbyKtkYXbk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;312&quot; height=&quot;659&quot; data-filename=&quot;ezgif-3-f516638558.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 변경해서 위와같이 foreground에서도 의도한 대로 정상적으로 동작하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;알림에서 PNG가 표시되지 않는 현상&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-06-07 오전 4.03.53.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c59OTs/btsH0mUrRxn/VVhpAqIUjVbyz3mI1OrFZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c59OTs/btsH0mUrRxn/VVhpAqIUjVbyz3mI1OrFZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c59OTs/btsH0mUrRxn/VVhpAqIUjVbyz3mI1OrFZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc59OTs%2FbtsH0mUrRxn%2FVVhpAqIUjVbyz3mI1OrFZ0%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;416&quot; height=&quot;126&quot; data-filename=&quot;스크린샷 2024-06-07 오전 4.03.53.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이머와 안내 텍스트는 잘 표시되었지만, PNG 이미지가 표시되지 않는 현상이 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 해상도(mdpi, xdpi, ...)별로 저장하고 ImageView의 app:srcCompat 속성을 통해 이미지 경로를 주지 않고, android:src 속성으로 변경하여 해결했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718709847455&quot; class=&quot;django&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;ImageView
    android:id=&quot;@+id/imageView_2&quot;
    android:layout_width=&quot;52dp&quot;
    android:layout_height=&quot;42dp&quot;
    android:layout_gravity=&quot;center&quot;
    android:layout_marginEnd=&quot;4dp&quot;
    android:layout_weight=&quot;5&quot;
    android:adjustViewBounds=&quot;true&quot;
    app:srcCompat=&quot;@drawable/iroomae&quot; &amp;lt;!-- 삭제 --&amp;gt;
    android:src=&quot;@drawable/iroomae&quot; &amp;lt;!-- 추가 --&amp;gt;
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리하며..&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kotlin과 android는 처음이라 Android의 intent와 PendingIntent의 동작원리 + ui를 그리는 부분에서 삽질을 좀 했지만, 웹앱을 만든 경험이 있어서 그런지 생각보다 구현에 큰 차이는 없구나라는 생각도 들었습니다. &lt;s&gt;구글과 stackoverflow 없이는 살아갈 수 없다는 점도 깨달은 점..&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림을 완전히 커스텀화해서 만든 경험이라 특수한 케이스이지만, RN 환경에서 Android의 알림을 구현하신다면 이 글이 도움이 되었으면 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고문서&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;android notification 공식문서&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/views/notifications/custom-notification?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.android.com/develop/ui/views/notifications/custom-notification?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;알림 클릭, pendingIntent 관련&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://proandroiddev.com/notification-trampoline-restrictions-android12-7d2a8b15bbe2&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://proandroiddev.com/notification-trampoline-restrictions-android12-7d2a8b15bbe2&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tussle.tistory.com/884&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tussle.tistory.com/884&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gun0912.tistory.com/13&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://gun0912.tistory.com/13&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=u3fBdJzV5OY&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=u3fBdJzV5OY&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;PNG 표시되지 않는 현상 관련&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/18251187/imageview-displaying-in-layout-but-not-on-actual-device&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/18251187/imageview-displaying-in-layout-but-not-on-actual-device&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/65526010/android-notification-custom-layout-xml-imageview-not-showing&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/65526010/android-notification-custom-layout-xml-imageview-not-showing&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;postDelay 관련&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/46466636/handler-postdelay-from-background-thread&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/46466636/handler-postdelay-from-background-thread&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/71940301/settimeoutafter-not-working-for-android-notification&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/71940301/settimeoutafter-not-working-for-android-notification&lt;/a&gt;&lt;/p&gt;</description>
      <category>Frontend/react native</category>
      <category>Android</category>
      <category>Intent</category>
      <category>native module</category>
      <category>notification</category>
      <category>pendingintent</category>
      <category>react native</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/40</guid>
      <comments>https://devpluto.tistory.com/entry/React-Native-native-module%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-android-notification-%EB%9D%84%EC%9A%B0%EA%B8%B0#entry40comment</comments>
      <pubDate>Tue, 18 Jun 2024 20:33:36 +0900</pubDate>
    </item>
    <item>
      <title>[React Native] RN에서 Dynamic island 위젯 만들기</title>
      <link>https://devpluto.tistory.com/entry/React-Native-RN%EC%97%90%EC%84%9C-dynamic-island-%EC%9C%84%EC%A0%AF-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Native에서 Dynamic Island 위젯을 만드는 방법에 대해 작성했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZHVNk/btsHBVoERK9/KkoBP7BzB2JcB8rG6BVtI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZHVNk/btsHBVoERK9/KkoBP7BzB2JcB8rG6BVtI1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;174&quot; data-filename=&quot;스크린샷 2024-05-24 오후 4.30.22.png&quot; style=&quot;width: 47.9382%; margin-right: 10px;&quot; data-widthpercent=&quot;49.08&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZHVNk/btsHBVoERK9/KkoBP7BzB2JcB8rG6BVtI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZHVNk%2FbtsHBVoERK9%2FKkoBP7BzB2JcB8rG6BVtI1%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;834&quot; height=&quot;174&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v3OgG/btsHBCCXQQN/2g5efNHqfU17fkSeZu3Mp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v3OgG/btsHBCCXQQN/2g5efNHqfU17fkSeZu3Mp1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;380&quot; data-filename=&quot;스크린샷 2024-05-24 오후 4.29.34.png&quot; width=&quot;556&quot; height=&quot;248&quot; data-widthpercent=&quot;22.96&quot; style=&quot;width: 22.4244%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v3OgG/btsHBCCXQQN/2g5efNHqfU17fkSeZu3Mp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv3OgG%2FbtsHBCCXQQN%2F2g5efNHqfU17fkSeZu3Mp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;852&quot; height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zbPEO/btsHArPVjR2/JEjkndz5mP7n5i057YIDn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zbPEO/btsHArPVjR2/JEjkndz5mP7n5i057YIDn1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;312&quot; data-filename=&quot;스크린샷 2024-05-24 오후 4.31.54.png&quot; style=&quot;width: 27.3118%;&quot; data-widthpercent=&quot;27.96&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zbPEO/btsHArPVjR2/JEjkndz5mP7n5i057YIDn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzbPEO%2FbtsHArPVjR2%2FJEjkndz5mP7n5i057YIDn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;852&quot; height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Info.plist&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 수정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 Live Activity를 사용하기 위해, 프로젝트의 info.plist에 Supports Live Activities: YES 옵션을 추가합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.20.53.png&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CrTli/btsHBqiem8w/P1KXKqLuKEBjKrRGxS7O1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CrTli/btsHBqiem8w/P1KXKqLuKEBjKrRGxS7O1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CrTli/btsHBqiem8w/P1KXKqLuKEBjKrRGxS7O1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCrTli%2FbtsHBqiem8w%2FP1KXKqLuKEBjKrRGxS7O1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1040&quot; height=&quot;66&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.20.53.png&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;66&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새로운 Widget Extension 생성하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lmaY4/btsHAztq9pS/2NwVcNeoAz5I7ELFmVzERK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lmaY4/btsHAztq9pS/2NwVcNeoAz5I7ELFmVzERK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;734&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.22.32.png&quot; data-widthpercent=&quot;32.1&quot; style=&quot;width: 31.3541%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lmaY4/btsHAztq9pS/2NwVcNeoAz5I7ELFmVzERK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlmaY4%2FbtsHAztq9pS%2F2NwVcNeoAz5I7ELFmVzERK%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;1028&quot; height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duw9zW/btsHAODUFRS/Qui3ObRR45Dq7FsOKUxzRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duw9zW/btsHAODUFRS/Qui3ObRR45Dq7FsOKUxzRk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;1018&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.23.26.png&quot; style=&quot;width: 31.4035%; margin-right: 10px;&quot; data-widthpercent=&quot;32.15&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duw9zW/btsHAODUFRS/Qui3ObRR45Dq7FsOKUxzRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fduw9zW%2FbtsHAODUFRS%2FQui3ObRR45Dq7FsOKUxzRk%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;1428&quot; height=&quot;1018&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJkTOT/btsHBtMMFUr/d7MJ8Anc89ZgwgCphkoS81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJkTOT/btsHBtMMFUr/d7MJ8Anc89ZgwgCphkoS81/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1176&quot; data-origin-height=&quot;754&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.23.57.png&quot; data-widthpercent=&quot;35.75&quot; style=&quot;width: 34.9168%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJkTOT/btsHBtMMFUr/d7MJ8Anc89ZgwgCphkoS81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJkTOT%2FbtsHBtMMFUr%2Fd7MJ8Anc89ZgwgCphkoS81%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;754&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;File &amp;gt; New &amp;gt; Target으로 들어가서, Widget Extension을 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 'Include Live Activity' 옵션을 체크한 다음, 원하는 이름으로 위젯을 생성합니다. 저는 이름을 DynamicIslandWidget으로 생성했습니다. (App Intent 옵션은 끄는 것을 추천합니다.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;App Group 생성하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱에서 쓴 데이터인 UserDefaults를 위젯에서도 사용할 수 있도록 App group을 만들어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱에 App Group이 없다면, &lt;a href=&quot;https://developer.apple.com/account/resources/identifiers/list&quot;&gt;Apple Developer &amp;gt; Identifier&lt;/a&gt;에서 새로운 App Group을 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kVVxy/btsHAulAFzc/p9UNRW5hJxGj99KKtSme31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kVVxy/btsHAulAFzc/p9UNRW5hJxGj99KKtSme31/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2380&quot; data-origin-height=&quot;1642&quot; data-filename=&quot;스크린샷 2024-05-24 오후 4.22.16.png&quot; style=&quot;width: 47.1644%; margin-right: 10px;&quot; data-widthpercent=&quot;47.72&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kVVxy/btsHAulAFzc/p9UNRW5hJxGj99KKtSme31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkVVxy%2FbtsHAulAFzc%2Fp9UNRW5hJxGj99KKtSme31%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;2380&quot; height=&quot;1642&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W6GEe/btsHANE6epu/Ey4SGjJ4ke7oMDU9kVKj0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W6GEe/btsHANE6epu/Ey4SGjJ4ke7oMDU9kVKj0k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2436&quot; data-origin-height=&quot;1534&quot; data-filename=&quot;스크린샷 2024-05-24 오후 4.22.49.png&quot; style=&quot;width: 51.6728%;&quot; data-widthpercent=&quot;52.28&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W6GEe/btsHANE6epu/Ey4SGjJ4ke7oMDU9kVKj0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW6GEe%2FbtsHANE6epu%2FEy4SGjJ4ke7oMDU9kVKj0k%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;2436&quot; height=&quot;1534&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 xcode 프로젝트에서 RN project의 target과 DynamicIslandWidget target에 Signing &amp;amp; Capabilities &amp;gt; '+ Capability'를 눌러 App Groups를 생성한 다음, 아까 생성한 group identifier를 선택합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-24 오후 4.24.28.png&quot; data-origin-width=&quot;1648&quot; data-origin-height=&quot;1158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bASPdR/btsHAeDeAyl/jeduaEKYZ3KsRGUVX6LIik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bASPdR/btsHAeDeAyl/jeduaEKYZ3KsRGUVX6LIik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bASPdR/btsHAeDeAyl/jeduaEKYZ3KsRGUVX6LIik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbASPdR%2FbtsHAeDeAyl%2FjeduaEKYZ3KsRGUVX6LIik%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;1648&quot; height=&quot;1158&quot; data-filename=&quot;스크린샷 2024-05-24 오후 4.24.28.png&quot; data-origin-width=&quot;1648&quot; data-origin-height=&quot;1158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dynamic Island 작성하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.30.31.png&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sJDZr/btsHAvYSykD/n2RAvlTcKAFLdjfnLufdnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sJDZr/btsHAvYSykD/n2RAvlTcKAFLdjfnLufdnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sJDZr/btsHAvYSykD/n2RAvlTcKAFLdjfnLufdnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsJDZr%2FbtsHAvYSykD%2Fn2RAvlTcKAFLdjfnLufdnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;523&quot; height=&quot;191&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.30.31.png&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 새로운 Widget Extension Target을 생성하면 사진과 같은 파일이 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 DynamicIslandWidget.swift 파일도 같이 생성되는데, 저는 LiveActivity만 사용하고 싶기 때문에 삭제했습니다. 위젯도 같이 작업하신다면 지우지 않으셔도 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;~Bundle.swift 파일 수정하기&lt;/h3&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;// DynamicIslandWidgetBundle.swift

import WidgetKit
import SwiftUI

@main
struct DynamicIslandWidgetBundle: WidgetBundle {
    var body: some Widget {
        DynamicIslandWidget() // 사용하지 않는다면 파일과 함께 삭제
        DynamicIslandWidgetLiveActivity()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;// DynamicIslandWidgetBundle.swift

import WidgetKit
import SwiftUI

@main
struct DynamicIslandWidgetBundle: WidgetBundle {
    var body: some Widget {
      if #available(iOS 16.1, *) {
        DynamicIslandWidgetLiveActivity()
      }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Project의 Deployment Target이 아래와 같이 16.1 이하로 설정되어있다면 DynamicIslandWidgetLiveActivity 해당 조건문을 추가합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.39.34.png&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blxZnu/btsHBBw8ubQ/xqitQeKlk2kkeHH4wNcXSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blxZnu/btsHBBw8ubQ/xqitQeKlk2kkeHH4wNcXSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blxZnu/btsHBBw8ubQ/xqitQeKlk2kkeHH4wNcXSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblxZnu%2FbtsHBBw8ubQ%2FxqitQeKlk2kkeHH4wNcXSK%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;686&quot; height=&quot;168&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.39.34.png&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;~LiveActivity.swift 파일 수정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 앱에서 해당 파일의 Dynamic Island를 사용하기 위해서 Target Membership에 RN project target도 체크해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.41.25.png&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2VVHv/btsHBnTrRLf/o60AgWeDeNHCym2LcxYdl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2VVHv/btsHBnTrRLf/o60AgWeDeNHCym2LcxYdl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2VVHv/btsHBnTrRLf/o60AgWeDeNHCym2LcxYdl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2VVHv%2FbtsHBnTrRLf%2Fo60AgWeDeNHCym2LcxYdl1%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;548&quot; height=&quot;204&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.41.25.png&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 해당 파일에서 swiftUI를 이용해 원하는 ui와 기능으로 코드를 작성하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// DynamicIslandWidgetLiveActivity.swift

@available(iOS 16.1, *)
struct DynamicIslandWidgetLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DynamicIslandWidgetAttributes.self) { context in
          LockScreenView(context: context)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                  LeadingView()
                }
                DynamicIslandExpandedRegion(.trailing) {
                  TrailingView(context: context)
                }

              DynamicIslandExpandedRegion(.bottom) {
                  ContentView(context: context)
                }
            } compactLeading: {
                CompactLeadingView(context: context)
            } compactTrailing: {
                CompactTrailingView(context: context)
            } minimal: {
                EmptyView()
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (2).png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CSCQi/btsHBVINQjI/fYqOZYaJUyloRCV6Rqh60K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CSCQi/btsHBVINQjI/fYqOZYaJUyloRCV6Rqh60K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CSCQi/btsHBVINQjI/fYqOZYaJUyloRCV6Rqh60K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCSCQi%2FbtsHBVINQjI%2FfYqOZYaJUyloRCV6Rqh60K%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;629&quot; height=&quot;210&quot; data-filename=&quot;image (2).png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (3).png&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vOZ3p/btsHAnfJ8p1/GgNa5p9H1yCu7Fg9kKC75k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vOZ3p/btsHAnfJ8p1/GgNa5p9H1yCu7Fg9kKC75k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vOZ3p/btsHAnfJ8p1/GgNa5p9H1yCu7Fg9kKC75k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvOZ3p%2FbtsHAnfJ8p1%2FGgNa5p9H1yCu7Fg9kKC75k%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;1520&quot; height=&quot;368&quot; data-filename=&quot;image (3).png&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 Dynamic Island는 이미지와 같이 Expanded View와 Compact View, Minimal View로 구분되어 있고, 각 View에는 세부적으로 영역이 구분되어있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RN과 통신을 위한 module과 bridge 작성하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;module 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 Swift File을 생성합니다. 이름은 DynamicIslandModule로 지었습니다. 해당 모듈의 target은 RN project만 체크해주세요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.51.52.png&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;1038&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckSW6u/btsHAxWIGPQ/ndFTirehC4HBpwAxFa9Yc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckSW6u/btsHAxWIGPQ/ndFTirehC4HBpwAxFa9Yc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckSW6u/btsHAxWIGPQ/ndFTirehC4HBpwAxFa9Yc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckSW6u%2FbtsHAxWIGPQ%2FndFTirehC4HBpwAxFa9Yc0%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;679&quot; height=&quot;486&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.51.52.png&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;1038&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 모듈에서 Dynamic Island를 컨트롤하는 함수를 작성합니다. 필요한 함수는 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;startActivity&lt;/li&gt;
&lt;li&gt;updateActivity&lt;/li&gt;
&lt;li&gt;endActivity&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;// DynamicIslandModule.swift

import Foundation
import ActivityKit

@available(iOS 16.2, *)

@objc(DynamicIslandModule)
class DynamicIslandModule: NSObject {
  @objc(startActivity:withSeatNumber:withIsUsing:withDateInterval:)
  func startActivity(seatRoomName: String, seatNumber: String, isUsing: Bool, dateInterval: NSNumber) -&amp;gt; Void {
    do {
      let ActivityAttributes = DynamicIslandWidgetAttributes(seatRoomName: seatRoomName, seatNumber: seatNumber)
      let ActivityContentState = DynamicIslandWidgetAttributes.ContentState(isUsing: isUsing, dateInterval: Double(truncating: dateInterval))

      let _ = try Activity&amp;lt;DynamicIslandWidgetAttributes&amp;gt;.request(attributes: ActivityAttributes, contentState: ActivityContentState, pushType: nil)
    } catch {
      print(&quot;Error&quot;)
    }
  }

  @objc(updateActivity:withDateInterval:)
  func updateActivity(isUsing: Bool, dateInterval: NSNumber) -&amp;gt; Void {
    let ActivityContentState = DynamicIslandWidgetAttributes.ContentState(isUsing: isUsing, dateInterval: Double(truncating: dateInterval))
    Task {
      for activity in Activity&amp;lt;DynamicIslandWidgetAttributes&amp;gt;.activities {
        await activity.update(using: ActivityContentState)
      }
    }
  }


  @objc(endActivity)
  func endActivity() -&amp;gt; Void {
    Task {
      for activity in Activity&amp;lt;DynamicIslandWidgetAttributes&amp;gt;.activities {
        await activity.end(dismissalPolicy: .default)
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Bridge 만들기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.57.40.png&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;914&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd6PBO/btsHBAZkLkS/987KpShiMgWhPMRy10gauK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd6PBO/btsHBAZkLkS/987KpShiMgWhPMRy10gauK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd6PBO/btsHBAZkLkS/987KpShiMgWhPMRy10gauK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd6PBO%2FbtsHBAZkLkS%2F987KpShiMgWhPMRy10gauK%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;599&quot; height=&quot;387&quot; data-filename=&quot;스크린샷 2024-05-24 오후 3.57.40.png&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;914&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 작성한 Dynamic Island를 컨트롤하는 함수를 RN에서도 사용할 수 있게 해당 모듈을 내보내고 등록하는 과정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 New File &amp;gt; Objective-C File을 선택을 통해 DynamicIslandBridge.m 파일을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 target은 RN project를 체크해주세요.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// DynamicIslandBridge.m

#import &amp;lt;Foundation/Foundation.h&amp;gt;
#import &amp;lt;React/RCTBridgeModule.h&amp;gt;

@interface RCT_EXTERN_MODULE(DynamicIslandModule, NSObject)

RCT_EXTERN_METHOD(startActivity:(NSString *) seatRoomName  withSeatNumber:(NSString *) seatNumber  withIsUsing:(BOOL *) isUsing withDateInterval:(nonnull NSNumber *) dateInterval)
RCT_EXTERN_METHOD(updateActivity:(BOOL *) isUsing withDateInterval:(nonnull NSNumber *) dateInterval)
RCT_EXTERN_METHOD(endActivity)

+ (BOOL)requiresMainQueueSetup
{
  return NO;
}

@end&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RCT_EXTERN_MODULE에 위에서 작성한 module을 담아주고, RCT_EXTERN_METHOD에 각 함수를 넣어주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 함수: 이후 구문에서 통신에 필요한 parameter를 정의할 수 있습니다. 저는 좌석번호, 사용 여부 등이 필요하여 넣어주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 parameter의 타입은 &lt;a href=&quot;https://reactnative.dev/docs/native-modules-ios#argument-types&quot;&gt;RN 공식문서의 Argument Type&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;React Native에서 module 호출하고 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 RN에서 LibraryDynamicIslandBridge class를 작성하여 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-native의 NativeModule을 호출하여 이전에 iOS에서 내보낸 DynamicIslandModule을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { NativeModules } from 'react-native';

interface DynamicIslandModule {
  startActivity: (
    seatRoomName: string,
    seatNumber: string,
    isUsing: boolean,
    dateInterval: number,
  ) =&amp;gt; void;
  updateActivity: (isUsing: boolean, dateInterval: number) =&amp;gt; void;
  endActivity: () =&amp;gt; void;
}

export class LibraryDynamicIslandBridge {
  static dynamicIslandModule(): DynamicIslandModule {
    return NativeModules.DynamicIslandModule;
  }

  static validateExistModule(func: keyof DynamicIslandModule): boolean {
    if (
      this.dynamicIslandModule() &amp;amp;&amp;amp;
      typeof this.dynamicIslandModule()[func] === 'function'
    )
      return true;
    console.warn('DynamicIslandModule not found');
    return false;
  }

  static onStartActivity({
    seatRoomName,
    seatNumber,
    isUsing,
    dateInterval,
  }: {
    seatRoomName: string;
    seatNumber: string;
    isUsing: boolean;
    dateInterval: number;
  }): void {
    if (!this.validateExistModule('startActivity')) return;
    this.dynamicIslandModule().startActivity(
      seatRoomName,
      seatNumber,
      isUsing,
      dateInterval,
    );
  }

  static onUpdateActivity({
    isUsing,
    dateInterval,
  }: {
    isUsing: boolean;
    dateInterval: number;
  }): void {
    if (!this.validateExistModule('updateActivity')) return;
    this.dynamicIslandModule().updateActivity(isUsing, dateInterval);
  }

  static onEndActivity(): void {
    if (!this.validateExistModule('endActivity')) return;
    this.dynamicIslandModule().endActivity();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 해당 모듈에 함수가 존재하는지 validate하고, 각 start와 end 등의 함수를 호출합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 예시&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// API가 호출되었을 경우
LibraryDynamicIslandBridge.onStartActivity({
  seatRoomName: response.seatRoomName,
  seatNumber: response.seatNo,
  isUsing: response.status === 'SEAT',
  // ...
});

// 상태가 변경되었을 경우
 LibraryDynamicIslandBridge.onUpdateActivity({
  isUsing: response.status === 'SEAT',
  dateInterval: ...
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif-6-584f7671ce.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1i7Lk/btsHB8gW3QG/xnANCn4kbkPY1tjTbHRzvk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1i7Lk/btsHB8gW3QG/xnANCn4kbkPY1tjTbHRzvk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1i7Lk/btsHB8gW3QG/xnANCn4kbkPY1tjTbHRzvk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/1i7Lk/btsHB8gW3QG/xnANCn4kbkPY1tjTbHRzvk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;290&quot; height=&quot;629&quot; data-filename=&quot;ezgif-6-584f7671ce.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1302&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;h2 data-ke-size=&quot;size26&quot;&gt;참고문서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.stackademic.com/unleashing-ios-dynamic-islands-in-your-react-native-app-a-step-by-step-guide-eee3c5ed3059&quot;&gt;https://blog.stackademic.com/unleashing-ios-dynamic-islands-in-your-react-native-app-a-step-by-step-guide-eee3c5ed3059&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://reactnative.dev/docs/native-modules-ios&quot;&gt;https://reactnative.dev/docs/native-modules-ios&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=BsJT26dkasA&quot;&gt;https://www.youtube.com/watch?v=BsJT26dkasA&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/hoaphantn7604/react-native-dynamic-island-tutorial&quot;&gt;https://github.com/hoaphantn7604/react-native-dynamic-island-tutorial&lt;/a&gt;&lt;/p&gt;</description>
      <category>Frontend/react native</category>
      <category>activity kit</category>
      <category>Dynamic Island</category>
      <category>react native</category>
      <category>Widget</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/39</guid>
      <comments>https://devpluto.tistory.com/entry/React-Native-RN%EC%97%90%EC%84%9C-dynamic-island-%EC%9C%84%EC%A0%AF-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry39comment</comments>
      <pubDate>Fri, 24 May 2024 16:33:57 +0900</pubDate>
    </item>
    <item>
      <title>[React] ky로 로그인 유지 구현하기 (with. JWT 인증)</title>
      <link>https://devpluto.tistory.com/entry/React-ky%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A7%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-JWT-%EC%9D%B8%EC%A6%9D</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ky는 fetch와 ESM을 기반으로 하는 HTTP Client를 제공하는 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 사용되는 Axios의 &lt;span style=&quot;text-align: start;&quot;&gt;interceptors와&amp;nbsp;&lt;/span&gt;비슷하게 ky는 응답을 intercept 하여 커스텀하게 조작이 가능한 &lt;span style=&quot;text-align: start;&quot;&gt;AfterResponse, BeforeRequest와 같은 훅을&amp;nbsp;제공합&lt;/span&gt;니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 해당 훅을 이용하여 JWT 인증을 통해 로그인 상태를 처리하는 앱에서, 로그인을 유지하도록 하는 방법을 소개하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;API 요청 전 Header에 access token 담기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;beforeRequest 훅에서는 HTTP request 이전에 수행 할 동작을 지정할 수 있습니다. 따라서 아래와 같이 accessToken을 Authorization Header에 담는 로직을 구현합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707980884290&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { BeforeRequestHook } from 'ky';
import storage from '../../storage';

const setAuthorizationHeader: BeforeRequestHook = request =&amp;gt; {
  const accessToken = storage.getString('accessToken');

  if (!accessToken) return;
  request.headers.set('Authorization', `Bearer ${accessToken}`);
};

export default setAuthorizationHeader;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 기본 ApiClient의 beforeRequest hooks에 해당 함수를 지정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707981037329&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import setAuthorizationHeader from './beforeRequest';

export const apiClient = baseApiClient.extend({
  hooks: {
    beforeRequest: [setAuthorizationHeader],
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Retry로 access token 재발급 로직 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ky는 beforeRetry 훅을 통해 API fetch retry시 처리할 동작을 구현하는 것이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 구성은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. status code가 401이 아닌 경우 retry를 중지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. access token을 2번 가져와도 실패한다면, 토큰 만료인 경우로 판단하여 로그아웃 처리를 합니다.(유저 정보와 token을 삭제하는 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 해당 1, 2번 경우가 아닐 때 refresh token을 이용하여 access token을 가져오고, 저장합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707980790292&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ky, {BeforeRetryHook} from 'ky';
import UserService from '../../services/user';
import {ErrorResponseType} from '../services/type';
import {DEFAULT_API_RETRY_LIMIT} from '../../configs/api';

const beforeRetry: BeforeRetryHook = async ({error, retryCount}) =&amp;gt; {
  const customError = error as ErrorResponseType;
  if (customError.status !== 401) return ky.stop; // status code가 401이 아닌 경우 retry를 중지합니다.

  if (retryCount === DEFAULT_API_RETRY_LIMIT - 1) {
    await UserService.onLoginDurationExpired(); // access token을 2번 가져와도 실패한다면, 토큰 만료 로그아웃 시킵니다.
    return ky.stop;
  }
  await UserService.getAccessTokenByRefreshToken(); // refresh token을 이용하여 access token을 가져옵니다.
};

export default beforeRetry;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 해당 함수를 이전과 마찬가지로 beforeRetry에 지정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1707986530226&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const apiClient = baseApiClient.extend({
  timeout: DEFAULT_API_TIMEOUT, // 10 * 1000
  retry: {
    limit: DEFAULT_API_RETRY_LIMIT, // 4
    backoffLimit: DEFAULT_API_RETRY_BACKOFFLIMIT, // 3 * 1000
  },
  hooks: {
    beforeRequest: [setAuthorizationHeader],
    beforeRetry: [handleToken],
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 모든 설정을 마쳤다면, APIClient에 access token이 만료되었을 때 refresh token을 이용해 앱 로그인을 유지하도록 구현할 수 있습니다.&lt;/p&gt;</description>
      <category>Frontend/react</category>
      <category>accss token</category>
      <category>KY</category>
      <category>Refresh Token</category>
      <category>로그인 유지</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/38</guid>
      <comments>https://devpluto.tistory.com/entry/React-ky%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A7%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-with-JWT-%EC%9D%B8%EC%A6%9D#entry38comment</comments>
      <pubDate>Fri, 16 Feb 2024 02:05:28 +0900</pubDate>
    </item>
    <item>
      <title>[React] 함수의 중복 호출을 막기 위한 throttling 기능 적용기</title>
      <link>https://devpluto.tistory.com/entry/React-%ED%95%A8%EC%88%98%EC%9D%98-%EC%A4%91%EB%B3%B5-%ED%98%B8%EC%B6%9C%EC%9D%84-%EB%A7%89%EA%B8%B0-%EC%9C%84%ED%95%9C-throttling-%EA%B8%B0%EB%8A%A5-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제사항&lt;/h2&gt;
&lt;pre id=&quot;code_1704287187004&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const handleSubmit = async () =&amp;gt; {
  // API 호출...
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API를 호출하는 위와 같은 함수가 있습니다. 이러한 함수는 보통 button, input의 이벤트 함수에서 실행되는데, 예를 들어 유저가 단시간 내에 버튼을 여러번 클릭시 API 통신 오류가 발생하는 경우가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 해당 함수의 클릭 시간을 조절하는 throttle기능이 필요했고, 이를 구현한 방법을 소개하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;중복 호출을 막기 위한 useThrottle 훅 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;throttle기능을 구현하는 방법 중에는 여러 방법이 있지만, 저는 &lt;a href=&quot;https://slash.page/ko/libraries/react/react/src/hooks/usethrottle.i18n/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;slash 라이브러리&lt;/a&gt;처럼 함수 전체를 감싸서 throttle기능을 적용하는 방식으로 구현하고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 throttle의 callback의 시간을 관리하는 방법으로 Date 객체를 사용하는 방식을 생각했으며,&amp;nbsp;아래와 같이 throttle기능을 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704294498469&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useRef } from 'react';

const THROTTLE_DEFAULT_TIME = 1 * 1000;

const useThrottle = &amp;lt;T extends Function&amp;gt;(
  callback: T,
  throttleTime: number | undefined = THROTTLE_DEFAULT_TIME,
): Function =&amp;gt; {
  const time = useRef&amp;lt;ReturnType&amp;lt;Date['valueOf']&amp;gt;&amp;gt;(0);

  return () =&amp;gt; {
    const callbackExecutionTime = new Date().valueOf();

    if (callbackExecutionTime - time.current &amp;lt; throttleTime) return;

    time.current = callbackExecutionTime;
    callback();
  };
};

export default useThrottle;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 시간의 ms시간을 반환하는 Date의 valueOf 메서드를 이용해 callbackExecutionTime을 선언합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값과 useRef에 저장된 time값을 비교하여 throttleTime보다 작다면(throttle 중)실행 callback함수를 실행하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않은 경우 ref에 해당 시간을 저장한 다음 callback함수를 실행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다른 방식으로 구현하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 해당 방식은 아래와 같은 문제가 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;throttleTime이 정확하게 동작하지 않을 수 있습니다. 미미한 시간이지만 훅에서 Date를 생성하고, if문을 체크하며 ref에 저장하는 동작을 실행하는 동안의 시간이 있기 때문입니다.&lt;/li&gt;
&lt;li&gt;callback이 실행될 때마다 callbackExecutionTime을 할당하기 때문에, useRef에 할당된 값이 메모리에서 해제되지 않아 메모리 누수 현상이 발생할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 lodash에서 사용하는 방식인 setTimeout 함수를 이용하는 방식으로 useThrottle 훅을 구현해보았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1704294506635&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useRef } from 'react';

const THROTTLE_DEFAULT_TIME = 1 * 1000;

const useThrottle = &amp;lt;T extends Function&amp;gt;(
  callback: T,
  throttleTime: number | undefined = THROTTLE_DEFAULT_TIME,
): Function =&amp;gt; {
  const timer = useRef&amp;lt;ReturnType&amp;lt;typeof setTimeout&amp;gt; | null&amp;gt;(null);

  return () =&amp;gt; {
    if (timer.current) return;

    callback();
    timer.current = setTimeout(() =&amp;gt; {
      timer.current = null;
    }, throttleTime);
  };
};

export default useThrottle;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;timeout 함수를 ref에 저장한 다음, timeout이 완료되면 현재 ref를 null로 초기화합니다. 만약 timer가 존재한다면 callback 함수를 실행하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://minoo.medium.com/useref-%EA%B0%80-%EC%88%9C%EC%88%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%83%9D%EC%84%B1%ED%95%9C%EB%8B%A4%EB%8A%94-%EC%9D%98%EB%AF%B8%EB%A5%BC-%EA%B3%B1%EC%94%B9%EC%96%B4%EB%B3%B4%EA%B8%B0-8a0857fc5ebb&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;useRef에 할당된 값이 저장되는 heap 공간&lt;/a&gt;에 현재 시간의 string값(Date.valueof) 대신 setTimeout함수를 저장한다면 약간의 메모리 비용이 증가하겠지만, 성능상에 큰 차이점은 없을것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 메모리 누수 현상이 발생하는 이전 방법과 다르게 ref를 해제하기 때문에 더 나은 방법이라 할 수 있을 것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과적으로&lt;/h2&gt;
&lt;pre id=&quot;code_1704285458477&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const handleSubmit = useThrottle(async () =&amp;gt; {
  // API 호출...
});

const handleClick = useThrottle(async () =&amp;gt; {
  // API 호출...
}, 2 * 1000);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 방식으로든 useThrottle을 원하는 동작으로 사용할 수 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고문서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://minoo.medium.com/useref-%EA%B0%80-%EC%88%9C%EC%88%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%83%9D%EC%84%B1%ED%95%9C%EB%8B%A4%EB%8A%94-%EC%9D%98%EB%AF%B8%EB%A5%BC-%EA%B3%B1%EC%94%B9%EC%96%B4%EB%B3%B4%EA%B8%B0-8a0857fc5ebb&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://minoo.medium.com/useref-%EA%B0%80-%EC%88%9C%EC%88%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%83%9D%EC%84%B1%ED%95%9C%EB%8B%A4%EB%8A%94-%EC%9D%98%EB%AF%B8%EB%A5%BC-%EA%B3%B1%EC%94%B9%EC%96%B4%EB%B3%B4%EA%B8%B0-8a0857fc5ebb&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend/react</category>
      <category>Hook</category>
      <category>react</category>
      <category>useThrottle</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/37</guid>
      <comments>https://devpluto.tistory.com/entry/React-%ED%95%A8%EC%88%98%EC%9D%98-%EC%A4%91%EB%B3%B5-%ED%98%B8%EC%B6%9C%EC%9D%84-%EB%A7%89%EA%B8%B0-%EC%9C%84%ED%95%9C-throttling-%EA%B8%B0%EB%8A%A5-%EC%A0%81%EC%9A%A9%EA%B8%B0#entry37comment</comments>
      <pubDate>Thu, 4 Jan 2024 00:06:42 +0900</pubDate>
    </item>
    <item>
      <title>[React Native] deeplink를 이용해 알림을 여는 방법(with. notifee)</title>
      <link>https://devpluto.tistory.com/entry/React-Native-deeplink%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%95%8C%EB%A6%BC%EC%9D%84-%EC%97%AC%EB%8A%94-%EB%B0%A9%EB%B2%95with-notifee</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가기 전에&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥링크(deeplink)는 특정 페이지에 도달 할 수 있는 링크를 말합니다. 딥링크를 이용하여 React Native 어플리케이션의 특정 페이지에 도달시킬 수 있습니다. 예를들어 'app://targetPage' 링크를 딥링크로 열게되면 targetPage가 표시됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현 요구사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 아래와 같이 notifee를 이용하여 서버에서 보내는 데이터인 notifee.data 영역에 deepLinkUrl을 담습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 알림을 전송하고, 앱에서 알림을 받으면 해당 deeplink url을 이용하여 앱을 열어야 하도록 구현합니다. 하지만 이런 방식은 레퍼런스가 없어 기능을 직접 구현하였고,지금부터 구현한 방법을 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703587878946&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;data&quot;: {
  &quot;notifee&quot;: {
    &quot;id&quot;: &quot;1&quot;,
    &quot;title&quot;: &quot;공지사항을 확인해 보세요!&quot;,
    &quot;subtitle&quot;: &quot;학사공지&quot;,
    &quot;body&quot;: &quot;서울시립대학교 대학원 시행세칙 및 통합대학원 학칙 일부개정(안) 사전예고&quot;,
    &quot;data&quot;: {
      &quot;deepLinkUrl&quot;: &quot;uoslife://announcement/detail/123&quot;
    },
    // ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트에 deep link 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세팅되어있는 React Native 프로젝트에서 아래 명령어를 사용하면 플랫폼 별로 알맞게 deep linking 환경 설정이 됩니다. 자세한 사항은 &lt;a href=&quot;https://reactnavigation.org/docs/deep-linking/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;해당 공식 문서&lt;/a&gt;를 참조해주세요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npx uri-scheme add 'deeplink 프로젝트이름' --ios&lt;br /&gt;npx uri-scheme add 'deeplink 프로젝트이름' --android&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;screen에 따라 deepLink url 설정하기&lt;/h3&gt;
&lt;pre id=&quot;code_1703590766714&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const DEEPLINK_PREFIX_URL = ['uoslife://'];

const deepLinksConfig = {
  initialRouteName: 'Main',
  screens: {
    Main: {
      initialRouteName: 'MainTab',
      screens: {
        // ...
      },
    },
    Library: 'library',
    Announcement: {
      initialRouteName: 'AnnouncementMain',
      screens: {
        AnnouncementMain: 'announcement',
        AnnouncementDetail: 'announcement/detail/:id',
        // ...
      },
    },
  },
};

const linking: LinkingOptions&amp;lt;RootStackParamList&amp;gt; = {
  prefixes: DEEPLINK_PREFIX_URL,
  config: deepLinksConfig,
  async getInitialURL() {
    // 딥링크를 이용해서 앱이 오픈되었을 때
    const url = await Linking.getInitialURL();
    if (url != null) return url;
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 해당 linking config를 NavigationConatiner에 넘겨줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703590888106&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;NavigationContainer linking={linking} ...&amp;gt;&amp;lt;/NavigationContainer&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앱 알림을 클릭했을 때 딥링크 열기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 앱에 deeplink설정을 완료했으니 원하는 동작인 앱 알림을 클릭했을 때 해당 딥링크를 여는 코드를 작성해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;앱이 Foreground 상태일 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deeplink는 react-native의 Linking.openURL에 url을 넣으면 열 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;notifee의 onForegroundEvent 메서드에 클릭 시 딥링크를 여는 onPressEvent를 추가하는 방식으로 구현합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703619638693&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Linking } from 'react-native';
import notifee, { Event } from '@notifee/react-native';

// ...

static async onPressEvent({ type, detail }: Event): Promise&amp;lt;void&amp;gt; {
    if (type !== EventType.PRESS) return;
    const {notification} = detail;
    if (!notification || !notification.data || !notification.data.deepLinkUrl)
      return;
    await Linking.openURL(notification.data.deepLinkUrl as string);
  }

static onForegroundEvent(): () =&amp;gt; void {
  return notifee.onForegroundEvent(
    async event =&amp;gt; await this.onPressEvent(event),
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 해당 onForegroundEvent 메서드를 App.tsx파일에서 실행시킵니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703619690390&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx
useEffect(() =&amp;gt; {
  NotificationService.onForegroundEvent();
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;앱이 Background 상태일 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백그라운드 상태일 때는 앱에 백그라운드 이벤트 이를 위해 notifee.getInitialNotification() 메서드를 사용합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;getInitialNotification&lt;br /&gt;: This API can be used to fetch which notification &amp;amp; press action has caused the application to open.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱을 열었을 때 해당 메서드를 이용하여 press action이 되어 들어왔는지 확인하고, 있다면 해당 notification data에서 deepLinkUrl을 이용해 딥링크를 열게 하는 동작을 구현하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따라 처음 구현은 아래와 같았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703590232862&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// App.tsx
useEffect(() =&amp;gt; {
  (async () =&amp;gt; {
    const initialNotification = await notifee.getInitialNotification();
    if (initialNotification) {
      const {data} = initialNotification.notification;
      if (!data || !data.deepLinkUrl) return;
      await Linking.openUrl(data.deepLinkUrl);
    }
  })();
}, [])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 앱 초기로딩 시 로그인 상태를 변경하는 과정에서 문제가 발생했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TroubleShooting: 로그인 되지 않은 상태에서 발생하는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Native Navigation에서는 로그인 상태를 처리하기 위해 아래 방식과 같이 isLoggedIn 상태에 따라 각기 다른 화면을 렌더링합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703589974717&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Stack.Navigator ...&amp;gt;
  {isLoggedIn ? (
    &amp;lt;&amp;gt;
      &amp;lt;Stack.Screen name=&quot;Main&quot; /&amp;gt;
      &amp;lt;Stack.Screen name=&quot;Mypage&quot; /&amp;gt;
      // ...
      /&amp;gt;
    ) : (
    &amp;lt;&amp;gt;
      &amp;lt;Stack.Screen name=&quot;Account&quot; /&amp;gt;
      // ...
    &amp;lt;/&amp;gt;
  )}
&amp;lt;/Stack.Navigator&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 isLoggedIn은 초기값이 false이기 때문에 앱 초기 로딩 과정에서 로그인 여부를 파악할 때 까지 Account 화면이 표시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황에서 만약&amp;nbsp; '~://main' 으로 deepLink 요청이 온다면 Main 페이지는 실제로 렌더링 되지 않았기 때문에 아래과 같은 오류가 발생했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-13 오전 3.12.33.png&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0ROHl/btsCyJPcqFN/oROjLLNoIr70d5lZ82Zmj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0ROHl/btsCyJPcqFN/oROjLLNoIr70d5lZ82Zmj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0ROHl/btsCyJPcqFN/oROjLLNoIr70d5lZ82Zmj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0ROHl%2FbtsCyJPcqFN%2FoROjLLNoIr70d5lZ82Zmj0%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;635&quot; height=&quot;215&quot; data-filename=&quot;스크린샷 2023-12-13 오전 3.12.33.png&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 로그인 로직 이후 deeplink url을 열어줘야 했고, 이 문제를 전달받은 url을 storage에 저장해서 넘겨주는 방식으로 해결했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 'openedDeepLinkUrl' key에 notifee에서 받은 deepLinkUrl을 저장합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703591288214&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const linking = {
  prefixes: DEEPLINK_PREFIX_URL,
  config: deepLinksConfig,
  async getInitialURL() {
    // 딥링크를 이용해서 앱이 오픈되었을 때
    const url = await Linking.getInitialURL();

    if (url != null) return url;

    // 백그라운드에서 알림 클릭 시 deepLink가 있는 경우 해당 url을 storage에 저장
    const initialNotification = await notifee.getInitialNotification();
    if (!initialNotification) return null;
    const {data} = initialNotification.notification;
    if (!data || !data.deepLinkUrl) return null;
    storage.set('openedDeepLinkUrl', data.deepLinkUrl as string);
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 이후 앱 로딩과 로그인이 완료된 이후, 해당 key를 가져와 딥링크를 open합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703591299244&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ...set app loading and login status true
const openedDeepLinkUrl = storage.getString('openedDeepLinkUrl');
if (openedDeepLinkUrl) {
  await Linking.openURL(openedDeepLinkUrl);
  storage.delete('openedDeepLinkUrl');
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구현하면 앱 로딩이 완료된 상태, 그리고 로그인이 완료된 상태에서만 deeplink가 열리게 되므로 유저가 느끼는 동작에도 문제가 없고, 상기 에러가 발생하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github의 react-navigation에도 &lt;a href=&quot;https://github.com/react-navigation/react-navigation.github.io/issues/97&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;해당 issue&lt;/a&gt;가 오픈되어 있는데, 로그인 상태에서 딥링크 공식적인 메뉴얼이 나와있지 않았기에 여러 방법이 제시되어 있었고 저는 위와 같은 방법을 제시했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-26 오후 8.46.16.png&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;1378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG48A8/btsCF719ZNa/NCgqCkYHTeu3WIazyOmFWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG48A8/btsCF719ZNa/NCgqCkYHTeu3WIazyOmFWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG48A8/btsCF719ZNa/NCgqCkYHTeu3WIazyOmFWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG48A8%2FbtsCF719ZNa%2FNCgqCkYHTeu3WIazyOmFWK%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;560&quot; height=&quot;415&quot; data-filename=&quot;스크린샷 2023-12-26 오후 8.46.16.png&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;1378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 알림을 deepLink로 열기 위해 react native에 딥링크 환경설정을 하고, 앱이 기기에서 foreground일 때와 background 상태일 때의 코드를 각각 작성하여 알림을 열었을 때 해당 deeplink url로 이동하는 동작을 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 구현 요구사항과 일치하게 동작이 잘 되는 모습을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciDAVg/btsC4JeHufy/l8SlWjZ6KCJkKEnXXXzGwk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciDAVg/btsC4JeHufy/l8SlWjZ6KCJkKEnXXXzGwk/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1267&quot; data-is-animation=&quot;true&quot; data-filename=&quot;ezgif-1-52e266a114.gif&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciDAVg/btsC4JeHufy/l8SlWjZ6KCJkKEnXXXzGwk/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciDAVg%2FbtsC4JeHufy%2Fl8SlWjZ6KCJkKEnXXXzGwk%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;1267&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OBrA5/btsCZ6aVOI7/rxNUktqrBLmIAU59t80lQ0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OBrA5/btsCZ6aVOI7/rxNUktqrBLmIAU59t80lQ0/img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1267&quot; data-is-animation=&quot;true&quot; data-filename=&quot;ezgif-1-e18719371e.gif&quot; width=&quot;252&quot; height=&quot;532&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.4186%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OBrA5/btsCZ6aVOI7/rxNUktqrBLmIAU59t80lQ0/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOBrA5%2FbtsCZ6aVOI7%2FrxNUktqrBLmIAU59t80lQ0%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;1267&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;좌: foreground 환경, 우: background 환경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 있는 부분이 있거나, 이해가 되지 않는 부분이 있다면 댓글로 남겨주세요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;+) TroubleShooting2: 뒤로가기가 되지 않는 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nested된, 즉 여러 화면 내부에 존재하는 화면에서 뒤로가기 동작시 아래와 같은 에러가 발생했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703591559150&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// nested screen
const handlePressBackButton = () =&amp;gt; {
  navigation.goBack();
};

return (
  &amp;lt;Header label={label} onPressBackButton={handlePressBackButton} /&amp;gt;
  // ...
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-12-13 오전 3.10.16.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDzz71/btsCCzY6MBb/dc6GtI9bOU73XqxV3tSLtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDzz71/btsCCzY6MBb/dc6GtI9bOU73XqxV3tSLtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDzz71/btsCCzY6MBb/dc6GtI9bOU73XqxV3tSLtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDzz71%2FbtsCCzY6MBb%2Fdc6GtI9bOU73XqxV3tSLtk%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;1080&quot; height=&quot;156&quot; data-filename=&quot;스크린샷 2023-12-13 오전 3.10.16.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;linking config에 아래와 같이 initialRouteName을 추가하면 해당 에러가 발생하지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1703591730582&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const deepLinksConfig = {
  initialRouteName: 'Main', // 추가
  screens: {
    Main: {
      initialRouteName: 'MainTab', // 추가
      screens: {
        // ...
      },
    },&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Frontend/react native</category>
      <category>deeplink</category>
      <category>deeplinking</category>
      <category>notification</category>
      <category>react native</category>
      <category>react navigation</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/36</guid>
      <comments>https://devpluto.tistory.com/entry/React-Native-deeplink%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%95%8C%EB%A6%BC%EC%9D%84-%EC%97%AC%EB%8A%94-%EB%B0%A9%EB%B2%95with-notifee#entry36comment</comments>
      <pubDate>Wed, 3 Jan 2024 21:25:05 +0900</pubDate>
    </item>
    <item>
      <title>[iOS] 스플래시 화면이 찌그러지는 문제</title>
      <link>https://devpluto.tistory.com/entry/iOS-%EC%8A%A4%ED%94%8C%EB%9E%98%EC%8B%9C-%ED%99%94%EB%A9%B4%EC%9D%B4-%EC%B0%8C%EA%B7%B8%EB%9F%AC%EC%A7%80%EB%8A%94-%EB%AC%B8%EC%A0%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 접근시 아래와 같이 splash screen이 xcode에서 작성한대로 표시가 되지 않고 로고(png)가 원래 사이즈대로 표시되는 문제가 발생했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif-3-9cb9f6b36b.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NFY1w/btsAGY7Dq64/HyhvMYql41XKywpM2fpV0k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NFY1w/btsAGY7Dq64/HyhvMYql41XKywpM2fpV0k/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NFY1w/btsAGY7Dq64/HyhvMYql41XKywpM2fpV0k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/NFY1w/btsAGY7Dq64/HyhvMYql41XKywpM2fpV0k/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;261&quot; height=&quot;566&quot; data-filename=&quot;ezgif-3-9cb9f6b36b.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결방안&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-22 오전 6.40.30.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;1152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d5ekB3/btsAFynTMID/VobbGyyrwM1iYUIDlMhlrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d5ekB3/btsAFynTMID/VobbGyyrwM1iYUIDlMhlrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5ekB3/btsAFynTMID/VobbGyyrwM1iYUIDlMhlrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd5ekB3%2FbtsAFynTMID%2FVobbGyyrwM1iYUIDlMhlrk%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;681&quot; height=&quot;507&quot; data-filename=&quot;스크린샷 2023-11-22 오전 6.40.30.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;1152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 로고 이미지의 하단에 'Add New Constraints' 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn03SQ/btsAF50fjF8/CcwnJxnGAN6UNVx84GyYEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn03SQ/btsAF50fjF8/CcwnJxnGAN6UNVx84GyYEk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;728&quot; data-filename=&quot;스크린샷 2023-11-22 오전 6.44.12.png&quot; style=&quot;width: 48.2167%; margin-right: 10px;&quot; data-widthpercent=&quot;48.78&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn03SQ/btsAF50fjF8/CcwnJxnGAN6UNVx84GyYEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn03SQ%2FbtsAF50fjF8%2FCcwnJxnGAN6UNVx84GyYEk%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;560&quot; height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L0QKO/btsAF7Rkfsw/l78IAZz3Ij8rBKzxdgk1n0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L0QKO/btsAF7Rkfsw/l78IAZz3Ij8rBKzxdgk1n0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;554&quot; data-origin-height=&quot;686&quot; data-filename=&quot;스크린샷 2023-11-22 오전 6.44.37.png&quot; style=&quot;width: 50.6205%;&quot; data-widthpercent=&quot;51.22&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L0QKO/btsAF7Rkfsw/l78IAZz3Ij8rBKzxdgk1n0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL0QKO%2FbtsAF7Rkfsw%2Fl78IAZz3Ij8rBKzxdgk1n0%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;554&quot; height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 간격을 모두 0으로 설정한 다음, 'Constrain to margins' 버튼을 선택 해제한 후 Add 4 Constraints 버튼을 눌러 새 Constraint를 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-22 오전 6.51.39.png&quot; data-origin-width=&quot;2044&quot; data-origin-height=&quot;1100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bam7e8/btsAJ37MsO0/sODqufNwK2q6o1FO5Vskkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bam7e8/btsAJ37MsO0/sODqufNwK2q6o1FO5Vskkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bam7e8/btsAJ37MsO0/sODqufNwK2q6o1FO5Vskkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbam7e8%2FbtsAJ37MsO0%2FsODqufNwK2q6o1FO5Vskkk%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;2044&quot; height=&quot;1100&quot; data-filename=&quot;스크린샷 2023-11-22 오전 6.51.39.png&quot; data-origin-width=&quot;2044&quot; data-origin-height=&quot;1100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0c0d0e; text-align: left;&quot;&gt;3. constrains의 trailing, leading의 기준을 safe area로 설정하고, 마찬가지로 Top, Bottom의 기준을 superView로 설정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-22 오전 6.53.00.png&quot; data-origin-width=&quot;2040&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bchqF0/btsAD1w9I7e/bBPlfTxM7FWWmqX5AMrNK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bchqF0/btsAD1w9I7e/bBPlfTxM7FWWmqX5AMrNK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bchqF0/btsAD1w9I7e/bBPlfTxM7FWWmqX5AMrNK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbchqF0%2FbtsAD1w9I7e%2FbBPlfTxM7FWWmqX5AMrNK0%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;2040&quot; height=&quot;1074&quot; data-filename=&quot;스크린샷 2023-11-22 오전 6.53.00.png&quot; data-origin-width=&quot;2040&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 마지막으로 전체 View의 Content Mode를 'Scale To Fill'로 설정합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif-4-434b916dc4.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxgJMU/btsAFFtY9De/mMdqo5PMEhKOGJKJrJ5tu0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxgJMU/btsAFFtY9De/mMdqo5PMEhKOGJKJrJ5tu0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxgJMU/btsAFFtY9De/mMdqo5PMEhKOGJKJrJ5tu0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dxgJMU/btsAFFtY9De/mMdqo5PMEhKOGJKJrJ5tu0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;295&quot; height=&quot;640&quot; data-filename=&quot;ezgif-4-434b916dc4.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;1302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 방법대로 설정을 완료했다면 의도한대로 splash screen이 표시됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/aparajita/capacitor-splash-screen/issues/8&quot;&gt;https://github.com/aparajita/capacitor-splash-screen/issues/8&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/65008327/my-launch-screen-image-has-a-wrong-size-on-small-screens&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/65008327/my-launch-screen-image-has-a-wrong-size-on-small-screens&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 링크는 스플래시 이미지를 만들고 적용하는 방법에 대해 자세히 설명한 글이니 참고해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://staktree.github.io/ios/IOS-splash-01/&quot;&gt;https://staktree.github.io/ios/IOS-splash-01/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>IOS</category>
      <category>Splash screen</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/35</guid>
      <comments>https://devpluto.tistory.com/entry/iOS-%EC%8A%A4%ED%94%8C%EB%9E%98%EC%8B%9C-%ED%99%94%EB%A9%B4%EC%9D%B4-%EC%B0%8C%EA%B7%B8%EB%9F%AC%EC%A7%80%EB%8A%94-%EB%AC%B8%EC%A0%9C#entry35comment</comments>
      <pubDate>Wed, 22 Nov 2023 07:00:22 +0900</pubDate>
    </item>
    <item>
      <title>[React Native] 앱 접근시 필요한 동작을 구현해보자</title>
      <link>https://devpluto.tistory.com/entry/React-Native-%EC%95%B1-%EC%A0%91%EA%B7%BC%EC%8B%9C-%ED%95%84%EC%9A%94%ED%95%9C-%EB%8F%99%EC%9E%91%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kScmj/btsAGUW9BxA/hDf7X309R0kMy48qOr7cW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kScmj/btsAGUW9BxA/hDf7X309R0kMy48qOr7cW0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1560&quot; data-filename=&quot;로그인.png&quot; width=&quot;215&quot; height=&quot;466&quot; data-widthpercent=&quot;52.06&quot; style=&quot;width: 51.4537%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kScmj/btsAGUW9BxA/hDf7X309R0kMy48qOr7cW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkScmj%2FbtsAGUW9BxA%2FhDf7X309R0kMy48qOr7cW0%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;720&quot; height=&quot;1560&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDMkfh/btsAFGSDLQf/IlhJyqZT9lmiOQANF0pPY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDMkfh/btsAFGSDLQf/IlhJyqZT9lmiOQANF0pPY0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1694&quot; data-filename=&quot;첫화면(수정).png&quot; style=&quot;width: 47.3835%;&quot; data-widthpercent=&quot;47.94&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDMkfh/btsAFGSDLQf/IlhJyqZT9lmiOQANF0pPY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDMkfh%2FbtsAFGSDLQf%2FIlhJyqZT9lmiOQANF0pPY0%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;720&quot; height=&quot;1694&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;시대생 첫화면, Account / Main&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시대생 앱은 최초 앱 접근시 로그인 유무에 따라 AccountScreen을 보여주거나, MainScreen을 보여줘야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 로그인이 되어있다면 device 정보를 서버에 업데이트 시키거나, 알림을 위한 firebasePushToken 정보를 얻어오는 과정이 필요하기 때문에 이와 관련된 로직이 앱 접근시 실행되어야합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;본 글은 앱 접근시 어떤 로직이 실행되도록 구현했는지, 그리고 어떻게 앱의 전체적인 회원 로그인 관련 처리를 구현했는지를 설명합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작하기 앞서&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시대생 앱의 사용자 인증 인가 방식은 JWT 방식을 사용합니다.&lt;/li&gt;
&lt;li&gt;React Native의 Device Storage로는 mmkv를 이용합니다.&lt;/li&gt;
&lt;li&gt;앱 화면을 보여주는 RootStackNavigator는 아래와 같습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1700519024250&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const [isLoading, setIsLoading] = useState(true);
const [isLoggedIn, setIsLoggedIn] = useState(false);

useEffect(() =&amp;gt; {
  if (isLoading) return;
  (async () =&amp;gt; await SplashScreen.hide())();
}, [isLoading]);

return (
  &amp;lt;Stack.Navigator
    initialRouteName=&quot;Main&quot;
    {isLoggedIn ? (
      &amp;lt;&amp;gt;
        &amp;lt;Stack.Screen
          name=&quot;Main&quot;
          component={RootBottomTapNavigator}
          options={{animationEnabled: false}}
        /&amp;gt;
        // ...
      &amp;lt;/&amp;gt;
    ) : (
      &amp;lt;Stack.Screen name=&quot;Account&quot; component={AccountStackNavigator} /&amp;gt;
    )}
  &amp;lt;/Stack.Navigator&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;본 글에서 설명할 앱 접근 시 로직과 앱 내 인증 인가 처리를 전체 구조를 그려보았습니다. 아래 이미지는 해당 구조도입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled-2023-11-09-1341.png&quot; data-origin-width=&quot;3232&quot; data-origin-height=&quot;2990&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bH7Vy9/btsAy8bADL3/HpkWvMKR7gpHSoa13DJNI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bH7Vy9/btsAy8bADL3/HpkWvMKR7gpHSoa13DJNI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH7Vy9/btsAy8bADL3/HpkWvMKR7gpHSoa13DJNI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH7Vy9%2FbtsAy8bADL3%2FHpkWvMKR7gpHSoa13DJNI0%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;758&quot; height=&quot;701&quot; data-filename=&quot;Untitled-2023-11-09-1341.png&quot; data-origin-width=&quot;3232&quot; data-origin-height=&quot;2990&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앱 접근시 실행 로직&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 접근시 처리해야 하는 과정은 아래와 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;notification permission을 가져온다.&lt;/li&gt;
&lt;li&gt;알림권한에 동의했다면 FCM Token을 가져오고, Device Storage에 저장한다.&lt;/li&gt;
&lt;li&gt;로그인 여부를 확인하기 위해 userInfo API를 서버에 요청한다.&lt;/li&gt;
&lt;li&gt;응답이 401이라면 로그인 상태가 아니기 때문에, 앱의 로그인 상태를 false로 만든 후 Account Screen을 보여준다.&lt;/li&gt;
&lt;li&gt;응답이 200이라면 앱의 로그인 상태를 true로 만든다.&lt;/li&gt;
&lt;li&gt;이후 응답받은 userInfo를 Device Storage에 저장한다.&lt;/li&gt;
&lt;li&gt;현재 Device의 정보(fcm token, app version 등)가 서버와 다르다면 최신 정보로 서버에 업데이트한다.&lt;/li&gt;
&lt;li&gt;로그인 상태를 true로 만든 후 Main Screen을 보여준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 코드로 아래와 같이 구현했습니다. 이를 하나하나 자세히 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700518319219&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  useEffect(() =&amp;gt; {
    (async () =&amp;gt; {
      await NotificationService.requestNotificationPermissions();
      await NotificationService.handleFirebasePushToken();

      const hasRefreshToken = UserService.getHasRefreshToken();
      if (!hasRefreshToken) {
        setLoadingFinish();
        return;
      }

      const userInfo = await UserService.getUserInfoFromServer();
      if (!userInfo) {
        setLoadingFinish();
        return;
      }
      UserService.setUserInfoToDevice(userInfo);

      await DeviceService.updateDeviceInfo();
      setAuthenticationSuccess();
    })();
  }, []);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FCM Token 관련 로직&lt;/h3&gt;
&lt;pre id=&quot;code_1700518386348&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;await NotificationService.requestNotificationPermissions();
await NotificationService.handleFirebasePushToken();&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;FCM Token은 한 device마다 생성됩니다. token을 가져오기 위해 알림 권한을 받아오고, 권한이 없다면 토큰을 받아오지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700518573793&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static async requestNotificationPermissions(): Promise&amp;lt;FirebaseMessagingTypes.AuthorizationStatus&amp;gt; {
  if (Platform.OS === 'android') {
    await Promise.all([
      // 생략
      // permission을 받고, notifee channel을 생성
    ]);
  }
  return messaging().requestPermission();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS에서는 토큰을 가져오는 messaging.getToken() 함수는 실행 이전에 requestPermission이 필요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;FCM token을 가져오는 방법에 대해서는 양이 많기 때문에 따로 포스팅 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700518639629&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static async handleFirebasePushToken(): Promise&amp;lt;void&amp;gt; {
  const isPermissionAuthorized =
    await this.checkPermissionIsAuthorizedStatus(); // messaging.hasPermission을 이용
  if (!isPermissionAuthorized) return;

  const token = await this.getFirebasePushToken();
  this.setFirebasePushToken(token);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한이 없다면 FCM 토큰을 가져오지 않고, 있다면 토큰을 가져온 다음 device storage에 해당 토큰을 저장합니다. 각 메서드들은 class 내부에 추상화하여 사용했습니다.&lt;span style=&quot;color: #000000; font-size: 1.44em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Storage의 RefreshToken 유무 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Device Storage에 refreshToken이 존재하지 않는다면 로딩을 종료합니다. 이 과정을 추가함으로써 refreshToken 유무만 확인하면 되므로 초기에 빠른 로딩이 가능해졌습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700518741312&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const hasRefreshToken = UserService.getHasRefreshToken();
if (!hasRefreshToken) {
  setLoadingFinish();
  return;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UserInfo API 요청 및 저장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT token을 이용하여 로그인 여부를 확인하기 위해 유저의 정보(닉네임, id등)를 가져오는 API를 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답이 400, 즉 유저 정보가 없다면 마찬가지로 로딩을 종료합니다.&lt;br /&gt;응답이 200으로 정상적으로 온다면 device storage에 응답받은 유저 정보를 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 accessToken이 만료되어 refreshToken을 이용해 재발급 받는 로직은 여기서 다루지 않고, 따로 포스팅하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700519177752&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const userInfo = await UserService.getUserInfoFromServer();
if (!userInfo) {
  setLoadingFinish();
  return;
}
UserService.setUserInfoToDevice(userInfo);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디바이스 정보 업데이트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700519211185&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;await DeviceService.updateDeviceInfo();
setAuthenticationSuccess();&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1700519265097&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static async updateDeviceInfo(): Promise&amp;lt;void&amp;gt; {
  const localDeviceInfo = this.getDeviceInfoFromLocal();
  const serverDeviceInfo = await this.getDeviceInfoFromServer();
  if (
    localDeviceInfo.appVersion !== serverDeviceInfo.appVersion ||
    localDeviceInfo.codePushVersion !== serverDeviceInfo.codePushVersion ||
    localDeviceInfo.firebasePushToken !==
      serverDeviceInfo.firebasePushToken ||
    // ...
  ) {
    await CoreAPI.patchDeviceInfo(localDeviceInfo);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 앱 또는 OS를 업데이트 하거나 FCM token이 업데이트 되는 상황같아 device 정보가 수정 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 만약 현재 device 정보가 서버와 다르다면 서버로 patch시키도록 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700519490115&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const setAuthenticationSuccess = () =&amp;gt; {
  storage.set('isLoggedIn', true);
  setIsLoggedIn(true);
  setLoadingFinish();
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 모든 과정이 종료되었다면 isLoggedIn과 isLoading상태를 true로 변경합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앱에서 로그인 상태가 변경되는 경우&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그인 / 회원가입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 또는 회원가입하는 경우 아래와 같은 작업을 수행합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로그인 / 회원가입 API의 응답에서 얻은 JWT token을 device storage에 저장&lt;/li&gt;
&lt;li&gt;deviceInfo를 서버에 저장&lt;/li&gt;
&lt;li&gt;userInfo를 받아와 device storage에 저장&lt;/li&gt;
&lt;li&gt;isLoggedIn 상태를 true로 변경&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1700519683900&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** SingIn 또는 SingUp시 실행되는 함수입니다. */
static async onRegister(params: OnRegisterParamsType): Promise&amp;lt;void&amp;gt; {
  storeToken(params.accessToken, params.refreshToken);
  await DeviceService.setDeviceInfo();
  await UserService.handleUserInfo();
  storage.set('isLoggedIn', true);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그아웃&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;device storage에 저장된 JWT token과 userInfo, isLoggedIn 상태를 삭제합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700519929717&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static deleteUserInfo(): void {
  storage.delete('accessToken');
  storage.delete('refreshToken');
  storage.delete('user');
  storage.set('isLoggedIn', false);
};
  
static logout(): void {
  this.deleteUserInfo();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의점: 직접적으로 Screen을 navigate하면 안됩니다.&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-20 오후 8.05.06.png&quot; data-origin-width=&quot;1666&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEhnir/btsAw6yPqhR/bZT4ShUdr3yEeyZrmIdtLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEhnir/btsAw6yPqhR/bZT4ShUdr3yEeyZrmIdtLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEhnir/btsAw6yPqhR/bZT4ShUdr3yEeyZrmIdtLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEhnir%2FbtsAw6yPqhR%2FbZT4ShUdr3yEeyZrmIdtLK%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;1666&quot; height=&quot;498&quot; data-filename=&quot;스크린샷 2023-11-20 오후 8.05.06.png&quot; data-origin-width=&quot;1666&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서 설명하듯이 직접적으로 useNavigation 훅 등을 이용하여 스크린으로 navigate 시키면 안됩니다. 대신에 isSignedIn, isLoggedIn과 같은 state를 이용하여 Account / Home Screen을 렌더링해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1700520041873&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 로그인 완료시
const navigation = useNavigation();
navigation.navigate('Main') // x

setIsLoggedIn(true) // o&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://reactnavigation.org/docs/auth-flow/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://reactnavigation.org/docs/auth-flow/&lt;/a&gt;&lt;/p&gt;</description>
      <category>Frontend/react native</category>
      <category>react native</category>
      <category>앱 초기 로딩</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/34</guid>
      <comments>https://devpluto.tistory.com/entry/React-Native-%EC%95%B1-%EC%A0%91%EA%B7%BC%EC%8B%9C-%ED%95%84%EC%9A%94%ED%95%9C-%EB%8F%99%EC%9E%91%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90#entry34comment</comments>
      <pubDate>Tue, 21 Nov 2023 07:52:57 +0900</pubDate>
    </item>
    <item>
      <title>[Yarn] Yarn v2에서 npm registry를 바라보도록 환경설정하기</title>
      <link>https://devpluto.tistory.com/entry/Yarn-Yarn-v2%EC%97%90%EC%84%9C-npm-registry%EB%A5%BC-%EB%B0%94%EB%9D%BC%EB%B3%B4%EB%8F%84%EB%A1%9D-%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-08 오전 4.09.12.png&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bD0eUp/btstr29m3At/k8CfvmabpxMezArxpwvQ8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bD0eUp/btstr29m3At/k8CfvmabpxMezArxpwvQ8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bD0eUp/btstr29m3At/k8CfvmabpxMezArxpwvQ8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbD0eUp%2Fbtstr29m3At%2Fk8CfvmabpxMezArxpwvQ8K%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;682&quot; height=&quot;142&quot; data-filename=&quot;스크린샷 2023-09-08 오전 4.09.12.png&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn은 위와 같이 기본적으로 registry.yarnpkg.com을 바라본다.&lt;br /&gt;따라서 npm registry를 사용하는 사내 패키지를 받기 위해서 yarn이 npm registry url을 바라보도록 설정해줘야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;npmRegistryServer 옵션 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.yarnrc.yml파일에 npmRegistryServer 옵션을 아래와 같이 설정하면 이제 npm registry로 바라본다.&lt;/p&gt;
&lt;pre id=&quot;code_1694114060131&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npmRegistryServer: 'https://npm.pkg.github.com'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 설정하게 되면 모든 패키지를 설치할 때 해당 npm registry를 바라보기 때문에 yarn에서 받아와야하는 기존 패키지들을 설치하지 못한다는 문제가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-08 오전 4.16.46.png&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mqrx1/btstlK9Zszo/7J2SQHgIjxN2sM6kVjHp4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mqrx1/btstlK9Zszo/7J2SQHgIjxN2sM6kVjHp4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mqrx1/btstlK9Zszo/7J2SQHgIjxN2sM6kVjHp4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmqrx1%2FbtstlK9Zszo%2F7J2SQHgIjxN2sM6kVjHp4K%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;791&quot; height=&quot;114&quot; data-filename=&quot;스크린샷 2023-09-08 오전 4.16.46.png&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 사내 패키지만 특정적으로 npm registry를 바라보도록 설정해주어야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;npmScopes 옵션 설정&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-08 오전 4.15.47.png&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7cwfS/btstlfvrgAB/hkdDJZVte7Hy0KATZTUIkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7cwfS/btstlfvrgAB/hkdDJZVte7Hy0KATZTUIkK/img.png&quot; data-alt=&quot;yarnrc.yml settings&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7cwfS/btstlfvrgAB/hkdDJZVte7Hy0KATZTUIkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7cwfS%2FbtstlfvrgAB%2FhkdDJZVte7Hy0KATZTUIkK%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;422&quot; height=&quot;211&quot; data-filename=&quot;스크린샷 2023-09-08 오전 4.15.47.png&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;494&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;yarnrc.yml settings&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarnrc는 위처럼 npmScopes옵션을 제공해서, 특정 패키지 이름(보통 회사명)에 해당하는 npmRegistryServer를 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 회사명은 @를 떼고 설정해주어야 한다!&lt;/p&gt;
&lt;pre id=&quot;code_1694114124488&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npmScopes:
  'company-name':
    npmRegistryServer: 'https://npm.pkg.github.com'&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-08 오전 4.21.15.png&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kBmQL/btstrOwxWwQ/s7p8L8zqZNaZty83Wk1ahK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kBmQL/btstrOwxWwQ/s7p8L8zqZNaZty83Wk1ahK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kBmQL/btstrOwxWwQ/s7p8L8zqZNaZty83Wk1ahK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkBmQL%2FbtstrOwxWwQ%2Fs7p8L8zqZNaZty83Wk1ahK%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;690&quot; height=&quot;182&quot; data-filename=&quot;스크린샷 2023-09-08 오전 4.21.15.png&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 회사 특정적으로 npm registry에 있는 패키지가 잘 받아와지는 것을 확인할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고문서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yarnpkg.com/configuration/yarnrc#npmRegistryServer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://yarnpkg.com/configuration/yarnrc#npmRegistryServer&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/61738819/installing-private-package-from-github-package-registry-using-yarn-fails-with-no&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/61738819/installing-private-package-from-github-package-registry-using-yarn-fails-with-no&lt;/a&gt;&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>npm registry</category>
      <category>yarn 2</category>
      <category>yarn berry</category>
      <category>yarn PnP</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/33</guid>
      <comments>https://devpluto.tistory.com/entry/Yarn-Yarn-v2%EC%97%90%EC%84%9C-npm-registry%EB%A5%BC-%EB%B0%94%EB%9D%BC%EB%B3%B4%EB%8F%84%EB%A1%9D-%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0#entry33comment</comments>
      <pubDate>Fri, 8 Sep 2023 04:24:50 +0900</pubDate>
    </item>
    <item>
      <title>[Xcode] Distribution failed with errors</title>
      <link>https://devpluto.tistory.com/entry/Xcode-Distribution-failed-with-errors</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Xcode에서 앱 빌드 후 Archive &amp;gt; Distribute 하는 과정에서 다음과 같은 에러가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-09-03 오전 5.03.21.png&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bO0s4T/btssSDKzXa7/IQZJHbWjucGWT8mkLFkk80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bO0s4T/btssSDKzXa7/IQZJHbWjucGWT8mkLFkk80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bO0s4T/btssSDKzXa7/IQZJHbWjucGWT8mkLFkk80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbO0s4T%2FbtssSDKzXa7%2FIQZJHbWjucGWT8mkLFkk80%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;701&quot; height=&quot;437&quot; data-filename=&quot;스크린샷 2023-09-03 오전 5.03.21.png&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Invalid bundle. The &quot;UlInterfaceOrientationPortrait,UlInterfaceOrientationLandscapeLeft,UlInterfaceOrientationLan dscapeRight&quot; orientations were provided for the UISupportedInterfaceOrientations Info.plist key~&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러를 읽어보면, UISupportedInterfaceOrientations key에 해당하는 모든 orientation이 포함되어야한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 info.plist파일에 누락된 UIInterfaceOrientationPortraitUpsideDown을 추가하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1693761803718&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// info.plist
&amp;lt;key&amp;gt;UISupportedInterfaceOrientations&amp;lt;/key&amp;gt;
&amp;lt;array&amp;gt;
    &amp;lt;string&amp;gt;UIInterfaceOrientationPortrait&amp;lt;/string&amp;gt;
    &amp;lt;string&amp;gt;UIInterfaceOrientationPortraitUpsideDown&amp;lt;/string&amp;gt; // 추가!
    &amp;lt;string&amp;gt;UIInterfaceOrientationLandscapeLeft&amp;lt;/string&amp;gt;
    &amp;lt;string&amp;gt;UIInterfaceOrientationLandscapeRight&amp;lt;/string&amp;gt;
&amp;lt;/array&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Invalid bundle. Because your app supports Multitasking on iPad, you need to include the LaunchScreen.storyboard launch storyboard file~&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 안내 링크에 가니 UILaunchStoryboardName 값의 .storyboard를 떼라고 안내되어있어서 아래처럼 .storyboard를 제거하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1693762037557&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// info.plist

&amp;lt;key&amp;gt;UILaunchStoryboardName&amp;lt;/key&amp;gt;
&amp;lt;string&amp;gt;LaunchScreen.storyboard&amp;lt;/string&amp;gt; // .storyboard 삭제&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Frontend</category>
      <category>distribution failed</category>
      <category>Xcode</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/32</guid>
      <comments>https://devpluto.tistory.com/entry/Xcode-Distribution-failed-with-errors#entry32comment</comments>
      <pubDate>Mon, 4 Sep 2023 02:28:50 +0900</pubDate>
    </item>
    <item>
      <title>[Vite] 컴포넌트 build시 이미지가 dynamic import되도록 환경설정</title>
      <link>https://devpluto.tistory.com/entry/Vite-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-build%EC%8B%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-dynamic-import%EB%90%98%EB%8F%84%EB%A1%9D-%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Icon 컴포넌트 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 라이브러리에서 icon 컴포넌트를 만들어야 했는데, 아래와 같이 동적으로 name props을 변경하면 이미지가 변경되도록 구현해야했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1692282845342&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import styled from &quot;@emotion/native&quot;;
import { IconsNameType } from &quot;../../..&quot;;

export type IconProps = {
  name: IconsNameType;
  size: 16 | 20 | 24;
};

export const Icon = ({ name, size }: IconProps) =&amp;gt; {
  return &amp;lt;S.ImageWrapper source={require(&quot;../../../assets/images/icons/${name}.png&quot;)} size={size} /&amp;gt;;
};

const S = {
  ImageWrapper: styled.Image&amp;lt;Pick&amp;lt;IconProps, &quot;size&quot;&amp;gt;&amp;gt;`
    width: ${({ size }) =&amp;gt; size}px;
    height: ${({ size }) =&amp;gt; size}px;
  `,
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드만 보면 잘 동작할 것 같지만, 빌드하여 패키지 업로드를 하려하니 여러 문제가 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점 1. build시 image가 같이 번들링되지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 시스템에서 라이브러리를 build하면 아래와 같이 js파일과 type파일들이 번들링됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-17 오후 11.38.37.png&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3NJcE/btsrti761LQ/gutRlxIbXQRzlVRWHuWcl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3NJcE/btsrti761LQ/gutRlxIbXQRzlVRWHuWcl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3NJcE/btsrti761LQ/gutRlxIbXQRzlVRWHuWcl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3NJcE%2Fbtsrti761LQ%2FgutRlxIbXQRzlVRWHuWcl1%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;274&quot; height=&quot;267&quot; data-filename=&quot;스크린샷 2023-08-17 오후 11.38.37.png&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 동적 Import를 위해 dist폴더에 이미지도 같이 빌드되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.vitejs.dev/guide/assets.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ko.vitejs.dev/guide/assets.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1692283604570&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Vite&quot; data-og-description=&quot;Vite, 차세대 프런트엔드 개발 툴&quot; data-og-host=&quot;ko.vitejs.dev&quot; data-og-source-url=&quot;https://ko.vitejs.dev/guide/assets.html&quot; data-og-url=&quot;https://ko.vitejs.dev&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bokGc9/hyTFeJJoiZ/lLiQ2kitkjdztLWqJoBlEK/img.png?width=2600&amp;amp;height=1302&amp;amp;face=0_0_2600_1302,https://scrap.kakaocdn.net/dn/c8AixZ/hyTFhzFKFW/DaHACgY8GORAsLv1Z4vlP0/img.png?width=640&amp;amp;height=640&amp;amp;face=0_0_640_640&quot;&gt;&lt;a href=&quot;https://ko.vitejs.dev/guide/assets.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.vitejs.dev/guide/assets.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bokGc9/hyTFeJJoiZ/lLiQ2kitkjdztLWqJoBlEK/img.png?width=2600&amp;amp;height=1302&amp;amp;face=0_0_2600_1302,https://scrap.kakaocdn.net/dn/c8AixZ/hyTFhzFKFW/DaHACgY8GORAsLv1Z4vlP0/img.png?width=640&amp;amp;height=640&amp;amp;face=0_0_640_640');&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;Vite&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Vite, 차세대 프런트엔드 개발 툴&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.vitejs.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vite 공식문서를 보면 public 디렉터리 아래에 asset들을 위치시키면 같이 빌드가 된다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 src/lib폴더밖에 없는 라이브러리 개발 환경에서는 public폴더가 존재하지 않으므로 아래와 같이 임의로 폴더경로를 지정해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1692283566804&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// vite.config.ts

export default defineConfig({
  // ...
  publicDir: path.resolve(__dirname, &quot;src/lib/assets&quot;),
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vite config에 publicDir옵션을 설정하면 해당 경로의 asset들이 자동으로 같이 build 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-17 오후 11.48.39.png&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnqwwu/btsrjqGqind/nKFkeCRWPPSSGQaL8KAp4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnqwwu/btsrjqGqind/nKFkeCRWPPSSGQaL8KAp4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnqwwu/btsrjqGqind/nKFkeCRWPPSSGQaL8KAp4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcnqwwu%2FbtsrjqGqind%2FnKFkeCRWPPSSGQaL8KAp4K%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;340&quot; height=&quot;463&quot; data-filename=&quot;스크린샷 2023-08-17 오후 11.48.39.png&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 image를 dist폴더에 빌드하는것은 성공했지만, 생각해보니 다른 문제가 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점2. 이미지를 가져오는 경로가 달라야한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;icon 컴포넌트 코드를 보면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;../../../assets/images/icons/${name}.png&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로로 동적 import 해오고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 모듈에서 가져와야하는 경로는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;../images/icons/${name}.png&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이므로 build시 import해오는 경로가 달라야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dist폴더에 이미지가 존재해도 dist폴더 경로에 맞게 이미지를 불러와야하는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-17 오후 11.51.53.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;1118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CUBu1/btsrypSDZ65/XRCkiWmiFnuPxIENQIFxd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CUBu1/btsrypSDZ65/XRCkiWmiFnuPxIENQIFxd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CUBu1/btsrypSDZ65/XRCkiWmiFnuPxIENQIFxd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCUBu1%2FbtsrypSDZ65%2FXRCkiWmiFnuPxIENQIFxd0%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;564&quot; height=&quot;418&quot; data-filename=&quot;스크린샷 2023-08-17 오후 11.51.53.png&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;1118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서는 전체 URL을 확인하는 방법으로 URL 생성자를 이용하는 방법을 제시합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;프로덕션 빌드 시, Vite는 번들링 및 에셋 해싱 후에도 해당 에셋에 대한 URL을 올바르게 가리키기 위해 필요한 변환 작업을 수행합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법을 사용하면 번들링 이후에 URL이 알맞게 변경된다고 하기 때문에, 이를 적용해보았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1692284279433&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ...
const getImageUrl = (name: IconProps['name']) =&amp;gt; {
  return new URL(`../../../assets/images/icons/${name}.png`, import.meta.url)
    .href;
};

export const Icon = ({ name, size }: IconProps) =&amp;gt; {
  return &amp;lt;S.ImageWrapper source={require(getImageUrl(name))} size={size} /&amp;gt;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성 후 빌드하면 URL 경로에 해당하는 image를 모두 가져와서 base64로 인코딩하고, 이를 동적으로 import하도록 번들링 된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-18 오전 12.00.37.png&quot; data-origin-width=&quot;2446&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5A3li/btsrqHOlhpP/SmMiYQcQTmN5DiMSLKukEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5A3li/btsrqHOlhpP/SmMiYQcQTmN5DiMSLKukEk/img.png&quot; data-alt=&quot;dist/*/design-system.es.js&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5A3li/btsrqHOlhpP/SmMiYQcQTmN5DiMSLKukEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5A3li%2FbtsrqHOlhpP%2FSmMiYQcQTmN5DiMSLKukEk%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;2446&quot; height=&quot;348&quot; data-filename=&quot;스크린샷 2023-08-18 오전 12.00.37.png&quot; data-origin-width=&quot;2446&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;dist/*/design-system.es.js&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결말&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공적으로 dynamic하게 이미지를 불러오는 icon컴포넌트를 만들었고, 이를 github registry에 배포하여 프로젝트에 불러왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 컴포넌트를 확인해보니 react native에서는 이미지 경로 require 함수에서 동적으로 가져오는것이 불가능하다고 합니다...&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-17 오후 11.36.25.png&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;1126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MRrS9/btsrtkdMVAT/KLoUIWQ42Oz4W4hCNKLPBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MRrS9/btsrtkdMVAT/KLoUIWQ42Oz4W4hCNKLPBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MRrS9/btsrtkdMVAT/KLoUIWQ42Oz4W4hCNKLPBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMRrS9%2FbtsrtkdMVAT%2FKLoUIWQ42Oz4W4hCNKLPBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;408&quot; height=&quot;640&quot; data-filename=&quot;스크린샷 2023-08-17 오후 11.36.25.png&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;1126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 라이브러리를 설정할 때 native 개발 환경은 설정하지 않았는데, 실제 native환경에서 확인해보니 동작하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 web 컴포넌트로도 컴포넌트를 만들 예정인데, 이 때 상기 동적 import 방식을 사용하려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 동적으로 import하는 컴포넌트를 만들고, vite로 빌드하기 위한 분들을 위해 이 글을 남깁니다..!&lt;/p&gt;
&lt;p data-ke-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>Frontend</category>
      <category>build</category>
      <category>Component</category>
      <category>dynamic import</category>
      <category>vite</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/31</guid>
      <comments>https://devpluto.tistory.com/entry/Vite-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-build%EC%8B%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-dynamic-import%EB%90%98%EB%8F%84%EB%A1%9D-%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%95#entry31comment</comments>
      <pubDate>Fri, 18 Aug 2023 00:12:27 +0900</pubDate>
    </item>
    <item>
      <title>[Strapi] Strapi 프로젝트를 Fly.io에 배포하기</title>
      <link>https://devpluto.tistory.com/entry/Strapi-Strapi-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-Flyio%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;strapi 프로젝트를 Fly.io에 배포하는 방법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Strapi 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Strapi 프로젝트 생성&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;yarn create strapi-app strapi-fly --quickstart&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어를 수행하면 strapi 프로젝트가 기본 설정으로 생성됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Postgresql 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Strapi 프로젝트를 생성하면 sqlite가 기본 데이터베이스로 설정되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fly.io는 postgresql을 기본 지원하기 때문에 이에 맞게 strapi에서 postgresql를 사용하도록 설정해주겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1689037340052&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// config/database.js

const path = require('path');

module.exports = ({ env }) =&amp;gt; {
  const client = env('DATABASE_CLIENT', 'postgres'); // sqlite &amp;gt; postgres
  // ...
  postgres: {
    connection: {
      connectionString: env('DATABASE_URL'), // 삭제해주세요
      host: env('DATABASE_HOST', 'localhost'),
      // ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;config/database.js 파일에서 client를 sqlite에서 postgres로 변경합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아래로 가면 postgres 설정이 있는데, connectionString을 삭제해주세요. 추후 Fly.io와 연동 이후 해당 설정이 있다면 에러가 발생합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1689037626322&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .env

DATABASE_CLIENT=postgres
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_NAME=데이터베이스이름
DATABASE_USERNAME=유저이름
DATABASE_PASSWORD=유저패스워드&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 .env파일도 DATABASE_CLIENT를 postgres로 수정하고, DB 이름과 유저이름, 비밀번호도 본인이 설정한 것으로 수정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dockerfile 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fly에 배포하기 위해서 Dockerfile를 생성해야합니다. 프로젝트 root 디렉터리에 Dockerfile 파일명으로 파일을 생성해주시고, 아래 코드를 작성해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1689037745169&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Dockerfile
FROM node:16
# Installing libvips-dev for sharp Compatability
RUN apt-get update &amp;amp;&amp;amp; apt-get install libvips-dev -y
# Set environment to production
ENV NODE_ENV=production
# Copy the configuration files
WORKDIR /opt/
COPY ./package.json ./yarn.lock ./
ENV PATH /opt/node_modules/.bin:$PATH
# Install dependencies
RUN yarn install
# Copy the application files
WORKDIR /opt/app
COPY ./ .
# Build the Strapi application
RUN yarn build
# Expose the Strapi port
EXPOSE 1337
# Start the Strapi application 
CMD [&quot;yarn&quot;, &quot;start&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 .dockerignore파일을 생성하여 아래와 같이 작성해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1689037886993&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.tmp/
.cache/
.git/
build/
node_modules/
data/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;plugins 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;config폴더에 plugins.js파일을 생성하여 아래 코드를 작성해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1689038684925&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// config/plugins.js

module.exports = ({ env }) =&amp;gt; ({
  &quot;users-permissions&quot;: {
    config: {
      jwtSecret: env(&quot;ADMIN_JWT_SECRET&quot;),
    },
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일이 없다면, 나중에 배포 후에 아래와 같은 에러가 발생합니다. &lt;s&gt;작성자처럼 삽질하지 말라고 &lt;/s&gt;설정 편의를 위해 미리 작성하였습니다..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-11 오전 8.54.23.png&quot; data-origin-width=&quot;2206&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAQfT6/btsm9uLDcyG/vTTdyUiAGkTVDnUBay7Whk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAQfT6/btsm9uLDcyG/vTTdyUiAGkTVDnUBay7Whk/img.png&quot; data-alt=&quot;Missing jwtSecret 에러..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAQfT6/btsm9uLDcyG/vTTdyUiAGkTVDnUBay7Whk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAQfT6%2Fbtsm9uLDcyG%2FvTTdyUiAGkTVDnUBay7Whk%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;2206&quot; height=&quot;774&quot; data-filename=&quot;스크린샷 2023-07-11 오전 8.54.23.png&quot; data-origin-width=&quot;2206&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Missing jwtSecret 에러..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fly.io 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Strapi 설정을 마쳤다면 Fly에 배포하기 위해 설정을 진행하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fly CLI 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 컴퓨터에 Fly CLI를 설치해야합니다. 설치에 필요한 명령어는 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Mac&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;brew install flyctl&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Window | Linux&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;curl -L https://fly.io/install.sh | sh&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fly에 로그인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLI를 설치했다면 아래 명령어로 로그인해야합니다. 만약 계정이 없다면 생성해주세요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;fly auth login&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fly 프로젝트 생성하기&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;fly launch&lt;/blockquote&gt;
&lt;pre id=&quot;code_1689038320235&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;? Choose an app name (leave blank to generate one): 프로젝트 이름을 지정합니다.
? Choose a region for deployment: Tokyo, Japan (nrt) 한국과 가까운 도쿄로 지정합니다.

Created app '프로젝트 이름' in organization ''
Admin URL: https://fly.io/apps/프로젝트 이름
Hostname: 프로젝트 이름.fly.dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;strapi 프로젝트 루트에서 fly launch 명령을 실행하면 프로젝트 이름과 region을 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Postgresql database를 셋업할거냐는 질문이 나오는데 Yes를 선택해 주시고, 저는 연습용으로 사용하기 때문에 가장 작은 용량으로 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;만약 셋업 질문이 나오지 않는다면 목차 아래의 TroubleShooting 부분으로 가서 내용을 확인해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-11 오전 10.32.16.png&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRqTt6/btsm5rWLZkO/otDBI5kFSjdyNaKAHLshW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRqTt6/btsm5rWLZkO/otDBI5kFSjdyNaKAHLshW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRqTt6/btsm5rWLZkO/otDBI5kFSjdyNaKAHLshW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRqTt6%2Fbtsm5rWLZkO%2FotDBI5kFSjdyNaKAHLshW1%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;1132&quot; height=&quot;234&quot; data-filename=&quot;스크린샷 2023-07-11 오전 10.32.16.png&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-11 오전 10.33.44.png&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7sznx/btsm8Ojz04m/qr0LvN9QXh48VCfCWKeYF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7sznx/btsm8Ojz04m/qr0LvN9QXh48VCfCWKeYF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7sznx/btsm8Ojz04m/qr0LvN9QXh48VCfCWKeYF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7sznx%2Fbtsm8Ojz04m%2Fqr0LvN9QXh48VCfCWKeYF0%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;1138&quot; height=&quot;200&quot; data-filename=&quot;스크린샷 2023-07-11 오전 10.33.44.png&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 DATABASE_URL=postgres://프로젝트이름:비밀번호...와 같은 정보가 표시됩니다. 이것을 복사해서 .env 파일에 추가해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1689039505889&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .env

// ...
DATABASE_URL=postgres://...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 기본 설정을 선택해 주시고, 다 마쳤다면 자동으로 fly.toml파일이 생성되며 프로젝트 생성 작업이 종료됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 이후에 아래와 같은 질문이 나오는데, 추가적으로 설정할 것이 있기 때문에 No를 선택해줍니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;? Would you like to deploy now? (y/N) &amp;gt; No&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TroubleShooting: Fly 프로젝트 생성시 셋업 질문이 나오지 않는다면&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 fly launch 명령어로 프로젝트를 생성했는데 fly.toml파일만 생성되고 Postgresql을 셋업해줄까? 하는 질문이 나오지 않는다면 귀찮지만 프로젝트를 삭제하고, 아래 명령어로 다시 프로젝트를 생성해주세요&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;fly launch --image flyio/hellofly:latest&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-11 오전 11.16.36.png&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;1026&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cU6Chd/btsm8gUWz9t/KWl1X7uUbO9YmcSUlYY2A0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cU6Chd/btsm8gUWz9t/KWl1X7uUbO9YmcSUlYY2A0/img.png&quot; data-alt=&quot;삭제 페이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cU6Chd/btsm8gUWz9t/KWl1X7uUbO9YmcSUlYY2A0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcU6Chd%2Fbtsm8gUWz9t%2FKWl1X7uUbO9YmcSUlYY2A0%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;521&quot; height=&quot;352&quot; data-filename=&quot;스크린샷 2023-07-11 오전 11.16.36.png&quot; data-origin-width=&quot;1520&quot; data-origin-height=&quot;1026&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;삭제 페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Postgresql 셋업 및 DATABASE_URL까지 env파일에 적었다면 생성된 fly.toml에서 아래 부분을 삭제해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드가 남아있으면 Strapi가 빌드된 Docker 이미지가 업로드 되지 않고, 기본 제공 이미지가 업로드됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1689041950847&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// fly.toml

// ...
[build] // 삭제
  image = &quot;flyio/hellofly:latest&quot; // 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fly.toml 파일 수정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 fly.toml파일에서 [http_service]의 internal_port를 1337로 수정하고, 아래 코드를 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1689038482303&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[http_service]
  internal_port = 1337 // 1337로 수정
  // ...

// 아래 코드 추가
[[services]]
  http_checks = []
  internal_port = 1337
  processes = [&quot;app&quot;]
  protocol = &quot;tcp&quot;
  script_checks = []&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;postgresql production 환경 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postgresql을 Fly에 배포되는 production 환경에서 사용하기 위해 config/env/production경로를 만들고, database.js파일을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 Strapi 설정 단계에서 .env파일에 DATABASE_URL을 지정했었습니다. 이를 파싱하기 위해 필요한 패키지를 설치합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;yarn add pg-connection-string pg&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 후 database.js 파일을 아래와 같이 작성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1689040928087&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// config/env/production/database.js

const parse = require(&quot;pg-connection-string&quot;).parse;
const config = parse(process.env.DATABASE_URL);

module.exports = () =&amp;gt; ({
  connection: {
    client: &quot;postgres&quot;,
    connection: {
      host: config.host,
      port: config.port,
      database: config.database,
      user: config.user,
      password: config.password,
      ssl: false,
    },
    debug: false,
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fly에 배포하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 설정을 마쳤다면 이제 배포만 하면 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;fly deploy&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-11 오전 8.56.39.png&quot; data-origin-width=&quot;2798&quot; data-origin-height=&quot;1518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kY0BL/btsnamzO2SN/zgII87q1i5rs47S4JmuTWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kY0BL/btsnamzO2SN/zgII87q1i5rs47S4JmuTWK/img.png&quot; data-alt=&quot;성공적으로 배포된 것을 확인할 수 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kY0BL/btsnamzO2SN/zgII87q1i5rs47S4JmuTWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkY0BL%2FbtsnamzO2SN%2FzgII87q1i5rs47S4JmuTWK%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;2798&quot; height=&quot;1518&quot; data-filename=&quot;스크린샷 2023-07-11 오전 8.56.39.png&quot; data-origin-width=&quot;2798&quot; data-origin-height=&quot;1518&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성공적으로 배포된 것을 확인할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 배포 환경에서는 content-type을 수정하지 못하기 때문에 수정이 필요하다면 개발 환경에서 진행해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;strapi 프로젝트를 수정하게 되면 다시 fly deploy 명령을 수행하여 재배포할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 heroku를 통해 배포하려했으나, heroku에서 이전에 제공하던 무료 플랜이 사라졌기에 대체제로 Fly.io를 찾아 배포하는 과정을 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 Fly.io에 배포하는 과정에서 거의 6시간 가량을 삽질했기 때문에 억울해서 저와 같이 시간 낭비하지 마시라고 정리해서 작성합니다.. (특히 TroubleShooting 부분.. 자동 셋업이 안떠서 postgresql 프로젝트를 따로 만들어서 연결하려다가 결국 해결 못했습니다ㅠㅠ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 계속된 삽질을 통해 Dockerfile에 사용되는 문법이나 DB설정 방법 등을 얻고가는 것 같아 그나마 만족합니다..,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 글은 아래 커뮤니티글을 토대로 작성되었고, 추가적인 설정이 필요한 부분과 에러가 안나도록 내용을 추가하였습니다. 또 패키지 매니저는 yarn을 기준으로 작성했기 때문에, npm을 사용한다면 해당 링크를 참고해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://forum.strapi.io/t/fly-io-deployment/22438&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://forum.strapi.io/t/fly-io-deployment/22438&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>etc</category>
      <category>Fly.io</category>
      <category>strapi</category>
      <category>배포</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/30</guid>
      <comments>https://devpluto.tistory.com/entry/Strapi-Strapi-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-Flyio%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0#entry30comment</comments>
      <pubDate>Tue, 11 Jul 2023 11:30:20 +0900</pubDate>
    </item>
    <item>
      <title>[next.js] intersection observer로 무한스크롤 구현하기</title>
      <link>https://devpluto.tistory.com/entry/nextjs-intersection-observer%EB%A1%9C-%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본 글은 Next.js 환경에서 무한스크롤 기능을 intersection observer를 이용해 구현한 방법에 대해 작성하였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현 설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 처음 데이터는 Next의 getServersideProps로 받고, 이후 데이터는 intersection event가 발생했을 때 다음 배열을 가져오는 api를 호출하도록 구현하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 API는 원하는 page를 쿼리 파라미터에 page=1과 같이 지정하면 해당 page의 데이터를 반환하도록 구현되어있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상세 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타입 지정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 가져오는 정보들을 memory라 명명하여 type도 연관되게 이름지었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GetMemoryListRes 타입은 데이터를 fetching해올 때 무한스크롤 할 정보의 response타입입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1687672876615&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** memoryList를 가져오는 api의 response type */
export type GetMemoryListRes = {
  total: number
  content: Array&amp;lt;MemoryType&amp;gt;
  pageable: {
    sort: {
      orders: [
        {
          direction: string
          property: string
          ignoreCase: boolean
          nullHandling: string
        }
      ]
    }
    page: number
    size: number
  }
}

/** Memory Type */
export type MemoryType = {
  id: number
  backgroundImage: string
  text: string
  videoId: string
  createdAt: string
  deletedAt?: string
}
/** Memory Type의 List 형식 */
export type MemoryListType = {
  memoryList: GetMemoryListRes['content']
  currentPage: GetMemoryListRes['pageable']['page']
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;getServerSideProps로 첫 배열 가져오기&lt;/h3&gt;
&lt;pre id=&quot;code_1687672826576&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { GetServerSideProps } from 'next'
import { API_URL } from '@/constants'

// server data fetching
export const getServerSideProps: GetServerSideProps&amp;lt;{
  initMemoryList: MemoryListType
}&amp;gt; = async () =&amp;gt; {
  /** fetch data */
  const getMemoryList = await fetch(
    `${process.env.NEXT_PUBLIC_SERVER_DEFAULT_END_POINT}post/page?page=1&amp;amp;size=${GET_MEMORY_LIST_DEFAULT_SIZE}&amp;amp;memberId=${MEMBER_ID}`
  )
  const getMemoryListRes: GetMemoryListRes = await getMemoryList.json()

  const initMemoryList: MemoryListType = {
    memoryList: getMemoryListRes.content,
    currentPage: getMemoryListRes.pageable.page,
  }
  return { props: { initMemoryList } }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js의 getServerSideProps로 처음(currentPage: 1)에 해당하는 데이터 배열을 가져왔습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 가져온 정보를 토대로 initMemoryList 변수를 설정하여 prop으로 내려줬습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;observer 생성과 무한스크롤 구현&lt;/h3&gt;
&lt;pre id=&quot;code_1687673058500&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { GetServerSideProps, InferGetServerSidePropsType } from 'next'

// ...

export default function MemoryList({ initMemoryList }: InferGetServerSidePropsType&amp;lt;typeof getServerSideProps&amp;gt;) {
  const [memoryList, setMemoryList] = useState&amp;lt;MemoryListType&amp;gt;(initMemoryList)

  const targetRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null)

  // 1
  const handleIntersect = useCallback(
    ([entry]: IntersectionObserverEntry[]) =&amp;gt; {
      if (entry.isIntersecting) {
        /** fetch data */
        const getMemoryList = axios.get&amp;lt;GetMemoryListRes&amp;gt;(
          `${process.env.NEXT_PUBLIC_SERVER_DEFAULT_END_POINT}post/page?page=${
            memoryList.currentPage + 1
          }&amp;amp;size=${GET_MEMORY_LIST_DEFAULT_SIZE}&amp;amp;memberId=${MEMBER_ID}`
        )
        getMemoryList
          .then((res) =&amp;gt; {
            if (res.status !== 200) return
            setMemoryList((prev) =&amp;gt; {
              return {
                ...prev,
                memoryList: [...prev.memoryList, ...res.data.content],
                currentPage: res.data.pageable.page,
              }
            })
          })
          .catch((error) =&amp;gt; console.error(error))
      }
    },
    [memoryList.currentPage]
  )

  // 2
  /** control observer */
  useEffect(() =&amp;gt; {
    const observer = new IntersectionObserver(handleIntersect, {
      threshold: 0.9,
      root: null,
    })

    targetRef.current &amp;amp;&amp;amp; observer.observe(targetRef.current)

    return () =&amp;gt; {
      observer.disconnect()
    }
  }, [handleIntersect, targetRef.current])

  // 3
  return (
    &amp;lt;S.Wrapper&amp;gt;
      {memoryList?.memoryList.map((memory, index) =&amp;gt; (
        &amp;lt;Card key={index} memory={memory} ref={memoryList.memoryList.length === index + 1 ? targetRef : null} /&amp;gt;
      ))}
    &amp;lt;/S.Wrapper&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 세가지 부분으로 나누어 설명하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. intersecting 했을 때 데이터를 가져오는 부분&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 관찰하는 요소(마지막 Card 컴포넌트)가 intersecting되었다면 getMemoryList를 통해 데이터를 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 page = currentPage + 1을 하여 현재 페이지 다음의 데이터를 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 가져온 데이터를 memoryList에 붙여줍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. intersection observer 생성 부분&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;targetRef는 null일수 있기때문에 targetRef.current가 존재하면 targetRef를 관찰할 대상으로 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언마운팅시에는 observer.disconnect() 함수를 호출해 등록해제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 데이터를 빠르게 보여주기 위해 observer의 threshold를 1이 아니라 &lt;span style=&quot;text-align: start;&quot;&gt;모든 요소가 보여지기 직전인&lt;/span&gt; 0.8로 설정했습니다. 이렇게하면 유저가 데이터를 기다린다는 느낌을 최소화할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 데이터를 보여주고, Card 컴포넌트에 ref를 할당하는 부분&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemoryList.map을 통해 Card 컴포넌트에 memory에 해당하는 props를 넘겨주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 ref도 함께 전달하였는데, observer가 마지막 하나의 컴포넌트만 관찰해야하므로&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;memoryList.memoryList.length === index + 1 ? targetRef : null&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드를 통해 마지막 컴포넌트일때 targetRef를 지정해주도록 하였습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;+) Card 컴포넌트의 ref 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 Card컴포넌트를 보시면 ref로 targetRef를 전달해줍니다. 하지만 Card 컴포넌트는 우리가 제작한 컴포넌트이므로 별도 설정이 없다면 ref는 제대로 할당되지 않을 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1687952973476&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type CardProps = {
  memory: MemoryType
} &amp;amp; React.ComponentProps&amp;lt;'div'&amp;gt;

const Card = forwardRef&amp;lt;HTMLDivElement, CardProps&amp;gt;(({ memory }, ref) =&amp;gt; {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;S.CardComponentContainer ref={ref} backgroundImage={memory.backgroundImage}&amp;gt;
      // ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 forwardRef를 이용하여 ref를 컴포넌트 내부에 할당하도록 Card컴포넌트를 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 Card 컴포넌트는 div로 감싸여진 컴포넌트이기에, 추후 확장성을 고려하여 CardProps 타입을 div컴포넌트 props에 해당하는 타입을 확장하여 선언했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과물&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면_기록_2023-06-29_오전_1_43_22_AdobeExpress.gif&quot; data-origin-width=&quot;411&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blbFcN/btslLEIz5eB/fhq2sq9O9MwyQ5pzOFnVeK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blbFcN/btslLEIz5eB/fhq2sq9O9MwyQ5pzOFnVeK/img.gif&quot; data-alt=&quot;데이터가 잘 가져와져서 무한스크롤되는 모습을 확인할 수 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blbFcN/btslLEIz5eB/fhq2sq9O9MwyQ5pzOFnVeK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/blbFcN/btslLEIz5eB/fhq2sq9O9MwyQ5pzOFnVeK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;411&quot; height=&quot;411&quot; data-filename=&quot;화면_기록_2023-06-29_오전_1_43_22_AdobeExpress.gif&quot; data-origin-width=&quot;411&quot; data-origin-height=&quot;411&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터가 잘 가져와져서 무한스크롤되는 모습을 확인할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;mock 데이터 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현당시 API가 나오지 않았기 때문에 API요청에 따른 데이터를 모킹해서 구현해야했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래와같이 MockMemoryType이라는 모킹 데이터를 반환하는 함수를 만들어 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1687673113179&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**mock data */
const MockMemoryType = (currentPage: number) =&amp;gt; {
  return {
    data: [
      {
        id: currentPage,
        backgroundImage: '',
        youtubeUrl: '',
        text: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
        createdAt: 'July 23',
        deletedAt: 'July 23',
      },
      {
        id: currentPage,
        backgroundImage: '',
        youtubeUrl: '',
        text: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
        createdAt: 'July 23',
        deletedAt: 'July 23',
      },
    ],
    currentPage: currentPage,
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 모킹해서 가져올 때는 mock함수 사용과, 컴포넌트 내부에서 mockPageNumber = useRef(2)로 설정하여 page를 가져온다는점 외에는 구현 상 달라진 것은 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;intersect해서 데이터를 가져왔을 때 mockPageNumber.current++를 해주어 요청할 페이지 번호를 관리하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1687673172306&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// server data fetching
export const getServerSideProps: GetServerSideProps&amp;lt;{
  initMemoryList: MemoryListType
}&amp;gt; = async () =&amp;gt; {
  /** use mock data */
  const initMemoryList = MockMemoryType(1)
  return { props: { initMemoryList } }
}

export default function MemoryList({ initMemoryList }: InferGetServerSidePropsType&amp;lt;typeof getServerSideProps&amp;gt;) {
  const [memoryList, setMemoryList] = useState&amp;lt;MemoryListType&amp;gt;(initMemoryList)

  const targetRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null)
  const mockId = useRef(2)

  const handleIntersect = useCallback(
    ([entry]: IntersectionObserverEntry[]) =&amp;gt; {
      if (entry.isIntersecting) {
        /** use mock data */
        setMemoryList((prev) =&amp;gt; {
          return {
            ...prev,
            data: [...prev.data, ...MockMemoryType(mockPageNumber.current).data],
            currentPage: MockMemoryType(mockPageNumber.current).currentPage,
          }
        })
      }
      mockPageNumber.current++
    },
    [targetRef.current]
  )

  /** control observer */
  useEffect(() =&amp;gt; {
    const observer = new IntersectionObserver(handleIntersect, {
      threshold: 0.9,
      root: null,
    })

    targetRef.current &amp;amp;&amp;amp; observer.observe(targetRef.current)

    return () =&amp;gt; {
      observer.disconnect()
    }
  }, [handleIntersect, targetRef.current])

  return (
    &amp;lt;S.Wrapper&amp;gt;
      {memoryList?.data.map((memory, index) =&amp;gt; (
        &amp;lt;Card key={index} memory={memory} ref={targetRef} /&amp;gt;
      ))}
    &amp;lt;/S.Wrapper&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TroubleShooting: 리렌더링이 많이되는 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현한 로직을 보며 페이지 성능에 대해 생각하던 중, 기존 memoryList state 배열을 새로운 데이터를 덧붙이듯이 관리하게되면 데이터를 가져올 때 마다 모든 Card 컴포넌트에서 리렌더링이 일어나지 않을까? 라는 생각이 들어서 확인해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Dev Tools로 확인해 본 결과, 예상대로 데이터를 많이 가져오면 많이 가져올 수록 모든 컴포넌트에서 리렌더링이 발생한다는 것이 확인되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-29 오전 1.28.23.png&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;818&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mKKAH/btslJGnsxJ3/n1dHnLSQfGZrbf0Lb3HmUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mKKAH/btslJGnsxJ3/n1dHnLSQfGZrbf0Lb3HmUk/img.png&quot; data-alt=&quot;모든 Card 컴포넌트가 데이터를 fetching해올 때 렌더링되는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mKKAH/btslJGnsxJ3/n1dHnLSQfGZrbf0Lb3HmUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmKKAH%2FbtslJGnsxJ3%2Fn1dHnLSQfGZrbf0Lb3HmUk%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;510&quot; height=&quot;432&quot; data-filename=&quot;스크린샷 2023-06-29 오전 1.28.23.png&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모든 Card 컴포넌트가 데이터를 fetching해올 때 렌더링되는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1687969986499&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default React.memo(Card);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 React.memo()를 이용하여 Card컴포넌트를 감싸주었고, 성공적으로 가져온 데이터에 대해서만 렌더링이 일어나도록 구현되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-29 오전 1.26.57.png&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfuQF0/btslJ2Rb4D5/qUD7K0NvkzKLap6GgTVTc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfuQF0/btslJ2Rb4D5/qUD7K0NvkzKLap6GgTVTc0/img.png&quot; data-alt=&quot;이제 데이터 fetching시 새로 불러온 데이터에 대해서만 렌더링이 일어납니다!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfuQF0/btslJ2Rb4D5/qUD7K0NvkzKLap6GgTVTc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfuQF0%2FbtslJ2Rb4D5%2FqUD7K0NvkzKLap6GgTVTc0%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;508&quot; height=&quot;331&quot; data-filename=&quot;스크린샷 2023-06-29 오전 1.26.57.png&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이제 데이터 fetching시 새로 불러온 데이터에 대해서만 렌더링이 일어납니다!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend/next.js</category>
      <category>intersection observer</category>
      <category>next.js</category>
      <category>react</category>
      <category>무한스크롤</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/29</guid>
      <comments>https://devpluto.tistory.com/entry/nextjs-intersection-observer%EB%A1%9C-%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0#entry29comment</comments>
      <pubDate>Thu, 29 Jun 2023 01:54:03 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] Next에서 Styled Components와 함께 Storybook 세팅하기</title>
      <link>https://devpluto.tistory.com/entry/Nextjs-Next%EC%97%90%EC%84%9C-Styled-Components%EC%99%80-%ED%95%A8%EA%BB%98-Storybook-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Create-Next-App을 통해 생성한 Next.js 프로젝트에서 storybook 초기 설정하는 방법에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Storybook 설치하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npx storybook@latest init&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어로 storybook의 최신버전을 프로젝트에 설치합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;yarn add -D @storybook/nextjs&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치를 완료했다면 nextjs를 지원하는 패키지도 설치해줍니다. 설치를 완료했다면 아래 코드를 .storybook/main.ts 파일에 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1687161732023&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .storybook/main.ts
const config: StorybookConfig = {
  //...
  framework: {
    name: '@storybook/nextjs',
    options: {},
  },
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm run storybook&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세팅을 마쳤다면 npm run storybook 명령어로 스토리북을 실행할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Storybook에서 Next.js의 절대 경로 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;create-next-app으로 next.js의 최신버전을 설치하면 tsconfig 파일에 기본적으로 src/를 @/로 치환하는 절대경로가 설정되어있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683388980223&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//tsconfig.json
{
  &quot;compilerOptions&quot;: {
    ...
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;./src/*&quot;]
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 설정없이 storybook의 컴포넌트 내에서 절대 경로를 사용한다면 다음과 같은 에러가 발생합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-07 오전 1.04.13.png&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDSErj/btsdZ0tDJWC/xVZLt5rXtdYRMAFfzLpo9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDSErj/btsdZ0tDJWC/xVZLt5rXtdYRMAFfzLpo9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDSErj/btsdZ0tDJWC/xVZLt5rXtdYRMAFfzLpo9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDSErj%2FbtsdZ0tDJWC%2FxVZLt5rXtdYRMAFfzLpo9k%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;481&quot; height=&quot;414&quot; data-filename=&quot;스크린샷 2023-05-07 오전 1.04.13.png&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 아래와 같이 .storybook/main.ts 파일에 절대경로 설정을 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683388860214&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .storybook/main.ts
const path = require('path');

const config: StorybookConfig = {
  ...
  webpackFinal: async config =&amp;gt; {
    config.resolve.alias['@'] = path.resolve(__dirname, '../src/');
    return config;
  },
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;storybook에 public 폴더의 이미지 불러오기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행할 때 public폴더에서 불러오고 싶다면 storybook에 정적 파일이 존재하는 위치를 알려줘야합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683388763894&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//main.ts
const config: StorybookConfig = {
  ...
  staticDirs: ['../public'],
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같이 static 경로를 ../public으로 설정한다면 storybook에서 public폴더의 이미지를 불러올 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Styled Components 적용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;storybook에서 styled-components를 사용하려면 이를 지원하는 관련 패키지를 설치해야합니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm add -D @storybook/addon-styling&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 @storybook/addon-styling 패키지를 &lt;span style=&quot;color: #2e3438; text-align: start;&quot;&gt;DevDependencies에 추가합니다. &lt;/span&gt;&lt;span style=&quot;color: #2e3438; text-align: start;&quot;&gt;그리고 아래 코드를 .storybook/main.ts에 추가해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687162210709&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .storybook/main.ts
module.exports = {
  stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-essentials', '@storybook/addon-styling'],
  // ...
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Styled Components의 GlobalStyles 적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 프로젝트에서 globalStyles를 사용한다면,&amp;nbsp;.storybook/preview.ts에 decorators를 추가하면 globalStyles가 적용됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683388303214&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .storybook/preview.ts
import { withThemeFromJSXProvider } from '@storybook/addon-styling';
import { GlobalStyles } from '../src/styles';

// ...

export const decorators = [
  withThemeFromJSXProvider({
    GlobalStyles,
  }),
];&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Styled Components의&lt;span&gt;&amp;nbsp;&lt;/span&gt;ThemeProvider 적용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 themeProvider를 사용하고싶다면 withThemeFromJSXProvider 속성값에 themeProvider를 추가해주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1683388540572&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .storybook/preview.ts
import { ThemeProvider } from 'styled-components';
import { withThemeFromJSXProvider } from '@storybook/addon-styling';
import { GlobalStyles, theme } from '../src/styles';

export const decorators = [
  withThemeFromJSXProvider({
    themes: { theme },
    Provider: ThemeProvider,
    GlobalStyles,
  }),
];

export default preview;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 작성시 참고한 문서입니다. 관련해서 추가적인 정보를 원하시면 아래 공식문서에서 확인해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://storybook.js.org/recipes/next&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://storybook.js.org/recipes/next&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://storybook.js.org/recipes/styled-components&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://storybook.js.org/recipes/styled-components&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend/next.js</category>
      <category>next.js</category>
      <category>storybook</category>
      <category>styled-components</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/28</guid>
      <comments>https://devpluto.tistory.com/entry/Nextjs-Next%EC%97%90%EC%84%9C-Styled-Components%EC%99%80-%ED%95%A8%EA%BB%98-Storybook-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0#entry28comment</comments>
      <pubDate>Mon, 19 Jun 2023 17:20:13 +0900</pubDate>
    </item>
    <item>
      <title>[React] vite와 함께 리액트 컴포넌트 npm에 배포하기</title>
      <link>https://devpluto.tistory.com/entry/React-vite%EC%99%80-%ED%95%A8%EA%BB%98-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-npm%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 흔히 사용하는 react, eslint와 같은 패키지를 우리도 npm에 직접 배포할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react 함수와 컴포넌트 등을 작성한 라이브러리를 추후 프로젝트에 만들어서 사용할 예정이기 때문에, 이를 연습하기 위해 여러 리액트 컴포넌트를 npm에 배포한 과정을 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로, 개발자 경험과 번들링 사이즈 개선을 위해 여기에 이점이 있는 pnpm과 vite를 사용해서 배포해보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;vite + pnpm으로 프로젝트 설정하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;pnpm create vite (프로젝트 명)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어를 실행하면 아래와같이 framework와 언어를 선택할 수 있습니다. react 컴포넌트를 배포하기 위해 React를 선택한 다음, typescript를 지원하기 위해 Typescript를 선택해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-15 오후 4.15.10.png&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nygKe/btskgf4ji4j/WoDhgk7xr9eAIw1MDbVVr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nygKe/btskgf4ji4j/WoDhgk7xr9eAIw1MDbVVr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nygKe/btskgf4ji4j/WoDhgk7xr9eAIw1MDbVVr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnygKe%2Fbtskgf4ji4j%2FWoDhgk7xr9eAIw1MDbVVr0%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;481&quot; height=&quot;166&quot; data-filename=&quot;스크린샷 2023-06-15 오후 4.15.10.png&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-15 오후 4.17.06.png&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuZrZQ/btskeMBUP3s/k6kikWtp56hXze0mHWKAS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuZrZQ/btskeMBUP3s/k6kikWtp56hXze0mHWKAS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuZrZQ/btskeMBUP3s/k6kikWtp56hXze0mHWKAS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuZrZQ%2FbtskeMBUP3s%2Fk6kikWtp56hXze0mHWKAS0%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;510&quot; height=&quot;122&quot; data-filename=&quot;스크린샷 2023-06-15 오후 4.17.06.png&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;pnpm i&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 생성된 폴더에서 pnpm i 명령어로 dependency를 설치해주고, 기본으로 생성된 css파일과 App.tsx파일을 삭제해줍니다. (public 폴더도 삭제해주세요!)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-15 오후 4.21.11.png&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nBjEH/btskeMu6fXB/ugEBKN1KUNABlYBRbW7zqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nBjEH/btskeMu6fXB/ugEBKN1KUNABlYBRbW7zqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nBjEH/btskeMu6fXB/ugEBKN1KUNABlYBRbW7zqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnBjEH%2FbtskeMu6fXB%2FugEBKN1KUNABlYBRbW7zqK%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;232&quot; height=&quot;179&quot; data-filename=&quot;스크린샷 2023-06-15 오후 4.21.11.png&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;262&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배포할 컴포넌트 생성하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 컴포넌트 파일이 들어갈 폴더를 src/lib/components경로에 생성해 준 다음 컴포넌트 2개를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm에 배포해보기 위한 연습파일이므로 간단하게 작성해보았습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;//lib/src/components/TestComponent.tsx

const TestComponent = () =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;span&amp;gt;Test&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default TestComponent;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;//lib/src/components/TestComponent2.tsx

const TestComponent2 = () =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;span&amp;gt;Test2&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default TestComponent2;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 컴포넌트들을 한번에 lib/index.tsx파일을 생성하여 여기에서 한번에 export 시켜줍니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// lib/index.tsx

export { default as TestComponent } from &quot;./components/TestComponent&quot;;
export { default as TestComponent2 } from &quot;./components/TestComponent2&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;만든 컴포넌트 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src 폴더의 main.tsx 파일을 다음과 같이 수정합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/main.tsx

import ReactDOM from &quot;react-dom&quot;;
import { TestComponent, Test2Component } from &quot;./lib&quot;;
import React from &quot;react&quot;;

ReactDOM.render(
    &amp;lt;&amp;gt;
        &amp;lt;TestComponent /&amp;gt;
        &amp;lt;Test2Component /&amp;gt;
    &amp;lt;/&amp;gt;,
    document.getElementById(&quot;root&quot;) as HTMLElement
);&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;pnpm dev&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 pnpm dev 명령어를 실행하면 2개의 컴포넌트가 잘 렌더링 되는것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-16 오후 4.50.00.png&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBTN6q/btskfeEP0xo/WlE9W1kNuilH7r7buhaJVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBTN6q/btskfeEP0xo/WlE9W1kNuilH7r7buhaJVK/img.png&quot; data-alt=&quot;컴포넌트 2개가 잘 렌더링 되었습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBTN6q/btskfeEP0xo/WlE9W1kNuilH7r7buhaJVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBTN6q%2FbtskfeEP0xo%2FWlE9W1kNuilH7r7buhaJVK%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;412&quot; height=&quot;168&quot; data-filename=&quot;스크린샷 2023-06-16 오후 4.50.00.png&quot; data-origin-width=&quot;476&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컴포넌트 2개가 잘 렌더링 되었습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생성한 컴포넌트 빌드하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vite config파일 수정하기&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;pnpm i -D path @types/node vite-plugin-dts&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로 설정을 위한 path, @types/node 패키지와 .d.ts파일 번들링을 위한 vite-plugin-dts 패키지를 설치해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 vite.config.ts 파일을 다음과 같이 수정합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// vite.config.ts

import * as path from &quot;path&quot;;

import { defineConfig } from &quot;vite&quot;;
import dts from &quot;vite-plugin-dts&quot;;

export default defineConfig({
  build: {
    lib: {
      entry: path.resolve(__dirname, &quot;src/lib/index.tsx&quot;),
        name: &quot;index&quot;,
        fileName: &quot;index&quot;,
    },
    rollupOptions: {
      external: [&quot;react&quot;],
        output: {
          globals: {
            react: &quot;React&quot;,
          },
        },
    },
    commonjsOptions: {
      esmExternals: [&quot;react&quot;],
    },
  },
  plugins: [dts()],
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lib폴더의 파일들을 build하기위해 tsconfig의 include경로도 수정해줍니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;// tsconfig.json

{
  ...
  &quot;include&quot;: [&quot;src/lib&quot;],
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빌드 명령어를 통해 배포할 파일 생성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 다 마쳤다면 이제 build 명령어를 통해 배포할 파일을 생성해보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;pnpm build&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.21.12.png&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6A3Q2/btskeEKlmCi/CWetEdjz89AroCMwFUPXsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6A3Q2/btskeEKlmCi/CWetEdjz89AroCMwFUPXsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6A3Q2/btskeEKlmCi/CWetEdjz89AroCMwFUPXsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6A3Q2%2FbtskeEKlmCi%2FCWetEdjz89AroCMwFUPXsK%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;283&quot; height=&quot;237&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.21.12.png&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/dist 폴더에 다음과같이 type 파일들과 컴포넌트가 담긴 index.js 파일이 생성되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;만든 컴포넌트를 npm에 배포하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;package.json 파일 수정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트를 npm에 배포하기 위해서 package.json에 패키지에 다음과 같이 entry point를 작성해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm은 public으로 배포해야 무료로 배포할 수 있기 때문에 기본으로 설정되어있는 private 속성은 제거해주세요.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;// package.json

{
  &quot;name&quot;: &quot;@eunbae/react-library-ex&quot;,
  &quot;version&quot;: &quot;0.0.1&quot;,
  &quot;description&quot;: &quot;react library ex&quot;,
  &quot;type&quot;: &quot;module&quot;,
  &quot;main&quot;: &quot;dist/index.umd.cjs&quot;,
  &quot;module&quot;: &quot;dist/index.js&quot;,
  &quot;types&quot;: &quot;dist/index.d.ts&quot;,
  &quot;exports&quot;: {
    &quot;.&quot;: {
      &quot;import&quot;: &quot;./dist/index.js&quot;,
        &quot;require&quot;: &quot;./dist/index.umd.cjs&quot;
      }
  },
  &quot;files&quot;: [
    &quot;dist&quot;
  ],
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;version은 첫 배포이기 때문에 0.0.1로 적어줍니다. 만약 배포 이후 수정된 파일을 배포하고 싶다면 0.0.2와 같이 다른 버전으로 적어줘야 아래와 같은 오류가 뜨지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.39.12.png&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK50Tb/btskfrKfCCN/m9QKcjTUXRh8kGmSarXro1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK50Tb/btskfrKfCCN/m9QKcjTUXRh8kGmSarXro1/img.png&quot; data-alt=&quot;error: You cannot publish over the previously published versions: -&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK50Tb/btskfrKfCCN/m9QKcjTUXRh8kGmSarXro1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK50Tb%2FbtskfrKfCCN%2Fm9QKcjTUXRh8kGmSarXro1%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;282&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.39.12.png&quot; data-origin-width=&quot;1336&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;error: You cannot publish over the previously published versions: -&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;npm 배포시 제외할 파일 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;package.json과 dist폴더 안에있는 파일 외에는 배포하지 않기 위해 .npmignore 파일을 생성하고, 아래와 같이 입력해줍니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// .npmignore

index.html               
package.json             
/src    
tsconfig.json            
tsconfig.node.json       
vite.config.ts&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;npm에 로그인 후 배포하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 npm에 배포하기 위해서는 npm 계정이 필요합니다. 만약 계정이 없다면 [&lt;a href=&quot;https://www.npmjs.com/%5D&quot;&gt;https://www.npmjs.com/]&lt;/a&gt;(공식 사이트)에서 계정을 생성한 후 진행하시면 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm login&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어를 실행하면 터미널에서 npm에 로그인할 수 있습니다. npm 계정의 Username, password, email, OTP(이메일로 발송)를 차례대로 입력하면 로그인됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm publish&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 했다면 npm publish 명령어를 통해 배포합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2023-06-16 오후 3.32.28.png&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beWO8n/btskeunpv4A/OSKw9H9yuuRnrdcWk96sq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beWO8n/btskeunpv4A/OSKw9H9yuuRnrdcWk96sq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beWO8n/btskeunpv4A/OSKw9H9yuuRnrdcWk96sq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeWO8n%2Fbtskeunpv4A%2FOSKw9H9yuuRnrdcWk96sq1%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;639&quot; height=&quot;367&quot; data-filename=&quot;edited_스크린샷 2023-06-16 오후 3.32.28.png&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/@eunbae/react-library-ex&quot;&gt;https://www.npmjs.com/package/@eunbae/react-library-ex&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 제가 배포한 패키지는 위 링크에서 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;+) 배포 전에 패키지 설치해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 배포 전 dist폴더에 있는 파일을 패키지처럼 설치해보고 싶다면,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;pnpm i ./dist&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어를 입력하면 &quot;dist&quot;라는 이름으로 패키지가 설치됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.42.34.png&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3HgsA/btskaDS3bTy/qXr03LNrCb1Bq0FksHBXJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3HgsA/btskaDS3bTy/qXr03LNrCb1Bq0FksHBXJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3HgsA/btskaDS3bTy/qXr03LNrCb1Bq0FksHBXJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3HgsA%2FbtskaDS3bTy%2FqXr03LNrCb1Bq0FksHBXJK%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;784&quot; height=&quot;176&quot; data-filename=&quot;스크린샷 2023-06-16 오후 3.42.34.png&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 main.tsx 파일에서 dist 명의 패키지 파일이 잘 생성되었는지 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/main.tsx

import ReactDOM from &quot;react-dom&quot;;
import { TestComponent, Test2Component } from &quot;dist&quot;;
import React from &quot;react&quot;;

ReactDOM.render(
  &amp;lt;&amp;gt;
    &amp;lt;TestComponent /&amp;gt;
    &amp;lt;Test2Component /&amp;gt;
  &amp;lt;/&amp;gt;,
  document.getElementById(&quot;root&quot;) as HTMLElement
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 vite를 이용해서 react 컴포넌트를 npm에 배포해보았습니다. 직접 패키지를 만들어보는 과정을 통해 여러 패키지들이 이런 방식으로 만들어진다는 점을 알게되었고, npm에 배포하는게 생각보다 쉬운 과정이라는것을 알게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 제가 곧 진행할 프로젝트에서 공통적으로 사용하는 컴포넌트나 함수들을 라이브러리로 만들어서 사용해볼 예정인데, 만들고 배포하기까지의 과정을 다음 글로 작성해보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.vitejs.dev/guide/build.html#library-mode&quot;&gt;https://ko.vitejs.dev/guide/build.html#library-mode&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/vite-plugin-dts&quot;&gt;https://www.npmjs.com/package/vite-plugin-dts&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onderonur.netlify.app/blog/creating-a-typescript-library-with-vite/&quot;&gt;https://onderonur.netlify.app/blog/creating-a-typescript-library-with-vite/&lt;/a&gt;&lt;/p&gt;</description>
      <category>Frontend/react</category>
      <category>NPM</category>
      <category>pnpm</category>
      <category>react</category>
      <category>vite</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/27</guid>
      <comments>https://devpluto.tistory.com/entry/React-vite%EC%99%80-%ED%95%A8%EA%BB%98-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-npm%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0#entry27comment</comments>
      <pubDate>Fri, 16 Jun 2023 17:29:49 +0900</pubDate>
    </item>
    <item>
      <title>[React] 메모이제이션 Hook으로 중복연산 피하기 (useCallback, useMemo)</title>
      <link>https://devpluto.tistory.com/entry/React-%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98-Hook%EC%9C%BC%EB%A1%9C-%EC%A4%91%EB%B3%B5%EC%97%B0%EC%82%B0-%ED%94%BC%ED%95%98%EA%B8%B0useCallback-useMemo</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;메모이제이션(Memoization)은 프로그램이 동일한 계산을 반복할 때, 이전에 계산한 값을 메모리에 저장함으로써 중복되는 연산을 제거해서 프로그램 실행 속도를 빠르게하는 기술입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 함수형 컴포넌트에서는 이러한 메모이제이션을 돕기 위한 두가지 Hook을 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모이제이션을 하지 않을 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수형 컴포넌트에서 상태값이 변경되면 해당 컴포넌트는 다시 렌더링합니다. 이때 컴포넌트 내부에 정의한 함수들도 다시 생성되고 실행되기 때문에 메모이제이션이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 경우 메모이제이션 훅 useCallback과 useMemo를 사용할 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;렌더링 마다 함수가 새로 생성되어 참조값이 변하는 경우&lt;/li&gt;
&lt;li&gt;실행되는 함수가 복잡한 연산을 수행하는 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 훅을 사용하는 다양한 경우가 있지만, 두가지 경우를 예시로 메모이제이션 훅에 대해 설명하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useCallback&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;useCallback(fn, deps);&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useCallback 함수는 &lt;b&gt;&lt;u&gt;콜백 함수&lt;/u&gt;를 메모이제이션&lt;/b&gt;하고 반환합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678434561923&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useCallback } from 'react';

const memoizedCallback = useCallback(
  () =&amp;gt; {
    doSomething(a, b);
  },
  [a, b],
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 useCallback함수는 doSomething함수를 반환합니다. 그리고 렌더링시 반환된 함수가 저장된 memoizedCallback 함수는 의존성 배열안에 들어있는 값 a, b의 상태가 변경되었을 때만 생성됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678771165710&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Example = () =&amp;gt; {
  const [a, setA] = useState(0);
  const fetchApi = () =&amp;gt; {
    fetch('www.example.com/api')
      .then((response) =&amp;gt; response.json())
      .then((data) =&amp;gt; console.log(data));
  };
  
  useEffect(() =&amp;gt; {
    fetchApi().then(data =&amp;gt; setA(data));
  }, [fetchApi]);
  
  return(
    &amp;lt;div&amp;gt;
      &amp;lt;span&amp;gt;{a}&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API를 가져오는 함수 fetchApi()와 이 함수를 useEffect를 통해 실행하는 코드를 작성해보았습니다. 위 예시에서 useEffect를 통해 api를 가져오면 setA가 실행되어 컴포넌트 Example이 리렌더링됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 fetchApi 함수는 다시 생성되어 이름은 같지만, 자바스크립트에서 다른 메모리값을 가지기 때문에 새로운 참조값으로 변경됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 useEffect는 fetchApi가 변경되었다고 판단하기 때문에 api를 다시 호출하고, 또 리렌더링이 발생하여 호출을 반복하는 무한루프가 발생하게됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678773675425&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Example = () =&amp;gt; {
  const [a, setA] = useState(0);
  const fetchApi = useCallback(() =&amp;gt; {
    fetch('www.example.com/api')
      .then((response) =&amp;gt; response.json())
      .then((data) =&amp;gt; console.log(data));
  }, []);
  
  useEffect(() =&amp;gt; {
    fetchApi().then(data =&amp;gt; setA(data));
  }, [fetchApi]);
  
  return(
    &amp;lt;div&amp;gt;
      &amp;lt;span&amp;gt;{a}&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 위와같이 useCallback 훅을 이용하면 상태 a가 바뀌어 리렌더링이 되어도 fetchApi의 참조값이 변경되지 않기 때문에 함수 재호출, 무한루프가 발생하지 않게됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useMemo&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;useMemo(() =&amp;gt; fn, deps);&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMemo 함수는 &lt;b&gt;콜백 함수의 &lt;u&gt;반환값&lt;/u&gt;을 메모이제이션&lt;/b&gt;하고 반환합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678434137008&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useMemo } from 'react';

const memoizedValue = useMemo(() =&amp;gt; computeExpensiveValue(a, b), [a, b]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 computeExpensiveValue() 함수의 반환값이 memoizedValue에 저장되며, 렌더링시 a와 b의 상태가 변경되었을 때만 선언됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678438230307&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Example = () =&amp;gt; {
  const [a, setA] = useState('');
  const [b, setB] = useState(0);
  const expensiveTask = () =&amp;gt; {
    let i = 0;
    while(i &amp;lt; 10000000) i++;
    return a;
  };
  
  return(
    &amp;lt;div&amp;gt;
      &amp;lt;span&amp;gt;
        {expensiveTask()}
      &amp;lt;/span&amp;gt;
      &amp;lt;span&amp;gt;
        {b}
      &amp;lt;/span&amp;gt;
      &amp;lt;button onClick={setB(prev =&amp;gt; prev + 1)} /&amp;gt;
    &amp;lt;/div&amp;gt;  
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산이 오래걸리는 함수가 렌더링때마다 수행되는 코드를 위와같이 작성해보았습니다. 버튼을 클릭하면 상태 b의 값이 변경되어 리렌더링되는데, 렌더링시에 expensiveTask 함수도 호출되기 때문에 연산이 끝날때까지 렌더링이 지연됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 아래와 같이 useMemo 훅으로 결과값을 메모이제이션 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678439092493&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Example = () =&amp;gt; {
  const [a, setA] = useState('');
  const [b, setB] = useState(0);
  const expensiveTask = useMemo(() =&amp;gt; {
    let i = 0;
    while(i &amp;lt; 10000000) i++;
    return a;
  }, [a]);
  
  return(
    &amp;lt;div&amp;gt;
      &amp;lt;span&amp;gt;
        {expensiveTask()}
      &amp;lt;/span&amp;gt;
      &amp;lt;span&amp;gt;
        {b}
      &amp;lt;/span&amp;gt;
      &amp;lt;button onClick={setB(prev =&amp;gt; prev + 1)} /&amp;gt;
    &amp;lt;/div&amp;gt;  
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 useMemo 훅을 이용하면 상태 a의 값이 변할 때만 함수가 호출됩니다. 따라서 상태 b가 변경되더라도 렌더링이 지연되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 useMemo 훅으로&amp;nbsp;전달된 함수는 &lt;b&gt;렌더링 중에 실행&lt;/b&gt;됩니다. 따라서 useMemo 내부에는 렌더링 시 수행할 작업만 작성해야 합니다. 렌더링 이후 작업을 작성하고 싶다면 useEffect를 사용해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useCallback은 콜백 함수 자체를, useMemo는 콜백 함수의 반환값을 메모이제이션 한다는 점에서 차이가 존재합니다. 두 훅을 이용하여&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useCallback(fn, deps)은&amp;nbsp;useMemo(() =&amp;gt; fn, deps)와 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.reactjs.org/docs/hooks-reference.html#usememo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ko.reactjs.org/docs/hooks-reference.html#usememo&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://thisblogfor.me/react/hooks_memoization/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://thisblogfor.me/react/hooks_memoization/&lt;/a&gt;&lt;/p&gt;</description>
      <category>Frontend/react</category>
      <category>react</category>
      <category>useCallback</category>
      <category>useMemo</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/26</guid>
      <comments>https://devpluto.tistory.com/entry/React-%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98-Hook%EC%9C%BC%EB%A1%9C-%EC%A4%91%EB%B3%B5%EC%97%B0%EC%82%B0-%ED%94%BC%ED%95%98%EA%B8%B0useCallback-useMemo#entry26comment</comments>
      <pubDate>Tue, 14 Mar 2023 15:08:07 +0900</pubDate>
    </item>
    <item>
      <title>[React] 리액트의 생명주기 메서드와 Hook</title>
      <link>https://devpluto.tistory.com/entry/React-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0-%EB%A9%94%EC%84%9C%EB%93%9C%EC%99%80-Hook</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;생명주기 내에서는 여러 메서드가 호출되며 이를 사용할 수 있는데 &lt;b&gt;클래스형 컴포넌트&lt;/b&gt;에서는 &lt;b&gt;생명주기 메서드&lt;/b&gt;를 사용하고, &lt;b&gt;함수형 컴포넌트&lt;/b&gt;에서는 &lt;b&gt;Hook&lt;/b&gt;을 사용할 수 있는 점에서 서로 차이가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 리액트의 클래스형 컴포넌트의 생명주기 메서드를 알아본 다음, 함수형 컴포넌트의 Hook중 하나인 useEffect 함수를 통해 차이점을 더 자세히 알아보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;메서드란?&lt;/b&gt;&amp;nbsp;&lt;br /&gt;: 클래스 내부에서 정의한 함수를 메서드라 부릅니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생명주기 메서드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스형 컴포넌트를 작성할 때는 아래와 같이 render() 메서드를 호출하고 JSX를 return하도록 작성해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1678341354754&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { Component } from 'react';

class Example extends Component {
  state = {
    number: 0
  };
  render() {
    const { number } = this.state;
    return (
      &amp;lt;div&amp;gt;
      	&amp;lt;span&amp;gt;example&amp;lt;/span&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }
}

export default Example;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성한 컴포넌트 안에는 아래 그림과 같이 constructer(), componentDidMount(), componentDidUpdate() 와 같은 다양한 생명주기 메서드들이 존재합니다. 이제 생성과 업데이트, 제거되는 과정에 대해 각각 설명하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-09 오후 2.54.16.png&quot; data-origin-width=&quot;2246&quot; data-origin-height=&quot;1250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/laDaJ/btr2PUKB5p9/TjokWnYmm1wv4DhVO0trAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/laDaJ/btr2PUKB5p9/TjokWnYmm1wv4DhVO0trAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/laDaJ/btr2PUKB5p9/TjokWnYmm1wv4DhVO0trAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlaDaJ%2Fbtr2PUKB5p9%2FTjokWnYmm1wv4DhVO0trAk%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;2246&quot; height=&quot;1250&quot; data-filename=&quot;스크린샷 2023-03-09 오후 2.54.16.png&quot; data-origin-width=&quot;2246&quot; data-origin-height=&quot;1250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Mounting&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 컴포넌트가 생성되는 것을 마운팅(mounting)이라 합니다. 컴포넌트가 마운팅이 시작되면 아래와 같은 과정을 거칩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컴포넌트가 처음 마운팅 될 때 React는 컴포넌트의 constructor를 호출합니다. constructor에서는 state를 초기화하는 등의 작업을 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;다음으로 React는 컴포넌트의&amp;nbsp;render()&amp;nbsp;메서드를 호출합니다. render() 메서드를 통해 React는 화면에 표시되어야 할 내용(JSX)을 알게 되며,&amp;nbsp;DOM을 업데이트합니다.&lt;/li&gt;
&lt;li&gt;마지막으로 컴포넌트의 출력값(JSX)이 DOM에 삽입되면, React는&amp;nbsp;componentDidMount()&amp;nbsp;생명주기 메서드를 호출합니다. componentDidMount() 메서드를 이용하면 컴포넌트가 마운팅 완료 되었을 때 수행할 작업, 예를들어 API를 호출하거나 DOM의 속성을 변경하는 등의 작업을 진행할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 constructor와 render 메서드의 호출 사이에 getDerivedStateFromProps() 메서드도 호출 가능한데, 받아온 props를&amp;nbsp;state에 넣어주고 싶을 때 사용할 수 있다 는 정도로만 간단히 설명하고 넘어가겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Updating&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 props를 받거나, setState()메서드로 state가 업데이트되는 등의 상황에서 리액트 컴포넌트는 업데이트됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;setState 등으로 state가 변경된 상황을 React가 인지하면&amp;nbsp;화면에 표시될 내용을 알아내기 위해&amp;nbsp;render()&amp;nbsp;메서드를 다시 호출합니다.&lt;/li&gt;
&lt;li&gt;render() 메서드가 호출되면 마운팅 될 때와 마찬가지로 DOM을 업데이트 하고, componentDidUpdate()메서드를 호출합니다. 이를 이용해 컴포넌트가 업데이트 되었을때의 작업을 지정할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Unmounting&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 컴포넌트가 삭제되는 것, 즉 화면에서 사라지는 것을 Unmount라 합니다. componentWillUnmount() 메서드는 컴포넌트가 화면에서 사라지기 직전에 호출됩니다. 이 메서드를 이용하여 API 연결을 해제하거나, 이전에 setInterval 함수를 이용하여 함수를 반복 호출했다면 clearInterval함수로 중단하는 등의 작업을 할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useEffect Hook&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스형 컴포넌트에서 사용하는 생명주기 메서드에 대하여 알아보았으니 이번에는 함수형 컴포넌트에서 사용하는 hook중 하나인 useEffect를 생명주기 메서드와 비교해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect를 사용할 때 아래와 같이 첫번째 파라미터에는 함수, 두번째 파라미터에는 의존값의 배열(deps)을 넣습니다. 함수를 작성할 때 반환(return)할 수 있다는 점도 기억해 주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1678347194344&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  ...
  return () =&amp;gt; {
    ...
  }
}, [...])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect는 componentDidMount, componentDidUpdate, componentWillUnmount의 역할을 수행하며 기본적으로 &lt;b&gt;처음 마운트 될 때 호출&lt;/b&gt;됩니다. 이는 useEffect가 componentDidMount() 메서드의 역할을 기본적으로 수행한다 할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;deps 배열이 존재하지 않을 때&lt;/h3&gt;
&lt;pre id=&quot;code_1678348353730&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  ...
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect 내부에 함수만 작성되었고 deps 배열이 없다면 컴포넌트가 리렌더링 될 때마다 항상 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에는 생명주기 메서드의 &lt;b&gt;componentDidMount, componetDidUpdate&lt;/b&gt; 메서드와 같은 역할을 수행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;deps 배열이 빈 배열일 때&lt;/h3&gt;
&lt;pre id=&quot;code_1678348368599&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  ...
}, [])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deps 배열이 빈 배열일 경우에는 처음 컴포넌트가 마운트 될 때만 실행되며, &lt;b&gt;componentDidMount &lt;/b&gt;메서드와 같은 역할을 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;deps 배열이 존재할 때&lt;/h3&gt;
&lt;pre id=&quot;code_1678348386282&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  ...
}, [state, props, ...])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deps 배열이 존재할 때, 즉 배열에 state나 prop이 들어있을 때에는 이 값들이 변경될 때만 useEffect가 실행됩니다. 이는 &lt;b&gt;componentDidUpdate, getDerivedStateFromProps&lt;/b&gt; 메서드와 같은 역할을 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수를 반환할 때 (return)&lt;/h3&gt;
&lt;pre id=&quot;code_1678348399520&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  ...
  return () =&amp;gt; {
    ...
  }
}, [...])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 인자의 함수를 반환(return)할 수 있는데, 이때 반환하는 함수를 clean-up 함수라 표현합니다. 이 clean-up 함수가 바로 &lt;b&gt;componentWillUnmount&lt;/b&gt;의 역할을 수행합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;+) useEffect를 렌더링 이전에 실행하려면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로, useEffect는 실행이 마운트가 끝났을 때, 즉 브라우저 렌더링이 완료되었을 때 실행 된다는 것에 유의해야합니다. 만약 렌더링이 완료되기 이전에, 즉 브라우저가 화면에 DOM을 그리기 전에 호출되어야 하면 useLayoutEffect() Hook을 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useLayoutEffect()는 useEffect와 사용방법은 동일하며, 실행 순서에만 차이가 있습니다.&lt;/p&gt;</description>
      <category>Frontend/react</category>
      <category>react</category>
      <category>useEffect</category>
      <category>생명주기 메서드</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/25</guid>
      <comments>https://devpluto.tistory.com/entry/React-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0-%EB%A9%94%EC%84%9C%EB%93%9C%EC%99%80-Hook#entry25comment</comments>
      <pubDate>Thu, 9 Mar 2023 16:53:38 +0900</pubDate>
    </item>
    <item>
      <title>[Firebase] Firebase hosting으로 next.js 배포 자동화하기</title>
      <link>https://devpluto.tistory.com/entry/Firebase-Firebase-hosting%EC%9C%BC%EB%A1%9C-nextjs-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;firebase는 웹 앱을 빠르게 배포 해주는 hosting기능을 제공합니다. 이번 글은 next.js 프로젝트를 firebase hosting을 이용하여 사이트에 배포하는 법에 대해 작성하였습니다. 또 github와 연결하여 pr을 날리고 병합할 때 자동으로 배포하는 방법에 대해서도 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬 환경에서 firebase deploy 적용하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Firebase CLI 설정하기&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;firebase cli 설치하기&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm i firebase-tools -D&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 firebase cli를 npm install을 통해 설치해줍니다. 로컬환경에 firebase cli를 설치하게 되면 sudo 권한 문제와 &lt;br /&gt;피할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;node_modules/.bin/firebase login&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 위 명령어를 통해 firebase에 로그인을 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;firebase init hosting 명령어를 통해 초기화하기&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;node_modules/.bin/firebase init hosting&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어를 입력하면 아래와 같이 프로젝트 셋업을 하는 질문들이 나타납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 dongne-market이라는 이름의 파이어베이스 프로젝트가 존재하므로 찾아서 선택해주고, Hosting Setup에서 public directory를 out으로 설정해줍니다. next build &amp;amp;&amp;amp; next export를 하게되면 out디렉토리에 정적 파일이 생성되기 때문입니다. 이후 질문에는 모두 No로 설정해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-03 오후 11.35.03.png&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;1242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LAHOE/btrWsUBXKZY/bqUoAg7YEli6YbKYNwjXQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LAHOE/btrWsUBXKZY/bqUoAg7YEli6YbKYNwjXQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LAHOE/btrWsUBXKZY/bqUoAg7YEli6YbKYNwjXQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLAHOE%2FbtrWsUBXKZY%2FbqUoAg7YEli6YbKYNwjXQ1%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;658&quot; height=&quot;596&quot; data-filename=&quot;스크린샷 2023-01-03 오후 11.35.03.png&quot; data-origin-width=&quot;1372&quot; data-origin-height=&quot;1242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Firebase deploy 명령어로 배포하기&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;node_modules/.bin/firebase deploy&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어를 실행하면 아래와 같이 firebase는 out폴더에있는 정적 파일을 hosting URL에 배포해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-03 오후 11.34.25.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w4Lf4/btrWq3NcLyL/mSYXeLKIlT2jk6B39WxGKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w4Lf4/btrWq3NcLyL/mSYXeLKIlT2jk6B39WxGKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w4Lf4/btrWq3NcLyL/mSYXeLKIlT2jk6B39WxGKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw4Lf4%2FbtrWq3NcLyL%2FmSYXeLKIlT2jk6B39WxGKk%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;649&quot; height=&quot;261&quot; data-filename=&quot;스크린샷 2023-01-03 오후 11.34.25.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코드에 변경사항이 있다면 배포 전에 꼭 npm build등의 명령어를 통해 정적 파일을 생성해야한다는 번거로움이 있습니다. 그래서 다음으로 github Action을 통해 자동으로 빌드하여 배포하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Github Action과 연동하여 배포하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트가 깃허브에 연동되어있다고 가정하고 환경을 구축하겠습니다. 우선 아래 명령어를 입력합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;node_modules/.bin/firebase init hosting:github&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 세팅시 github와 연결하기위해 브라우저 창이 뜨며 github에 로그인이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인이 완료되었다면 다시 터미널 창으로 돌아와 호스팅하기위한 질문에 답변이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 원하는 github repository를 사용자 이름/저장소 이름 형식으로 입력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;workflow에서 build script를 사용할지 묻는 질문에는 Yes로 설정합니다. 이는 package.json에서 설정한 npm build(next build &amp;amp;&amp;amp; next export)명령어를 사용하기 위함입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 매 배포시마다 어떤 script가 실행될 지 묻는 질문에는 &lt;b&gt;npm ci &amp;amp;&amp;amp; npm run build &lt;/b&gt;를 입력해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 질문에는 아래와같이 Yes, main을 입력해 줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-04 오전 5.15.43.png&quot; data-origin-width=&quot;2220&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btLMfe/btrWjkXgGZG/bghlVhykuC7ZKAJxiOckR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btLMfe/btrWjkXgGZG/bghlVhykuC7ZKAJxiOckR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btLMfe/btrWjkXgGZG/bghlVhykuC7ZKAJxiOckR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtLMfe%2FbtrWjkXgGZG%2FbghlVhykuC7ZKAJxiOckR1%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;2220&quot; height=&quot;700&quot; data-filename=&quot;스크린샷 2023-01-04 오전 5.15.43.png&quot; data-origin-width=&quot;2220&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 완료되었다면 두개의 yml파일이 생성됩니다. yml 파일의 내용에 따라 main브랜치로 pr이 생성될 때 미리보기 사이트(7일간 확인 가능)에 배포되며, pr이 merge된다면 라이브 사이트(projectId.web.app)에 자동 배포가 진행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-17 오전 12.48.34.png&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lnfXZ/btrWqqPLS16/MBIWbUG7nFeKGKt7pRrwjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lnfXZ/btrWqqPLS16/MBIWbUG7nFeKGKt7pRrwjK/img.png&quot; data-alt=&quot;PR 생성 시 미리보기 사이트에 배포&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lnfXZ/btrWqqPLS16/MBIWbUG7nFeKGKt7pRrwjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlnfXZ%2FbtrWqqPLS16%2FMBIWbUG7nFeKGKt7pRrwjK%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;1822&quot; height=&quot;636&quot; data-filename=&quot;스크린샷 2023-01-17 오전 12.48.34.png&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PR 생성 시 미리보기 사이트에 배포&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-17 오전 12.54.02.png&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMuYln/btrWtfGcahd/bJMaST3LJK8EHVQRuCZnh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMuYln/btrWtfGcahd/bJMaST3LJK8EHVQRuCZnh0/img.png&quot; data-alt=&quot;main 브랜치에 병합(merge)시 라이브 사이트에 배포&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMuYln/btrWtfGcahd/bJMaST3LJK8EHVQRuCZnh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMuYln%2FbtrWtfGcahd%2FbJMaST3LJK8EHVQRuCZnh0%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;625&quot; height=&quot;378&quot; data-filename=&quot;스크린샷 2023-01-17 오전 12.54.02.png&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;main 브랜치에 병합(merge)시 라이브 사이트에 배포&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TroubleShooting: 프로젝트에서 .env파일을 사용한다면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 프로젝트에서 .env파일을 사용한다면 gitignore에 등록되어있으므로 깃허브에 따로 변수를 등록해주는 과정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 설정(settings)탭으로 들어가서 Security &amp;gt; Secrets and Variables &amp;gt; Action 탭으로 이동 후 아래 사진과같이 New repository secret 버튼을 클릭하여 env파일에 들어갈 변수를 하나씩 저장합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgSSMK/btrWtdVU1g2/TRrjaTMKmKp3Mg1dqEAqYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgSSMK/btrWtdVU1g2/TRrjaTMKmKp3Mg1dqEAqYK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;828&quot; data-filename=&quot;스크린샷 2023-01-17 오전 1.03.22.png&quot; style=&quot;width: 61.9765%; margin-right: 10px;&quot; data-widthpercent=&quot;62.71&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgSSMK/btrWtdVU1g2/TRrjaTMKmKp3Mg1dqEAqYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgSSMK%2FbtrWtdVU1g2%2FTRrjaTMKmKp3Mg1dqEAqYK%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;1610&quot; height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uoSJg/btrWqIinEtg/WaUbPsyniU0wt5kLoJA1bK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uoSJg/btrWqIinEtg/WaUbPsyniU0wt5kLoJA1bK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;882&quot; data-filename=&quot;스크린샷 2023-01-17 오전 1.03.00.png&quot; style=&quot;width: 36.8607%;&quot; data-widthpercent=&quot;37.29&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uoSJg/btrWqIinEtg/WaUbPsyniU0wt5kLoJA1bK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuoSJg%2FbtrWqIinEtg%2FWaUbPsyniU0wt5kLoJA1bK%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;1020&quot; height=&quot;882&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이전에 생성된 yml파일에서 actions/checkout@v2 와 run npm ci &amp;amp;&amp;amp; npm build 스텝 사이에 아래 코드와 같이 env파일을 생성하는 스텝을 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1673885198650&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- name: Generate Environment Variables File for Production
    run: |
      echo &quot;DONGNE_MARKET_FIREBASE_APIKEY=$DONGNE_MARKET_FIREBASE_APIKEY&quot; &amp;gt;&amp;gt; .env
      echo &quot;DONGNE_MARKET_AUTH_DOMAIN=$DONGNE_MARKET_AUTH_DOMAIN&quot; &amp;gt;&amp;gt; .env
      echo &quot;DONGNE_MARKET_PROJECT_ID=$DONGNE_MARKET_PROJECT_ID&quot; &amp;gt;&amp;gt; .env
      echo &quot;DONGNE_MARKET_STORAGE_BUCKET=$DONGNE_MARKET_STORAGE_BUCKET&quot; &amp;gt;&amp;gt; .env
      echo &quot;DONGNE_MARKET_MESSAGING_SENDER_ID=$DONGNE_MARKET_MESSAGING_SENDER_ID&quot; &amp;gt;&amp;gt; .env
      echo &quot;DONGNE_MARKET_APP_ID=$DONGNE_MARKET_APP_ID&quot; &amp;gt;&amp;gt; .env
      echo &quot;DONGNE_MARKET_MEASUREMENT_ID=$DONGNE_MARKET_MEASUREMENT_ID&quot; &amp;gt;&amp;gt; .env
    env:
      DONGNE_MARKET_FIREBASE_APIKEY: ${{ secrets.DONGNE_MARKET_FIREBASE_APIKEY }}
      DONGNE_MARKET_AUTH_DOMAIN: ${{ secrets.DONGNE_MARKET_AUTH_DOMAIN }}
      DONGNE_MARKET_PROJECT_ID: ${{ secrets.DONGNE_MARKET_PROJECT_ID }}
      DONGNE_MARKET_STORAGE_BUCKET: ${{ secrets.DONGNE_MARKET_STORAGE_BUCKET }}
      DONGNE_MARKET_MESSAGING_SENDER_ID: ${{ secrets.DONGNE_MARKET_MESSAGING_SENDER_ID }}
      DONGNE_MARKET_APP_ID: ${{ secrets.DONGNE_MARKET_APP_ID }}
      DONGNE_MARKET_MEASUREMENT_ID: ${{ secrets.DONGNE_MARKET_MEASUREMENT_ID }}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-17 오전 1.08.53.png&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TsadT/btrWqCvUWDJ/aFF33MnSKkJpGFQKknRl2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TsadT/btrWqCvUWDJ/aFF33MnSKkJpGFQKknRl2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TsadT/btrWqCvUWDJ/aFF33MnSKkJpGFQKknRl2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTsadT%2FbtrWqCvUWDJ%2FaFF33MnSKkJpGFQKknRl2k%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;622&quot; height=&quot;144&quot; data-filename=&quot;스크린샷 2023-01-17 오전 1.08.53.png&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고문서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/hosting&quot;&gt;https://firebase.google.com/docs/hosting&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=P0x0LmiknJc&quot;&gt;https://www.youtube.com/watch?v=P0x0LmiknJc&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ji5485.github.io/post/2021-06-26/create-env-with-github-actions-secrets/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ji5485.github.io/post/2021-06-26/create-env-with-github-actions-secrets/&lt;/a&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;</description>
      <category>etc/firebase</category>
      <category>firebase</category>
      <category>배포</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/24</guid>
      <comments>https://devpluto.tistory.com/entry/Firebase-Firebase-hosting%EC%9C%BC%EB%A1%9C-nextjs-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%98%EA%B8%B0#entry24comment</comments>
      <pubDate>Tue, 17 Jan 2023 01:09:23 +0900</pubDate>
    </item>
    <item>
      <title>[React] recoil로 주문내역 관리하기</title>
      <link>https://devpluto.tistory.com/entry/React-recoil%EB%A1%9C-%EC%A3%BC%EB%AC%B8%EB%82%B4%EC%97%AD-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
      <description>&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;학교 수업의 팀 프로젝트인 음식 주문 서비스를 구현하는 과정에서 음식 주문 내역을 추가하거나 수정, 삭제하는 기능을 구현하는 과정을 정리하였다. &lt;/span&gt;recoil을 사용하는 이유는 여러 컴포넌트에서 주문 정보를 사용해야하기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;recoil의 중첩객체(nested object)를 함수형 업데이트하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 내역을 추가, 삭제하기 이전에 중첩 객체를 함수형 업데이트하는 방법을 알아보자. 보통 useState안의 배열을 수정하기 위해서 아래와 같은 방법으로 함수형 업데이트를 사용해야한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1668142320615&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;setOrderInfo((prev) =&amp;gt; ([...prev, newList]));&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1668142000397&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { atom } from &quot;recoil&quot;

export const orderInfoState = atom({
  key: &quot;orderInfo&quot;,
  default: {
    finalAmount: 0,
    orderList: [{
      menu: '',
      style: '',
      amount: 0,
      orderListId: 0,
      quantity: 0,
    }]
  }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 주문 내역에 필요한 객체는 위와 같이 최종 가격인 finalAmount 속성과 주문 목록 리스트인 &lt;span&gt;orderList&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;속성이 포함되어있다. 따라서 객체 안의 배열을 함수형 업데이트해야하는데 그 방법은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1668142198772&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;setOrderInfo((prev) =&amp;gt; ({
  ...prev, 
  finalAmount: prev.finalAmount + 100,
  orderList: [...prev.orderList, newOrder],
}))&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주문 내역 추가하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-10 오후 2.13.39.png&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXuGWw/btrQXNh01nO/UyxcZyY471kEz04KChVH80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXuGWw/btrQXNh01nO/UyxcZyY471kEz04KChVH80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXuGWw/btrQXNh01nO/UyxcZyY471kEz04KChVH80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXuGWw%2FbtrQXNh01nO%2FUyxcZyY471kEz04KChVH80%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;462&quot; height=&quot;351&quot; data-filename=&quot;스크린샷 2022-11-10 오후 2.13.39.png&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림처럼 두번째 선택인 디너 스타일을 선택했을때 주문서에 추가하도록 구현했다.&lt;/p&gt;
&lt;pre id=&quot;code_1668143438735&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// order.jsx
const [orderInfo, setOrderInfo] = useRecoilState(orderInfoState);
const menuRef = useRef(null);
const styleRef = useRef(null);
const orderListIdRef = useRef(0);

const onChangeStyleSelect = () =&amp;gt; {
  const menu = menuRef.current;
  const style = styleRef.current;
  const totalAmount = totalAmountbyMenuAndStyle(menu.value, style.value);
  
  const newOrder = {
        menu: menu.value, 
        style: style.value, 
        amount: totalAmount, 
        orderListId: orderListIdRef.current, 
        quantity: 1
      };
  orderListIdRef.current += 1;
  setOrderInfo((prev) =&amp;gt; ({
    ...prev,
    finalAmount: prev.finalAmount + totalAmount,
    orderList: [...prev.orderList, newOrder]
  }));
  
  menu.value = '';
  style.value = '';
};

return (
  &amp;lt;div className=&quot;my-3 flex justify-between&quot;&amp;gt;
    &amp;lt;h3 className=&quot;text-lg font-bold&quot;&amp;gt;디너 메뉴&amp;lt;/h3&amp;gt;
    &amp;lt;select ref={menuRef} className=&quot;outline-none&quot;&amp;gt;
      &amp;lt;option value=&quot;&quot;&amp;gt;--디너 메뉴를 선택하세요--&amp;lt;/option&amp;gt;
      &amp;lt;option value=&quot;valentine&quot;&amp;gt;Valentine dinner ($100)&amp;lt;/option&amp;gt;
      &amp;lt;option value=&quot;french&quot;&amp;gt;French dinner ($130)&amp;lt;/option&amp;gt;
      &amp;lt;option value=&quot;english&quot;&amp;gt;English dinner ($130)&amp;lt;/option&amp;gt;
      &amp;lt;option value=&quot;champagne&quot;&amp;gt;Champagne Feast dinner ($250)&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div className=&quot;my-3 flex justify-between&quot;&amp;gt;
    &amp;lt;h3 className=&quot;text-lg font-bold&quot;&amp;gt;디너 스타일&amp;lt;/h3&amp;gt;
    &amp;lt;select ref={styleRef} onChange={onChangeStyleSelect} className=&quot;outline-none&quot; disabled&amp;gt;
      &amp;lt;option value=&quot;&quot;&amp;gt;--디너 스타일을 선택하세요--&amp;lt;/option&amp;gt;
      &amp;lt;option value=&quot;simple&quot;&amp;gt;Simple&amp;lt;/option&amp;gt;
      &amp;lt;option value=&quot;grand&quot;&amp;gt;Grand (+ $20)&amp;lt;/option&amp;gt;
      &amp;lt;option value=&quot;deluxe&quot;&amp;gt;Deluxe (+ $40)&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;h2 className=&quot;font-bold text-lg&quot;&amp;gt;주문서&amp;lt;/h2&amp;gt;
  {orderInfo.orderList
    .filter((list) =&amp;gt; list.orderListId &amp;gt; 0)
    .map(order =&amp;gt; (
      &amp;lt;OrderSheetBox key={order.orderListId} order={order} orderInfo={orderInfo} setOrderInfo={setOrderInfo} progress={progress}/&amp;gt;
    ))}
  &amp;lt;div className=&quot;text-lg font-bold&quot;&amp;gt;최종 결제 금액: ${orderInfo.finalAmount}&amp;lt;/div&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 스타일이 변경되었을 때 메뉴와 수량, 가격, 수량, id를 포함한 새로운 객체를 생성하여 orderList배열에 추가해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id는 삭제할때와 map에 고유한 key를 주기위해서 생성했는데, useRef를 이용하여 새 주문이 생성될때마다 1을 더한값이 id가 되도록 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 totalAmount값은 주문의 메뉴의 가격의 합으로 설정해서 전체 가격에다가 더해주었다. totalAmountbtMenuAndStyle 함수(더보기)&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1668142984814&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function totalAmountbyMenuAndStyle(menu, style) {
  let totalAmount = 0;
  switch(menu) {
    case 'valentine': totalAmount += 100; break;
    case 'french': totalAmount += 130; break;
    case 'english': totalAmount += 130; break;
    case 'champagne': totalAmount += 250; break;
    default: totalAmount += 0; 
  }
  switch(style) {
    case 'simple': totalAmount += 0; break;
    case 'grand': totalAmount += 20; break;
    case 'deluxe': totalAmount += 40; break;
  }
  return totalAmount;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TroubleShooting: 동일 주문은 수량만 증가하도록 처리하기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면_기록_2022-11-11_오후_2_14_13_AdobeExpress.gif&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9dTpM/btrQWXeiFlK/nslKeMnKYQkNHqKB6qiQP0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9dTpM/btrQWXeiFlK/nslKeMnKYQkNHqKB6qiQP0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9dTpM/btrQWXeiFlK/nslKeMnKYQkNHqKB6qiQP0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b9dTpM/btrQWXeiFlK/nslKeMnKYQkNHqKB6qiQP0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;529&quot; height=&quot;358&quot; data-filename=&quot;화면_기록_2022-11-11_오후_2_14_13_AdobeExpress.gif&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 동작처럼 동일한 주문인데도 주문서에서 각각의 다른 주문으로 추가된다. 따라서 두 주문을 한번에 수량과 가격만 증가하도록 하는 처리가 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1668057728290&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const orderListIdRef = useRef(0);
const onChangeStyleSelect = () =&amp;gt; {
  const menu = menuRef.current;
  const style = styleRef.current;
  const totalAmount = totalAmountbyMenuAndStyle(menu.value, style.value);

  const sameOrder = orderInfo.orderList.filter(order =&amp;gt; order.menu === menu.value &amp;amp;&amp;amp; order.style === style.value)
  if(sameOrder.length === 0) {
    orderListIdRef.current += 1;
    setOrderInfo((prev) =&amp;gt; ({
      ...prev,
      finalAmount: prev.finalAmount + totalAmount,
      orderList: [...prev.orderList, {menu: menu.value, style: style.value, amount: totalAmount, orderListId: orderListIdRef.current, quantity: 1}]
    }));
  } else {
    const newSameOrder = {...sameOrder[0], quantity: parseInt(sameOrder[0].quantity) + 1, amount: sameOrder[0].amount + totalAmount}
    const orderListExcept = orderInfo.orderList.filter(order =&amp;gt; order.orderListId !== sameOrder[0].orderListId);
    
    setOrderInfo((prev) =&amp;gt; ({
      ...prev, 
      finalAmount: prev.finalAmount + totalAmount,
      orderList: [...orderListExcept, newSameOrder]
    }))
  }
  menu.value = '';
  style.value = '';
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;orderList에 필터 함수를 적용해서 menu와 style이 같은 객체의 배열을 가져올 수 있다. 이 배열의 길이가 0이라면 기존 작업을 수행하면 되고, 아니라면 수량과 가격을 증가시킨 newSameOrder 객체와 orderListExecpt 배열을 업데이트한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;orderListExecpt 배열은 업데이트할 객체 외의 모든 객체를 orderList에서 필터해서 받아온 배열이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 sameOrder[0]으로 작성한 이유는 sameOrder는 배열을 반환하므로 [{menu: '', style: '', ...}]와 같기 때문에 배열의 0번 인덱스를 사용해야하기 때문이다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면_기록_2022-11-11_오후_2_30_02_AdobeExpress.gif&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cT4QQA/btrQVaMeNqY/ba0IPj0IsSvUC2lWl9oa5k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cT4QQA/btrQVaMeNqY/ba0IPj0IsSvUC2lWl9oa5k/img.gif&quot; data-alt=&quot;같은 주문일 때는 수량만 증가하도록 구현된 것을 확인할 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cT4QQA/btrQVaMeNqY/ba0IPj0IsSvUC2lWl9oa5k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cT4QQA/btrQVaMeNqY/ba0IPj0IsSvUC2lWl9oa5k/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;529&quot; height=&quot;356&quot; data-filename=&quot;화면_기록_2022-11-11_오후_2_30_02_AdobeExpress.gif&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;356&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;같은 주문일 때는 수량만 증가하도록 구현된 것을 확인할 수 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주문 수량 수정하기&lt;/h2&gt;
&lt;pre id=&quot;code_1668144039017&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function OrderBox({order, orderInfo, setOrderInfo, progress}) {
  const onChangeOrderQuantity = (e, orderListId) =&amp;gt; {
    const num = e.target.value;
    const orderList = orderInfo.orderList.filter(order =&amp;gt; order.orderListId === orderListId)[0];
    const initAmount = orderList.amount / orderList.quantity;
    const newOrderList = {...orderList, quantity: num, amount: initAmount * num}
    const orderListExcept = orderInfo.orderList.filter(order =&amp;gt; order.orderListId !== orderListId);
    
    setOrderInfo((prev) =&amp;gt; ({
      ...prev, 
      finalAmount: prev.finalAmount - orderList.amount + initAmount * num,
      orderList: [...orderListExcept, newOrderList]
    }))
  }

  return (
    &amp;lt;div className=&quot;flex my-1&quot;&amp;gt;
      &amp;lt;span&amp;gt;메뉴: {order.menu}&amp;lt;/span&amp;gt;
      &amp;lt;span className=&quot;ml-2&quot;&amp;gt;스타일: {order.style}&amp;lt;/span&amp;gt;
      &amp;lt;span className=&quot;ml-2&quot;&amp;gt;가격: {order.amount}&amp;lt;/span&amp;gt;
      &amp;lt;span className=&quot;ml-2&quot;&amp;gt;주문 수량: &amp;lt;/span&amp;gt;
      &amp;lt;input type=&quot;number&quot; min=&quot;1&quot; value={order.quantity} onChange={e =&amp;gt; onChangeOrderQuantity(e, order.orderListId)} className=&quot;text-center w-10 outline-none&quot; /&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 주문을 OrderBox 컴포넌트로 분리하여 작성했다. 여기서 주문 수량이 변경되었을 때 orderInfo객체가 수정되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 수량은 증가하거나 감소해야하기 때문에 단순히 수량을 +1로 처리하면 안된다. 따라서 현재 입력된 숫자값을 수량으로 지정한 다음, 수량에따른 주문 가격과 총 가격도 변경시켰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 가격을 구하기 위해서 복잡하게 더하기와 빼기로 구현했는데 이것보다 좋은 방법이 있을 것 같아서 더 찾아봐야겠다..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면_기록_2022-11-11_오후_2_33_41_AdobeExpress.gif&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lYzsq/btrQYGCVv3K/97tKwIkDjukj6vqGLZm3TK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lYzsq/btrQYGCVv3K/97tKwIkDjukj6vqGLZm3TK/img.gif&quot; data-alt=&quot;주문 수량을 변경할 수 있도록 구현된 것을 확인할 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lYzsq/btrQYGCVv3K/97tKwIkDjukj6vqGLZm3TK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/lYzsq/btrQYGCVv3K/97tKwIkDjukj6vqGLZm3TK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;529&quot; height=&quot;356&quot; data-filename=&quot;화면_기록_2022-11-11_오후_2_33_41_AdobeExpress.gif&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;356&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;주문 수량을 변경할 수 있도록 구현된 것을 확인할 수 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TroubleShooting2: 주문 내역 오름차순 정렬하기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면_기록_2022-11-11_오후_2_41_20_AdobeExpress.gif&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;395&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/se2cq/btrQVaey1xM/WbCkw2iRjqF4zuKYTaHNC1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/se2cq/btrQVaey1xM/WbCkw2iRjqF4zuKYTaHNC1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/se2cq/btrQVaey1xM/WbCkw2iRjqF4zuKYTaHNC1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/se2cq/btrQVaey1xM/WbCkw2iRjqF4zuKYTaHNC1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;529&quot; height=&quot;395&quot; data-filename=&quot;화면_기록_2022-11-11_오후_2_41_20_AdobeExpress.gif&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;395&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 주문 수량을 변경할 때마다 배열의 순서가 바뀌어서 주문서에서 표시되는 순서도 같이 변경되는 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 orderList 배열을 불러올 때 sort함수를 이용해서 오름차순으로 배열을 정렬하도록 구현했다.&lt;/p&gt;
&lt;pre id=&quot;code_1668145402628&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{ orderInfo.orderList
  .filter((list) =&amp;gt; list.orderListId &amp;gt; 0)
  .sort((a, b) =&amp;gt; a.orderListId - b.orderListId)
  .map(order =&amp;gt; (
    &amp;lt;OrderBox key={order.orderListId} order={order} orderInfo={orderInfo} setOrderInfo={setOrderInfo} progress={progress}/&amp;gt;
))}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주문 내역 삭제하기&lt;/h2&gt;
&lt;pre id=&quot;code_1668144571228&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function OrderBox({order, orderInfo, setOrderInfo, progress}) {
  ...

  const onClickDeleteBtn = (orderListId) =&amp;gt; {
    const orderList = orderInfo.orderList.filter(order =&amp;gt; order.orderListId === orderListId)[0];
    const orderListExcept = orderInfo.orderList.filter(order =&amp;gt; order.orderListId !== orderListId);
    setOrderInfo((prev) =&amp;gt; ({
      ...prev, 
      finalAmount: prev.finalAmount - orderList.amount,
      orderList: [...orderListExcept]
    }))
  }

  return (
    &amp;lt;div className=&quot;flex my-1&quot;&amp;gt;
      &amp;lt;span&amp;gt;메뉴: {order.menu}&amp;lt;/span&amp;gt;
      &amp;lt;span className=&quot;ml-2&quot;&amp;gt;스타일: {order.style}&amp;lt;/span&amp;gt;
      &amp;lt;span className=&quot;ml-2&quot;&amp;gt;가격: {order.amount}&amp;lt;/span&amp;gt;
      &amp;lt;span className=&quot;ml-2&quot;&amp;gt;주문 수량: &amp;lt;/span&amp;gt;
      &amp;lt;input type=&quot;number&quot; min=&quot;1&quot; value={order.quantity} onChange={e =&amp;gt; onChangeOrderQuantity(e, order.orderListId)} className=&quot;text-center w-10 outline-none&quot; /&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; onClickDeleteBtn(order.orderListId)} className=&quot;px-2 font-bold&quot;&amp;gt;X&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 기능을 구현하기위해 삭제 버튼을 만들고, 삭제버튼이 눌린 주문의 id를 삭제 버튼에 전달해서 orderList에 이와 일치하지않는 id의 배열을 추가하였다.(orderListExecpt)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면_기록_2022-11-11_오후_2_45_44_AdobeExpress.gif&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buXBhB/btrQVAYmsyw/a3bEHPuSpz1KNeDgvzJQnk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buXBhB/btrQVAYmsyw/a3bEHPuSpz1KNeDgvzJQnk/img.gif&quot; data-alt=&quot;주문을 삭제할 수 있도록 구현된 것을 확인할 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buXBhB/btrQVAYmsyw/a3bEHPuSpz1KNeDgvzJQnk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/buXBhB/btrQVAYmsyw/a3bEHPuSpz1KNeDgvzJQnk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;529&quot; height=&quot;406&quot; data-filename=&quot;화면_기록_2022-11-11_오후_2_45_44_AdobeExpress.gif&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;주문을 삭제할 수 있도록 구현된 것을 확인할 수 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;+) select 순차 선택하도록 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문을 위해서 디너 메뉴 -&amp;gt; 디너 스타일 순으로 선택하기 위해서 메뉴가 선택되기 전에는 스타일 선택은 비활성화(disabled)되어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1668145803545&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 순차선택 로직
const styleRef = useRef(null);

const [isSelectedMenu, setIsSelectedMenu] = useState(false);
const onChangeMenuSelect = () =&amp;gt; {
  setIsSelectedMenu(true);
}

const onChangeStyleSelect = () =&amp;gt; {
  ...
  setIsSelectedMenu(false);
  menu.value = '';
  style.value = '';
};

useEffect(() =&amp;gt; {
    const style = styleRef.current;
    if(isSelectedMenu) style.disabled = false;
    else style.disabled = true;
  }, [isSelectedMenu]);

return (
  &amp;lt;div className=&quot;my-3 flex justify-between&quot;&amp;gt;
    &amp;lt;h3 className=&quot;text-lg font-bold&quot;&amp;gt;디너 메뉴&amp;lt;/h3&amp;gt;
    &amp;lt;select ref={menuRef} name=&quot;menu&quot; onChange={onChangeMenuSelect} className=&quot;outline-none&quot;&amp;gt;
      &amp;lt;option value=&quot;&quot;&amp;gt;--디너 메뉴를 선택하세요--&amp;lt;/option&amp;gt;
      ...
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순차 선택을 구현하기 위해 useState와 useEffect를 이용해서 앞의 메뉴가 선택되었을 때 스타일 선택이 활성화되도록 구현했다.&lt;/p&gt;</description>
      <category>Frontend/react</category>
      <category>RECOIL</category>
      <category>상태관리</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/23</guid>
      <comments>https://devpluto.tistory.com/entry/React-recoil%EB%A1%9C-%EC%A3%BC%EB%AC%B8%EB%82%B4%EC%97%AD-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0#entry23comment</comments>
      <pubDate>Fri, 11 Nov 2022 14:56:00 +0900</pubDate>
    </item>
    <item>
      <title>[자료구조] c언어로 큐, 원형 큐 구현하기</title>
      <link>https://devpluto.tistory.com/entry/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-c%EC%96%B8%EC%96%B4%EB%A1%9C-%ED%81%90-%EC%9B%90%ED%98%95-%ED%81%90-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;큐(Queue)란?&lt;/h2&gt;
&lt;div data-page-number=&quot;2&quot; data-loaded=&quot;true&quot;&gt;
&lt;div&gt;&lt;span&gt;큐(Queue)는 먼저&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;들어온&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;데이터가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;먼저&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;나가는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;자료구조이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;선입선출&lt;/span&gt;&lt;span&gt;(FIFO: First&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;In First&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;Out)한다는 특징이 있고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;매표소나 계산대의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;대기열을 생각해보면 이해하기 쉬울 것이다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;이번 글에서는 큐와 큐의 더 발전된 형태인 원형 큐도 구현해 보겠다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-27 오전 10.54.26.png&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BqfCb/btrPHyZY0Yj/zTyT2KTcy43a3cXXAHa6Lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BqfCb/btrPHyZY0Yj/zTyT2KTcy43a3cXXAHa6Lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BqfCb/btrPHyZY0Yj/zTyT2KTcy43a3cXXAHa6Lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBqfCb%2FbtrPHyZY0Yj%2FzTyT2KTcy43a3cXXAHa6Lk%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;491&quot; height=&quot;340&quot; data-filename=&quot;스크린샷 2022-10-27 오전 10.54.26.png&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;큐 타입 구조체 정의 및 초기화 함수&lt;/h3&gt;
&lt;pre id=&quot;code_1666845259983&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

#define SIZE 100

typedef char element;

typedef struct
{
  element data[SIZE];
  int rear, front;
} QueueType;

void init(QueueType *Q)
{
  Q-&amp;gt;rear = Q-&amp;gt;front = -1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;큐의 요소에 해당하는 data 배열과, 큐의 가장 앞과 뒤에 해당하는 front, rear이 포함된 QueueType 구조체를 선언한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러체크함수 is_empty(), is_full()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러체크함수 is_empty는 아무것도 차있지 않은 상태, 즉 front와 rear가 같은지 살핀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;is_full은 큐가 가득 차있는 상태인 rear가 size -1이 같은지 살핀다. -1을 하는 이유는 큐 data 배열은 0부터 시작하기때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1666845790914&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int is_empty(QueueType *Q)
{
  return Q-&amp;gt;front == Q-&amp;gt;rear;
}

int is_full(QueueType *Q)
{
  return Q-&amp;gt;rear == SIZE - 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삽입함수 enqueue()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삽입을 하기위해 rear을 한칸 움직이고, 데이터를 한칸 움직인 공간에 삽입한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666845812243&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void enqueue(QueueType *Q, element e)
{
  if (is_full(Q))
    printf(&quot;Overflow\n&quot;);
  else
  {
    Q-&amp;gt;rear++;

    Q-&amp;gt;data[Q-&amp;gt;rear] = e;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삭제함수 dequeue()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 들어온 데이터부터 삭제해야하므로 front를 한칸 움직이고 데이터를 return한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666845891884&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;element dequeue(QueueType *Q)
{
  if (is_empty(Q))
  {
    printf(&quot;Empty\n&quot;);
    return 0;
  }
  else
  {
    Q-&amp;gt;front++;
    return Q-&amp;gt;data[Q-&amp;gt;front];
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;큐 출력함수 print()&lt;/h3&gt;
&lt;pre id=&quot;code_1666845928262&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void print(QueueType *Q)
{
  printf(&quot;Front Pos : %d\n, Rear Pos : %d\n&quot;, Q-&amp;gt;front, Q-&amp;gt;rear);
  for (int i = Q-&amp;gt;front + 1; i &amp;lt;= Q-&amp;gt;rear; i++)
  {
    printf(&quot;[%c] &quot;, Q-&amp;gt;data[i]);
  }
  printf(&quot;\n&quot;);
}&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐를 사용하면 할수록 front가 뒤로 밀려나게 되므로 마지막 공간의 front와 Rear가 같아지게 되어 삭제된 front 앞의 공간(이전에 할당된 공간)은 쓸 수 없게 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원형 큐(Circular Queue)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐의 이미 사용한 앞 공간은 쓸수 없다는 단점을 보완하기 위해 원형 큐를 사용할 수 있다. 원형 큐를 구현하기 위해서는 앞서 구현한 큐에서 일부 코드만 수정하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-03 오전 11.07.05.png&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K5X01/btrQeAR7TQN/QeAhpAkF7o4KqMnwBu1f80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K5X01/btrQeAR7TQN/QeAhpAkF7o4KqMnwBu1f80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K5X01/btrQeAR7TQN/QeAhpAkF7o4KqMnwBu1f80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK5X01%2FbtrQeAR7TQN%2FQeAhpAkF7o4KqMnwBu1f80%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;552&quot; height=&quot;467&quot; data-filename=&quot;스크린샷 2022-11-03 오전 11.07.05.png&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러체크함수 is_empty(), is_full()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;is_empty함수는 큐의 코드와 동일하고, is_full함수는 공백 상태와 구분하기 위해서 한 칸 비워둔다. 그리고 원형큐를 구현하기 위해서는 나머지 모듈러 연산이 필요하다.(front == rear + 1을 전체 size로 나눈 나머지)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 칸 비워두지 않으면 0번 index에 front==rear인 경우 공백 상태와 포화 상태가 중복되어서 구분할 수 없기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-03 오전 11.05.19.png&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;862&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3D5dR/btrQjk03oDb/xCWw722keqqqh9kh7mOXx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3D5dR/btrQjk03oDb/xCWw722keqqqh9kh7mOXx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3D5dR/btrQjk03oDb/xCWw722keqqqh9kh7mOXx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3D5dR%2FbtrQjk03oDb%2FxCWw722keqqqh9kh7mOXx1%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;464&quot; height=&quot;391&quot; data-filename=&quot;스크린샷 2022-11-03 오전 11.05.19.png&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;862&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1667393286988&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int is_empty(QueueType *Q)
{
  return Q-&amp;gt;front == Q-&amp;gt;rear;
}

int is_full(QueueType *Q)
{
  return Q-&amp;gt;front == (Q-&amp;gt;rear + 1) % SIZE;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삽입함수 enqueue()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삽입함수는 rear를 1만큼 증가시킨 값과 size와의 나머지로 설정하고 데이터를 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 현재 rear가 4이고, size가 8이라면 (4 + 1) % 8 = 5번 인덱스, rear가 7이라면 (7 + 1) % 8 = 0번 인덱스에 위치한다.&lt;/p&gt;
&lt;pre id=&quot;code_1667393470371&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void enqueue(QueueType *Q, element e)
{
  if (is_full(Q))
    printf(&quot;Overflow\n&quot;);
  else
  {
    Q-&amp;gt;rear = (Q-&amp;gt;rear + 1) % SIZE;

    Q-&amp;gt;data[Q-&amp;gt;rear] = e;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삭제함수 dequeue()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삽입함수와 마찬가지로 front를 증가시키고 데이터를 return한다.&lt;/p&gt;
&lt;pre id=&quot;code_1667441366461&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;element dequeue(QueueType *Q)
{
  if (is_empty(Q))
  {
    printf(&quot;Empty\n&quot;);
    return 0;
  }
  else
  {
    Q-&amp;gt;front = (Q-&amp;gt;front + 1) % SIZE;
    return Q-&amp;gt;data[Q-&amp;gt;front];
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원형큐 출력함수 print()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력함수 print()는 while문을 통해 i값을 front부터 rear까지 순차적으로 증가시키면서 출력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1667441397978&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void print(QueueType *Q)
{
  printf(&quot;Front Pos : %d\n, Rear Pos : %d\n&quot;, Q-&amp;gt;front, Q-&amp;gt;rear);
  int i = Q-&amp;gt;front; // 제일 앞에있는 원소 바로 하나 전부터 시작
  while (i != Q-&amp;gt;rear)
  {
    i = (i + 1) % SIZE;
    printf(&quot;[%c]&quot;, Q-&amp;gt;data[i]);
  }
  printf(&quot;\n&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;큐 (더보기)&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1666848384690&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#define SIZE 100

typedef char element;

typedef struct
{
  element data[SIZE];
  int rear, front;
} QueueType;

void init(QueueType *Q)
{
  Q-&amp;gt;rear = Q-&amp;gt;front = -1;
}

int is_empty(QueueType *Q)
{
  return Q-&amp;gt;front == Q-&amp;gt;rear;
}

int is_full(QueueType *Q)
{
  return Q-&amp;gt;rear == SIZE - 1;
}

void enqueue(QueueType *Q, element e)
{
  if (is_full(Q))
    printf(&quot;Overflow\n&quot;);
  else
  {
    Q-&amp;gt;rear++;

    Q-&amp;gt;data[Q-&amp;gt;rear] = e;
  }
}

element dequeue(QueueType *Q)
{
  if (is_empty(Q))
  {
    printf(&quot;Empty\n&quot;);
    return 0;
  }
  else
  {
    Q-&amp;gt;front++;
    return Q-&amp;gt;data[Q-&amp;gt;front];
  }
}

void print(QueueType *Q)
{
  printf(&quot;Front Pos : %d\n, Rear Pos : %d\n&quot;, Q-&amp;gt;front, Q-&amp;gt;rear);
  for (int i = Q-&amp;gt;front + 1; i &amp;lt;= Q-&amp;gt;rear; i++)
  {
    printf(&quot;[%c] &quot;, Q-&amp;gt;data[i]);
  }
  printf(&quot;\n&quot;);
}

int main()
{
  QueueType Q;
  init(&amp;amp;Q);
  
  enqueue(&amp;amp;Q, 'A');
  enqueue(&amp;amp;Q, 'B');
  enqueue(&amp;amp;Q, 'C');
  print(&amp;amp;Q);
  printf(&quot;[%c] \n&quot;, dequeue(&amp;amp;Q));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원형 큐 (더보기)&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1667441601329&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#define SIZE 10

typedef char element;

typedef struct
{
  element data[SIZE];
  int rear, front;
} QueueType;

void init(QueueType *Q)
{
  Q-&amp;gt;rear = Q-&amp;gt;front = 0;
}

int is_empty(QueueType *Q)
{
  return Q-&amp;gt;front == Q-&amp;gt;rear;
}

int is_full(QueueType *Q)
{
  return Q-&amp;gt;front == (Q-&amp;gt;rear + 1) % SIZE;
}

void enqueue(QueueType *Q, element e)
{
  if (is_full(Q))
    printf(&quot;Overflow\n&quot;);
  else
  {
    Q-&amp;gt;rear = (Q-&amp;gt;rear + 1) % SIZE;

    Q-&amp;gt;data[Q-&amp;gt;rear] = e;
  }
}

element dequeue(QueueType *Q)
{
  if (is_empty(Q))
  {
    printf(&quot;Empty\n&quot;);
    return 0;
  }
  else
  {
    Q-&amp;gt;front = (Q-&amp;gt;front + 1) % SIZE;
    return Q-&amp;gt;data[Q-&amp;gt;front];
  }
}

element peek(QueueType *Q)
{
  if (is_empty(Q))
  {
    printf(&quot;Empty\n&quot;);
    return 0;
  }
  return Q-&amp;gt;data[(Q-&amp;gt;front + 1) % SIZE];
}

void print(QueueType *Q)
{
  printf(&quot;Front Pos : %d\n, Rear Pos : %d\n&quot;, Q-&amp;gt;front, Q-&amp;gt;rear);
  int i = Q-&amp;gt;front; // 제일 앞에있는 원소 바로 하나 전부터 시작
  while (i != Q-&amp;gt;rear)
  {
    i = (i + 1) % SIZE;
    printf(&quot;[%c]&quot;, Q-&amp;gt;data[i]);
  }
  printf(&quot;\n&quot;);
}

int main()
{
  QueueType Q;
  init(&amp;amp;Q);
  
  enqueue(&amp;amp;Q, 'A');
  enqueue(&amp;amp;Q, 'B');
  enqueue(&amp;amp;Q, 'C');
  print(&amp;amp;Q);
  printf(&quot;[%c] \n&quot;, dequeue(&amp;amp;Q));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도움이 되었다면 아래 '공감' 버튼을 눌러주세요! 읽어주셔서 감사합니다 :)&lt;/p&gt;</description>
      <category>cs/자료구조</category>
      <category>원형큐</category>
      <category>큐</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/22</guid>
      <comments>https://devpluto.tistory.com/entry/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-c%EC%96%B8%EC%96%B4%EB%A1%9C-%ED%81%90-%EC%9B%90%ED%98%95-%ED%81%90-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0#entry22comment</comments>
      <pubDate>Thu, 3 Nov 2022 14:10:08 +0900</pubDate>
    </item>
    <item>
      <title>[티스토리 스킨 만들기] 2. 개발 환경 세팅하기(Typescript, TailwindCSS, Webpack)</title>
      <link>https://devpluto.tistory.com/entry/%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-%EC%8A%A4%ED%82%A8-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0Typescript-TailwindCSS-Webpack</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티스토리 스킨 적용에 필요한 파일에는 index.xml, skin.html, style.css, script.js가 있습니다. 이 파일들을 빌드하기 위해 개발 환경을 세팅해보겠습니다. 이 글은 타입스크립트와 tailwind css의 사용 설정과 webpack을 이용해 이들을 한번에 빌드하는 과정에 대해서 작성하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티스토리 스킨 개발을 위한 파일 디렉토리는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667368291359&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tistory_skin
 |- index.xml
 |- skin.html
 |- style.css
 |- /images
   |- script.js&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Typescript로 개발하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 개발을 진행할 src폴더를 만들어 script.ts파일을 생성합니다. 이 파일에서 스킨에 적용할(사이드바 동작, TOC 등) 기능을 개발하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ts파일을 생성했다면 이제 타입스크립트 개발환경을 세팅하고, js로 빌드해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Typescript 설치&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm init&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm init 명령어를 실행하면 package.json파일이 생성되며 npm 환경이 세팅됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm install typescript&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 npm을 통해 typescript를 설치합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;tsconfig.json 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typescript를 설치했다면 tsconfig.json파일을 생성하여 다음 내용을 입력해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667304748290&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;es2016&quot;,                            
    &quot;module&quot;: &quot;es6&quot;,
    &quot;strict&quot;: true,
    &quot;skipLibCheck&quot;: true,                               
    &quot;outDir&quot;: &quot;images&quot;,
    &quot;watch&quot;: true
  },
  &quot;include&quot;: [
    &quot;src/script.ts&quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;outDirectory를 images로 설정한 다음, include속성에 js로 변환할 파일(src/script.ts)을 지정해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Typescript 빌드하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입스크립트에서 자바스크립트로 빌드하는것은 쉽습니다. 아래 명령어를 터미널에서 실행하면 자동으로 빌드됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;tsc&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 tsconfig파일에서 watch속성을 true로 주었기때문에 ts파일을 변경 및 저장할 때마다 자동으로 빌드합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 따라왔다면 파일 구조는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667369193688&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tistory_skin
 |- index.xml
 |- skin.html
 |- style.css
 |- /images
   |- script.js
 |- /src
   |- script.ts
 |- /node_modules
 |- package.json
 |- package-lock.json
 |- tsconfig.json&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TailwindCSS 적용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발의 편의를 위해 style.css파일에 모든 스타일 속성을 적지않고, tailwind css를 설치하고 적용해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 설치 전에 스킨 배포의 용이성을 위해 dist폴더에 배포에 필요한 파일들만 담도록 파일 구조를 변경하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667369469502&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tistory_skin
 |- /dists
   |- index.xml
   |- skin.html
   |- style.css
   |- /images
     |- script.js
 |- /src
   |- script.ts
 |- /node_modules
 |- package.json
 |- package-lock.json
 |- tsconfig.json&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;tailwindcss 설치&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm install -D tailwindcss&lt;br /&gt;npx tailwindcss init&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm을 이용해 tailwindcss를 설치해주고, npx tailwindcss init 명령어를 통해 tailwindcss.config.js파일을 생성하며 초기화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 src경로에 input.css파일을 생성하고 다음과 같이 설정해줍니다. input.css파일에 필요한 스타일 속성을 작성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667370026576&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* src/input.css */

@tailwind base;
@tailwind components;
@tailwind utilities;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;tailwind.config.js 파일 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 tailwind config 파일에 아래와 같이 코드를 입력해줍니다. 파일의 content속성은 dist폴더의 skin.html을 템플릿파일로 설정한다는 의미입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667299361033&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// tailwindcss.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [&quot;./dist/skin.html&quot;],
  theme: {
    extend: {},
  },
  plugins: [],
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Tailwind CLI로 빌드하기&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npx tailwindcss -i ./src/input.css -o ./dist/style.css --watch&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또다른 터미널에서 위 명령어를 실행하면 dist경로에 style.css파일로 빌드됩니다. 명령어에서 --watch옵션을 주었기 때문에 input.css파일을 변경 후 저장할 때마다 자동으로 빌드합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 설정을 마쳤다면 파일 구조는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667370187775&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tistory_skin
 |- /dists
   |- index.xml
   |- skin.html
   |- style.css
   |- /images
     |- script.js
 |- /src
   |- script.ts
   |- input.css
 |- /node_modules
 |- package.json
 |- package-lock.json
 |- tsconfig.json
 |- tailwind.config.js&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Webpack으로 타입스크립트와 tailwindCSS 한번에 빌드하기&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;typescript와 tailwind를 빌드하기 위해서는 각각의 명령어를 입력해야합니다. 이렇게 하면 개발환경에서 터미널 두개를 띄워야하기 때문에 작업하는데 번거로움이 있습니다. 그래서 웹팩을 설치하여 한 명령어로 빌드과정을 통합하도록 해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Webpack 설치&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm install webpack webpack-cli --save-dev&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어로 webpack과 webpack-cli를 설치하면 웹팩을 프로젝트에서 사용할 수 있습니다. 그리고 webpack.config.js파일을 생성해서 아래와 같이 파일을 작성해 줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667269517492&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/script.ts',
  mode: 'production',
  output: {
    filename: 'script.js',
    path: path.resolve(__dirname, 'dist/images'),
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;package.json파일을 다음과같이 설정하고, 터미널에서 npm run build 명령어를 실행하면 웹팩을 빌드할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667279201601&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// package.json
...
&quot;scripts&quot;: {
  &quot;build&quot;: &quot;webpack&quot;,
},&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm run build&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;watch옵션으로 자동 빌드하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 저장할 때마다 자동으로 빌드하고싶다면 package.json에서 watch옵션을 설정해야합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667279173258&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// package.json
...
&quot;scripts&quot;: {
    &quot;build&quot;: &quot;webpack&quot;,
    &quot;watch&quot;: &quot;webpack --watch&quot;
  },&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm run watch&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 명령어를 실행하면 파일이 저장될 때 마다 자동으로 웹팩이 빌드됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;webpack.config.js 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 웹팩 설정을 마쳤다하더라도, 우리가 원하는 css파일과 js파일에 대한 config 파일 설정을 하지 않았기 때문에 빌드 과정에서 오류가 발생할 것입니다. 이제 관련 설정을 해보겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ts-loader로 typescript 빌드하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩은 타입스크립트는 이해하지 못하기 때문에 관련 loader를 설치해줘야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm install -D ts-loader&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ts-loader는 웹팩이 타입스크립트를 해석하도록 도와주는 역할을 합니다. 다음 코드처럼 webpack.config.js 파일을 구성하면 script.js 파일을 dist/images경로에 빌드합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667051724049&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/script.ts',
  module: {
    rules: [
      {
        test: /\.ts?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  output: {
    filename: 'script.js',
    path: path.resolve(__dirname, 'dist/images'),
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-29 오후 10.58.34.png&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pTBYq/btrPTkHGc5A/q1QOkU1eQwscXajEXG0KD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pTBYq/btrPTkHGc5A/q1QOkU1eQwscXajEXG0KD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pTBYq/btrPTkHGc5A/q1QOkU1eQwscXajEXG0KD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpTBYq%2FbtrPTkHGc5A%2Fq1QOkU1eQwscXajEXG0KD1%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;525&quot; height=&quot;103&quot; data-filename=&quot;스크린샷 2022-10-29 오후 10.58.34.png&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 따라왔다면 script.ts 파일이 script.js로 잘 빌드된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로, 파일 용량을 확인해보면&amp;nbsp;&lt;span&gt;웹팩 적용 전보다 파일 크기가 2kb가량 파일 크기가 줄어든 것을 확인할 수 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;웹팩은 코드를 주석과 공백없이 한줄로 압축하며 빌드하기 때문입니다. 이는 빠른 웹페이지의 로딩에 도움을 줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-28 오후 4.18.34.png&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;46&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSoVpT/btrPP8BXHpD/ykVq1727I2GO0C3JK6pWW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSoVpT/btrPP8BXHpD/ykVq1727I2GO0C3JK6pWW0/img.png&quot; data-alt=&quot;webpack 적용 전 (4kb)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSoVpT/btrPP8BXHpD/ykVq1727I2GO0C3JK6pWW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSoVpT%2FbtrPP8BXHpD%2FykVq1727I2GO0C3JK6pWW0%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;761&quot; height=&quot;31&quot; data-filename=&quot;스크린샷 2022-10-28 오후 4.18.34.png&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;46&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;webpack 적용 전 (4kb)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-28 오후 4.20.14.png&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;50&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WxdYU/btrPPORifsO/ELaYYKrQUb6NF8OwuDLv60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WxdYU/btrPPORifsO/ELaYYKrQUb6NF8OwuDLv60/img.png&quot; data-alt=&quot;webpack 적용 후 (2kb)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WxdYU/btrPPORifsO/ELaYYKrQUb6NF8OwuDLv60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWxdYU%2FbtrPPORifsO%2FELaYYKrQUb6NF8OwuDLv60%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;769&quot; height=&quot;34&quot; data-filename=&quot;스크린샷 2022-10-28 오후 4.20.14.png&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;50&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;webpack 적용 후 (2kb)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MiniCssExtractPlugin으로 css 분리하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티스토리 스킨은 style.css라는 파일을 따로 업로드 해줘야 하기 때문에 js파일에 같이 번들링을 하면 안되고, 따로 분리된 css파일이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 css파일을 따로 분리해주는 MiniCssExtractPlugin을 적용해 봅시다. 보통 MiniCssExtractPlugin은 js 모듈의 개수가 많을 경우 css파일의 빠른 로드가 필요할 때 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667052705391&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require(&quot;mini-css-extract-plugin&quot;);

module.exports = {
  entry: './src/script.ts',
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.ts?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, &quot;css-loader&quot;],
      },
    ],
  },
  output: {
    filename: 'script.js',
    path: path.resolve(__dirname, 'dist/images'),
  },
  plugins: [new MiniCssExtractPlugin({filename : &quot;../style2.css&quot;})],
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1667052807089&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// srcipt.ts

import './input.css';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같이 script.ts 파일에 css를 import해주어야지 웹팩이 css 파일을 인식하고 플러그인에 따라 새 css 파일로 분리합니다. 위 코드로 설정을 해주고 빌드까지 끝났다면 아래와 같이 style2.css파일이 생성됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-29 오후 11.11.24.png&quot; data-origin-width=&quot;344&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d5ydUy/btrPU11YNtd/jfZlIfmUOMKkQrsmP0SE5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d5ydUy/btrPU11YNtd/jfZlIfmUOMKkQrsmP0SE5k/img.png&quot; data-alt=&quot; &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5ydUy/btrPU11YNtd/jfZlIfmUOMKkQrsmP0SE5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd5ydUy%2FbtrPU11YNtd%2FjfZlIfmUOMKkQrsmP0SE5k%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;207&quot; height=&quot;481&quot; data-filename=&quot;스크린샷 2022-10-29 오후 11.11.24.png&quot; data-origin-width=&quot;344&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리가 원하는 파일은 input.css파일 그 자체가 아니라, input.css에서 Tailwind CLI로 빌드된 파일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 postCSS를 사용해보겠습니다. 참고로 style2로 파일명을 설정한 이유도 이때문입니다.ㅎㅎ&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;postCSS 설치 및 세팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파트에서는 &lt;a href=&quot;https://tailwindcss.com/docs/installation/using-postcss&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;tailwindcss의 도큐멘트&lt;/a&gt;를 참고하여 구현했습니다. 도큐멘트에 따라 tailwindcss는 이미 설치하였으므로 postcss와 autoprefixer, postcss-loader를 설치하여줍니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm install -D postcss autoprefixer&lt;br /&gt;npm install -D postcss-loader&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 프로젝트에 postcss.config.js 파일을 추가하고, webpack.config.js 파일도 수정해줍니다.(postcss-loader를 추가해주고, 파일 이름을 style.css로 변경하였습니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1667266749098&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1667055251530&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require(&quot;mini-css-extract-plugin&quot;);

module.exports = {
  entry: './src/script.ts',
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.ts?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, &quot;css-loader&quot;, &quot;postcss-loader&quot;],
      },
    ],
  },
  output: {
    filename: 'script.js',
    path: path.resolve(__dirname, 'dist/images'),
  },
  plugins: [new MiniCssExtractPlugin({filename : &quot;../style.css&quot;})],
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-01 오전 10.41.04.png&quot; data-origin-width=&quot;1704&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bax96m/btrP3LTwWAa/NQr30GiYIHoqYyi1q3PHV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bax96m/btrP3LTwWAa/NQr30GiYIHoqYyi1q3PHV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bax96m/btrP3LTwWAa/NQr30GiYIHoqYyi1q3PHV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbax96m%2FbtrP3LTwWAa%2FNQr30GiYIHoqYyi1q3PHV0%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;1704&quot; height=&quot;474&quot; data-filename=&quot;스크린샷 2022-11-01 오전 10.41.04.png&quot; data-origin-width=&quot;1704&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 마치고 npm run watch를 실행하면 잘 빌드되는것을 확인할 수 있으며, 파일 구조는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667371122479&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tistory_skin
 |- /dists
   |- index.xml
   |- skin.html
   |- style.css
   |- /images
     |- script.js
 |- /src
   |- script.ts
   |- input.css
 |- /node_modules
 |- package.json
 |- package-lock.json
 |- tsconfig.json
 |- tailwind.config.js
 |- webpack.config.js
 |- postcss.config.js&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;+) CssMinimizerPlugin으로 css파일 압축하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 js파일이 빌드될 때 파일이 한줄로 압축되는것을 알 수 있었습니다. 그렇다면 css파일도 압축하여 블로그의 로딩 속도를 단축시켜 볼 수 있습니다. 이를 위해 CssMinimizerPlugin을 적용해보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm install css-minimizer-webpack-plugin --save-dev&lt;/blockquote&gt;
&lt;pre id=&quot;code_1667267579802&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require(&quot;mini-css-extract-plugin&quot;);
const CssMinimizerPlugin = require(&quot;css-minimizer-webpack-plugin&quot;);

module.exports = {
  entry: './src/script.ts',
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.ts?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, &quot;css-loader&quot;, &quot;postcss-loader&quot;],
      },
    ],
  },
  output: {
    filename: 'script.js',
    path: path.resolve(__dirname, 'dist/images'),
  },
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
    ]
  },
  plugins: [new MiniCssExtractPlugin({filename : &quot;../style.css&quot;})],
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-01 오전 10.55.05.png&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkAHfS/btrP36iThJ7/kGM2wE4NA0A9woyRc43hk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkAHfS/btrP36iThJ7/kGM2wE4NA0A9woyRc43hk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkAHfS/btrP36iThJ7/kGM2wE4NA0A9woyRc43hk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkAHfS%2FbtrP36iThJ7%2FkGM2wE4NA0A9woyRc43hk1%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;538&quot; height=&quot;244&quot; data-filename=&quot;스크린샷 2022-11-01 오전 10.55.05.png&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 따라왔다면 style.css파일이 잘 압축된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-01 오전 10.45.24.png&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;46&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKGVWl/btrP4XyYToT/DRs4hf5i0q2lZrnvFR5B31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKGVWl/btrP4XyYToT/DRs4hf5i0q2lZrnvFR5B31/img.png&quot; data-alt=&quot;css 압축 전 (27kb)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKGVWl/btrP4XyYToT/DRs4hf5i0q2lZrnvFR5B31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKGVWl%2FbtrP4XyYToT%2FDRs4hf5i0q2lZrnvFR5B31%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;717&quot; height=&quot;30&quot; data-filename=&quot;스크린샷 2022-11-01 오전 10.45.24.png&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;46&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;css 압축 전 (27kb)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-01 오전 10.52.16.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;36&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bixoAV/btrP3L0jWA3/wX8fRTOADX1wzHSrSUfpF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bixoAV/btrP3L0jWA3/wX8fRTOADX1wzHSrSUfpF1/img.png&quot; data-alt=&quot;css 압축 후 (18kb)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bixoAV/btrP3L0jWA3/wX8fRTOADX1wzHSrSUfpF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbixoAV%2FbtrP3L0jWA3%2FwX8fRTOADX1wzHSrSUfpF1%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;736&quot; height=&quot;24&quot; data-filename=&quot;스크린샷 2022-11-01 오전 10.52.16.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;36&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;css 압축 후 (18kb)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TroubleShooting 1 : script.js 압축 불가 현상&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CssMinimizerPlugin로 css압축 시 script.js는 한줄로 압축이 되지 않는 현상이 발생합니다. 웹팩 config파일의&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #222222;&quot;&gt;optimization.minimizer설정에 css만 포함되어있기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 자바스크립트를 압축해주는 TerserPlugin을 적용해서 js파일도 설정에 추가해줘야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;npm install terser-webpack-plugin --save-dev&lt;/blockquote&gt;
&lt;pre id=&quot;code_1667268605352&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require(&quot;mini-css-extract-plugin&quot;);
const CssMinimizerPlugin = require(&quot;css-minimizer-webpack-plugin&quot;);
const TerserPlugin = require(&quot;terser-webpack-plugin&quot;);

module.exports = {
  entry: './src/script.ts',
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.ts?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, &quot;css-loader&quot;, &quot;postcss-loader&quot;],
      },
    ],
  },
  output: {
    filename: 'script.js',
    path: path.resolve(__dirname, 'dist/images'),
  },
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
      new TerserPlugin(),
    ]
  },
  plugins: [new MiniCssExtractPlugin({filename : &quot;../style.css&quot;})],
};&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TroubleShooting 2 : 선언되었지만 읽히지 않은 함수&lt;/h4&gt;
&lt;pre id=&quot;code_1667296251705&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- skin.html --&amp;gt;
&amp;lt;button onclick=&quot;{closeSidebar()}&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-11-01 오후 6.51.18.png&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3Eq4w/btrP7MSBcE6/sCCag6HWXszlJdcrgyjImK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3Eq4w/btrP7MSBcE6/sCCag6HWXszlJdcrgyjImK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3Eq4w/btrP7MSBcE6/sCCag6HWXszlJdcrgyjImK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3Eq4w%2FbtrP7MSBcE6%2FsCCag6HWXszlJdcrgyjImK%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;693&quot; height=&quot;153&quot; data-filename=&quot;스크린샷 2022-11-01 오후 6.51.18.png&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;html에서 onclick으로 사용하도록 구현한 closeSidebar함수는 선언되었지만 읽히지 않은 함수는 웹팩 빌드 과정에서 삭제가 된다는 문제가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 구현시에 html에서 onclick속성을 주지 말고, 자바스크립트에서 dom요소를 받아와 addEventListener로 선언해야합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667297650935&quot; class=&quot;reasonml&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;document.getElementById(&quot;writeBtn&quot;)!.addEventListener(&quot;click&quot;, onClickWriteBtn);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티스토리 스킨 개발을 위해&amp;nbsp;제가 했던 개발환경 세팅 과정을 글로 정리해보았습니다. 여기서는 티스토리 스킨 개발을 위한 설정을 했지만, 다른 프로젝트의 설정이나 개발에 도움이 되길 바랍니다.&lt;/p&gt;</description>
      <category>프로젝트/블로그 테마</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/21</guid>
      <comments>https://devpluto.tistory.com/entry/%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-%EC%8A%A4%ED%82%A8-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0Typescript-TailwindCSS-Webpack#entry21comment</comments>
      <pubDate>Wed, 2 Nov 2022 15:46:20 +0900</pubDate>
    </item>
    <item>
      <title>[개발자의 글쓰기] 글의 종류별로 목차 잡는 법</title>
      <link>https://devpluto.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EA%B8%80%EC%93%B0%EA%B8%B0-%EA%B8%80%EC%9D%98-%EC%A2%85%EB%A5%98%EB%B3%84%EB%A1%9C-%EB%AA%A9%EC%B0%A8-%EC%9E%A1%EB%8A%94-%EB%B2%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;개발자를 위한 글쓰기&quot; 책을 읽는 중 책의&amp;nbsp;&lt;b&gt;[글의 종류별로 목차 잡는 법]&lt;/b&gt;에 대한 내용이 개발자가 글을 쓸 때 도움이 될 것 같은 내용이어서 요약하여 글로 남겨놓으려 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글의 종류별로 목차 잡는 법, 저술편집&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;직접 경험한 개발기 작성하기, 저&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'저'&lt;/b&gt;는 직접 경험한 것을 쓴 것으로, 개발 과정과 결과를 쓴 개발기가 여기에 해당한다. 저에 해당하는 글쓰기 예시는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TenserFlow를 활용한 네이버 쇼핑의 상품 카테고리 자동 분류&lt;/li&gt;
&lt;li&gt;TailwindCSS 적용기&lt;/li&gt;
&lt;li&gt;브라우저 렌더링 속도의 개선 과정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발기를 쓸 때는 목차를 잘 구성해야 한다. 개발을 하는 과정에서 최종적으로 성공한 루트와 중간에 실패한 루트를 구별하는것이 중요하다. 성공한 루트를 기준으로 목차를 잡고, 실패한 루트는 최종루트 다음에 문제해결이나 팁으로 정리해 덧붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서 소개한 예문의 목차를 정리하면 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;머리말&lt;br /&gt;- 서비스 설명&lt;br /&gt;- 개발의 필요성&lt;/li&gt;
&lt;li&gt;본문&lt;br /&gt;- 아키텍처나 알고리즘&lt;br /&gt;- 개발 모델(최종 루트)&lt;br /&gt;- 개발 과정에서 발견한 문제와 해결 방법&lt;br /&gt;- 남은 작업&lt;/li&gt;
&lt;li&gt;맺음말&lt;br /&gt;- 소감이나 회고&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목차를 정리했다면 본문부터 쓰는것이 하나의 팁이다. 개발자가 가장 잘 쓸 수 있는 내용이 본문이기 때문이다. 그렇게 해서 본문을 쓰고 나면 맺음말을 쓰고, 머리말은 맨 마지막에 간략하게 떠오를 때마다 적는 것이 좋을 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내용을 분석해 의미를 풀고 해석하기, 술&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'술'&lt;/b&gt;은 어떤 것을 분석해서 의미를 풀이하고 해석한 것이다. 개발에서는 새로운 기술을 자세히 설명한것이나 비슷한 용어를 비교해 풀이한 것, 에러 해결 방법 등이 술에 속한다. 술에 해당하는 글쓰기 예시는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET과 POST의 차이&lt;/li&gt;
&lt;li&gt;Webpack 4의 Tree Shaking에 대한 이해&lt;/li&gt;
&lt;li&gt;typescript 에러 해결 방안&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서 제시한 예문은 GET과 POST의 차이인데, 이 예문의 목차를 정리하면 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;HTTP&lt;/li&gt;
&lt;li&gt;GET&lt;/li&gt;
&lt;li&gt;POST&lt;/li&gt;
&lt;li&gt;GET과 POST의 차이&lt;/li&gt;
&lt;li&gt;참고&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 목차를 잘 보면 실제로 개발자가 쓴 글은 4장 뿐이다. 나머지는 참조한 HTTP 프로토콜의 문서를 정리한 것이다. 이렇게 원전의 내용을 먼저 쓰고 비교한 내용을 자기 생각으로 이론적으로 펼쳐 정리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 원전의 내용을 &lt;b&gt;직접 실험해 비교&lt;/b&gt;한 뒤 그 내용을 정리해서 풀이할 수도 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복잡한 자료를 편집하여 질서를 부여하기, 편&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'편'&lt;/b&gt;은 &lt;b&gt;산만하고 복잡한 자료를 편집&lt;/b&gt;해 &lt;b&gt;질서를 부여&lt;/b&gt;한 것이다. 보통 시간 순서로 일어난 일이나 해야할 일을 쓴 것을 통칭하기도 한다. 개발에서는 프로그램 설치나 설정 순서, 개발 방법, 튜토리얼, 개발자 컨퍼런스 후기 같은 것이 여기에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편에 해당하는 글쓰기 예시는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ES2015단위 테스트 환경 구축하기&lt;/li&gt;
&lt;li&gt;간단하게 구축해 보는 JavaScript 환경&lt;/li&gt;
&lt;li&gt;Ubuntu의 apt-get 명령어 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 쓸 때는 한 일을 먼저 시간 순서로 적은 다음, 내용을 다 적는다. 그리고 단계를 만들고 묶은 뒤 하위 내용을 간략히 요약한다. 이렇게 쓴다면 아래 예시와 같이 글을 쓸 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Git과 GitHub를 활용한 협업 공간으로 개발 시작하기&lt;br /&gt;- Git저장소 만들기&lt;br /&gt;- GitHub에 이슈 등록하기&lt;/li&gt;
&lt;li&gt;Node.js와 Yarn으로 개발 환경 설정하기&lt;br /&gt;- Node.js와 npm, yarn&lt;br /&gt;- Node.js와 yarn 설치&lt;br /&gt;- 프로젝트 생성&lt;br /&gt;- package.json파일과 패키지 관리&lt;br /&gt;- ...&lt;/li&gt;
&lt;li&gt;Jest로 테스트 환경 설정하기&lt;br /&gt;- 테스트 코드 작성&lt;br /&gt;- Jest 설치&lt;br /&gt;- 구현 코드 작성&lt;br /&gt;- 테스트를 통한 구현 코드 수정&lt;br /&gt;- 테스트 환경 공유&lt;/li&gt;
&lt;li&gt;Travis CI를 활용해 리뷰 환경 개선하기&lt;br /&gt;- Travis Ci를 활용한 테스트 자동화&lt;br /&gt;- 애플리케이션의 설정 정보 관리&lt;br /&gt;- Node.js 버전 유지&lt;/li&gt;
&lt;li&gt;마치며&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 글쓰기에 능숙하지 않다면 처음부터 구조를 생각하지 말고 본인이 한 일 등을 쭉 나열하고 그 내용부터 쓴 다음, 각 단계로 묶어 구조화하는것이 팁이 될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;흩어진 자료를 한곳에 모아 정리하기, 집&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'집'&lt;/b&gt;은 여러 사람의 견해나 흩어진 자료를 한데 모아 정리하는 것이다. 내용을 많이 쓰기보단 핵심만 간결하게 정리하는 것이 좋고, 글 전체가 길지 않고 내용이 각각 나열 되어있다. 기술 블로그에서는 명령어 모음, 팁, ~가지 규칙 같은 것이 여기에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집에 해당하는 글쓰기 예시는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바스크립트 정규표현식 코딩 팁&lt;/li&gt;
&lt;li&gt;스크롤과 관련된 css 속성 n가지&lt;/li&gt;
&lt;li&gt;좋은 코딩을 위한 n가지 규칙&lt;/li&gt;
&lt;li&gt;데이터 사이언스 인터뷰 질문 모음집&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 다른 여러 사람의 견해나 자료를 모아야 하는 것은 아니며 본인의 경험에서 터득한 것을 핵심만 정리해 나열하는것도 '집'이 될 수 있다.&lt;/p&gt;</description>
      <category>개발 도서 정리</category>
      <category>개발자를 위한 글쓰기</category>
      <category>저술편집</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/20</guid>
      <comments>https://devpluto.tistory.com/entry/%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-%EA%B8%80%EC%93%B0%EA%B8%B0-%EA%B8%80%EC%9D%98-%EC%A2%85%EB%A5%98%EB%B3%84%EB%A1%9C-%EB%AA%A9%EC%B0%A8-%EC%9E%A1%EB%8A%94-%EB%B2%95#entry20comment</comments>
      <pubDate>Fri, 28 Oct 2022 15:55:01 +0900</pubDate>
    </item>
    <item>
      <title>[자료구조] c언어 구조체로 스택 구현하기</title>
      <link>https://devpluto.tistory.com/entry/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-c%EC%96%B8%EC%96%B4%EB%A1%9C-%EC%8A%A4%ED%83%9D-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;스택(Stack)이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택은 데이터를 후입선출(LIFO:Last-In First-Out)하는 자료구조로,&amp;nbsp;가장 최근에 들어온 데이터가 가장 먼저 나간다는 특징이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상자나 책을 쌓아놓은 더미를 생각하면 이해하기 쉬울 것이다. 이번 글에서는 스택을 배열 구조체를 이용해서 구현해 볼 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-26 오후 3.45.25.png&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI61cA/btrPDqBzNne/wMjeSPJHtEGAqlzb2xJGuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI61cA/btrPDqBzNne/wMjeSPJHtEGAqlzb2xJGuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI61cA/btrPDqBzNne/wMjeSPJHtEGAqlzb2xJGuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI61cA%2FbtrPDqBzNne%2FwMjeSPJHtEGAqlzb2xJGuk%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;426&quot; height=&quot;381&quot; data-filename=&quot;스크린샷 2022-10-26 오후 3.45.25.png&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스택 타입 구조체 정의 및 초기화 함수&lt;/h3&gt;
&lt;pre id=&quot;code_1666764746583&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#define SIZE 100

typedef int element; // 배열안에 들어오는 값의 타입을 element로 한번에 지정

typedef struct {
  element data[SIZE];
  int top; // Index 번호
} StackType;

// 초기화
void init(StackType *S)
{
  S -&amp;gt; top = -1; // 포인터 연산자로 top에 접근해서 -1로 초기화
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택의 요소에 해당하는 data 배열과, 스택의 index 번호에 해당하는 top을 포함하는 StackType 구조체를 선언한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러체크함수 is_full(), is_empty()&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;is_full함수는 스택이 공백상태인지 아닌지를 검사한다.&lt;/li&gt;
&lt;li&gt;is_empty함수는 스택이 포화상태인지 아닌지를 검사한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1666764776802&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 에러 체크 함수
int is_full(StackType *S)
{
  return S-&amp;gt;top == SIZE - 1;
}

int is_empty(StackType *S)
{
  return S-&amp;gt;top == -1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삽입함수 push()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;push함수는 스택에 데이터를 추가한다. 만약 스택이 가득 차있다면 더이상 넣을 공간이 없으므로 가득 차있다는 문구를 출력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666764835406&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 삽입
void push(StackType *S, element e)
{
  if (isFull(S))
    printf(&quot;Overflow\n&quot;);
  else
  {
    S-&amp;gt;top++;              // top을 하나 증가시킨후
    S-&amp;gt;data[S-&amp;gt;top] = e;   // 데이터를 삽입한다
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삭제함수 pop()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pop함수는 스택에서 데이터를 삭제하고, 그 데이터를 반환한다. 만약 스택이 비어있는 상태라면 동작하지않고 비어있다는 문구를 출력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666765210041&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 삭제
element pop(StackType *S)
{
  if (is_empty(S))
  {
    printf(&quot;Empty\n&quot;);
    return -1;
  }
  else
  {
    element e = S-&amp;gt;data[S-&amp;gt;top]; // 가장 상위에 있는 데이터를 e에 저장한 후
    S-&amp;gt;top--;                    // top을 하나 감소시킨다
    return e;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조회함수 peek()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;peek함수는 스택에서 데이터를 삭제하지 않고 &lt;b&gt;들여다 보기&lt;/b&gt;만 하는 함수이다. pop과 마찬가지로 스택의 비어있음을 체크한다.&lt;/p&gt;
&lt;pre id=&quot;code_1666765234743&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 조회
element peek(StackType *S)
{
  if (is_empty(S))
  {
    printf(&quot;Empty\n&quot;);
    return -1;
  }
  else
  {
    return S-&amp;gt;data[S-&amp;gt;top];
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스택 출력 함수 print()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;print함수를 만들어 스택을 처음부터(0) 끝까지(top) 차례로 출력함으로써 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1666765304596&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void print(StackType *S)
{
  for (int i = 0; i &amp;lt;= S-&amp;gt;top; i++)
  {
    printf(&quot;%c &quot;, S-&amp;gt;data[i]);
  }
  printf(&quot;\n&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택을 구현한 전체 코드는 아래와 같다. (더보기)&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1666765467631&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#define SIZE 100

typedef int element;

typedef struct
{
  element data[SIZE];
  int top; 
} StackType;

// 초기화
void init(StackType *S)
{
  S-&amp;gt;top = -1;
}

// 에러 체크 함수
int is_empty(StackType *S)
{
  return S-&amp;gt;top == -1;
}

int is_full(StackType *S)
{
  return S-&amp;gt;top == SIZE - 1;
}

// 삽입
void push(StackType *S, element e)
{
  if (is_full(S))
    printf(&quot;Overflow\n&quot;);
  else
  {
    S-&amp;gt;top++;
    S-&amp;gt;data[S-&amp;gt;top] = e;
  }
}

// 삭제
element pop(StackType *S)
{
  if (is_empty(S))
  {
    printf(&quot;Empty\n&quot;);
    return -1;
  }
  else
  {
    element e = S-&amp;gt;data[S-&amp;gt;top];
    S-&amp;gt;top--;
    return e;
  }
}

// 조회
element peek(StackType *S)
{
  if (is_empty(S))
  {
    printf(&quot;Empty\n&quot;);
    return -1;
  }
  else
  {
    return S-&amp;gt;data[S-&amp;gt;top];
  }
}

void print(StackType *S)
{
  for (int i = 0; i &amp;lt;= S-&amp;gt;top; i++)
  {
    printf(&quot;%c &quot;, S-&amp;gt;data[i]);
  }
  printf(&quot;\n&quot;);
}

int main()
{
  StackType S;
  init(&amp;amp;S);

  pop(&amp;amp;S);
  push(&amp;amp;S, 'a');
  push(&amp;amp;S, 'b');
  push(&amp;amp;S, 'c');
  print(&amp;amp;S);

  element p = pop(&amp;amp;S);
  printf(&quot;%c\n&quot;, p);
  print(&amp;amp;S);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;도움이 되었다면 아래 '공감' 버튼을 눌러주세요! 읽어주셔서 감사합니다 :)&lt;/span&gt;&lt;/p&gt;</description>
      <category>cs/자료구조</category>
      <category>C언어</category>
      <category>스택</category>
      <category>자료구조</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/19</guid>
      <comments>https://devpluto.tistory.com/entry/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-c%EC%96%B8%EC%96%B4%EB%A1%9C-%EC%8A%A4%ED%83%9D-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0#entry19comment</comments>
      <pubDate>Wed, 26 Oct 2022 15:50:08 +0900</pubDate>
    </item>
    <item>
      <title>[티스토리 스킨 만들기] 1. 구조 파악하기</title>
      <link>https://devpluto.tistory.com/entry/%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-%EC%8A%A4%ED%82%A8-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-%EA%B5%AC%EC%A1%B0-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;어느날 내 티스토리에 적용할 블로그 테마를 찾는 중에 마음에 드는 테마가 없어서 내 입맛대로 스킨을 만들어 보기로 했다.&lt;br&gt;하지만 어떻게 만들어야 할 지 몰라서 찾아보는데 시간 투자를 많이했는데, 처음 개발하는 사람들을 위한 정리된 가이드 글이 있으면 좋겠다 싶어서 글을 작성한다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;스킨 제작 가이드&lt;/h2&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3dscA/btrOqT5FtEs/EoPdjgHzCbhJOqza4K2nvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3dscA/btrOqT5FtEs/EoPdjgHzCbhJOqza4K2nvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3dscA/btrOqT5FtEs/EoPdjgHzCbhJOqza4K2nvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3dscA%2FbtrOqT5FtEs%2FEoPdjgHzCbhJOqza4K2nvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;254&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;스킨 개발을 위해서 티스토리에서는 &lt;a href=&quot;https://tistory.github.io/document-tistory-skin/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;스킨 제작 가이드&lt;/span&gt;&lt;/a&gt;를 제공한다. 하지만 제작 가이드가 친절하게 설명되어있지 않아서 앞으로 해당 문서를 토대로 최대한 이해하기 쉽게 설명하도록 하겠다.&lt;br&gt;스킨을 적용하기 위해 필요한 파일에는 skin.html, index.xml, style.css와 추가적으로 js, image 파일이 있다. 스킨의 메인 템플릿 파일인 skin.html과 스킨 정보 파일인 index.xml에 대해서 자세히 알아보도록 하자.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;기본 skin.html 파일 파악하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;블로그의 뼈대를 세우는 skin.html 파일을 알아보기 이전에 티스토리가 제공하는 치환자라는 개념을 알아보자.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;티스토리 치환자?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;티스토리 치환자는 그룹 치환자, 값 치환자의 2가지 형태를 가지고 있다. 치환자를 사용하면 각 url에 해당하는 HTML 결과물로 치환된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
 &lt;li&gt;그룹치환자: 내부에 스킨 데이터를 포함하며 렌더링된 값으로 변환된다&lt;/li&gt;
 &lt;li&gt;값치환자: 해당하는 값으로 변환된다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;s_tag_rep&amp;gt;
  &amp;lt;li&amp;gt;
    &amp;lt;a href=&quot;&quot; class=&quot;&quot;&amp;gt;&amp;lt;/a&amp;gt;
  &amp;lt;/li&amp;gt;
&amp;lt;/s_tag_rep&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;그룹치환자와 값 치환자 예시 - 태그가 표시되야하는 상황이라면 그룹치환자 &amp;lt;s_tag_rep&amp;gt;으로 감싸진 안의 내용을 렌더링하고, 값치환자 등을 통해 해당 태그의 링크나 이름 값으로 치환한다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;head 및 메타데이터 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;검색엔진 최적화(SEO)를 위해서 &amp;lt;title&amp;gt;요소와 &amp;lt;meta&amp;gt;요소를 다음과 같이 추가할 수 있다.&lt;br&gt;그리고 &amp;lt;link&amp;gt;요소를 이용해 style.css 경로를 꼭 삽입해야한다. 스킨을 등록하기 위해서 꼭 필요한 파일이기 때문이다.&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;html&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;head&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;title&amp;gt;개발 창고&amp;lt;/title&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;meta name=&quot;title&quot; content=&quot;개발 창고 :: 개발 창고&quot; /&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;meta name=&quot;description&quot; content=&quot;안녕하세요, 개발자 플루토입니다.&quot; /&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;meta
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name=&quot;viewport&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;content=&quot;width=device-width, height=device-height, initial-scale=1, minimum-scale=1.0, maximum-scale=1.0&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;/&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;chrome=1&quot; /&amp;gt;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;link
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;rel=&quot;alternate&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;type=&quot;application/rss+xml&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;title=&quot;개발 창고&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;href=&quot;https://devpluto.tistory.com/rss&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;/&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;./style.css&quot; /&amp;gt;
&amp;lt;/head&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;기본으로 쓰이는 티스토리 치환자들&lt;/h3&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
  &amp;lt;s_t3&amp;gt;
    ...
  &amp;lt;/s_t3&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;티스토리 치환자를 사용하기 위해서는 위 같이 &amp;lt;s_t3&amp;gt; 치환자로 전체를 감싸줘야한다. 이 치환자 안에 홈 커버, 게시글 목록, 사이드바 등의 치환자들을 추가해서 원하는 블로그를 디자인 할 수 있다. 본문 내부에 쓸 수 있는 중요한 치환자들을 알아보자.&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;본문 구성, 인덱스와 파머링크 페이지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;인덱스 치환자는 메인페이지에서 게시글 목록을 반복하여 반환하고 , 퍼머링크 치환자는 게시글을 클릭했을 때 보여지는 글을 반환한다. 예를들어 현재 사용자의 접속이 메인페이지라면 인덱스 치환자 내부 HTML은 보여주고, 퍼머링크 치환자는 보여주지 않는 방식으로 동작한다.&lt;br&gt;인덱스 치환자 안에는 메인에서 보여질 목록 하나의 구조를 작성하면 되고, 퍼머링크 치환자 안에는 본문에 보여질 구조(제목, 내용, 이전-다음글, 댓글 등)을 작성하면 된다. 각각 해당되는 자세한 치환자는 &lt;a href=&quot;https://tistory.github.io/document-tistory-skin/contents/post.html&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;스킨 제작 가이드의 글 카테고리&lt;/span&gt;&lt;/a&gt;에서 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- 본문 --&amp;gt;
&amp;lt;s_article_rep&amp;gt;
  &amp;lt;!-- 인덱스 페이지 --&amp;gt;
  &amp;lt;s_index_article_rep&amp;gt;
  ...
  &amp;lt;/s_index_article_rep&amp;gt;

  &amp;lt;!-- 퍼머링크 페이지--&amp;gt;
  &amp;lt;s_permalink_article_rep&amp;gt;
  ...
  &amp;lt;/s_permalink_article_rep&amp;gt;
&amp;lt;/s_article_rep&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;리스트 페이징 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;페이징 치환자는 아래 보이는 게시글의 페이지를 출력한다.&lt;br&gt;&amp;lt;s_paging&amp;gt; 치환자는 다음과 같이 본문 치환자 안이 아니라, 아래에 위치시키면 된다. 만약 본문 안에 위치시키면 게시글 목록마다 반복하여 리스트가 표시된다.&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;s_t3&amp;gt;
  &amp;lt;s_article_rep&amp;gt;
  &amp;lt;/s_article_rep&amp;gt;
  &amp;lt;!-- //본문 --&amp;gt;

  &amp;lt;s_paging&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;a &amp;gt;
      &amp;lt;
      &amp;lt;/a&amp;gt;
        &amp;lt;span class=&quot;flex&quot;&amp;gt;
          &amp;lt;s_paging_rep&amp;gt;
            &amp;lt;a &amp;gt;
              
            &amp;lt;/a&amp;gt;
          &amp;lt;/s_paging_rep&amp;gt;
        &amp;lt;/span&amp;gt;
      &amp;lt;a &amp;gt;
      &amp;gt;
      &amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/s_paging&amp;gt;
&amp;lt;/s_t3&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;사이드바&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;사이드바를 적용하기 위해서는 아래와 같이 적용하고싶은 위치에 &amp;lt;s_sidebar&amp;gt; 사이드바 그룹치환자 안에 &amp;lt;s_sidebar_element&amp;gt; 사이드바 개별 그룹치환자를 각각 넣으면 된다.&lt;br&gt;주로 볼수있는 방문자수나 최근 글, 댓글, 공지사항, 태그 목록 등을 넣을 수 있고 각 항목의 상세 구현은 &lt;a href=&quot;https://tistory.github.io/document-tistory-skin/sidebar/basic.html&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;스킨 제작 가이드의 사이드바 카테고리&lt;/span&gt;&lt;/a&gt;에서 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;  &amp;lt;s_sidebar&amp;gt;
    &amp;lt;!-- 오른쪽 사이드바 --&amp;gt;
    &amp;lt;s_sidebar_element&amp;gt;
      &amp;lt;!-- 카테고리 --&amp;gt;
      ...
    &amp;lt;/s_sidebar_element&amp;gt;
    &amp;lt;s_sidebar_element&amp;gt;
      &amp;lt;!--최근에 올라온 글 --&amp;gt;
      ...
    &amp;lt;/s_sidebar_element&amp;gt;
  &amp;lt;/s_sidebar&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;기타&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;티스토리의 방명록, 태그 페이지도 본문과 마찬가지로 &amp;lt;s_t3&amp;gt;치환자 안에 작성하면 된다. (유저가 접근한 url이 /tag라면 태그 클라우드 치환자만 표시하고 나머지는 표시하지 않는다.)&lt;br&gt;이것들도 마찬가지로 스킨제작가이드의 컨텐츠 &amp;gt; 태그클라우드, 방명록 카테고리에서 확인할 수 있다.&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;스킨 정보 파일 (index.xml) 파악하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;처음 개발을 시작할 때 xml은 처음보는 파일 형식이었고, index.xml 파일 예시 코드도 난해해서 해석하는데 많은 시간을 투자했었다. 그렇기 때문에 이해를 돕고자 설명을 덧붙이겠다.&lt;br&gt;&lt;span style=&quot;color: #1E1E1E;&quot;&gt;xml 파일은 알고보면 간단한데, html 파일과 같이 마크업 언어로 태그로 데이터를 둘러싼 형태를 가지고 있다. &lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #FFFFFF;&quot;&gt;html은 웹페이지를 위해 정해진 태그를 사용하여 작성하는 반면, xml은 사용자가 태그를 직접 정의할 수 있고, 데이터 저장 및 전달을 위해 작성된다는면에서 차이가 있다.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #FFFFFF;&quot;&gt;티스토리 스킨에 필요한 정보가 담겨있는 xml 파일 예시코드는 &lt;a href=&quot;https://tistory.github.io/document-tistory-skin/common/index.xml.html&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;해당 카테고리&lt;/span&gt;&lt;/a&gt;에 들어가서 복사-붙여넣기 하면 된다. 나는 추가적으로 작성 할 수 있는 스킨 정보에 대해 조금 더 다뤄보겠다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;홈 커버&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;홈 커버 치환자를 통해 슬라이더, 썸네일 리스트, 글 목록 등 다양한 홈 커버를 만들 수 있다. 하지만 홈 커버를 설정하게 되면 기본 설정인 최신 글을 표시할 수 없으므로 염두에 둬야한다.&lt;br&gt;마찬가지로 홈 커버도 &lt;a href=&quot;https://tistory.github.io/document-tistory-skin/common/cover.html#%EC%A0%95%EC%9D%98-indexxml&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;홈 커버 카테고리&lt;/span&gt;&lt;/a&gt;에서 설명을 확인할 수 있고, 이해하기 쉽도록 작성한 홈 커버 예시는 다음과 같다.&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;html&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!-- index.xml --&amp;gt;
&amp;lt;default&amp;gt;
&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;lt;cover&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;![CDATA[
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;: &quot;cover-thumbnail-list&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;title&quot;: &quot;게시물이 존재할 경우 아래에 최신순 5건이 배열됩니다&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;dataType&quot;: &quot;RECENT&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;data&quot;: {&quot;category&quot;:&quot;ALL&quot;,&quot;size&quot;:&quot;5&quot;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]]&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;lt;/cover&amp;gt;
&amp;lt;/default&amp;gt;

&amp;lt;cover&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;lt;item&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;name&amp;gt;cover-thumbnail-list&amp;lt;/name&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;label&amp;gt;&amp;lt;![CDATA[ 글 리스트 ]]&amp;gt;&amp;lt;/label&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;description&amp;gt;&amp;lt;![CDATA[ 메인 화면에 표시할 기본 글 리스트 입니다. ]]&amp;gt;&amp;lt;/description&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;lt;/item&amp;gt;
&amp;lt;/cover&amp;gt;

&amp;lt;!-- skin.html --&amp;gt;
&amp;lt;s_cover name=&quot;cover-thumbnail-list&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;lt;s_cover_item&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;s_cover_item_article_info&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;xml 파일에서 홈 커버 치환자와 이름를 정의하고, html에 해당 이름을 &amp;lt;s_cover&amp;gt; 치환자의 name 유형에 작성함으로써 사용할 수 있다.&lt;br&gt;이렇게 작성하면 사용자는 아래와 같이 티스토리 관리자 화면 &amp;gt; 스킨편집 &amp;gt; 홈 설정에서 커버를 선택해서 사용할 수 있다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/947MP/btrPAqOw6Gd/Uqm9rdVHyT4akMFsW6zYRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/947MP/btrPAqOw6Gd/Uqm9rdVHyT4akMFsW6zYRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/947MP/btrPAqOw6Gd/Uqm9rdVHyT4akMFsW6zYRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F947MP%2FbtrPAqOw6Gd%2FUqm9rdVHyT4akMFsW6zYRk%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;524&quot; height=&quot;183&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JhedC/btrPzBQp4NE/lR2xMQdvyi5uspyHgxRhn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JhedC/btrPzBQp4NE/lR2xMQdvyi5uspyHgxRhn1/img.png&quot; data-alt=&quot; 커버 수정에 들어가면 default &amp;amp;amp;gt; cover에서 정의한 json 데이터가 표시된다. &quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JhedC/btrPzBQp4NE/lR2xMQdvyi5uspyHgxRhn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJhedC%2FbtrPzBQp4NE%2FlR2xMQdvyi5uspyHgxRhn1%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;491&quot; height=&quot;374&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt; 커버 수정에 들어가면 default &amp;amp;gt; cover에서 정의한 json 데이터가 표시된다. &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;스킨 옵션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;xml 파일에서 스킨의 옵션을 정의하면 위에서 봤던 티스토리 치환자를 커스텀하여 사용할 수 있다.&lt;br&gt;다음 예시와 같이 xml 파일에서 커스텀 할 옵션들을 정의한 다음 html 파일에서 해당 치환자를 사용할 수 있다.&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;html&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!-- index.xml --&amp;gt;
&amp;lt;variables&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;variablegroup name=&quot;커스텀 주소 링크&quot;&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;variable&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;name&amp;gt;portfolio_link&amp;lt;/name&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;label&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;![CDATA[포트폴리오 주소]]&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/label&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;description&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;![CDATA[ 개인 포트폴리오 사이트 링크를 설정할 수 있습니다. ]]&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/description&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;type&amp;gt;STRING&amp;lt;/type&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;option&amp;gt;&amp;lt;/option&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;default&amp;gt;&amp;lt;/default&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/variable&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;/variablegroup&amp;gt;&amp;nbsp;&amp;nbsp;
&amp;lt;/variables&amp;gt;

&amp;lt;!-- skin.html --&amp;gt;
&amp;lt;s_if_var_portfolio_link&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;a href=&quot;&quot; target=&quot;_blank&quot;&amp;gt;link&amp;lt;/a&amp;gt;
&amp;lt;/s_if_var_portfolio_link&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;스킨을 옵션을 정의하여 작성하면 사용자는 관리자 화면 &amp;gt; 스킨 편집에서 다음과 같이 옵션을 선택할 수 있다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baHXlD/btrPfaFoNJh/fRYKmsVTIRb6cvBOycCs1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baHXlD/btrPfaFoNJh/fRYKmsVTIRb6cvBOycCs1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baHXlD/btrPfaFoNJh/fRYKmsVTIRb6cvBOycCs1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaHXlD%2FbtrPfaFoNJh%2FfRYKmsVTIRb6cvBOycCs1k%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;448&quot; height=&quot;266&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;마무리와 짤막한 팁&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;티스토리 스킨을 만들기 위해 기본적으로 필요한 요소들에 대해서 설명헀고, 티스토리 스킨 구조를 이해하는데 도움이 되었으면 한다.&lt;br&gt;처음부터 짜기 막막하다면 해당 글을 참고하면서 이미 만들어진 스킨의 html 구조를 파악하여 작성하면 유용할 것이다. 티스토리 치환자가 포함된 html파일을 보고싶다면 원하는 스킨을 적용한 후 스킨 편집 &amp;gt; html 편집에서 확인 할 수 있다.&lt;/p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0Dlk8/btrPwZrNebg/282DZiksuY0UjRKOguY0n1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0Dlk8/btrPwZrNebg/282DZiksuY0UjRKOguY0n1/img.png&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;350&quot; style=&quot;width: 63.4694%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0Dlk8/btrPwZrNebg/282DZiksuY0UjRKOguY0n1/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0Dlk8%2FbtrPwZrNebg%2F282DZiksuY0UjRKOguY0n1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1150&quot; height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rwpLC/btrPzMK40VU/UiYMlYmi1EqfIftnjeXkmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rwpLC/btrPzMK40VU/UiYMlYmi1EqfIftnjeXkmK/img.png&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;556&quot; style=&quot;width: 35.3678%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rwpLC/btrPzMK40VU/UiYMlYmi1EqfIftnjeXkmK/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrwpLC%2FbtrPzMK40VU%2FUiYMlYmi1EqfIftnjeXkmK%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;1018&quot; height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;글이 도움이 되었다면 아래 공감버튼을 눌러주세요! :)&lt;/p&gt;</description>
      <category>프로젝트/블로그 테마</category>
      <category>스킨 만들기</category>
      <category>티스토리 스킨 만드는 방법</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/18</guid>
      <comments>https://devpluto.tistory.com/entry/%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-%EC%8A%A4%ED%82%A8-%EB%A7%8C%EB%93%A4%EA%B8%B0-1-%EA%B5%AC%EC%A1%B0-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0#entry18comment</comments>
      <pubDate>Fri, 21 Oct 2022 15:19:06 +0900</pubDate>
    </item>
    <item>
      <title>[모두의 네트워크] 1장. 네트워크의 첫 걸음</title>
      <link>https://devpluto.tistory.com/entry/%EB%AA%A8%EB%91%90%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-1%EC%9E%A5</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;네트워크의 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴퓨터 네트워크란 무엇일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'네트워크' 라는 단어에 포함된 의미는 단순히 컴퓨터 간의 연결만을 말하는 것이 아니라, 사람과 사람간의 네트워크, 물류 네트워크와 같이 다양한 종류의 네트워크가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터 간의 네트워크를 연결한 것을 컴퓨터 네트워크라 한다. 컴퓨터 네트워크를 통해 컴퓨터 간 필요한 데이터, 즉 정보를 서로 주고받을 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;인터넷&lt;/b&gt;: 네트워크의 한 종류로, 전 세계의 큰 네트워크부터 작은 네트워크까지를 연결하는 거대한 네트워크를 말한다. 이러한 광범위한 연결을 통해 인터넷으로 해외 웹 사이트도 볼 수 있다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패킷&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷은 컴퓨터 간 데이터를 주고 받을 때 네트워크를 통해 전송되는 데이터의 작은 조각을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 데이터를 네트워크로 그대로 보내게 되면 데이터가 네트워크의 대역폭을 많이 점유하기 때문에 다른 패킷의 흐름을 막을 수 있다. 따라서 데이터를 보낼 때는 작은 조각(패킷)으로 나눠서 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적지에 도착한 패킷들은 순서없이 도착하기 때문에 송신 측에서 수신 측으로 패킷을 보낼 때 각 패킷에 순서대로 번호를 붙여서 보낸다. 이렇게 하면 번호에 맞춰 정렬하여 원래 상태로 복구할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디지털 데이터와 네트워크 범위&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디지털 데이터란 컴퓨터가 다루는 0과 1의 집합을 말한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0과 1의 정보를 나타내는 최소 단위를 비트(bit)라고 하고, 비트 8개를 모아 표시하는 단위를 바이트(byte)라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크를 통해 데이터를 전송하는 경우는 비트 정보를 전기 신호로 변환하기 때문에 네트워크에서는 전기 신호가 전송된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;랜과 왠?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;랜(LAN, Local Area Network): 건물 안이나 특정 지역을 범위로 하는 네트워크&lt;/li&gt;
&lt;li&gt;왠(WAN, Wide Area Network):&amp;nbsp; 지리적으로 넓은 범위에 구축된 네트워크, 인터넷 서비스 제공자(ISP)가 제공하는 서비스를 이용하여 구축된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왠은 랜과 랜을 연결하는 것으로 생각해도 되는데, 예를들어 서울과 부산에 각각 있는 랜을 왠을 통해 서로 이어줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;랜은 범위가 좁기 때문에 속도가 빠르고 오류가 적지만, 왠은 범위가 넓기 때문에 랜에 비해 속도도 느리고 오류가 발생활 확률이 더 높다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;ISP(Internet Service Provider)&lt;/b&gt;: 인터넷 상용 서비스 사업을 하고 있는 KT, U+, SKT와 같은 사업자를 말한다. 인터넷 서비스 제공자는 왠을 구축할 수 있도록 해준다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;랜의 구성 방식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가정에서 하는 랜 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 가정에서 인터넷을 사용하기 위해서는 일반적으로 인터넷 서비스 제공자(ISP)와 인터넷 회선을 두가지를 결정해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷 서비스 제공자와 네트워크를 연결하기위해 인터넷 공유기가 필요하며 공유기 연결을 통해 가정에서 인터넷을 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회사에서 하는 랜 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서의 네트워크 구성에는 DMZ라는 네트워크 영역있을 수 있다. DMZ는 외부에 공개하기 위한 네트워크이며 공개 서버에는 웹 서버, DNS 서버, 메일 서버 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사는 서버를 운영하기 위해서 주로 서버를 사내에 설치하거나 데이터 센터에 두거나 클라우드에 둬서 관리 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;사내 또는 데이터 센터에 서버를 두고 운영하는 것을 &lt;b&gt;온프레미스(on-premise)&lt;/b&gt;라 한다. 주로 클라우드와 비교할때 사용하는 용어.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 도서 정리/모두의 네트워크</category>
      <category>모두의 네트워크</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/17</guid>
      <comments>https://devpluto.tistory.com/entry/%EB%AA%A8%EB%91%90%EC%9D%98-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-1%EC%9E%A5#entry17comment</comments>
      <pubDate>Thu, 13 Oct 2022 15:55:09 +0900</pubDate>
    </item>
    <item>
      <title>CleanDev 티스토리 스킨을 소개합니다.</title>
      <link>https://devpluto.tistory.com/entry/CleanDev-%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-%EC%8A%A4%ED%82%A8%EC%9D%84-%EC%86%8C%EA%B0%9C%ED%95%A9%EB%8B%88%EB%8B%A4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;스킨 소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CleanDev 스킨은 개발 블로그를 운영하는 개발자를 위한 깔끔한 티스토리 스킨입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스킨 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무채색의 컬러톤을 사용한 깔끔한 디자인&lt;/li&gt;
&lt;li&gt;필요 이상의 부가적인 기능 제거&lt;/li&gt;
&lt;li&gt;mac 스타일의 코드 스니펫 디자인&lt;/li&gt;
&lt;li&gt;작성 글의 가이드를 위한 TOC(Table of Contents)기능 지원&lt;/li&gt;
&lt;li&gt;커스텀 주소 링크를 통한 포트폴리오, 이메일주소, 깃허브 링크 이미지 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C0kwH/btrOre9w9Fy/UZkR5faIGKnAAJdDaQK0kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C0kwH/btrOre9w9Fy/UZkR5faIGKnAAJdDaQK0kK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;484&quot; data-filename=&quot;스크린샷 2022-10-12 오후 3.23.05.png&quot; style=&quot;width: 22.3033%; margin-right: 10px;&quot; data-widthpercent=&quot;22.57&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C0kwH/btrOre9w9Fy/UZkR5faIGKnAAJdDaQK0kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC0kwH%2FbtrOre9w9Fy%2FUZkR5faIGKnAAJdDaQK0kK%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;382&quot; height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHGpDn/btrOozG8neA/82OBC3seCcb4BmmaEyTKBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHGpDn/btrOozG8neA/82OBC3seCcb4BmmaEyTKBk/img.png&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;480&quot; data-filename=&quot;스크린샷 2022-10-12 오후 3.10.47.png&quot; data-is-animation=&quot;false&quot; style=&quot;width: 76.5339%;&quot; data-widthpercent=&quot;77.43&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHGpDn/btrOozG8neA/82OBC3seCcb4BmmaEyTKBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHGpDn%2FbtrOozG8neA%2F82OBC3seCcb4BmmaEyTKBk%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;1300&quot; height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;TOC와 코드 스니펫 디자인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;저작권&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 스킨의 저작권은 plutos에 있습니다. 본 스킨은 무료로 배포되나, &lt;b&gt;재배포와 상업적 이용은 불가능합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 개인적으로 수정하여 사용하는 것은 가능합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스킨 편집 지원 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;폰트 변경&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2022-10-12 오후 2.50.16.png&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbQilk/btrOpR8wPTU/ntXQp3VO9CLeLNo43PaSH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbQilk/btrOpR8wPTU/ntXQp3VO9CLeLNo43PaSH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbQilk/btrOpR8wPTU/ntXQp3VO9CLeLNo43PaSH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbQilk%2FbtrOpR8wPTU%2FntXQp3VO9CLeLNo43PaSH1%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;639&quot; height=&quot;374&quot; data-filename=&quot;edited_스크린샷 2022-10-12 오후 2.50.16.png&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폰트를 변경할 수 있습니다. Pretendard와 나눔 고딕 폰트를 지원합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 주소 링크&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트폴리오, 이메일, 깃허브 주소를 설정하면 블로그 소개글 우측 하단에 해당 링크 이미지가 표시됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-12 오후 3.00.40.png&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nabn0/btrOqMyQNHU/6wwFUMiXEJR0K22xQFaK3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nabn0/btrOqMyQNHU/6wwFUMiXEJR0K22xQFaK3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nabn0/btrOqMyQNHU/6wwFUMiXEJR0K22xQFaK3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnabn0%2FbtrOqMyQNHU%2F6wwFUMiXEJR0K22xQFaK3k%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;693&quot; height=&quot;249&quot; data-filename=&quot;스크린샷 2022-10-12 오후 3.00.40.png&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-12 오후 2.56.44.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ADkLj/btrOpTL5Tdb/85Nl4p8jYPGpnuK3l9lB81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ADkLj/btrOpTL5Tdb/85Nl4p8jYPGpnuK3l9lB81/img.png&quot; data-alt=&quot;적용 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ADkLj/btrOpTL5Tdb/85Nl4p8jYPGpnuK3l9lB81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FADkLj%2FbtrOpTL5Tdb%2F85Nl4p8jYPGpnuK3l9lB81%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;697&quot; height=&quot;193&quot; data-filename=&quot;스크린샷 2022-10-12 오후 2.56.44.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;적용 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기타 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다크 모드 변경 기능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그 접근시 기본으로 디바이스 다크모드 설정에 따라 적용되고, 우측 하단 다크모드 버튼을 클릭하여 변경할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coMC5a/btrWI9U6AGU/WBGYhO6POEpC3l7ncMi4t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coMC5a/btrWI9U6AGU/WBGYhO6POEpC3l7ncMi4t1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2850&quot; data-origin-height=&quot;1612&quot; data-filename=&quot;스크린샷 2023-01-20 오전 3.22.38.png&quot; style=&quot;width: 49.7282%; margin-right: 10px;&quot; data-widthpercent=&quot;50.31&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coMC5a/btrWI9U6AGU/WBGYhO6POEpC3l7ncMi4t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoMC5a%2FbtrWI9U6AGU%2FWBGYhO6POEpC3l7ncMi4t1%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;2850&quot; height=&quot;1612&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR3H3Y/btrWK2mSCUe/A10AtQzgd5guOrN10qH8o1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR3H3Y/btrWK2mSCUe/A10AtQzgd5guOrN10qH8o1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2818&quot; data-origin-height=&quot;1614&quot; data-filename=&quot;스크린샷 2023-01-20 오전 3.23.43.png&quot; style=&quot;width: 49.109%;&quot; data-widthpercent=&quot;49.69&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR3H3Y/btrWK2mSCUe/A10AtQzgd5guOrN10qH8o1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR3H3Y%2FbtrWK2mSCUe%2FA10AtQzgd5guOrN10qH8o1%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;2818&quot; height=&quot;1614&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TOC(Table On Contents) 기능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그 글 내용의 헤더 목록을 표시해주는 TOC기능을 글 우측에 표시하여 지원합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-20 오전 3.30.47.png&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/40wDP/btrWLjosh93/WQht71CeZ65yOdE6dPHa4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/40wDP/btrWLjosh93/WQht71CeZ65yOdE6dPHa4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/40wDP/btrWLjosh93/WQht71CeZ65yOdE6dPHa4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F40wDP%2FbtrWLjosh93%2FWQht71CeZ65yOdE6dPHa4k%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;259&quot; height=&quot;348&quot; data-filename=&quot;스크린샷 2023-01-20 오전 3.30.47.png&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;738&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반응형 디자인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CleanDev 스킨은 반응형 디자인을 지원합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ce9g79/btrWIQnYh0H/IuE2k9SQRxYTGE4y9cMuM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ce9g79/btrWIQnYh0H/IuE2k9SQRxYTGE4y9cMuM0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2090&quot; data-origin-height=&quot;1622&quot; data-filename=&quot;스크린샷 2023-01-20 오전 3.26.22.png&quot; style=&quot;width: 61.8004%; margin-right: 10px;&quot; data-widthpercent=&quot;62.53&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ce9g79/btrWIQnYh0H/IuE2k9SQRxYTGE4y9cMuM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fce9g79%2FbtrWIQnYh0H%2FIuE2k9SQRxYTGE4y9cMuM0%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;2090&quot; height=&quot;1622&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/odwRx/btrWMQlLHxh/32zl8ATdiUC8ekdUay7br0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/odwRx/btrWMQlLHxh/32zl8ATdiUC8ekdUay7br0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;1598&quot; data-filename=&quot;스크린샷 2023-01-20 오전 3.26.44.png&quot; style=&quot;width: 37.0368%;&quot; data-widthpercent=&quot;37.47&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/odwRx/btrWMQlLHxh/32zl8ATdiUC8ekdUay7br0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FodwRx%2FbtrWMQlLHxh%2F32zl8ATdiUC8ekdUay7br0%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;1234&quot; height=&quot;1598&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추후 지원 예정 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CleanDev 스킨은 꾸준히 기능을 업데이트 하고 있습니다. 추후 지원 예정 기능은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;광고 지원&lt;/li&gt;
&lt;li&gt;Progress Bar 추가&lt;/li&gt;
&lt;li&gt;코드 복사 기능 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스킨 적용하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;V1.0.3&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://github.com/eunbae11/tistory_skin_cleandev/releases/download/v1.0.1/clean_dev_version_1.0.1.zip&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/eunbae0/tistory_skin_cleandev/releases/download/v1.0.3/clean_dev_version_1.0.3.zip&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위 링크를 통해 스킨 파일을 다운로드 할 수 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;스킨을 적용하는 방법은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 티스토리 관리자 화면의 '꾸미기' &amp;gt; '스킨변경' 페이지로 가서 스킨 등록 버튼을 누릅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-12 오후 2.38.59.png&quot; data-origin-width=&quot;1854&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vf84j/btrOmUEUmmr/cjl5KH2ztXEyP3SWasUKxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vf84j/btrOmUEUmmr/cjl5KH2ztXEyP3SWasUKxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vf84j/btrOmUEUmmr/cjl5KH2ztXEyP3SWasUKxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvf84j%2FbtrOmUEUmmr%2Fcjl5KH2ztXEyP3SWasUKxk%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;672&quot; height=&quot;296&quot; data-filename=&quot;스크린샷 2022-10-12 오후 2.38.59.png&quot; data-origin-width=&quot;1854&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 추가 버튼을 누른 다음, 위에서 다운로드한 스킨 파일 &lt;b&gt;모두&lt;/b&gt;를 선택하고 저장합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-12 오후 2.40.27.png&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;1394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpAYY2/btrOq3G2Q0o/yC6hFbJxx1lNGMJKLfGe51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpAYY2/btrOq3G2Q0o/yC6hFbJxx1lNGMJKLfGe51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpAYY2/btrOq3G2Q0o/yC6hFbJxx1lNGMJKLfGe51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpAYY2%2FbtrOq3G2Q0o%2FyC6hFbJxx1lNGMJKLfGe51%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;605&quot; height=&quot;478&quot; data-filename=&quot;스크린샷 2022-10-12 오후 2.40.27.png&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;1394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 저장한 스킨을 적용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-12 오후 2.38.40.png&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CFm0G/btrOqBj2nLg/VLxKNKRRmQAkA7xPsPkAXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CFm0G/btrOqBj2nLg/VLxKNKRRmQAkA7xPsPkAXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CFm0G/btrOqBj2nLg/VLxKNKRRmQAkA7xPsPkAXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCFm0G%2FbtrOqBj2nLg%2FVLxKNKRRmQAkA7xPsPkAXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;288&quot; data-filename=&quot;스크린샷 2022-10-12 오후 2.38.40.png&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CleanDev 스킨은 처음으로 배포해보는 스킨이기 때문에 발생하는 문제들이 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류나 개선사항에 대해서는 댓글로 남겨주시면 감사하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하신다면 &lt;a title=&quot;Github&quot; href=&quot;https://github.com/eunbae0/tistory_skin_cleandev&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt; Star와 아래 공감 버튼을 눌러주세요! 제작자에게 큰 힘이 됩니다 :)&lt;/p&gt;</description>
      <category>티스토리 스킨</category>
      <category>티스토리 스킨</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/16</guid>
      <comments>https://devpluto.tistory.com/entry/CleanDev-%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-%EC%8A%A4%ED%82%A8%EC%9D%84-%EC%86%8C%EA%B0%9C%ED%95%A9%EB%8B%88%EB%8B%A4#entry16comment</comments>
      <pubDate>Wed, 12 Oct 2022 15:22:49 +0900</pubDate>
    </item>
    <item>
      <title>ssh: connect to host github.com port 22: Operation timed out 에러</title>
      <link>https://devpluto.tistory.com/entry/ssh-connect-to-host-githubcom-port-22-Operation-timed-out-%EC%97%90%EB%9F%AC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 발생&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git push를 하려 했을 때 다음과 같은 에러가 발생했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ssh:&amp;nbsp;connect&amp;nbsp;to&amp;nbsp;host&amp;nbsp;github.com&amp;nbsp;port&amp;nbsp;22:&amp;nbsp;Operation&amp;nbsp;timed&amp;nbsp;out&lt;br /&gt;Please make sure you have the correct access rightsand the repository exists.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-02 오전 12.39.13.png&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QYxxc/btrNvyptUYl/UbFieqdeUEDE1xJFGhAerK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QYxxc/btrNvyptUYl/UbFieqdeUEDE1xJFGhAerK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QYxxc/btrNvyptUYl/UbFieqdeUEDE1xJFGhAerK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQYxxc%2FbtrNvyptUYl%2FUbFieqdeUEDE1xJFGhAerK%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;1728&quot; height=&quot;758&quot; data-filename=&quot;스크린샷 2022-10-02 오전 12.39.13.png&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. ~/.ssh/config 경로의 config파일에 아래 텍스트를 입력한 후 저장한다&lt;/p&gt;
&lt;pre id=&quot;code_1664639762427&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Host github.com
Hostname ssh.github.com
Port 443
User git&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&amp;nbsp;&lt;span style=&quot;color: #232629;&quot;&gt;이후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #e3e6e8; color: #232629;&quot;&gt;ssh -T git@github.com&lt;/span&gt;&lt;span style=&quot;color: #232629;&quot;&gt; 커맨드를 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #232629;&quot;&gt;3. &lt;/span&gt;&lt;span style=&quot;color: #232629; background-color: #dddddd;&quot;&gt;Are you sure you want to continue connecting (yes/no/[fingerprint])?&lt;/span&gt;&lt;span style=&quot;color: #232629;&quot;&gt; 에서 yes를 입력하면 해결된다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 상세&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-02 오전 12.41.19.png&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWn5qh/btrNwGtjiN7/Xd1TNfKYMzGXroVleGlIJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWn5qh/btrNwGtjiN7/Xd1TNfKYMzGXroVleGlIJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWn5qh/btrNwGtjiN7/Xd1TNfKYMzGXroVleGlIJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWn5qh%2FbtrNwGtjiN7%2FXd1TNfKYMzGXroVleGlIJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1412&quot; height=&quot;694&quot; data-filename=&quot;스크린샷 2022-10-02 오전 12.41.19.png&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 경우 맥을 사용해서, config파일에 텍스트를 입력한 후, 터미널에서 커맨드를 실행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숨김파일을 보기 위해서는 finder에서 command + shift + .(점) 을 입력하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/15589682/ssh-connect-to-host-github-com-port-22-connection-timed-out&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/15589682/ssh-connect-to-host-github-com-port-22-connection-timed-out&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1664639386068&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;ssh: connect to host github.com port 22: Connection timed out&quot; data-og-description=&quot;I am under a proxy and I am pushing in to git successfully for quite a while. Now I am not able to push into git all of a sudden. I have set the RSA key and the proxy and double checked them, with no&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/15589682/ssh-connect-to-host-github-com-port-22-connection-timed-out&quot; data-og-url=&quot;https://stackoverflow.com/questions/15589682/ssh-connect-to-host-github-com-port-22-connection-timed-out&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/sKTW0/hyPZbwru4i/trtS7zDOlw6XUkkFrWrT4k/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/15589682/ssh-connect-to-host-github-com-port-22-connection-timed-out&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/15589682/ssh-connect-to-host-github-com-port-22-connection-timed-out&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/sKTW0/hyPZbwru4i/trtS7zDOlw6XUkkFrWrT4k/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;ssh: connect to host github.com port 22: Connection timed out&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I am under a proxy and I am pushing in to git successfully for quite a while. Now I am not able to push into git all of a sudden. I have set the RSA key and the proxy and double checked them, with no&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.github.com/en/authentication/troubleshooting-ssh/using-ssh-over-the-https-port&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.github.com/en/authentication/troubleshooting-ssh/using-ssh-over-the-https-port&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1664639462677&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;Using SSH over the HTTPS port - GitHub Docs&quot; data-og-description=&quot;GitHub Enterprise Server users: Accessing GitHub Enterprise Server via SSH over the HTTPS port is currently not supported. To test if SSH over the HTTPS port is possible, run this SSH command: $ ssh -T -p 443 git@ssh.github.com &amp;gt; Hi username! You've succes&quot; data-og-host=&quot;docs.github.com&quot; data-og-source-url=&quot;https://docs.github.com/en/authentication/troubleshooting-ssh/using-ssh-over-the-https-port&quot; data-og-url=&quot;https://ghdocs-prod.azurewebsites.net/en/authentication/troubleshooting-ssh/using-ssh-over-the-https-port&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bqPjSl/hyPZaYAX5b/H8V76L6Lk642N8z5gEkTqk/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://docs.github.com/en/authentication/troubleshooting-ssh/using-ssh-over-the-https-port&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.github.com/en/authentication/troubleshooting-ssh/using-ssh-over-the-https-port&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bqPjSl/hyPZaYAX5b/H8V76L6Lk642N8z5gEkTqk/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&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;Using SSH over the HTTPS port - GitHub Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;GitHub Enterprise Server users: Accessing GitHub Enterprise Server via SSH over the HTTPS port is currently not supported. To test if SSH over the HTTPS port is possible, run this SSH command: $ ssh -T -p 443 git@ssh.github.com &amp;gt; Hi username! You've succes&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.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>etc</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/13</guid>
      <comments>https://devpluto.tistory.com/entry/ssh-connect-to-host-githubcom-port-22-Operation-timed-out-%EC%97%90%EB%9F%AC#entry13comment</comments>
      <pubDate>Sun, 2 Oct 2022 00:59:01 +0900</pubDate>
    </item>
    <item>
      <title>개인 프로젝트 - 동네마켓</title>
      <link>https://devpluto.tistory.com/entry/%EA%B0%9C%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%8F%99%EB%84%A4%EB%A7%88%EC%BC%93</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 시작 동기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 개발을 공부하면서 개인적으로 토이 프로젝트를 진행하고 싶었는데, 한날 당근마켓 채용공고를 보다 문득 나도 당근마켓과 비슷한 기능을 조금 쉽게 다듬으면 이정도는 나 혼자서도 개발할 수 있지 않을까? 라는 생각에 프로젝트를 시작했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용 기술 스택 선정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 어짜피 혼자하는 프로젝트니까 내가 지금껏 사용해보지 않은 기술을 사용하여 경험을 쌓자! 는 목적으로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 언어를 제외하고는 거의 처음 사용하고, 사용해보고싶었는데 못했던 스택 위주로 선정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전부터 Next.js를 한번 사용해보고 싶었기 때문에 Next.js와 편리한 Tailwind css를 사용해보기로 했다. 그리고 백엔드는 이전에 사용해본 적 있는 파이어베이스를 이용하여 구축했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기능명세서와 데이터베이스 구조 작성하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 구현해야하는 기능을 파악하여 &lt;a href=&quot;https://docs.google.com/spreadsheets/d/1C9d5SRllIB5jguL8zY5kZxrIWjlpRTAf4Pa25olYSW0/edit?usp=drivesdk&quot;&gt;스프레드시트(링크)&lt;/a&gt;로 작성했으며, &lt;a href=&quot;https://nosqldbm.ru/&quot;&gt;https://nosqldbm.ru/&lt;/a&gt; 사이트를 이용해서 NoSQL 데이터베이스 컬렉션구조를 작성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-27 오전 9.24.38.png&quot; data-origin-width=&quot;1878&quot; data-origin-height=&quot;1064&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSCvqb/btrM6N1iIK3/rnbIpQhK2hGFdHg4ZsQBD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSCvqb/btrM6N1iIK3/rnbIpQhK2hGFdHg4ZsQBD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSCvqb/btrM6N1iIK3/rnbIpQhK2hGFdHg4ZsQBD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSCvqb%2FbtrM6N1iIK3%2FrnbIpQhK2hGFdHg4ZsQBD1%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;1878&quot; height=&quot;1064&quot; data-filename=&quot;스크린샷 2022-09-27 오전 9.24.38.png&quot; data-origin-width=&quot;1878&quot; data-origin-height=&quot;1064&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;애자일 방법론 사용하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면_기록_2022-09-27_오전_11_17_01_AdobeExpress.gif&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cv5rJU/btrM9P4qUfo/HKAksg16nEOL1bUMDw4fQ1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cv5rJU/btrM9P4qUfo/HKAksg16nEOL1bUMDw4fQ1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cv5rJU/btrM9P4qUfo/HKAksg16nEOL1bUMDw4fQ1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cv5rJU/btrM9P4qUfo/HKAksg16nEOL1bUMDw4fQ1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;469&quot; data-filename=&quot;화면_기록_2022-09-27_오전_11_17_01_AdobeExpress.gif&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 프로세스를 관리하는 방법 중에서 애자일 방법론의 하나인 스크럼에 대해서 찾았고, 이를 이용해서 스프린트 주기별로 할 리스트(백로그)를 위와같이 작성하여 관리했다. 자세한 사항은 &lt;a href=&quot;https://fragrant-spectrum-fff.notion.site/4c0efc846ee4421980d668203f1b81da&quot;&gt;노션 링크&lt;/a&gt;에서 확인할수있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 개발기간이 늘어지면서 스프린트 주기를 지키지는 못했지만, 각 스프린트 별로 작성한 코드를 구현하는 과정에서 문제점이나 해결과정 등은 따로 작성해보려한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 개발을 시작하고 구조를 짠 다음 며칠간 진행하다가 방학동안 번아웃이와서 제대로 공부하지도않고 그냥 하루하루를 흐르듯이 살았었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음 계획으로는 방학 내에 기능개발을 마치려했지만 개발기간이 늘어나서 아쉬움이 남는 프로젝트이다. 자세한 실제 구현사항이나 회고는 다음 글에서 풀어보겠다.&lt;/p&gt;</description>
      <category>프로젝트</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/10</guid>
      <comments>https://devpluto.tistory.com/entry/%EA%B0%9C%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%8F%99%EB%84%A4%EB%A7%88%EC%BC%93#entry10comment</comments>
      <pubDate>Tue, 27 Sep 2022 15:04:16 +0900</pubDate>
    </item>
    <item>
      <title>[c언어] 배열, 포인터 간단 정리</title>
      <link>https://devpluto.tistory.com/entry/c%EC%96%B8%EC%96%B4-%EB%B0%B0%EC%97%B4-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;pointer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인터를 선언할 때도 *를 사용하고 역참조를 할 때도 *를 사용한다. 같은 * 기호를 사용해서 헷갈릴수 있지만 선언과 사용을 구분해서 생각하면 된다. 즉, 포인터를 선언할 때 *는 &lt;b&gt;이 변수가 포인터다&lt;/b&gt;라고 알려주는 역할이고, 포인터에 사용할 때 *는 &lt;b&gt;포인터의 메모리 주소를 역참조하겠다&lt;/b&gt;라는 뜻.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;int *numPtr;                // 포인터. 포인터를 선언할 때 *
printf(&quot;%d\\n&quot;, *numPtr);    // 역참조. 포인터에 사용할 때 *

int num;
int *numPtr;
*numPtr = &amp;amp;num; (x)
numPtr = &amp;amp;num; (o)
*numPtr = num; (o)

// *numPtr은 int형, numPtr은 포인터형(int형 포인터)
// num 은 int형, &amp;amp;num은 주소형
// 이때 주소형과 포인터형은 동일시함. ( pointer to int와 address of int는 자료형이 같음 )&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;sizeof&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;int numArr[10] = { 11, 22, 33, 44, 55, 66, 77, 88, 99, 110 };    // 크기가 10인 int형 배열

printf(&quot;%d\\n&quot;, sizeof(numArr)); // 40
printf(&quot;%d\\n&quot;, sizeof(numArr) / sizeof(int)); // 10 (int의 size는 4이므로)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 numArr의 사이즈는 배열 공간 10 x int 자료형의 크기 4이므로 40이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;malloc&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;malloc(할당) &amp;rarr; 사용 &amp;rarr; free(해제) 과정을 거쳐 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;malloc으로 메모리를 할당하면 &lt;b&gt;할당된 주소값&lt;/b&gt;이 주어지기 때문에 포인터로 주소값을 저장해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;int x = 6; 이런식으로 할당한 값은 메모리공간의 스택(stack)에 할당되어 생성 뒤 따로 처리를 해주지 않아도 되지만, malloc으로 할당하면 메모리공간의 힙(Heap) 공간에 할당되기 때문에 꼭 메모리 해제를 해주어야함!(그렇지 않으면 메모리누수 발생)&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;int *numptr;
numptr = malloc(sizeof(int));
// *numptr = malloc(sizeof(int))와 같다

printf(&quot;%p\\n&quot;, numptr); // 메모리 주솟값!!

*numPtr = 10;   // 포인터를 역참조한 뒤 값 할당

printf(&quot;%d\\n&quot;, *numPtr);    // 10: 포인터를 역참조하여 메모리에 저장된 값 출력

free(numptr); // 꼭 해제해주어야함.&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;array&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열을 포인터에 할당한 뒤 포인터를 역참조해보면 배열의 첫 번째 요소의 값이 나온다. 마찬가지로 배열 자체도 역참조해보면 배열의 첫 번째 요소의 값이 나온다. 따라서 실제로는 배열도 포인터라 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 포인터를 선언하고 배열처럼 쓸수있고, 배열을 선언하고 포인터로 쓸 수 있다!&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;int numArr[10] = { 11, 22, 33, 44, 55, 66, 77, 88, 99, 110 };   
 // 크기가 10인 int형 배열

int *numPtr = numArr;       // 포인터에 int형 배열을 할당 - 배열자체가 주소값가짐

printf(&quot;%d\\n&quot;, *numPtr);    // 11: 배열의 주소가 들어있는 포인터를 역참조하면 배열의 
                            // 첫 번째 요소에 접근

printf(&quot;%d\\n&quot;, *numArr);    // 11: 배열 자체를 역참조해도 배열의 첫 번째 요소에 접근

배열의 주소가 들어있는 포인터는 인덱스를 통하여 요소에 접근할 수 있다.
printf(&quot;%d\\n&quot;, numPtr[5]);    // 66: 배열의 주소가 들어있는 포인터는 인덱스로 접근할 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;int *numPtr = numArr; 처럼 배열을 포인터에 바로 할당할 수 있다. 단, 자료형이 서로 같아야 하며 1차원 배열이라면 *가 한 개인 단일 포인터야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;array &amp;amp; pointer&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int numArr[10];                           // int형 요소 10개를 가진 배열 생성
int *numPtr = malloc(sizeof(int) * 10);   // int 10개 크기만큼 메모리 할당

numArr[0] = 10;    // 배열을 인덱스로 접근하여 값 할당
numPtr[0] = 10;    // 배열처럼 포인터를 인덱스로 접근하여 값 할당

free(numPtr);   // 메모리 해제
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dojang.io/course/view.php?id=2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dojang.io/course/view.php?id=2&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1663820511818&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;강좌: C 언어 코딩 도장&quot; data-og-description=&quot;모두 펼치기모두 접기&quot; data-og-host=&quot;dojang.io&quot; data-og-source-url=&quot;https://dojang.io/course/view.php?id=2&quot; data-og-url=&quot;https://dojang.io/course/view.php?id=2&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://dojang.io/course/view.php?id=2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://dojang.io/course/view.php?id=2&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;강좌: C 언어 코딩 도장&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;모두 펼치기모두 접기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;dojang.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>cs/c언어</category>
      <category>C언어</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/9</guid>
      <comments>https://devpluto.tistory.com/entry/c%EC%96%B8%EC%96%B4-%EB%B0%B0%EC%97%B4-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC#entry9comment</comments>
      <pubDate>Thu, 22 Sep 2022 13:30:04 +0900</pubDate>
    </item>
    <item>
      <title>Firebase에서 Github 인증 사용설정하기</title>
      <link>https://devpluto.tistory.com/entry/Firebase%EC%97%90%EC%84%9C-Github-%EC%9D%B8%EC%A6%9D-%EC%82%AC%EC%9A%A9%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Firebase 콘솔에서 승인 콜백 URL 얻기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXZtTd/btrMJQcehZ1/Sq7DaPBCsyizqYJfu455M0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXZtTd/btrMJQcehZ1/Sq7DaPBCsyizqYJfu455M0/img.png&quot; data-origin-width=&quot;1844&quot; data-origin-height=&quot;640&quot; data-filename=&quot;스크린샷 2022-05-11 오후 3.07.54.png&quot; data-is-animation=&quot;false&quot; width=&quot;580&quot; height=&quot;201&quot; style=&quot;width: 61.2781%; margin-right: 10px;&quot; data-widthpercent=&quot;62&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXZtTd/btrMJQcehZ1/Sq7DaPBCsyizqYJfu455M0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXZtTd%2FbtrMJQcehZ1%2FSq7DaPBCsyizqYJfu455M0%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;1844&quot; height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4VWSa/btrMMlbulQc/QNbZ4f82g6tuLXckwGM5N0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4VWSa/btrMMlbulQc/QNbZ4f82g6tuLXckwGM5N0/img.png&quot; width=&quot;558&quot; height=&quot;316&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;906&quot; data-filename=&quot;스크린샷 2022-05-11 오후 3.06.18.png&quot; data-is-animation=&quot;false&quot; style=&quot;width: 37.5592%;&quot; data-widthpercent=&quot;38&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4VWSa/btrMMlbulQc/QNbZ4f82g6tuLXckwGM5N0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4VWSa%2FbtrMMlbulQc%2FQNbZ4f82g6tuLXckwGM5N0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;906&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Firebase 콘솔의 인증창에서 &quot;로그인 방법 설정&quot;버튼을 누르고 로그인 제공업체 탭에서 깃허브를 선택하면 위와같이 승인 콜백 URL와 클라이언트 ID, 비밀번호를 입력하는 창이 뜨게된다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 깃허브 설정에서 OAuth App 등록하기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMHCl9/btrMMaOAjVR/c5pcSnqiTkzK6N0TnndXI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMHCl9/btrMMaOAjVR/c5pcSnqiTkzK6N0TnndXI0/img.png&quot; style=&quot;width: 54.0402%; margin-right: 10px;&quot; width=&quot;119&quot; height=&quot;325&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;298&quot; data-origin-height=&quot;814&quot; data-filename=&quot;스크린샷 2022-05-11 오후 3.15.01.png&quot; data-widthpercent=&quot;54.68&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMHCl9/btrMMaOAjVR/c5pcSnqiTkzK6N0TnndXI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMHCl9%2FbtrMMaOAjVR%2Fc5pcSnqiTkzK6N0TnndXI0%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;298&quot; height=&quot;814&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CJ3Ra/btrMJutI9JS/JopLvTRtKEkq8CjLrQElw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CJ3Ra/btrMJutI9JS/JopLvTRtKEkq8CjLrQElw0/img.png&quot; style=&quot;width: 44.797%;&quot; width=&quot;149&quot; height=&quot;491&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;1496&quot; data-filename=&quot;스크린샷 2022-05-11 오후 3.11.55.png&quot; data-widthpercent=&quot;45.32&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CJ3Ra/btrMJutI9JS/JopLvTRtKEkq8CjLrQElw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCJ3Ra%2FbtrMJutI9JS%2FJopLvTRtKEkq8CjLrQElw0%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;454&quot; height=&quot;1496&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-05-11 오후 3.18.06.png&quot; data-origin-width=&quot;1806&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M4ddf/btrMJuf9EN5/3eRdYRSjEOGCUxEnRiSjh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M4ddf/btrMJuf9EN5/3eRdYRSjEOGCUxEnRiSjh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M4ddf/btrMJuf9EN5/3eRdYRSjEOGCUxEnRiSjh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM4ddf%2FbtrMJuf9EN5%2F3eRdYRSjEOGCUxEnRiSjh0%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;748&quot; height=&quot;174&quot; data-filename=&quot;스크린샷 2022-05-11 오후 3.18.06.png&quot; data-origin-width=&quot;1806&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-05-10 오전 12.26.30.png&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NPdeH/btrMK1dfGNq/ebdbSkEVNfzURR2Hovdk21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NPdeH/btrMK1dfGNq/ebdbSkEVNfzURR2Hovdk21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NPdeH/btrMK1dfGNq/ebdbSkEVNfzURR2Hovdk21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNPdeH%2FbtrMK1dfGNq%2FebdbSkEVNfzURR2Hovdk21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;637&quot; height=&quot;510&quot; data-filename=&quot;스크린샷 2022-05-10 오전 12.26.30.png&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;깃허브로 가서 Settings &amp;gt; Developer Settings &amp;gt; OAuth Apps로 들어가면 새로운 Oauth App을 생성할 수 있다.&lt;br /&gt;생성하기에서 App이름과 홈페이지 URL, 그리고 1번 과정에서 얻은 승인 콜백 URL을 입력해준다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 클라이언트 ID, 비밀번호 등록하기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-05-10 오전 12.35.16.png&quot; data-origin-width=&quot;767&quot; data-origin-height=&quot;581&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9LjeR/btrMK2b9mDT/6vqODBHKJiZc7ZesQEFfD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9LjeR/btrMK2b9mDT/6vqODBHKJiZc7ZesQEFfD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9LjeR/btrMK2b9mDT/6vqODBHKJiZc7ZesQEFfD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9LjeR%2FbtrMK2b9mDT%2F6vqODBHKJiZc7ZesQEFfD1%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;767&quot; height=&quot;581&quot; data-filename=&quot;스크린샷 2022-05-10 오전 12.35.16.png&quot; data-origin-width=&quot;767&quot; data-origin-height=&quot;581&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-05-11 오후 3.26.58.png&quot; data-origin-width=&quot;1948&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTGSQH/btrMIH8dcoH/ooW2xf0CNOD4kP43lPfUwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTGSQH/btrMIH8dcoH/ooW2xf0CNOD4kP43lPfUwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTGSQH/btrMIH8dcoH/ooW2xf0CNOD4kP43lPfUwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTGSQH%2FbtrMIH8dcoH%2FooW2xf0CNOD4kP43lPfUwK%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;1948&quot; height=&quot;378&quot; data-filename=&quot;스크린샷 2022-05-11 오후 3.26.58.png&quot; data-origin-width=&quot;1948&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;생성된 OAuth App에 들어가면 클라이언트 ID와 비밀번호를 얻을 수 있다. 이를 다시 파이어베이스 콘솔창으로 돌아와서 입력해주면 사용 설정된다!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고링크 -&lt;span style=&quot;color: #666666;&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://firebase.google.com/docs/auth/web/github-auth?authuser=1&quot;&gt;https://firebase.google.com/docs/auth/web/github-auth?authuser=1&lt;/a&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>etc/firebase</category>
      <category>firebase</category>
      <category>github인증설정</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/8</guid>
      <comments>https://devpluto.tistory.com/entry/Firebase%EC%97%90%EC%84%9C-Github-%EC%9D%B8%EC%A6%9D-%EC%82%AC%EC%9A%A9%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0#entry8comment</comments>
      <pubDate>Thu, 22 Sep 2022 11:25:52 +0900</pubDate>
    </item>
    <item>
      <title>[모던 자바스크립트 Deep Dive] 04. 변수</title>
      <link>https://devpluto.tistory.com/entry/%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-Deep-Dive-04-%EB%B3%80%EC%88%98</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.1 변수란 무엇이고, 왜 필요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;자바스크립트 엔진이 10+20 이라는 식의 의미를 해석하면 + 연산을 수행하기 위해서 10과 20이라는 숫자 값(피연산자)을 기억한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터는 연산(CPU)과 기억(Memory)을 수행하는 부품이 나누어져 있기 때문에 피연산자 값은 메모리주소에 저장되고, CPU는 이 값을 읽어 들여 연산을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;연산 결과로 생성된 숫자 값 30도 메모리 주소에 저장되는데 이 값을 재사용하려면 주소를 통해 메모리 공간에 직접 접근해야하지만, 이는 치명적 오류를 야기하는 매우 위험한 방법이기에 자바스크립트는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;개발자의 직접적인 메모리 제어를 허용하지 않는다&lt;/b&gt;. 따라서 30이라는 값은 재사용이 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;만약 직접적인 메모리 제어를 허용하더라도 코드가 실행될 때마다 값이 저장될 메모리 주소는 변경되므로, 코드 실행 이전에는 메모리 주소의 값을 알 방법이 없으므로 직접 접근하는 방법은 올바른 방법이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍 언어는 기억하고싶은 값을 메모리에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;저장&lt;/b&gt;하고, 저장된 값을 읽어들여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;재사용&lt;/b&gt;하기 위해 &quot;변수&quot;라는 메커니즘을 제공한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;변수는 하나의 값을 저장하기 위해 확보한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;메모리 공간 자체&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는 그 메모리 공간을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;식별하기 위해 붙인 이름&lt;/b&gt;을 말한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;변수는 프로그래밍 언어의 컴파일러 또는 인터프리터에 의해 값이 저장된 메모리 공간의 주소로 치환되어 실행된다. 따라서 변수를 통해 직접적인 메모리 제어 없이 안전하게 값에 접근할 수 있다!&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변수에 값을 저장하는 것을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;할당(assignment-대입, 저장)이라 하고,&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;변수에 저장된 값을 읽어 들이는 것을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;참조(reference)&lt;/b&gt;라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 변수 이름을 이용해서 참조를 요청하면, 자바스크립트 엔진은 변수 이름과 매핑된 메모리 주소를 통해 메모리 공간에 접근해서 저장된 값을 반환한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.2 식별자&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;변수 이름을 식별자(identifier)라고도 하며, 어떤 값을 구별해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;식별할 수 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;고유한 이름&lt;/span&gt;&lt;/b&gt;을 말한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;식별자는 값이 저장되어 있는 메모리 주소와 매핑관게를 맺으며, 이&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;매핑 정보도 메모리에 저장&lt;/b&gt;되어야 한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;  식별자는 값이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;메모리 주소를 기억&lt;/b&gt;하고있으며, 식별자는 메모리 주소에 붙인 이름이라고 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.3 변수 선언&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;변수 선언은 값을 저장하기 위한 메모리 공간을 확보하고, 변수 이름과 확보된 메모리 공간의 주소를 연결해서 값을 저장하도록 준비하는 것이다. 확보된 메모리 공간은 확보가 해제되기 전까지 누구도 확보된 메모리 공간을 사용할 수 없도록 보호되므로 안전히 공간을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;변수를 사용하려면 선언이 반드시 필요하며 var, const, let 키워드를 사용한다. let과 const는 ES6에서 도입된 키워드로, var 키워드의 함수 레벨 스코프 지원 등의 단점을 보완하기 위해 등장한 키워드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4장에서는 우선 var키워드를 살펴본다. var 키워드의 변수 선언 과정을 살펴보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;var score; 의 변수 선언문은 변수 이름을 등록하고, 값을 저장할 메모리 공간을 확보한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;(선언 단계)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;변수는 선언되었지만 아직 값은 할당되지 않았다. 이때 메모리 공간은 비어있지 않고, 확보된 메모리 공간에는 자바스크립트 엔진에 의해 undefined라는 값으로 암묵적으로 할당되어 초기화된다!&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;(초기화 단계)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;일반적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;초기화&lt;/b&gt;란 변수가 선언된 이후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;최초로 값을 할당&lt;/b&gt;하는 것을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;만약 초기화 단계를 거치지 않으면 확보된 메모리 공간에는 이전에 다른 앱이 사용했던 값이 남아있을 수 있는데, 이 값을 쓰레기값(garbage value)라 한다. 이러한 쓰레기 값을 참조하는것을 방지하기 위해 var 키워드는 암묵적으로 초기화를 수행한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.4 변수 선언의 실행 지점과 변수 호이스팅&lt;/h2&gt;
&lt;pre id=&quot;code_1663813181295&quot; class=&quot;stata&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;console.log(score); // undefined

var score; // 변수 선언문&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;위 코드를 살펴보면 변수 선언문보다 변수를 참조하는 코드가 앞에 있지만, 인터프리터에 의해 한 줄씩 순차적으로 코드가 실행되는 과정에서 참조 에러가 발생하지 않고 undefined가 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 이는 변수 선언이 소스코드가 순차적으로 실행되는 시점(런타임)이 아니라 그 이전단계(소스코드의 평가과정)에서 먼저 실행되기 때문이다! 소스코드의 평과과정에서는 변수, 함수 선언문 등을 소스코드에서 찾아내 먼저 실행하고, 이 과정이 끝나면 모든 선언문을 제외하고 소스코드를 한 줄씩 순차적으로 실행한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이처럼 변수 선언문이 런타임 이전단계에서 먼저 실행되어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;코드의 선두로 끌어올려진 것 처럼 동작하는 자바스크립트 고유의 특징&lt;/b&gt;을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;변수 호이스팅&lt;/b&gt;&lt;/span&gt;이라 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;; var, let, const, function, class등의 키워드를 사용해서 선언하는 모든 식별자는 런타임 이전단계에서 실행되기 때문에 모두 호이스팅된다고 할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.5 값의 할당&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;변수에 값을 할당할 때는 할당 연산자 &quot;=&quot;를 사용한다. 할당 연산자는 우변의 값을 좌변의 변수에 할당한다.&lt;/p&gt;
&lt;pre id=&quot;code_1663813181295&quot; class=&quot;gml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var score; // 변수 선언
score = 80; // 값의 할당

var score = 80; // 변수 선언 + 값의 할당&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;위 코드와 같이 변수 선언과 값의 할당을 2개의 문으로 나누어 표현할 수도 있고, 하나의 문으로 단축하여 표현할 수도 있다.&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; &amp;nbsp;이 때 변수 선언은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;런타임 이전에 먼저 실행&lt;/b&gt;되지만, 값의 할당은 소스코드가 순차적으로 실행되는 시점인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;런타임에 실행&lt;/b&gt;된다!&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;변수에 값을 할당할 때는 이전 값 undefined(초기화된 값)가 저장되어 있던 메모리 공간을 지우고 할당값을 저장하는 것이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;새로운 메모리 공간을 확보&lt;/b&gt;하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그곳에 할당 값을 저장&lt;/b&gt;한다는 점을 주의해야 한다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.6 값의 재할당&lt;/h2&gt;
&lt;pre id=&quot;code_1663813181296&quot; class=&quot;gml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var score = 80;
score = 90; // 값의 재할당&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;위 코드와 같이 var 키워드로 선언한 변수는 값을 재할당할 수 있다. 재할당은 현재 변수에 저장된 값을 버리고 새로운 값을 저장하는 것이다. (엄밀이 말하면 변수를 선언 후 초기화하는 과정도 재할당이라 할 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;처음 변수에 값을 할당할 때와 마찬가지로 이전 값 80이 저장되어있던 메모리 공간을 지우고 90을 저장하는것이 아니라, 새로운 메모리 공간을 확보하고 그 메모리 공간에 90을 저장한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;score변수의 이전 값인 undefined와 80은 아무도 사용하지 않는 필요하지 않은 값이므로 이 값들은 가비지 콜렉터에 의해 특정 시점에 메모리에서 자동 해제된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;가비지 콜렉터는 어플리케이션이 할당한 메모리 공간을 주기적으로 검사하여 더 이상 사용되지 않는 메모리를 해제하는 기능이다. 자바스크립트는 가비지 콜렉터를 내장하고있는 매니지드 언어로서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;메모리 누수를 방지&lt;/b&gt;한다. (언매니지드 언어에는 개발자가 명시적으로 메모리를 할당하고 해제할 수 있는 c언어가 있다.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;변수는 하나의 값을 저장하기 위해 확보한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;메모리 공간 자체&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는 그 메모리 공간을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;식별하기 위해 붙인 이름&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;을 말한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;변수는 식별자라고도 하며, 식별자는 값이 아니라&amp;nbsp;&lt;b&gt;메모리 주소를 기억&lt;/b&gt;하고있고 메모리 주소에 붙인 이름이라고 할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;변수는 선언과정에서 선언단계와 초기화단계를 거친다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;변수 선언문이 런타임 이전단계에서 먼저 실행되어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;코드의 선두로 끌어올려진 것 처럼 동작하는 자바스크립트 고유의 특징&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;변수 호이스팅&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;이라 한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;span&gt;변수 선언은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;런타임 이전에 먼저 실행&lt;/b&gt;&lt;span&gt;되지만, 값의 할당은 소스코드가 순차적으로 실행되는 시점인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;런타임에 실행&lt;/b&gt;된다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;변수에 값을 (재)할당할 때는 이전 값이 저장되어 있던 메모리 공간을 지우고 할당값을 저장하는 것이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;새로운 메모리 공간을 확보&lt;/b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;그곳에 할당 값을 저장&lt;/b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666;&quot;&gt;한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발 도서 정리/모던 자바스크립트 deep dive</category>
      <category>deep dive</category>
      <category>javascript</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/7</guid>
      <comments>https://devpluto.tistory.com/entry/%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-Deep-Dive-04-%EB%B3%80%EC%88%98#entry7comment</comments>
      <pubDate>Thu, 22 Sep 2022 11:21:53 +0900</pubDate>
    </item>
    <item>
      <title>Firebase 이메일 사용자 재인증하기 (requires-recent-login 에러)</title>
      <link>https://devpluto.tistory.com/entry/Firebase-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9E%AC%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0-requires-recent-login-%EC%97%90%EB%9F%AC</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-22 오전 12.02.28.png&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uD2Zy/btrMK1jLo66/oU43GXkQH1gY7SBOD6FSDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uD2Zy/btrMK1jLo66/oU43GXkQH1gY7SBOD6FSDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uD2Zy/btrMK1jLo66/oU43GXkQH1gY7SBOD6FSDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuD2Zy%2FbtrMK1jLo66%2FoU43GXkQH1gY7SBOD6FSDK%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;952&quot; height=&quot;370&quot; data-filename=&quot;스크린샷 2022-09-22 오전 12.02.28.png&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이어베이스를 이용해서 사용자를 삭제하기 위해 deleteUser()함수를 이용하여 구현했는데, 위와같은 오류가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;a href=&quot;https://firebase.google.com/docs/auth/web/manage-users?hl=ko#re-authenticate_a_user&quot;&gt;공식 문서&lt;/a&gt;를 확인해보니 사용자 계정 삭제, 비밀번호 변경과 같이 보안에 민간한 작업을 하려면 최근에 로그인한 적이 있어야 한다고 설명되어있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-21 오후 9.54.56.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;90&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qGtZn/btrMIrRJg4x/rJy0ogGyKcSFJVZZ1alF9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qGtZn/btrMIrRJg4x/rJy0ogGyKcSFJVZZ1alF9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qGtZn/btrMIrRJg4x/rJy0ogGyKcSFJVZZ1alF9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqGtZn%2FbtrMIrRJg4x%2FrJy0ogGyKcSFJVZZ1alF9K%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;1326&quot; height=&quot;90&quot; data-filename=&quot;스크린샷 2022-09-21 오후 9.54.56.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;90&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-21 오후 9.58.30.png&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC5hXn/btrMKs9ECLv/PfhFVvtF4AuBOew2cQeii1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC5hXn/btrMKs9ECLv/PfhFVvtF4AuBOew2cQeii1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC5hXn/btrMKs9ECLv/PfhFVvtF4AuBOew2cQeii1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC5hXn%2FbtrMKs9ECLv%2FPfhFVvtF4AuBOew2cQeii1%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;1734&quot; height=&quot;1230&quot; data-filename=&quot;스크린샷 2022-09-21 오후 9.58.30.png&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 사용자 상태를 업데이트하기위해 재인증을 하는 코드를 추가해주어야하는데, 예시코드에는 credential을 받아오는 함수를 직접 작성하라 되어있고, 어떤 인자를 넣어줘야하는지 알려주지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-22 오전 12.10.46.png&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EgG1i/btrMJPc343N/P8T0gjxkpMCnrN8U2cMhF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EgG1i/btrMJPc343N/P8T0gjxkpMCnrN8U2cMhF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EgG1i/btrMJPc343N/P8T0gjxkpMCnrN8U2cMhF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEgG1i%2FbtrMJPc343N%2FP8T0gjxkpMCnrN8U2cMhF1%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;1266&quot; height=&quot;130&quot; data-filename=&quot;스크린샷 2022-09-22 오전 12.10.46.png&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 reauthenticateWithCredential을 선언한 파일로가서 받아야하는 인자의 타입이 AuthCredential임을 알아냈고, 구글에 검색해서 &lt;a href=&quot;https://firebase.google.com/docs/reference/android/com/google/firebase/auth/AuthCredential&quot;&gt;아래와 같은 결과&lt;/a&gt;를 찾았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-09-22 오전 12.14.19.png&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OfdBy/btrMK0SGRPa/aDNZFJrjlw4vMjQaKhIg21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OfdBy/btrMK0SGRPa/aDNZFJrjlw4vMjQaKhIg21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OfdBy/btrMK0SGRPa/aDNZFJrjlw4vMjQaKhIg21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOfdBy%2FbtrMK0SGRPa%2FaDNZFJrjlw4vMjQaKhIg21%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;1364&quot; height=&quot;608&quot; data-filename=&quot;스크린샷 2022-09-22 오전 12.14.19.png&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이메일을 이용하여 로그인을 구현하였기때문에 EmailAuthCrediential을 사용하였다. 만약 다른 소셜 로그인을 이용했다면 서비스에 맞는 클래스를 사용하면 될 것이다!&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import { User, EmailAuthProvider, reauthenticateWithCredential } from 'firebase/auth';

async function userRecertification(email: string, password: string) {
  const user = auth.currentUser as User;
  const authCredential = EmailAuthProvider.credential(email, password);
  
  let bool;
  await reauthenticateWithCredential(user, authCredential)
    .then(() =&amp;gt; {
      bool = true;
    })
    .catch((error) =&amp;gt; {
      console.log(error);
      bool = false;
    });
  return bool;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 구현 코드이다. 재인증 함수 컴포넌트를 분리해서 작성하였고, EmailAuthCrediential.credential()을 이용해 authCredential정보를 받아와서 공식문서에 적힌대로 사용자 재인증 기능을 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 bool을 선언하고 재인증 함수를 불러오는동안 return이 되어버려 undefined값이 반환되었기 때문에, await을 넣어주어 제대로 변수에 값을 지정한 다음 return하도록 했다.&lt;/p&gt;</description>
      <category>etc/firebase</category>
      <category>firebase</category>
      <category>requires-recent-login</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/6</guid>
      <comments>https://devpluto.tistory.com/entry/Firebase-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9E%AC%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0-requires-recent-login-%EC%97%90%EB%9F%AC#entry6comment</comments>
      <pubDate>Thu, 22 Sep 2022 00:28:58 +0900</pubDate>
    </item>
    <item>
      <title>HTTP와 HTTPS의 차이점</title>
      <link>https://devpluto.tistory.com/entry/HTTP%EC%99%80-HTTPS%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP는 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;HTTP&lt;/b&gt;는 Hyper Text Transfer Protocol의 약자로, 인터넷 상에서 데이터(Hyper Text)를 주고 받기 위해서(Transfer) 서버/클라이언트 모델을 따르는 프로토콜입니다. 주로 웹 브라우저와 웹 서버간의 커뮤니케이션을 위해서 디자인 되었으며 ??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;HTTP는 주로 TCP를 사용하고, 2020년부터 도입된 HTTP/3에서는 UDP를 사용합니다. 또한 기본 포트로 80번을 사용한다는 특징이 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;서버/클라이언트 모델: 클라이언트가 &lt;b&gt;요청을 생성&lt;/b&gt;하기 위한 연결을 연다음 &lt;b&gt;응답을 받을때 까지 대기&lt;/b&gt;하는 전통적인 방식&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;프로토콜: 컴퓨터 내부 또는 컴퓨터 사이에서 데이터의 교환 방식을 정의하는 &lt;b&gt;규칙 체계&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP의 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;HTTP 서버는 기본 80번 포트에서 서비스를 대기하고 있으며, 만약 웹 브라우저가 80번 포트를 연결하여 데이터를 요청하면 서버는 요청에 해당하는 데이터(Hyper Text)를 전송하면서 응답하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;이 과정에서 누군가가 네트워크 신호를 가로채는 경우, 데이터는 외부에 그대로 노출되어 &lt;b&gt;데이터가 도난당할 수 있다&lt;/b&gt;는 문제점이 존재합니다. 이러한 보안 취약점을 해결하기 위해 HTTPS가 등장하였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTPS!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp;HTTPS&lt;/b&gt;는 &lt;b&gt;HTTP&lt;/b&gt;에서 &lt;b&gt;S&lt;/b&gt;ecure socket layer, 즉 데이터 전송 과정에서 보안이 추가된 프로토콜입니다. 데이터 전송시 텍스트를 사용하는 대신, SSL이나 TLS 프로토콜을 통해 세션 데이터를 암호화하기 때문에 데이터의 보호를 보장합니다. 그리고 HTTP와는 다르게 기본 포트로 443번을 사용한다는 특징이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;따라서 HTTPS를 사용하면 클라이언트가&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #1b1b1b;&quot;&gt;금융 활동 이나 온라인 쇼핑을 할 때 민감한 개인 정보를 서버와 안전하게 주고 받을 수 있게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SSL? TLS? : SSL(Secure Sockets Layer), TSL(Transport Layer Security)는 &lt;b&gt;네트워크에 통신 보안을 제공&lt;/b&gt;하기 위해 설계된 &lt;b&gt;암호 규약&lt;/b&gt;입니다. 이때 TLS는 SSL이 표준화되면서 바뀐 이름입니다. 따라서 TLS안에 SSL이 있다 할 수 있습니다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 HTTPS에서 보안이 어떤 방식으로 동작할까?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-07-20 오후 3.35.49.png&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dr18UM/btrHN38hNSE/2Ho0aBLy0UP2u0CqBlnyy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dr18UM/btrHN38hNSE/2Ho0aBLy0UP2u0CqBlnyy1/img.png&quot; data-alt=&quot;브라우저에서 https를 사용하는 사이트일 경우 보안 연결이 사용되었다는 문구를 볼 수 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dr18UM/btrHN38hNSE/2Ho0aBLy0UP2u0CqBlnyy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdr18UM%2FbtrHN38hNSE%2F2Ho0aBLy0UP2u0CqBlnyy1%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;587&quot; height=&quot;216&quot; data-filename=&quot;스크린샷 2022-07-20 오후 3.35.49.png&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;브라우저에서 https를 사용하는 사이트일 경우 보안 연결이 사용되었다는 문구를 볼 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;HTTPS는 SSL 암호화 통신, 즉 공개키 암호화 방식의 알고리즘을 통해 보안을 구현합니다. 그렇다면 공개키 암호화 방식에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공캐키 암호화 방식&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Public_key_encryption.svg&quot; data-origin-width=&quot;525&quot; data-origin-height=&quot;513&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxWzqH/btrHPI5jXmr/Sd7bXDYK54kikZ2rFKQEn1/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxWzqH/btrHPI5jXmr/Sd7bXDYK54kikZ2rFKQEn1/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxWzqH/btrHPI5jXmr/Sd7bXDYK54kikZ2rFKQEn1/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxWzqH%2FbtrHPI5jXmr%2FSd7bXDYK54kikZ2rFKQEn1%2Ftfile.svg&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;525&quot; height=&quot;513&quot; data-filename=&quot;Public_key_encryption.svg&quot; data-origin-width=&quot;525&quot; data-origin-height=&quot;513&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202122;&quot;&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp;공개키 암호화 방식은 암호 방식의 한 종류로 &lt;/span&gt;암호화와 복호화에 이용하는 키가 다른 방식을 말합니다. 공개키와 암호키가 존재하며, 위 그림에서 Hello Alice!라는 문구를 공개키로 암호화했다면 오직 암호키로만 이를 복호화할 수 있습니다. 반대로 암호키로 암호화했다면 공개키로만 복호화 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202122;&quot;&gt;&amp;nbsp; &amp;nbsp;이 방식을 이용한 클라이언트와 서버간의 흐름을 간단하게 정리해보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; 클라이언트는 &lt;b&gt;공개키&lt;/b&gt;로 HTTP요청을 암호화합니다.&lt;/li&gt;
&lt;li&gt;이 요청을 받은 서버는&lt;b&gt; 암호키&lt;/b&gt;로 요청을 복호화해서 해독합니다.&lt;/li&gt;
&lt;li&gt;서버는 요청에 해당하는 데이터를 &lt;b&gt;암호키&lt;/b&gt;로 암호화하여 클라이언트에 응답합니다.&lt;/li&gt;
&lt;li&gt;클라이언트는 응답을&lt;b&gt; 공개키&lt;/b&gt;로 응답을 복호화하여 해독합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보안 말고 또 다른 차이점?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;구글 등의 검색 엔진에서는 HTTPS를 사용한 사이트에 SEO(Search Engine Optimization) 가산점을 부여합니다. 따라서 동일한 키워드를 검색하더라도 HTTPS가 적용된 안전한 사이트가 더 상위에 노출됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;HTTPS는 인터넷 상에서 데이터를 주고받기위한 프로토콜인 HTTP에 보안이 추가된 방식입니다. 보안은 공개키 암호화 방식을 사용하는 SSL 암호화 통신을 통해 구현됩니다. 따라서 둘의 차이점은 보안의 적용유무에 있다고 할 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;참고문서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP&quot;&gt;https://developer.mozilla.org/ko/docs/Web/HTTP&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/HTTPS&quot;&gt;https://ko.wikipedia.org/wiki/HTTP&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/HTTP&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ko.wikipedia.org/wiki/HTTPS&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://post.naver.com/viewer/postView.nhn?volumeNo=16561296&amp;amp;memberNo=1834&quot;&gt;https://post.naver.com/viewer/postView.nhn?volumeNo=16561296&amp;amp;memberNo=1834&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EA%B3%B5%EA%B0%9C_%ED%82%A4_%EC%95%94%ED%98%B8_%EB%B0%A9%EC%8B%9D&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ko.wikipedia.org/wiki/%EA%B3%B5%EA%B0%9C_%ED%82%A4_%EC%95%94%ED%98%B8_%EB%B0%A9%EC%8B%9D&lt;/a&gt;&lt;/p&gt;</description>
      <category>cs</category>
      <category>HTTP</category>
      <category>https</category>
      <category>차이점</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/5</guid>
      <comments>https://devpluto.tistory.com/entry/HTTP%EC%99%80-HTTPS%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90#entry5comment</comments>
      <pubDate>Thu, 21 Jul 2022 16:14:09 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트로 하는 자료구조와 알고리즘 - 10장. 검색과 정렬</title>
      <link>https://devpluto.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-%ED%95%98%EB%8A%94-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-10%EC%9E%A5-%EA%B2%80%EC%83%89%EA%B3%BC-%EC%A0%95%EB%A0%AC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;10장. 검색과 정렬&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검색 (search)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 검색은 자료 구조 내에 특정 항목을 찾는 일을 말하며, 배열이 정렬됐는지 여부에따라 두 가지 주요 기법이 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;선형 검색&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배열의 각 항목을 한 인덱스씩 순차적으로 접근하면서 동작한다.&lt;/li&gt;
&lt;li&gt;시간 복잡도 : O(n)&lt;/li&gt;
&lt;li&gt;배열의 정렬 여부와는 관계없이 동작하기때문에 좋으므로 정렬되지 않은 배열을 검색하기 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;function linearSearch(arr, n) {
    for(var i = 0; i &amp;lt; arr.length; i++){
        if (arr[i] == n) return true;
    }
    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이진 검색 (탐색)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중간 값을 확인해서 원하는 값보다 중간 값이 작은지 큰지를 확인하면서 동작한다.&lt;/li&gt;
&lt;li&gt;시간 복잡도: O(logn)&lt;/li&gt;
&lt;li&gt;이진 탐색은 빠르지만 배열이 정렬된 경우에만 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function binarySearch(arr, n) {
    var lowIdx = 0, highIdx = arr.length - 1;

    while (lowIdx &amp;lt;= highIdx) {
        var midIdx = Math.floor((highIdx + lowIdx) / 2);
        if (arr[midIdx] == n) return arr[midIdx];
        else if (n&amp;gt;arr[midIdx]) lowIdx = midIdx;
        else highIdx = midIdx;
    }
    return -1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정렬 (sort)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;거품 정렬 (bubble sort)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 간단한 알고리즘으로, 전체 배열을 순회하면서 항목이 다른 항목보다 큰 경우 두 항목을 교환한다.&lt;/li&gt;
&lt;li&gt;시간 복잡도: O(n^2) (중첩 루프)&lt;/li&gt;
&lt;li&gt;거품 정렬은 모든 가능한 짝을 비교하기 때문에 최악의 정렬이라 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657864124292&quot; class=&quot;matlab&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function bubbleSort(array) {
  for (var i = 0, arrayLength = array.length; i &amp;lt; arrayLength; i++) {
    for (var j = 0; j &amp;lt;= i; j++) {
      if (array[i] &amp;lt; array[j]) {
        swap(array, i, j);
      }
    }
  }
  return array;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책의 예시는 인접 두 항목을 교환하는 코드가 아니어서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.zerocho.com/category/Algorithm/post/57f67519799d150015511c38&quot;&gt;제로초님의 블로그&lt;/a&gt;글을 참고하여 공부하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864124293&quot; class=&quot;php&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function bubbleSort(array) {
  for (var i = 0, arrayLength = array.length; i &amp;lt; arrayLength - 1; i++) {
    for (var j = 0; j &amp;lt; arrayLength - 1 - i; j++) {
      if (array[j] &amp;gt; array[j+1]) { // 인접항목 교환
        swap(array, j, j+1);
      }
    }
  }
  return array;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;선택 정렬 (selection sort)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 작은 항목을 찾아서 해당 항목을 배열의 현 위치에 삽입한다.&lt;/li&gt;
&lt;li&gt;시간 복잡도 : O(n^2) (중첩 루프)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657864124296&quot; class=&quot;lua&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function selectionSort(items) {
  var len = items.length, min;

  for (var i = 0; i &amp;lt; len; i++) {
    min = i;
    for (j = i+1; j &amp;lt; len; j++) { 
      if (items[j] &amp;lt; items[min]) {
        min = j; // 최솟값의 위치를 min값에 할당
      }
    }
    if (i != min) { // 현재 위치가 최솟값의 위치가 아니면 교환
      swap(items, i, min);
    }
  }
  return items;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;삽입 정렬 (insertion sort)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배열을 순차적으로 검색하면서 정렬되지 않은 항목들을 배열의 왼쪽의 정렬된 부분으로 이동시킨다.&lt;/li&gt;
&lt;li&gt;시간 복잡도 : O(n^2) (중첩 루프)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657864124296&quot; class=&quot;sqf&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function insertionSort(items) {
  var len = items.length, value, i, j;
  
  for (i=0; i &amp;lt; len; i++) {
    value = items[i]; // 현재 값 덮어써질수 있으므로 저장함

    for (j = i-1; j &amp;gt; -1 &amp;amp;&amp;amp; items[j] &amp;gt; value; j--) { // value가 검사값보다 작다면 검사값 위치 +1
      items[j+1] = items[j];
    } // j는 검사값이 크지 않을 때 까지 감소한다.
    items[j+1] = value; // 감소한 j값 +1이 value의 위치(가장 작은값)
    console.log(items);
  }
  return items;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;빠른 정렬 (quick sort)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기준점을 획득한 다음 해당 기준점을 기준으로 왼쪽은 작은 항목, 오른쪽은 큰 항목으로 배열을 나눈다.&lt;/li&gt;
&lt;li&gt;시간복잡도: 평균 - O(nlog_2(n)), 최악 - O(n^2)&lt;/li&gt;
&lt;li&gt;기준점을 항상 잘못 선택하는 경우는 시간 복잡도가 O(n^2)이 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657864124297&quot; class=&quot;swift&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function quickSort(items) {
  return quickSortHelper(items, 0, items.length - 1);
}
function quickSortHelper(items, left, right) {
  var index;
  if (items.length &amp;gt; 1) {
    index = partition(items, left, right);

    if (left &amp;lt; index - 1) { // 기준점 왼쪽 빠른정렬
      quickSortHelper(items, left, index - 1);
    }
    if (index &amp;lt; right) { // 기준점 오른쪽 빠른정렬
      quickSortHelper(items, index, right);
    }
  }
  return items;
}
function partition(array, left, right) { // 기준점 중앙으로
  var pivot = array[Math.floor((right + left) / 2)];
  while (left &amp;lt;= right) {
    while (pivot &amp;gt; array[left]) {
      left++;
    }
    while (pivot &amp;lt; array[right]) {
      right--;
    }
    if (left &amp;lt;= right) {
      var temp = array[left];
      array[left] = array[right];
      array[right] = temp;
      left++;
      right--;
    }
  }
  return left;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;빠른 선택 (quick select)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정렬되지 않은 목록에서 k번째로 작은 항목을 찾는 선택 알고리즘이다.&lt;/li&gt;
&lt;li&gt;빠른 정렬과 같은 접근법을 사용하지만, 기준점의 양쪽 모두를 재귀적으로 수행하는 대신 한쪽만을 재귀적으로 수행한다는 차이점이있다.&lt;/li&gt;
&lt;li&gt;이로인해 복잡도는 O(nlog_2(n))에서 O(n)으로 낮아진다.&lt;/li&gt;
&lt;li&gt;시간복잡도: O(n)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657864124299&quot; class=&quot;reasonml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function quickSelectInPlace(A, l, h, k) {
  var p = partition(A, l, h);
  if (p == (k-1)) {
    return A[p]; // 일치하면 값 return
  } else if (p &amp;gt; (k-1)) {
    return quickSelectInPlace(A, l, p-1, k); // pivot 왼쪽배열 재귀
  } else {
    return quickSelectInPlace(A, p+1, h, k); // pivot 오른쪽배열 재귀
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;병합 정렬 (merge sort)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 하위 배열에 하나의 항목이 존재할 때까지 배열을 하위 배열로 나눈 다음, 각 하위 배열을 정렬된 순서로 병합한다.&lt;/li&gt;
&lt;li&gt;시간복잡도: O(nlog_2(n)), 공간복잡도: O(n)&lt;/li&gt;
&lt;li&gt;병합 정렬은 O(n)의 공간을 사용한다는 단점이 있다.&lt;/li&gt;
&lt;li&gt;왼쪽, 오른쪽의 두 배열을 가지고 하나의 결과 배열로 병합한다. (아래의 merge 함수)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657864124300&quot; class=&quot;maxima&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function merge(leftA, rightA) { // 병합 함수
  var results = [], leftIndex = 0, rightIndex = 0;

  while (leftIndex &amp;lt; leftA.length &amp;amp;&amp;amp; rightIndex &amp;lt; rightA.length) {
    if (leftA[leftIndex] &amp;lt; rightA[rightIndex]) {
      results.push(leftA[leftIndex++]);
    } else {
      results.push(rightA[rightIndex++]);
    }
  }
  var leftRemains = leftA.slice(leftIndex),
  rightRemains = rightA.slice(rightIndex);

  return results.concat(leftRemains.concat(rightRemains));
}

function mergeSort(array) {
  if (array.length&amp;lt;2){
    return array;
  }
  var midpoint = Math.floor((array.length)/2),
  leftArray = array.slice(0, midpoint),
  rightArray = array.slice(midpoint);

  return merge(mergeSort(leftArray), mergeSort(rightArray)); // left와 right로 계속 분할
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;계수 정렬 (count sort)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값을 비교하지 않기 때문에 O(k+n)시간 안에 수행된다.&lt;/li&gt;
&lt;li&gt;숫자에 대해서만 동작하며, 특정 범위가 주어져야 한다.&lt;/li&gt;
&lt;li&gt;배열의 각 항목의 등장 횟수를 세어 해당 등장 횟수를 사용해서 새로운 배열을 생성한다.&lt;/li&gt;
&lt;li&gt;주로 제한된 범위의 정수를 정렬할 때 가장 빠른 정렬 방법이기 때문에 계수 정렬을 사용한다.&lt;/li&gt;
&lt;li&gt;시간복잡도: O(k+n), 공간복잡도: O(k)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657864124302&quot; class=&quot;maxima&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function countSort(arr) {
  var hash = {}, countArr = [];
  for (var i=0; i&amp;lt;arr.length; i++) {
    if (!hash[arr[i]]) {
      hash[arr[i]] = 1;
    } else {
      hash[arr[i]]++;
    }
  }
  for (var key in hash) { // 항목이 몇개가 되든 해당 항목을 배열에 추가한다
    for (var i=0; i&amp;lt;hash[key]; i++) {
      countArr.push(parseInt(key)); // key 를 hash[key] 값만큼 arr에 push한다.
    }
  }
  return countArr;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;자바스크립트 내장 정렬&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바스크립트는 배열 객체에 사용 가능한 내장 메소드 sort()가 존재하며, 이는 항목들을 오름차순으로 정리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657864124303&quot; class=&quot;yaml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = [12, 3, 4, 2, 1, 34, 23];
arr.sort(); // [1, 12, 2, 23, 3, 34, 4]&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;요약&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 61.9767%; height: 271px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;알고리즘&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;시간 복잡도&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;공간 복잡도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;빠른 정렬(quick sort)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(nlog_2(n))&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(nlog_2(n))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;병합 정렬(merge sort)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(nlog_2(n))&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(nlog_2(n))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;거품 정렬(bubble sort)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;삽입 정렬(insertion sort)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;선택 정렬(select sort)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(n^2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;계수 정렬(count sort)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(k+n)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;O(k)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 도서 정리/자바스크립트로 하는 자료구조와 알고리즘</category>
      <category>이진탐색</category>
      <category>자료구조</category>
      <category>정렬</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/4</guid>
      <comments>https://devpluto.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-%ED%95%98%EB%8A%94-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-10%EC%9E%A5-%EA%B2%80%EC%83%89%EA%B3%BC-%EC%A0%95%EB%A0%AC#entry4comment</comments>
      <pubDate>Fri, 15 Jul 2022 14:49:25 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트로 하는 자료구조와 알고리즘 - 7장</title>
      <link>https://devpluto.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-%ED%95%98%EB%8A%94-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-7%EC%9E%A5</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;7장. 자바스크립트 메모리 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 누수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트는 타언어와는 달리 프로그래머가 직접 메모리를 수동으로 할당하고 해제하지 않고&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용하지않는 변수, 즉 메모리를 삭제해주는&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;가비지 컬렉터가 있기 때문에 매니지드언어라고 부른다. 하지만 이러한 기능에도 메모리가 올바른 방식으로 해제되지 않아 메모리 누수가 발생할 수 있기 때문에 이를 피하기위한 여러 방법이 존재한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;객체에 대한 참조&lt;/h4&gt;
&lt;pre id=&quot;code_1657864077918&quot; class=&quot;actionscript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var foo = {
	bar1: memory(), // 5kb
    bar2: memory(), // 5kb
}

function clickEvent() {
	alert(foo.bar1[0]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체에 대한 참조가 있다면 해당 참조는 메모리에 존재하는 것이다. foo객체가 bar1만을 참조하더라도 foo객체 전체를 clickEvent() 함수의 범위에 로딩해야 하기 때문에 실제로는 5KB가 아니라, 10KB의 메모리를 사용한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DOM 메모리 누수&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM 항목을 가리키는 변수가 이벤트 콜백 외부에 선언된 경우 해당 DOM 항목을 제거하더라도 해당 항목은 여전히 메모리에 남게된다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864077920&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var one = document.getElementById(&quot;one&quot;);
var two = document.getElementById(&quot;two&quot;);

one.addEventListner('click', function() {
    two.remove();
    console.log(two); // 삭제 후에도 html을 출력한다(참조 유지)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 웹페이지의 two항목은 사라지지만, 해당 DOM이 HTML에서 사라지더라도 해당 DOM이 이벤트 콜백에서 사용되었다면 참조는 남는다. 이렇게 two 항목이 더 이상 사용중이 아닌경우를 '메모리 누수'라 부르며 미리 누수를 피해야한다. 따라서 아래와 같은 코드를 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864077923&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var one = document.getElementById(&quot;one&quot;);

one.addEventListner('click', function() {
    var two = document.getElementById(&quot;two&quot;); // DOM항목을 이벤트 콜백 내부에서 사용한다.
    two.remove();
}
one.removeEventListner('click', function () {
    one.addEventListener('click', callBackExample);
}); // 또는 클릭 핸들러를 사용한 뒤 등록 해지하는 방법도 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;window 전역 객체&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체가 window 전역 객체에 포함되는 경우 해당 객체는 메모리에 존재하는 것이고, window의 속성으로 선언된 추가적인 객체는 모두 제거할 수 없다. window는 브라우저가 실행하는데 필요한 객체이기 때문이다. 따라서 가능하면 전역변수는 사용하지 않는 것이 좋다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;객체 참조 제한하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체에 대한 모든 참조가 제거되면 해당 객체는 제거되기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;함수&lt;/b&gt;에는 객체의 전체범위가 아닌&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;필요한 범위, 속성만 전달&lt;/b&gt;해야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864077924&quot; class=&quot;stata&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var test = { prop1: 'test' };

function printProp(test) {
    console.log(test.prop1); // x
    console.log(test) // o
}

printProp(test); // x
printProp(test.prop1); // o&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;delete 연산자&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원치 않는 객체 속성을 제거하기 위해 delete 연산자를 사용할 수 있다. 이때 delete 연산자는 객체에서만 동작한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864077925&quot; class=&quot;stata&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var test = { prop1: 'test' };

console.log(test.prop1); // test

delete test.prop1;

console.log(test.prop1); // undefined&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 도서 정리/자바스크립트로 하는 자료구조와 알고리즘</category>
      <category>자료구조</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/3</guid>
      <comments>https://devpluto.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-%ED%95%98%EB%8A%94-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-7%EC%9E%A5#entry3comment</comments>
      <pubDate>Fri, 15 Jul 2022 14:48:24 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트로 하는 자료구조와 알고리즘 - 5~6장</title>
      <link>https://devpluto.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-%ED%95%98%EB%8A%94-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-56%EC%9E%A5</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;5장. 자바스크립트 배열&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배열&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;삽입&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 항목을 자료 구조 내에 추가하는것. .push(element)메소드를 사용해 배열 삽입을 구현할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014804&quot; class=&quot;yaml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = [1, 2, 3, 4]
arr.push(5); // arr = [1, 2, 3, 4, 5]
arr.push(8); // arr = [1, 2, 3, 4, 5, 8]

// 시간복잡도: O(1)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.pop()메소드를 사용해 배열 삭제를 구현할 수 있다. 해당 메소드는 제거된 항목을 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014805&quot; class=&quot;angelscript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = [1, 2, 3, 4]
arr.pop(); // return 4, arr = [1, 2, 3]

// 시간복잡도: O(1)

arr.shift(); // return 1, arr = [2, 3]
// shift()는 첫번째 항목을 제거한 다음 해당 항목을 반환한다.&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;접근&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 인덱스의 배열에 접근하는 연산은 인덱스를 이용해서(0부터 시작)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;메모리의 주소&lt;/b&gt;로부터&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;직접 값을 얻기 때문&lt;/b&gt;에 시간 복잡도가 O(1)이다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014807&quot; class=&quot;angelscript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = [1, 2, 3, 4]
arr[1] // return 2
arr[3] // return 4&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반복&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복은 어떤 자료구조 내에 담긴 항목들을 하나씩 접근하는 과정이며 모두 O(n)의 시간복잡도를 가진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;for (변수; 조건; 수정)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for은 변수 i를 초기화하고 코드 실행 전 조건이 거짓인지 확인한다. 만약 거짓이라면 변수를 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하며 동작한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; for&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657864014807&quot; class=&quot;matlab&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (var i=0, len=arr.length; i&amp;lt;len; i++) {
	console.log(arr[i])
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;while&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657864014808&quot; class=&quot;actionscript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var counter = 0;
while(counter&amp;lt;arr.length){
	// ~
    counter++
} //while은 counter가 루프의 바깥에서 초기화되어야 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무한 루프&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657864014808&quot; class=&quot;kotlin&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while(true){
	if(breakCondition) {
   		break;
	}
}

for(;;) {
	if(breakCondition) {
   		break;
	}
} //조건을 설정하지 않음으로써 무한루프를 구현할 수 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;for (in)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 하나씩 호출하는 배열을 반복 접근하기위한 방법으로, in 앞에 지정된 변수는 배열의 인덱스이다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014808&quot; class=&quot;delphi&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = ['a', 'b', 'c', 'd']

for (var index in arr) {
	console.log(index)
} // 0, 1, 2, 3&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;for (of)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;of 앞에 지정된 변수는 배열의 항목(값)이다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014809&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = ['a', 'b', 'c', 'd']

for (var element of arr) {
	console.log(element)
} // a, b, c, d&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;forEach()&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forEach는 반복 중 바깥으로 빠져나오거나 배열 내의 특정 항목을 건너뛸 수 없다. 따라서 반복 접근한다는 의미에 있어 조금 더 명시적이라 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014809&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = ['a', 'b', 'c', 'd']

arr.forEach( function (element, index) {
	console.log(element, index)
} // a 0, b 1, c 2, d 3&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;도움 함수&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;.slice(begin, end)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 배열을 수정하지 않고 해당 배열의 일부를 반환하며, 시작과 끝 인덱스를 매개변수로 받는다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014810&quot; class=&quot;angelscript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = [1, 2, 3, 4];
arr.slice(0, 2) // return [1, 2]
arr.slice(1) // return [2, 3, 4]
arr.slice() // return [1, 2, 3, 4]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.slice()는 배열 전체의 복사본을 반환하기 때문에 배열을 복사하는데 유용하다. 배열에 신규변수를 할당하면 기존변수를 '참조'하기때문에&amp;nbsp; 신규변수를 변경하면 기존변수에도 변경점이 의도치않게 적용될 수 있다. 새로운 배열을 만드려면 .from()이나 .slice()등을 이용해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014810&quot; class=&quot;angelscript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr1 = [1, 2, 3], arr2 = arr1;

arr2[0] = 5;
console.log(arr1) // [5, 2, 3]
console.log(arr2) // [5, 2, 3]
// arr2는 arr1을 참조한다.

var arr3 = [1, 2, 3];
var arr4 = Array.from(arr3);
var arr5 = arr3.slice();
// arr4와 arr5는 새로운 배열로 생성된다.&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;.splice(begin, size, element1, element2, ...)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.splice()는 기존 항목을 제거하거나 신규 항목을 추가함으로써 배열의 내용을 반환하고 변경한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; .splice(시작 인덱스, 제거할 항목의 크기, 추가할 신규항목들...)&lt;/p&gt;
&lt;pre id=&quot;code_1657864014811&quot; class=&quot;angelscript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = [1,2,3,4];
arr.splice(); // return [], arr = [1,2,3,4]
arr.splice(1, 2); // return [2, 3], arr = [1, 4]
arr.splice(1, 2, 7, 7, 7) // return [2, 3], arr = [1,7,7,7,4]
arr.splice(1, 2, { 'ss': 1 }) // return [2, 3], arr = [1, { 'ss': 1 }, 4]&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;.concat()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신규 항목을 배열의 맨 뒤에 추가한 다음 해당 배열을 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014812&quot; class=&quot;angelscript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = [1,2,3,4];
arr.concat() // return [1,2,3,4], arr = [1,2,3,4]
arr.concat([6, 6, 6]) // return [1,2,3,4,6,6,6], arr = [1,2,3,4]
// concat은 기존 배열은 바꾸지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;.length 속성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열의 크기로 반환한다. 배열을 더 작은 크기로 변경하면 배열에서 항목들이 제거된다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014813&quot; class=&quot;angelscript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = [1,2,3,4];
console.log(arr.length) // 4
arr.length = 3; // arr = [1,2,3]&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전개(spread) 연산자&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점 3개(...)로 표현하며 인자를 확장하는데 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014814&quot; class=&quot;reasonml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = [1,2,3,4];
function addFourNums(a, b, c, d) {
	return a+b+c+d;
}
console.log(addFourNums(...arr)); // 10&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Math.max와 Math.min&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 함수 모두 무한개의 매개변수를 받을수 있으며 spread연산자를 사용하면 배열의 최댓값과 최솟값을 찾을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014814&quot; class=&quot;angelscript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var arr = [3, 2, -123, 1234, 12];
Math.max(...arr); // 1234
Math.min(...arr); // -123&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6장. 자바스크립트 객체&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자바스크립트 객체 속성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서는 객체 상수 {} 또는 new Object(); 구문을 통해 객체를 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성을 추가하거나 접근하기 위해서는 objcet.propertyName 또는 object['propertyName']을 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014815&quot; class=&quot;maxima&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var jsObj = {};
var arr = [1,2,3,4];
jsObj.array = arr;
console.log(jsObj); // {array: [1,2,3,4]}

jsObj['title'] = 'Algorithms';
console.log(jsObj); // {array: [1,2,3,4], title: 'Algorithms'}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로토타입 활용 상속&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서는 함수가 클래스의 자바스크립트 Object속성으로 추가되어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014815&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function ExampleClass() {
	this.name = &quot;js&quot;;
    this.sayName = function() {
		console.log(this.name);
}

var example1 = new ExampleClass();
example1.sayName(); // &quot;js&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 class는 생성자에서 sayName함수를 동적으로 추가하는데, 이러한 패턴을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;프로토타입 활용 상속&lt;/b&gt;이라고 한다. 이 상속은 자바스크립트에서 유일한 상속 방법이며 클래스에 함수를 추가하기 위해서는 .prototype 속성을 사용한 후 함수의 이름을 지정하기만 하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1657864014816&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function ExampleClass() {
	this.arr = [1,2,3];
    this.name = &quot;js&quot;;
}

var example1 = new ExampleClass();

ExampleClass.prototype.sayName = function() {
	console.log(this.name);
}
example1.sayName(); // &quot;js&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 함수를 클래스에 동적으로 추가하는 방식이 자바스크립트가 프로토타입 활용 상속을 구현하는 방식이다.&lt;/p&gt;</description>
      <category>개발 도서 정리/자바스크립트로 하는 자료구조와 알고리즘</category>
      <category>자료구조</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/2</guid>
      <comments>https://devpluto.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-%ED%95%98%EB%8A%94-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-56%EC%9E%A5#entry2comment</comments>
      <pubDate>Fri, 15 Jul 2022 14:47:26 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트로 하는 자료구조와 알고리즘 - 1~4장</title>
      <link>https://devpluto.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-%ED%95%98%EB%8A%94-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-14%EC%9E%A5</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;배세민, ⌜자바스크립트로 하는 자료구조와 알고리즘⌟, 에이콘, 2019&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 요약 및 배운점 정리&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1장. 빅 오 표기법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빅오 표기법이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빅오 표기법은 알고리즘의 최악의 경우 복잡도를 측정하는 방법이다. 빅오 표기법에서 n은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;입력의 개수&lt;/b&gt;를 나타내며, 알고리즘 구현시 해당 알고리즘이 얼마나 효율적인지를 나타낼 수 있는 방법이기에 중요하다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빅오 표기법은 O()로 나타낼 수 있는데 O(1)은 상수시간, 즉 입력 공간에 대해 변하지 않음을 나타내고 O(n)은 선형시간으로 최악의 경우에 n번의 연산을 수행해야하는 알고리즘이 이에 해당한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빅오 표기법의 규칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘의 시간 복잡도를 f(n)이라 표현한다. f(n)을 계산함으로써 알고리즘의 효율성을 이해할 수 있지만 계산이 어려울 수 있기 때문에 이에 도움이 되는 기본적인 여러가지 규칙이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계수 법칙 - &quot;상수를 제거하라&quot;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;상수 k &amp;gt; 0인 경우 f(n)이 O(g(n))이면 kf(n)은 O(g(n))이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;n과 관련되지 않은 계수는 제거하는 규칙으로, 5f(n)과 f(n)은 모두 동일한 O(f(n))이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;-&amp;gt; f(n) = 5n, f(n) = n + 1인 경우는 모두 O(n)의 빅오표기법이다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;합의 법칙 - &quot;빅오를 더하라&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 f(n)=n과 f(n)=4n이 각각 실행된다면 합의 법칙에 의해 f(n) = 5n이다. 이때 계수법칙에 의해 최종적 결과는 O(n)=n이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;곱의 법칙 - &quot;빅오를 곱하라&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 루프에 의해 5n번 실행되고 내부 루프가 외부 루프에의해 n번 실행된다면 f(n) = 5n x n이다. 이때 계수법칙에 의해 O(n) = n^이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다항 법칙 - &quot;빅오의 k승&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;f(n)이 k차 다항식이면 f(n)은 O(n^k)이다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1657863660092&quot; class=&quot;excel&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
for (var i=0; i&amp;lt;n*n; i++) {
	conut+=1;
}
// count+=1;이 n*n번 실행되기 떄문에 f(n)=n^2이다.&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2장. 자바스크립트의 독특한 특징&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자바스크립트 범위&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범위(scope)는 자바스크립트 변수에 대한 접근 권한을 정의하는 것이다. 자바스크립트에서 변수는 전역 범위 또는 지역 범위에 속할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전역 범위 - 연산자 없이 변수를 선언하게되면 전역변수를 생성함&lt;/li&gt;
&lt;li&gt;함수 범위 - var&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 변수 호이스팅 (variable hoisting) : var를 사용하여 변수를 선언하면 어디에서 선언하든&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;변수 선언&lt;/b&gt;이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;함수의 맨 앞으로 이동&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657863698952&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function scope(print){
	if(print){
    	var insideIf = '12';
    }
    console.log(insideIf);
}
scope(true) // 12출력, 오류발생x&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1657863703824&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function scope(print){
	var insideIf
    
	if(print){
    	insideIf = '12';
    }
    console.log(insideIf);
}
scope(true)
//위의 함수와 동일하다. var은 함수범위로 정의되며 호이스팅이 일어난다.&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;블록 범위 - let, const : 변수가 선언된 { } 내에서만 유효함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657863709446&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function scope(print){
	if(print){
    	let insideIf = '12';
    }
    console.log(insideIf);
}
scope(true) // 12 x, ''를 출력함&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;등가와 형&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변수형&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;boolean, number, string, undefined, object, function, symbol&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 선언만 되고 값이 할당되지 않은 변수에는 undefined가 할당된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;true / false 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if 내의 매개변수는 대개의 다른 언어와는 달리 꼭 boolean형이 아니어도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- false로 평가되는 경우 : false, 0, '', &quot;&quot;, NaN, undefined, null&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- true로 평가되는 경우 : true, 0이 아닌 다른 숫자, 비어있지 않은 문자열, 비어있지 않은 객체&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;=== vs ==&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트는 변수 선언 시 변수에 형이 할당되지 않으며, 코드가 실행될 때 해당 변수의 형이 해석된다. 따라서 ===는 ==에 비해 등가를 조금 더 엄격히 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;==: 값만 같은지 여부를 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;===: 형과 값 모두 같은지 여부를 확인&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657863732303&quot; class=&quot;ceylon&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var object1 = {};
var object2 = {};

console.log(object1 == object2); //false
console.log(object1 === object2); //false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 동일한 속성과 값을 가져도 두 변수의 메모리상 주소는 다르기 때문에 두 객체는 동일하지 않다.&lt;/p&gt;
&lt;pre id=&quot;code_1657863747796&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var function1 = function(){console.log(2)};
var function2 = function(){console.log(2)};

console.log(function1 == function2); //false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 동일한 연산을 수행해도 두 함수의 메모리상 주소는 다르기 때문에 두 함수는 동일하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #bdc1c6;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;rarr;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #bdc1c6;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;==, ===연산자는 숫자, 문자열, 불리언과 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;비객체형&lt;/b&gt;에만 사용가능하다! (객체와 함수에서는 사용x)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3장. 자바스크립트 숫자&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;숫자 체계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트는 숫자에 대해 64비트 부동소수점 표현을 사용한다. 63번째 비트는 부호를, 63~52번째 비트는 지수 값 e를, 나머지 52비트는 분수 값을 나타낸다. 이 64비트값은 어려운 공식에 의해 계산되는데, 이러한 십진분수로 인하여 자바스크립트에서 부동소수점 체계가 반올림 오류를 일으킬 수 있다. 이러한 이유 때문에 0.1과 0.2는 정확히 표현할 수 없다. 따라서 0.1 + 0.2 === 0.3의 결과는 false이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자바스크립트 숫자 객체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같은 숫자 체계의 문제를 해결하는데 도움이 되는 Number객체의 내장된 속성이 있다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정수 반올림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트는 정수를 정수형으로 명시적으로 선언하지 않고 모든 숫자를 표현할 때 부동소수점을 사용하기 때문에 정수 나눗셈을 하면 부동소숫점의 결과가 나온다. (5/4 = 1.25) 따라서 정수 나눗셈을 하기위해서는 아래와 같은 함수를 이용해야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657863763474&quot; class=&quot;reasonml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Math.floor()
Math.round()
Math.ceil()&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Number.EPSILON&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 표현 가능한 숫자 사이의 가장 작은 간격을 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657863774751&quot; class=&quot;reasonml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function numberEquals(x, y) {
	return Math.abs(x - y) &amp;lt; Number.EPSILON;
}
numberEquals(0.1 + 0.2, 0.3) // true&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최대치&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657863786369&quot; class=&quot;yaml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2; // true

Number.MAX_VALUE + 1 === Number.VALUE + 2; // true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Number.MAX_SAFE_INTEGER: 가장 큰 정수를 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Number.VALUE: 가장 큰 부동소수점 수를 반환&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최소치&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657863797012&quot; class=&quot;yaml&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Number.MIN_SAFE_INTEGER - 1 === Number.MIN_SAFE_INTEGER -2; // true

Number.MIN_VALUE -1 == -1; // true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Number.MIN_SAFE_INTEGER: 가장 작은 정수를 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Number.MIN_VALUE: 가능한 가장 작은 부동소수점 수를 반환 -&amp;gt; 음수가 아님! (Number.MIN_SAFE_INTEGER &amp;lt; Number.MIN_VALUE)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무한&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657863807728&quot; class=&quot;arcade&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Infinity &amp;gt; Number.MAX_SAFE_INTEGER; // true
-Infinity &amp;lt; -Number.MAX_SAFE_INTEGER; // true
-Infinity - 32323323 == -Infinity - 1; //true: -Infinity보다 작아질 수 있는것은 없음&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크기 순서&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1657863818694&quot; class=&quot;arcade&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-Infinity &amp;lt; Number.MIN_SAFE_INTEGER &amp;lt; Number.MIN_VALUE &amp;lt; 0 &amp;lt; Number.MAX_SAFE_INTEGER &amp;lt; Number.MAX_VALUE &amp;lt; Infinity&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무작위 수 생성기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에는 숫자를 생성하기 위핸 내장 함수인 Math.random()이 있다. 이 함수는 0과 1 사이의 무작위 부동 소수점을 반환한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4장. 자바스크립트 문자열&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자바스크립트 문자열 기본&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문자열 접근&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자에 접근하기 위해 .charAt(index)을 사용할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1657863838537&quot; class=&quot;scheme&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'dog'.charAt(1); // &quot;o&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 접근을 위해 .subString(startIndex, endIndex)을 사용할 수 있다. subString은 지정된 인덱스 사이의 문자들을 반환한다. endIndex를 전달하지 않으면 지정된 시작위치부터 끝까지 모든 문자를 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657863846396&quot; class=&quot;awk&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'YouTube'.substring(1,2) // 'o'
'YouTube'.substring(3,6) // 'Tub'
'YouTube'.substring(2) // 'uTube'&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문자열 비교&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트에서는 &amp;lt; 와 &amp;gt;를 이용하여 쉽게 문자열을 비교할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1657863855074&quot; class=&quot;1c&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;console.log('a' &amp;lt; 'b') // true
console.log('abb' &amp;lt; 'b') // true
console.log('add' &amp;lt; 'ab') // false&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문자열 검색&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 내에 특정 문자열을 찾기위해 .indexOf(searchValue[, fromIndex])를 사용할 수 있다. 일치하는 문자열을 찾으면 위치를 반환하고, 찾지 못한다면 -1을 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657863862633&quot; class=&quot;1c&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'Red Dragon'.indexOf('Red') // 0
'Red Dragon'.indexOf('Dragon') // 4
'Red Dragon'.indexOf('Blue') // -1
'Red Dragon'.indexOf('Dragon', 2) // 4
'Red Dragon'.indexOf('Dragon', 6) // -1
'Red Dragon'.indexOf('', 9) // 9&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;startWith는 문자열이 특정 입력으로 시작하면 true를 반환하고, endsWith는 끝나면 true를 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657863870193&quot; class=&quot;mel&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'Red Dragon'.startsWith('Dragon') //false
'Red Dragon'.endsWith('Dragon') //true&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문자열 분해&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열을 여러 부분으로 나누기 위해 .split(separator)를 사용할 수 있다. separator를 입력받으면 부분 문자열 배열(Arr)를 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1657863881222&quot; class=&quot;awk&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'chicken,cow,pig'.split(&quot;,&quot;); // [&quot;chicken&quot;, &quot;cow&quot;, &quot;pig&quot;]
'chicken'.split(&quot;&quot;); // [&quot;c&quot;, &quot;h&quot;, &quot;i&quot;, &quot;c&quot;, &quot;k&quot;, &quot;e&quot;, &quot;n&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문자열 바꾸기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.replace(string, replaceString) 문자열 변수 내에 특정 문자열을 다른 문자열로 대체할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1657863891051&quot; class=&quot;awk&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;Red Dragon&quot;.replace(&quot;Red&quot;, &quot;Blue&quot;); // &quot;Blue Dragon&quot;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발 도서 정리/자바스크립트로 하는 자료구조와 알고리즘</category>
      <category>자료구조</category>
      <author>pIutos</author>
      <guid isPermaLink="true">https://devpluto.tistory.com/1</guid>
      <comments>https://devpluto.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-%ED%95%98%EB%8A%94-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-14%EC%9E%A5#entry1comment</comments>
      <pubDate>Fri, 15 Jul 2022 14:46:20 +0900</pubDate>
    </item>
  </channel>
</rss>