<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Stella</title>
        <link>https://velog.io/</link>
        <description>공부 기록</description>
        <lastBuildDate>Sat, 11 Apr 2026 14:12:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Stella</title>
            <url>https://velog.velcdn.com/images/seonguul_2/profile/5a9bb513-6214-4a19-b9c9-ef43a11881cf/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Stella. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/seonguul_2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[HTTP Section9 - HTTP 헤더2 캐시와 조건부 요청]]></title>
            <link>https://velog.io/@seonguul_2/HTTP-%ED%97%A4%EB%8D%942-%EC%BA%90%EC%8B%9C%EC%99%80-%EC%A1%B0%EA%B1%B4%EB%B6%80-%EC%9A%94%EC%B2%AD</link>
            <guid>https://velog.io/@seonguul_2/HTTP-%ED%97%A4%EB%8D%942-%EC%BA%90%EC%8B%9C%EC%99%80-%EC%A1%B0%EA%B1%B4%EB%B6%80-%EC%9A%94%EC%B2%AD</guid>
            <pubDate>Sat, 11 Apr 2026 14:12:24 GMT</pubDate>
            <description><![CDATA[<h3 id="캐시가-없을-때">캐시가 없을 때</h3>
<p>첫 번째 요청
GET /star.jpg 요청
1.1M 전송 : HTTP 헤더 : 0.1M, HTTP 바디 : 1.0M </p>
<p>데이터가 변경되지 않아도 계속 네트워크를 통해서 데이터를 다운로드 받아야 한다.
인터넷 네트워크는 매우 느리고 비싸다.
브라우저 로딩 속도가 느리다.
느린 사용자 경험</p>
<p>cache-control : max-age=60초 동안 유효하다.
응답 결과를 캐시에 저장한다. (60초 유효)</p>
<h3 id="캐시-적용">캐시 적용</h3>
<p>캐시 덕분에 캐시 가능 시간동안 네트워크를 사용하지 않아도 된다.
비싼 네트워크 사용량을 줄일 수 있다.
브라우저 로딩 속도가 매우 빠르다.
빠른 사용자 경험</p>
<h3 id="캐시-시간-초과될-때">캐시 시간 초과될 때</h3>
<p>다시 요청을 해야한다. 응답 결과를 다시 캐시에 저장 = 60초 유효
캐시 유효 시간이 초과하면, 서버를 통해 데이터를 다시 조회하고, 캐시를 갱신한다.
이때 다시 네트워크 다운로드가 발생한다.</p>
<p><strong>- 두 가지 상황이 나타난다.</strong></p>
<p>1) 서버에서 기존 데이터를 변경함
2) 서버에서 기존 데이터를 변경하지 않음</p>
<h3 id="1-검증-헤더와-조건부-요청-검증-헤더-추가">1) 검증 헤더와 조건부 요청 (검증 헤더 추가)</h3>
<p>캐시 만료후에도 서버에서 데이터를 변경하지 않음
저장해 두었던 캐시를 재사용 할 수 있다.
단 클라이언트의 데이터와 서버의 데이터가 같다는 사실을 확인할 수 있는 방법이 필요하다.</p>
<p>Last-Modified: 2020년 11월 10일 10:00:00 데이터가 마지막에 수정된 시간
응답 결과를 캐시에 저장한다. 60초 초과</p>
<p>if-modified-since: 2020년 11월 10일 10:00:00이 코드로 검증을 요청한다.
응답 : HTTP/1.1 304 Not Modified (HTTP Body가 없다), Last-Modified와 같이
(헤더 메타 정보만 응답한다. HTTP Body 전송X)
브라우저 캐시 : 응답 결과를 재사용, 헤더 데이터 갱신한다. -&gt; 캐시에서 조회한다.</p>
<p>= 네트워크 다운로드가 발생하지만 용량이 적은 헤더 정보만 다운로드 한다. (매우 실용적인 해결책)</p>
<h3 id="2-검증-헤더와-조건부-요청">2) 검증 헤더와 조건부 요청</h3>
<ul>
<li><p>검증 헤더
캐시 데이터와 서버 데이터가 같은지 검증하는 데이터
Last-Modified, ETag</p>
</li>
<li><p>조건부 요청 헤더
검증 헤더로 조건에 따른 분기
If-Modified-Since : Last-Modified 사용
If-None-Match : ETag 사용
조건이 만족하면 200 OK
조건이 만족하지 않으면 304 Not Modified</p>
</li>
</ul>
<h3 id="캐시와-조건부-요청-헤더">캐시와 조건부 요청 헤더</h3>
<p>If-Modified-Since : 이후에 데이터가 수정되었으면?</p>
<ul>
<li><p>데이터 미변경 예시
캐시 : 2020년 11월 10일 10:00:00 vs 서버 : 2020년 11월 10일 10:00:00
304 Not Modified, 헤더 데이터만 전송(BODY 미포함)
전송 용량 0.1M (헤더 0.1M, 바디 1.0M) </p>
</li>
<li><p>데이터 변경 예시
캐시 : 2020년 11월 10일 10:00:00 vs 서버 : 2020년 11월 10일 11:00:00
200 OK, 모든 데이터 전송(BODY 미포함)
전송 용량 1.1M (헤더 0.1M, 바디 1.0M) </p>
</li>
</ul>
<p>단점 : 1초 미만(0.x초) 단위로 캐시 조정이 불가능하다. (그럴 일이 별로 없음)
날짜 기반의 로직 사용
만약 데이터를 수정해서 날짜가 다르지만, 같은 데이터를 수정해서 데이터 결과가 똑같은 경우
서버에서 별도의 캐시 로직을 관리하고 싶은 경우 = ETag
ex. 스페이스나 주석처럼 크게 영향이 없는 변경에서 캐시를 유지하고 싶은 경우</p>
<h4 id="검증-헤더-etag-서버에서-관리">검증 헤더 ETag (서버에서 관리)</h4>
<p>1) ETag(Entity Tag) : 캐시용 데이터에 임의의 고유한 버전 이름을 달아둔다.
ex. ETag: &quot;v1.0&quot;, ETag: &quot;a2xddwsdf3&quot;
2) 데이터가 변경되면 이 이름을 바꾸어서 변경한다. (Hash를 다시 생성)
ex. ETag: &quot;aaaaa&quot; -&gt; ETag: &quot;bbbbb&quot;
= ETag만 보내서 같으면 유지, 다르면 다시 받기</p>
<p>서버에서 내려줌 -&gt; 응답 결과를 캐시에 저장
미변경시 = 304 Not Modified
응답 결과를 재사용, 
변경시 = 헤더 데이터 갱신 헤더 데이터 갱신한다.</p>
<p>ex. 베타 기간 파일이 변경되어도 ETag 동일하게 유지, 배포 주기에 맞추어 ETag 모두 갱신한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP Section 8. HTTP 헤더 ]]></title>
            <link>https://velog.io/@seonguul_2/HTTP-Section-8.-HTTP-%ED%97%A4%EB%8D%94</link>
            <guid>https://velog.io/@seonguul_2/HTTP-Section-8.-HTTP-%ED%97%A4%EB%8D%94</guid>
            <pubDate>Thu, 09 Apr 2026 23:44:12 GMT</pubDate>
            <description><![CDATA[<h3 id="http-헤더-개요">HTTP 헤더 개요</h3>
<ul>
<li>header-field
field-name&quot;:&quot; OWS field-value OWS 대소문자 구분이 없다. HTTP 전송에 필요한 모든 부가 정보이다.
표준 헤더가 너무 많다.</li>
</ul>
<p>General 헤더 : 메시지 전체에 적용되는 정보 ex. connection: close
Request 헤더 : 요청 정보, ex. User-Agent: Mozilla/5.0
Response 헤더 : 응답 정보 ex. Server: Apache
Entity 헤더 : 엔티티 바디 정보 ex. Content-Type: text/html, Content-Length: 3423</p>
<h3 id="http-body">HTTP BODY</h3>
<ul>
<li>message body
메시지 본문은 엔티티 본문을 전다하는데 사용한다.
엔티티 본문은 요청이나 응답에서 전달할 실제 데이터이다.
엔티티 헤더는 엔티티 본문의 데이터를 해석할 수 있는 정보를 제공한다.
데이터 유형(html, json), 데이터 길이, 압축 정보 등</li>
</ul>
<pre><code>// 표현 데이터
&lt;html&gt;
    &lt;body&gt;...&lt;/body&gt;
&lt;/html&gt;</code></pre><p>표현은 요청이나 응답에서 전달할 실제 데이터,
표현 헤더는 표현 데이터를 해석할 수 있는 정보를 제공한다.
데이터 유형(html, json), 데이터 길이, 압축 정보 등</p>
<h3 id="표현-리소스">표현 (리소스)</h3>
<p>Content-Type : 표현 데이터의 형식
미디어 타입, 문자 인코딩
ex. text/html; charset=utf-8, application/json, image/png</p>
<p>Content-Encoding : 표현 데이터의 압축 방식
데이터를 전달하는 곳에서 압축 후 인코딩 헤더 추가
데이터를 읽는 쪽에서 인코딩 헤더의 정보로 압축 해제
ex. gzip, deflate, identity</p>
<p>Content-Language : 표현 데이터의 자연 언어
ex. ko, en, en-US</p>
<p>Content-Length : 표현 데이터의 길이
바이트 단위, Transfer-Encoding(전송 코딩)을 사용하면 Content-Length를 사용하면 안된다.(청크 전송)
= 표현 헤더는 전송, 응답 둘 다 사용한다.</p>
<h3 id="협상콘텐츠-네고시에이션--요청">협상(콘텐츠 네고시에이션) = 요청</h3>
<p>클라이언트가 선호하는 표현 요청 (헤더로 처리한다)</p>
<p>Accept : 클라이언트가 선호하는 미디어 타입 전달
Accept-Charset : 클라이언트가 선호하는 문자 인코딩
Accept-Encoding : 클라이언트가 선호하는 압축 인코딩
Accept-Language : 클라이언트가 선호하는 자연 언어
= 협상 헤더는 요청시에만 사용</p>
<h3 id="협상과-우선순위-quality-valuesq">협상과 우선순위 Quality Values(q)</h3>
<p>Quality Values(q) 값 사용
0~1, 클수록 높은 우선순위
생략하면 1</p>
<p>Accept-Language: ko-KR, ko;q=0.9, en-US; q=0.8, en; q=0.7</p>
<p>1) ko-KR; q=1(q생략)
2) ko;q=0.9
3) en-US;q=0.8
4) en:q=0.7</p>
<ul>
<li><p>구체적인 것이 우선한다.
Accept: text/<em>, text/plain, text/plain;format=flowed, */</em>
1) text/plain;format=flowed
2) text/plain
3) text/*
4) <em>/</em></p>
</li>
<li><p>구체적인 것을 기준으로 미디어 타입을 맞춘다.
Accept: text/<em>;q=0.3, text/html;q=0.7, text/html;level=1,
text/html;level=2;q=0.4, */</em>;q=0.5</p>
</li>
</ul>
<p>text/html;level=1 quality=1
text/html quality=0.7
text/plain quality=0.3
image/jpeg quality=0.5
text/html;level=2 quality=0.4
text/html;level=3 quality=0.7</p>
<h3 id="전송-방식">전송 방식</h3>
<ul>
<li><p>단순 전송
HTTP/1.1 200 OK
Context-Type: text/html;charset=UTF-8
Content-Length: 3423</p>
<html>
<body>...</body>
</html>
</li>
<li><p>압축 전송 : 압축해서 전송한다.
Content-Encoding: gzip
Content-Length: 521</p>
</li>
<li><p>분할 전송 : 오면 바로바로 표시가 가능
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked (덩어리로 쪼개서 보낸다)
5
Hello
5
World
0
\r\n</p>
</li>
<li><p>범위 전송 : 범위를 지정해서 요청한다.
Content-Range: bytes 1001-2000 / 2000</p>
</li>
</ul>
<h3 id="from--유저-에이전트의-이메일-정보">From : 유저 에이전트의 이메일 정보</h3>
<p>일반적으로 잘 사용되지 않는다.
검색 엔진 같은 곳에서, 주로 사용
요청에서 사용된다.</p>
<h3 id="referer--이전-웹-페이지-주소">Referer = 이전 웹 페이지 주소</h3>
<p>현재 요청된 페이지의 이전 웹 페이지 주소
A-&gt;B로 이동하는 경우 B를 요청할 때 Referer: A를 포함해서 요청
Referer를 사용해서 <strong>유입 경로 분석 가능</strong>
요청에서 사용</p>
<h3 id="user-agent--유저-에이전트-애플리케이션-정보">User-Agent : 유저 에이전트 애플리케이션 정보</h3>
<p>네트워크 탭에 눌러보면 확인 가능! 
user-agent: Mozilla/5.0 </p>
<p>클라이언트의 애플리케이션 정보(웹 브라우저 정보, 등)
통계 정보
어떤 종류의 브라우저에서 장애가 발생하는지 파악 가능
요청에서 사용한다.</p>
<h3 id="server--요청을-처리하는-origin-서버의-소프트웨어-정보">Server : 요청을 처리하는 ORIGIN 서버의 소프트웨어 정보</h3>
<p>Server: Apache/2.2.22(Debian)
server: nginx
응답에서 사용한다.</p>
<h3 id="date--메시지가-발생한-날짜와-시간-응답에서-사용">Date : 메시지가 발생한 날짜와 시간 (응답에서 사용)</h3>
<h3 id="host--요청한-호스트-정보도메인">Host : 요청한 호스트 정보(도메인)</h3>
<p>요청에서 사용하는 필수값이다.
하나의 서버가 여러 도메인을 처리해야 할 때</p>
<p>GET /search?q=hello&amp;hl=ko HTTP/1.1
Host: <a href="http://www.google.com">www.google.com</a></p>
<h3 id="location--페이지-리다이렉션">Location : 페이지 리다이렉션</h3>
<p>웹 브라우저는 3xx 응답의 결과에 Location 헤더가 있으면, Location 위치로 자동 이동(리다이렉트)
응답코드 3xx에서 설명
201(Created) : Location 값은 요청에 의해 생성된 리소스 URI이다.
3xx(Redirection) : Location 값은 요청을 자동으로 리디렉션하기 위한 대상 리소스를 가리킨다.</p>
<h3 id="allow--허용-가능한-http-메서드">Allow : 허용 가능한 HTTP 메서드</h3>
<p>405 (Method Not Allowed)에서 응답에 포함해야 한다.
Allow : GET, HEAD, PUT</p>
<h3 id="retry-after">Retry-After</h3>
<p>유저 에이전트가 다음 요청을 하기까지 기다려야 하는 시간
503(Service Unavailable) : 서비스가 언제까지 불능인지 알려줄 수 있음
Retry-After : Fri, 31 Dec 1999 23:59:59 GMT(날짜 표기)
Retry-After : 120 (초단위 표기)</p>
<h3 id="인증">인증</h3>
<p>Authorization : 클라이언트 인증 정보를 서버에 전달한다.
Authorization: Basic xxxxxxxxxxxx</p>
<p>WWW-Authenticate : 리소스 접근시 필요한 인증 방법 정의
ex. 401 Unauthorized 응답과 함께 사용한다.
Newauth realm=&quot;apps&quot;, type=1, title=&quot;Login to&quot;apps&quot;&quot;, Basic realm=&quot;simple&quot;</p>
<h3 id="쿠키-사용자를-구분하기-위해--모든-요청에-쿠키-정보-자동-포함">쿠키 (사용자를 구분하기 위해) : 모든 요청에 쿠키 정보 자동 포함</h3>
<p>HTTP는 무상태 프로토콜이기 때문에, 이전 요청을 기억하지 못한다.
GET /welcome?user=홍길동 HTTP/1.1 모든 요청에 사용자 정보를 포함하지 못함
이 문제들을 해결하기 위해 쿠키가 생겼다.</p>
<p>Set-Cookie : 서버에서 클라이언트로 쿠키 전달(응답)
HTTP/1.1 200 OK
Set-Cookie: user=홍길동
서버는 쿠키 저장소에 저장 -&gt; 클라이언트는 쿠키 저장소에서 조회하고 서버에 보낸다. 
Cookie : 클라이언트가 서버에서 받은 쿠키를 저장하고, HTTP 요청시 서버로 전달한다.</p>
<p>ex. set-cookie: sessionId=abcde1234; expires=Sat, 26-Dec-2020 00:00:00 GMT; path=/; domain=.google.com; Secure</p>
<ul>
<li><p>사용처 : 사용자 로그인 세션 관리, 광고 정보 트래킹</p>
</li>
<li><p>쿠키 정보는 항상 서버에 전송된다.
네트워크 트래픽 추가 유발</p>
</li>
<li><p><em>최소한의 정보만 사용(세션 id, 인증 토큰)해야 한다.*</em>
서버에 전송하지 않고, 웹 브라우저 내부에 데이터를 저장하고 싶으면 웹 스토리지 참고</p>
</li>
<li><p>주의
보안에 민감한 데이터는 저장하면 안된다(주민번호, 신용카드 번호 등)</p>
</li>
</ul>
<h3 id="쿠키---생명주기">쿠키 - 생명주기</h3>
<p>Set-Cookie: expires=Sat, 26-Dec-2020 04:39:21 GMT
만료일이 되면 쿠키를 삭제한다.</p>
<p>Set-Cookie: max-age=3600 (3600초)
0이나 음수를 지정하면 쿠키를 삭제한다.</p>
<p>세션 쿠키 : 만료 날짜를 생략하면 브라우저 종료시 까지만 유지한다.
영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지한다.</p>
<h3 id="쿠키---도메인">쿠키 - 도메인</h3>
<p>domain=example.org 도메인을 지정하여 생성</p>
<ul>
<li><p>명시 : 명시한 문서 기준 도메인 + 서브 도메인 포함
domain=example.org를 지정해서 쿠키 생성
example.org는 물론이고
dev.example.org도 쿠키 접근</p>
</li>
<li><p>생략 : 현재 문서 기준 도메인만 적용
example.org에서 쿠키를 생성하고 domain지정을 생략
example.org 에서만 쿠키 접근
dev.example.org는 쿠키 미접근</p>
</li>
</ul>
<h3 id="쿠키---경로">쿠키 - 경로</h3>
<p>ex. path=/home
이 경로를 포함한 하위 경로 페이지만 쿠키 접근
일반적으로 path=/ 루트로 지정</p>
<p>ex. path=/home 지정
/home -&gt; 가능
/home/level1 -&gt; 가능
/home/level1/level2 -&gt; 가능
/hello -&gt; 불가능</p>
<h3 id="쿠키---보안">쿠키 - 보안</h3>
<ul>
<li><p>Secure
쿠키는 http, https를 구분하지 않고 전송
Secure을 적용하면 https인 경우에만 전송</p>
</li>
<li><p>HttpOnly
XSS 공격 방지
자바스크립트에서 접근 불가(document.cookie)
HTTP 전송에만 사용</p>
</li>
<li><p>SameSite (브라우저에서 확인하고 사용할 것)
XSRF 공격 방지
요청 도메인과 쿠키에 설정된 도메인이 같은 경우만 쿠키 전송</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트 고급 - Portal, Profiler API 성능 측정, 엄격모드, AbortController]]></title>
            <link>https://velog.io/@seonguul_2/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B3%A0%EA%B8%89-Portal-Profiler-API-%EC%84%B1%EB%8A%A5-%EC%B8%A1%EC%A0%95-%EC%97%84%EA%B2%A9%EB%AA%A8%EB%93%9C-AbortController</link>
            <guid>https://velog.io/@seonguul_2/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B3%A0%EA%B8%89-Portal-Profiler-API-%EC%84%B1%EB%8A%A5-%EC%B8%A1%EC%A0%95-%EC%97%84%EA%B2%A9%EB%AA%A8%EB%93%9C-AbortController</guid>
            <pubDate>Tue, 31 Mar 2026 05:32:25 GMT</pubDate>
            <description><![CDATA[<h3 id="리액트의-고급-개념">리액트의 고급 개념</h3>
<ul>
<li><p>포털 : 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 특정 DOM 노드에 자식 컴포넌트를 렌더링할 수 있게 해주는 기능입니다.</p>
</li>
<li><p>포털을 사용하는 이유 : CSS 레이아웃 제약 해결, 시각적 계층 구조와 논리적 구조의 분리, 접근성 및 스크린 리더 최적화 (부모 크기에 상관없이 전체화면 활용 가능)</p>
</li>
</ul>
<p>1) 모달 창, 다이얼로그 컴포넌트
2) 툴팁(말풍선), 로딩 창
3) 팝오버 : 사용자에게 빠르게 컨텍스트 빠르게 제공
4) 쿠키 얼럿
5) 드롭다운 메뉴 : 부모 컴포넌트 내부에 드롭다운 메뉴가 위치 -&gt; overflow 스타일에 의해 보이지 않을 수도 있기 때문에 포털을 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/3be5ee80-cb13-4774-9b77-f1ddbee90e55/image.png" alt=""></p>
<pre><code>import { createPortal } from &#39;react-dom&#39;

const ModalWindow = ({ description, isOpen, onClose }) =&gt; {
    if (!isOpen) return null
    return createPortal (
        &lt;div className=&quot;modal&quot;&gt;
            &lt;span&gt;{description}&lt;/span&gt;
            &lt;button onClick={onClose}&gt;Close&lt;/button&gt;
        &lt;/div&gt;,
        document.body
    )
}</code></pre><p>react-dom 패키지에서 불러온 createPortal을 호출해서 생성할 수 있다.</p>
<p>DOMNode : 포털 내용을 렌더링할 DOM노드
key : 컴포넌트 트리에서 포털을 구분할 수 있는 고유 식별자, 선택사항</p>
<p>HTML(DOM) 트리 : 브라우저가 보는 구조 portal을 쓰면 root를 벗어나 body바로 아래 붙는다. 
React 트리 : React가 관리하는 컴포넌트 계층이다. 부모 컴포넌트의 자식이다.</p>
<h4 id="--이벤트-버블링의-흐름">- 이벤트 버블링의 흐름</h4>
<p>portal 내부 버튼 -&gt; 
브라우저 레벨 : html -&gt; body 올라간다. (부모 컴포넌트x) 
React 레벨 : React가 이벤트를 가로채서 자신이 알고있는 컴포넌트 트리를 따라 이벤트 전파 
-&gt; Portal밖의 부모 컴포넌트에 설정된 핸들러 실행</p>
<h4 id="--포털에서-관리하는-접근성-주의사항">- 포털에서 관리하는 접근성 주의사항?</h4>
<p>다이얼로그나 모달이 열릴 때 포커스는 다이얼로그 내부의 요소로 이동,</p>
<h4 id="--에러-바운더리">- 에러 바운더리</h4>
<p>1) try~catch문
2) 에러 바운더리 : 특정 작업 목록이 담긴 리액트 컴포넌트일 뿐이다.
자식 컴포넌트 트리에서 발생할 수 있는 자바스크립트 오류를 잡아내고, 특정 오류를 기록한 다음, 화면을 대체 UI로 리디렉션해서 오류 상태에서 복구하는 데 사용된다.</p>
<p>전체 컴포넌트 트리의 생성자에서 발생하는 오류를 잡아낸다. 에러 바운더리는 클래스 컴포넌트로 생명주기 메서드 중 하나 이상을 사용해서 생성될 수 있다.</p>
<p>static getDerivedStateFromError : 오류가 발생한 후 대체 UI를 렌더링
componentDidCatch : 오류 정보를 기록하는 데 사용한다.</p>
<pre><code>class MyErrorBoundary extends Component {
    constructor(props) {
        super(props)
        this.state = { isErrorThron: false }
       }

    static getDerivedStateFromError(error) { // 오류가 발생한 대체 UI
        return { isErrorThrown: true }
    }

    componentDidCatch(error, errorInfo) {
        logErrorToReposrtingService(error, errorInfo)
    }

    render() {
        if (this.state.isErrorThrown) { 
            return &lt;h1&gt;Oops, the application is unavaialble.&lt;/h1&gt;
        }
        return this.props.children
    }
}</code></pre><p>에러 컴포넌트를 사용할 컴포넌트에 감싸면 된다.
<img src="https://velog.velcdn.com/images/seonguul_2/post/cdaaa88b-02e0-4276-86ab-1e1b5bf849d7/image.png" alt=""></p>
<ul>
<li>suspense와 error boundary 사용 장점
1) 서비스 전체의 화이트 아웃 방지 : 에러가 난 부분만 격리 가능하다. (방화벽 역할)</li>
</ul>
<p>2) 폭포수 현상 해결을 통한 성능 최적화 : 기존 방식 useEffect는 부모 데이터 로딩이 끝나야 자식 데이터 로딩이 시작되는 폭포수 현상이 생기기 쉽다. Suspense를 사용하면 데이터 요청을 병렬로 처리할 수 있다. = 빠르고, 매끄러운 앱 구현 가능</p>
<p>3) 선언적 에러 복구 : 페이지 전체를 새로고침 하지 않아도, ui만 새로고침 하도록 만들 수 있다.</p>
<ul>
<li><p>Jest같은 테스트 프레임워크로 단위 테스트 작성 가능하다.</p>
</li>
<li><p>에러 바운더리를 함수 컴포넌트로 생성하는 것이 불가능하다.
클래스 컴포넌트를 사용해 에러 바운더리를 만들 수 있다.</p>
</li>
<li><p>에러 바운더리를 사용하지 못하는 경우
이벤트 핸들러, 비동기 코드(지연로딩, requestAnimationFrame 등) 비동기 콜백 내부 에러, SSR, 에러 바운더리 내부에서 에러가 발생한 경우(에러 바운더리 자체에서 발생한 에러)</p>
</li>
</ul>
<p>버튼 클릭 시 발생하는 API 에러 등은 try ~ catch로 잡아서 얼럿을 띄우는 게 맞습니다. 데이터를 렌더링하다가 예상치 못한 undefined 참조 등으로 화면이 깨지는 상황은 에러 바운더리가 방어해야 한다.</p>
<h4 id="--suspense-api를-활용한-비동기-작업-관리">- Suspense API를 활용한 비동기 작업 관리</h4>
<p>로딩 인디케이터 같은 대체 UI를 표시하는 데 사용된다.</p>
<ul>
<li><p>모든 데이터 불러오기에서 서스펜스 컴포넌트 사용 가능한가? 
lazy API사용한 지연로딩, 릴레이, Next.js, 리믹스, 하이드로겐 등의 서스펜스 기능을 지원하는 프레임워크를 활용한 데이터 불러오기</p>
</li>
<li><p>업데이트 중 불필요한 폴백 방지
리액트의 Suspense는 하위 컴포넌트가 아직 준비되지 않았을 때(데이터 패칭 중 등) 가장 가까운 fallback UI를 보여줍니다. 문제는 이미 렌더링 된 상태에서 새로운 데이터를 가져올 때 발생합니다.</p>
<pre><code>function navigate(url) {
  startTransition(() =&gt; {
      setPage(url) 
  })
}</code></pre><p>페이지를 탐색하고 페이지 업데이트에 전환을 적용해 불필요한 폴백 방지</p>
</li>
</ul>
<h4 id="--동시성-렌더링을-활용한-렌더링-성능-최적화">- 동시성 렌더링을 활용한 렌더링 성능 최적화</h4>
<p>비동기적으로 렌더링 하도록 보장하는 동시성 렌더링 도입했다.</p>
<ul>
<li>동시성 렌더링 가능하게 하는 방법?
react 및 react-dom 패키지를 18버전으로 업데이트해야 한다.
ReactDOM.render 메서드를 ReactDOM.createRoot 메서드로 교체해야 한다.</li>
</ul>
<p>애플리케이션이 복잡해지면 성능을 분석하는 데 상당한 시간을 투자해야 한다.</p>
<h4 id="--profiler-api를-활용한-리액트-애플리케이션-디버깅">- Profiler API를 활용한 리액트 애플리케이션 디버깅</h4>
<p>컴포넌트가 얼마나 자주 리렌더링되는지 각각의 리렌더링 비용을 추적하는 것이 애플리케이션 내의 문제 영역이나, 부분을 파악하는 데 도움이 된다. (컴포넌트의 병목을 수치로 확인하여 최적화의 근거를 찾는 과정)</p>
<p>1) React Profiler API</p>
<ul>
<li>렌더링 성능을 측정하는 방법
Profiler API를 제공한다. UI 일부를 식별하는 데 사용되는 id prop, 트리 업데이트 시 호출되는 onRender 콜백 두가지 </li>
</ul>
<pre><code>// id, phase, actualDuration, baseDuration, startTime, commitTime 인수를 받아 렌더링 시간 기록
&lt;App&gt;
    &lt;Profiler id=&quot;bio&quot; onRender={onRender}&gt; // 렌더링 성능을 보고 싶다면 전체를 감싼다.
        &lt;AuthorBio /&gt;
    &lt;/Profiler&gt;
    &lt;Posts /&gt;
&lt;/App&gt;</code></pre><p>2) 리액트 개발자 도구</p>
<h4 id="--엄격-모드">- 엄격 모드</h4>
<p>잠재적인 버그와 이슈를 식별하기 위한 디버깅 도구 = strict mode
리액트 API의 StrictMode 컴포넌트로 사용한다.</p>
<p>컴포넌트 mount -&gt; unmount -&gt; mount 되기 때문에 fetch가 2번 호출된다.</p>
<p>cleanup없이 2번 호출되면 메모리 누수 or 중복요청 존재</p>
<h4 id="--abortcontroller를-사용하기-csr에서만-작동">- AbortController를 사용하기 (CSR)에서만 작동</h4>
<p>useEffect의 cleanup함수와 짝을 이뤄서, 진행 중인 요청을 자동으로 취소하는 패턴이다.
메모리 누수를 방지한다. = 최신 요청만 유효, 1번만 출력한다. 
<strong>fetch, axios 등 비동기 네트워크 요청일 경우에만</strong></p>
<pre><code>const controller = new AbortController()

controller.signal  // ← fetch에 연결하는 신호선
controller.abort() // ← 취소 명령

- 동작 방식
mount → unmount → mount  (Strict Mode가 의도적으로 실행)
         ↑
     abort() 호출 → 첫번째 요청 취소 → 두번째 요청만 유효

const root = createRoot(document.getElementById(&#39;root&#39;))
root.render(
    &lt;StrictMode&gt;
        &lt;App/&gt;
    &lt;/StrictMode&gt;,
)</code></pre><p>렌더링 영역에서 부적절한 로직,  정리 코드 부재 문제를 엄격 모드를 통해 파악한다.
자주 발생하는 버그를 찾기 위한 개발 전용 점검 항목을 활성화한다.</p>
<p>1) 순수하지 않은 렌더링으로 인한 버그를 찾기 위해 컴포넌트를 한 번 더 리렌더링
2) 이펙트에 대한 cleanup 함수 누락에 의한 버그를 찾기 위해 컴포넌트가 한 번 더 effect실행
3) 사용되는 API 중 더 이상사용되지 않는 API를 확인하고 경고</p>
<ul>
<li>두 번 호출되는 함수는?
1) 함수 컴포넌트의 내부
2) 훅에 전달된 함수 useState, useReducer, useMemo
3) state updater 함수
4) constructor, render 등 클래스 컴포넌트 메서드</li>
</ul>
<h4 id="--정적-타입-체크">- 정적 타입 체크</h4>
<p>라이브러리 prop-types
타입스크립트, Flow로 두 가지 방법으로 정적 타입 검사를 구현한다.</p>
<p>1) 런타임 이전에 타입 오류를 식별
2) 초기 단계에서 버그와 오류를 감지
3) 최적화와 코드 가독성 향상
3) IDE 지원 향상
4) 문서 생성</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Native - 4장 스타일링 ]]></title>
            <link>https://velog.io/@seonguul_2/React-Native-4%EC%9E%A5-%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81</link>
            <guid>https://velog.io/@seonguul_2/React-Native-4%EC%9E%A5-%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81</guid>
            <pubDate>Tue, 10 Mar 2026 03:58:44 GMT</pubDate>
            <description><![CDATA[<h3 id="4장-스타일링">4장. 스타일링</h3>
<h4 id="1-인라인-스타일링">1) 인라인 스타일링</h4>
<pre><code>&lt;View style={{ flex: 1, backgroundColor: &#39;#fff&#39;, alignItems: &#39;center&#39;, justifyContent: &#39;center&#39; }}&gt;
&lt;Text style={{ padding: 10, fontSize: 26, fontWeight: &#39;400&#39;, color: &#39;red&#39; }}&gt;</code></pre><h4 id="2-클래스-스타일링">2) 클래스 스타일링</h4>
<pre><code>const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: &#39;#fff&#39;,
        alignItems: &#39;center&#39;,
        justifyContent: &#39;center&#39;,
    },
    text: {
        padding: 10,
        fontSize: 26,
        fontWeight: &#39;600&#39;,
        color: &#39;black&#39;,
       },
    error: {
        padding: 10,
        fontSize: 26,
        fontWeight: &#39;400&#39;,
        color: &#39;red&#39;,
    }
});</code></pre><h4 id="3-여러-개-스타일-적용">3) 여러 개 스타일 적용</h4>
<h4 id="--여러-개-스타일">- 여러 개 스타일</h4>
<pre><code>&lt;Text style={[styles.text, styles.error]}&gt;Inline Styling - Error&lt;/Text&gt;</code></pre><h4 id="--인라인-스타일과-혼용">- 인라인 스타일과 혼용</h4>
<pre><code>&lt;Text style={[styles.text, { color: &#39;green&#39; }]}&gt;</code></pre><h4 id="4-외부-스타일-이용하기">4) 외부 스타일 이용하기</h4>
<pre><code>src/styles.js
import { StyleSheet } from &#39;react-native&#39;;

export const viewStyles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: &#39;#fff&#39;,
        alignItems: &#39;center&#39;,
        justifyContent: &#39;center&#39;,
     },
});

export const textStyles = StyleSheet.create({
    text: {
        padding: 10,
        fontSize: 26,
        fontWeight: &#39;600&#39;,
        color: &#39;black&#39;,
    },
    error: }
        fontWeight: &#39;400&#39;,
        color: &#39;red&#39;,
   },
});</code></pre><p>App.js에서
import { viewStyles, textStyles } from &#39;./styles&#39;;로 불러오면 된다.</p>
<pre><code>&lt;View style={viewStyles.container}&gt;
    &lt;Text style={[textStyles.text, { color: &#39;green&#39; }]}&gt; 이런식으로 사용 가능</code></pre><h4 id="5-flex와-범위">5) flex와 범위</h4>
<p>Header와 Contents, Footer로 나뉜다.
flex로 비율을 지정해주면 모든 기종에서 비율이 일정하게 보인다.</p>
<pre><code>const styles = StyleSheet.create({
    container: {
        width: &#39;100%&#39;,
        alignItems: &#39;center&#39;,
        justifyContent: &#39;center&#39;,
        height: 80,
    },
    header: {
        flex: 1,
        backgroundColor: &#39;#f1c40f&#39;,
    },
    contents: {
        flex: 2,
        backgroundColor: &#39;#1abc9c&#39;,
    },
    footer: {
        flex: 1,
        backgroundColor: &#39;#3498db&#39;,
    },
    text: {
        fontSize: 26,
    }
});


App.js 에서
return (
    &lt;View style={{ flex: 1 }}&gt;
      &lt;Header/&gt;
      &lt;Contents/&gt;
      &lt;Footer/&gt;
    &lt;/View&gt;
  );</code></pre><h4 id="6-flexdirection">6) flexDirection</h4>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/e9e631f8-49ce-4399-ab5f-afa8ee26a642/image.png" alt=""></p>
<ul>
<li>column : 세로 방향으로 정렬(기본값)</li>
<li>column-reverse : 세로 방향 역순 정렬</li>
<li>row : 가로 방향으로 정렬</li>
<li>row-reverse : 가로 방향 역순 정렬</li>
</ul>
<h4 id="7-justifycontent">7) justifyContent</h4>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/1edac03f-0864-4f64-9887-b740ecd76ae6/image.png" alt="">
<img src="https://velog.velcdn.com/images/seonguul_2/post/d9d479e1-835d-4019-8d3f-be33b84d8c98/image.png" alt=""></p>
<ul>
<li>flex-start : 시작점에서부터 정렬(기본값)</li>
<li>flex-end : 끝에서부터 정렬</li>
<li>center : 중앙 정렬</li>
<li>space-between : 컴포넌트 사이의 공간을 동일하게 만들어서 정렬</li>
<li>space-around : 컴포넌트 각각의 주변 공간을 동일하게 만들어서 정렬</li>
<li>space-evenly : 컴포넌트 사이와 양 끝에 동일한 공간을 만들어서 정렬</li>
</ul>
<h4 id="8-alignitems">8) alignItems</h4>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/d986b6e9-ae19-4d0e-926e-24f2de677aa3/image.png" alt=""></p>
<ul>
<li>flex-start : 시작점에서부터 정렬(기본값)</li>
<li>flex-end : 끝에서부터 정렬</li>
<li>center : 중앙 정렬</li>
<li>stretch : alignItems의 방향으로 컴포넌트 확장</li>
<li>baseline : 컴포넌트 내부의 텍스트(text) 베이스라인(baseline)을 기준으로 정렬</li>
</ul>
<h4 id="9-그림자">9) 그림자</h4>
<ul>
<li>shadowColor : 그림자 색 설정</li>
<li>shadowOffset : width와 height값을 지정하여 그림자 거리 설정</li>
<li>shadowOpacity : 그림자 불투명도 설정</li>
<li>shadowRadius : 그림자의 흐림 반경 설정</li>
</ul>
<pre><code>import { View, StyleSheet } from &#39;react-native&#39;;
import ShadowBox from &#39;./src/components/ShadowBox&#39;;

export default function App() {
  return (
    &lt;View style={styles.container}&gt;
      &lt;ShadowBox/&gt;
    &lt;/View&gt;
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: &#39;center&#39;,
    justifyContent: &#39;center&#39;,
  },
});</code></pre><h3 id="10-스타일드-컴포넌트">10) 스타일드 컴포넌트</h3>
<p>자바스크립트 파일 안에 스타일을 작성하는 라이브러리이다.
npm install styled-components</p>
<pre><code>import styled from &#39;styled-components/native&#39;;

const MyTextomponent = styled.Text`
    color: #fff;`;
const whiteText = css` color: #fff; font-size: 14px;`;
const MyBoldTextComponent = styled.Text`${whiteText} font-weight: 600;`;
const ErrorText = styled(StyledText)`font-weight: 600; color: red;`;</code></pre><p>style.컴포넌트이름 <code>백틱</code>안에 스타일 지정한다.</p>
<h3 id="11-스타일시트-사용">11) 스타일시트 사용</h3>
<h4 id="--props사용하기">- props사용하기</h4>
<p>props로 값에 따라 다른 색이 지정되도록 수정할 수 있다.</p>
<pre><code>const ButtonContainer = styled.TouchableOpacity`
    background-color: ${props =&gt; props.title === &#39;Hanbit&#39; ? &#39;#3498db&#39; : &#39;#9b59b6&#39;};
    border-radius: 15px;
    padding: 15px 40px;
    margin: 10px 0px;
    justify-content: center;
`;

const Title = styled.Text`
    font-size: 20px;
    font-weight: 600;
    color: #fff;
`;

const Button = props =&gt; {
    return (
        &lt;ButtonContainer title={props.title}&gt;
            &lt;Title&gt;{props.title}&lt;/Title&gt;
        &lt;/ButtonContainer&gt;
    )
}
export default Button;</code></pre><h4 id="--attrs사용하기">- attrs사용하기</h4>
<p>스타일드 컴포넌트에서 속성을 설정할 때 사용하는 attrs의 사용법
고정된 Props나 &quot;동적으로 변하는 스타일&quot;을 HTML(또는 Native) 태그에 미리 붙여두는 도구이다.</p>
<p>스타일 정의 시점에 속성을 포함한다.</p>
<pre><code>const StyledInput = styled.TextInput.attrs({
  placeholder: &quot;Enter a text...&quot;, // 기본값 설정
  placeholderTextColor: &quot;#3498db&quot;,
  keyboardType: &quot;email-address&quot;, // 키보드 타입 고정
})`
  width: 200px;
  height: 60px;
  border: 2px solid #3498db;
`;

// 이제 호출할 때 아무것도 안 적어도 위 속성들이 적용됨!
&lt;StyledInput /&gt;</code></pre><p>전달한 값에 따라 수정되게 하기</p>
<pre><code>const StyledInput = styled.TextInput.attrs(props =&gt; ({
    placeholder: &#39;Enter a text...&#39;,
    placeholderTextColor: props.borderColor,
}))
`
    width: 200px;
    height: 60px;
    margin: 5px;
    padding: 10px;
    border-radius: 10px;
    border: 2px;
    border-color: ${props =&gt; props.borderColor};
    font-size: 24px;
`;

const Input = props =&gt; {
    return &lt;StyledInput borderColor={props.borderColor}/&gt;
};

export default Input; </code></pre><h4 id="--themeprovider">- ThemeProvider</h4>
<p>theme.js에서 배경 색 지정</p>
<pre><code>export const theme = {
    purple: &#39;#9b59b6&#39;,
    blue: &#39;#3498db&#39;,
}</code></pre><p>모든 컴포넌트를 감싸는 최상위 컴포넌트로 ThemeProvider 컴포넌트를 사용하여 theme 속성에 설정한다.</p>
<pre><code>const App = () =&gt; {
    return (
        &lt;ThemeProvider theme={theme}&gt;
            &lt;Container&gt;
            ...
            &lt;/Container&gt;
        &lt;/ThemeProvider&gt;
    )
}</code></pre><h3 id="--화면-다크라이트-모드">- 화면 다크/라이트 모드</h3>
<pre><code>- theme.js 에서 정의
export const theme = {
    purple: &#39;#9b59b6&#39;,
    blue: &#39;#3498db&#39;,
}

export const lightTheme = {
    background: &#39;#ffffff&#39;,
    text: &#39;#ffffff&#39;,
    purple: &#39;#9b59b6&#39;,
    blue: &#39;#3498db&#39;,
};

export const darkTheme = {
    background: &#39;#34495e&#39;,
    text: &#39;#34495e&#39;,
    purple: &#39;#9b59b6&#39;,
    blue: &#39;#3498db&#39;,
}

- App.js 에서 호출하여 사용한다.
const Container = styled.View`
  flex: 1;
  background-color: #ffffff;
  align-items: center;
  justify-content: center;
  background-color: ${props =&gt; props.theme.background};
`;

const App = () =&gt; {
  const [isDark, setIsDark] = useState(false);
  const _toggleSwitch = () =&gt; setIsDark(!isDark);
  return (
    &lt;ThemeProvider theme={isDark ? darkTheme : lightTheme}&gt;
      &lt;Container&gt;
        &lt;Switch value={isDark} onValueChange={_toggleSwitch}/&gt;
        &lt;Button title=&quot;Hanbit&quot; /&gt;
        &lt;Button title=&quot;React Native&quot; /&gt;
        &lt;Input borderColor=&quot;#3498db&quot;/&gt;
        &lt;Input borderColor=&quot;#9b59b6&quot;/&gt;
      &lt;/Container&gt;
    &lt;/ThemeProvider&gt;
  );
};</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Lighthouse 활용한 웹사이트 성능 측정]]></title>
            <link>https://velog.io/@seonguul_2/Lighthouse-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%84%B1%EB%8A%A5-%EC%B8%A1%EC%A0%95</link>
            <guid>https://velog.io/@seonguul_2/Lighthouse-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9B%B9%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%84%B1%EB%8A%A5-%EC%B8%A1%EC%A0%95</guid>
            <pubDate>Mon, 09 Mar 2026 04:37:53 GMT</pubDate>
            <description><![CDATA[<h2 id="lighthouse-4대-핵심-메뉴">Lighthouse 4대 핵심 메뉴</h2>
<h3 id="1-performance-성능">1. Performance (성능)</h3>
<p>핵심: &quot;이 사이트가 얼마나 빠른가?&quot;</p>
<p>설명: 앞서 설명한 FCP, LCP, TBT 등이 포함된 영역입니다. 브라우저가 콘텐츠를 얼마나 빨리 렌더링하고, 사용자의 입력에 얼마나 기민하게 반응하는지를 측정합니다.</p>
<p>개선 포인트: 이미지 압축, 코드 분할(Code Splitting), 캐싱(Blob 활용 등).</p>
<h3 id="2-accessibility-접근성">2. Accessibility (접근성)</h3>
<p>핵심: &quot;장애가 있거나 보조 공학 기기를 사용하는 사용자도 이용 가능한가?&quot;</p>
<p>설명: 시각 장애인이 사용하는 스크린 리더가 내용을 잘 읽을 수 있는지, 키보드만으로 모든 메뉴를 조작할 수 있는지 등을 체크합니다.</p>
<p>개선 포인트: 버튼에 aria-label 넣기, 이미지에 alt 속성 쓰기, 적절한 색상 대비(Contrast) 유지하기.</p>
<h3 id="3-best-practices-권장사항">3. Best Practices (권장사항)</h3>
<p>핵심: &quot;웹 표준과 보안 지침을 잘 따르고 있는가?&quot;</p>
<p>설명: 최신 웹 기술(HTTPS, HTTP/2)을 사용하는지, 콘솔에 에러가 찍히지는 않는지, 보안에 취약한 라이브러리를 쓰지는 않는지 검사합니다.</p>
<p>개선 포인트: 보안 프로토콜 준수, 더 이상 사용되지 않는(Deprecated) API 제거.</p>
<h3 id="4-seo-검색엔진-최적화">4. SEO (검색엔진 최적화)</h3>
<p>핵심: &quot;구글이나 네이버 같은 검색엔진이 이 사이트를 잘 찾을 수 있는가?&quot;</p>
<p>설명: 검색 로봇이 사이트의 주제를 파악하기 좋게 메타 태그가 잘 설정되어 있는지, 모바일에서 보기 편한지 등을 평가합니다.</p>
<pre><code>개선 포인트: &lt;title&gt;, &lt;meta description&gt; 설정,  robots.txt 파일 확인.</code></pre><h2 id="🚀-lighthouse-핵심-지표-5가지">🚀 Lighthouse 핵심 지표 5가지</h2>
<h3 id="1-fcp-first-contentful-paint">1. FCP (First Contentful Paint)</h3>
<p>의미: 사용자가 페이지에 접속했을 때 브라우저가 텍스트나 이미지 등 첫 번째 콘텐츠를 그리기 시작하는 시점입니다.</p>
<p>사용자 체감: &quot;오, 웹사이트가 로딩을 시작했네?&quot;</p>
<p>팁: 서버 응답 속도를 높이고 렌더링을 방해하는 자바스크립트를 최소화하면 빨라집니다.</p>
<h3 id="2-lcp-largest-contentful-paint">2. LCP (Largest Contentful Paint)</h3>
<p>의미: 페이지 내에서 가장 큰 이미지나 텍스트 블록이 화면에 완전히 나타나는 시점입니다.</p>
<p>사용자 체감: &quot;이제 메인 화면이 다 떴구나!&quot; (실질적인 로딩 완료 시점)</p>
<p>최적화: 가장 큰 배너 이미지의 용량을 줄이거나 우선순위를 높여야 합니다.</p>
<h3 id="3-tbt-total-blocking-time">3. TBT (Total Blocking Time)</h3>
<p>의미: 앞서 설명해 드린 것처럼, 메인 스레드가 50ms 이상 차단되어 사용자의 입력(클릭 등)에 반응하지 못하는 전체 시간입니다.</p>
<p>사용자 체감: &quot;버튼을 눌렀는데 왜 반응이 없지? 렉 걸렸나?&quot;</p>
<p>최적화: 무거운 자바스크립트 실행을 줄이는 것이 핵심입니다 (님께서 Blob 캐싱으로 해결하신 부분!).</p>
<h3 id="4-speed-index">4. Speed Index</h3>
<p>의미: 페이지가 로드되는 동안 콘텐츠가 시각적으로 얼마나 빨리 채워지는지를 측정하는 종합 지수입니다.</p>
<p>사용자 체감: 화면이 단계별로 빈틈없이 채워지는 속도.</p>
<p>최적화: 전체적인 로딩 흐름이 매끄러워야 점수가 잘 나옵니다.</p>
<h3 id="5-cls-cumulative-layout-shift">5. CLS (Cumulative Layout Shift)</h3>
<p>의미: 로딩 과정에서 화면의 요소들이 갑자기 툭 튀어나오거나 위치가 바뀌는 현상을 수치화한 것입니다.</p>
<p>사용자 체감: &quot;버튼 누르려는데 갑자기 광고가 떠서 엉뚱한 거 클릭했네!&quot; (짜증 지수)</p>
<p>최적화: 이미지나 광고 영역에 미리 고정된 높이/너비를 설정해 주어야 합니다.</p>
<h2 id="🔍-lighthouse-진단-도구의-정체">🔍 Lighthouse 진단 도구의 정체</h2>
<h3 id="1-insights-인사이트--metrics">1. Insights (인사이트 / Metrics)</h3>
<p>의미: 현재 웹사이트의 상태 수치입니다.</p>
<p>내용: 앞서 우리가 공부한 FCP, LCP, TBT 같은 지표들이 여기에 표시됩니다. &quot;지금 당신의 사이트는 첫 화면이 뜨는 데 0.8초가 걸립니다&quot;와 같은 결과 값을 보여주는 영역입니다.</p>
<h3 id="2-diagnostics-진단">2. Diagnostics (진단)</h3>
<p>의미: &quot;왜 점수가 낮은가?&quot;에 대한 구체적인 원인 분석입니다.</p>
<p>특징: 점수에는 직접적인 영향을 주지 않더라도, 페이지의 전반적인 건강 상태(성능, 보안 등)를 위해 해결해야 할 과제들을 나열합니다.</p>
<p>예시: * &quot;이미지 크기가 너무 큽니다.&quot;</p>
<p>&quot;사용하지 않는 자바스크립트가 너무 많습니다.&quot;</p>
<p>&quot;캐시 정책이 효율적이지 않습니다.&quot;</p>
<h3 id="3-opportunities-기회--절약-가능-시간">3. Opportunities (기회 / 절약 가능 시간)</h3>
<p>의미: &quot;이것만 고치면 이만큼 빨라질 수 있다&quot;라는 가이드라인입니다.</p>
<p>핵심: 가장 효율적으로 점수를 올릴 수 있는 &#39;지름길&#39;을 알려줍니다.</p>
<p>예시: &quot;이미지 형식을 WebP로 바꾸면 로딩 속도를 1.2초 단축할 수 있습니다.&quot;</p>
<h3 id="중점적으로-봐야-할-사항">중점적으로 봐야 할 사항</h3>
<p>LCP → 가장 큰 콘텐츠(이미지, 텍스트)가 화면에 그려지는 시간. 사용자가 &quot;로딩됐다&quot;고 느끼는 핵심 지표
TBT → 메인 스레드가 블로킹된 총 시간. JS 번들이 무거울수록 높아짐
CLS → 레이아웃이 갑자기 틀어지는 정도. 스켈레톤 UI로 바로잡기
FCP → 첫 번째 콘텐츠가 그려지는 시간</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Native - 2장 React Native, 3장 Component]]></title>
            <link>https://velog.io/@seonguul_2/2%EC%9E%A5-React-Native-3%EC%9E%A5-Component</link>
            <guid>https://velog.io/@seonguul_2/2%EC%9E%A5-React-Native-3%EC%9E%A5-Component</guid>
            <pubDate>Thu, 05 Mar 2026 08:15:40 GMT</pubDate>
            <description><![CDATA[<ul>
<li>브릿지 </li>
</ul>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/cd154cc2-8e6e-4b1d-b3be-5da572b93ad2/image.png" alt=""></p>
<p>main thread : UI담당
shadow thread : 레이아웃 계산
native module : 각 모듈에 자체 스레드가 있다.
안드로이드 = 스레드 폴을 공유</p>
<ul>
<li>JSX
xml과 매우 유사하다. UI작업을 할 때 가독성을 준다.
바벨을 사용하여 자바스크립트로 변환한다.</li>
</ul>
<h2 id="2장-react-native">2장 React Native</h2>
<p>맥, 윈도우, 리눅스 환경에서 개발 가능
맥 - ios, 안드로이드 개발 가능
윈도우, 리눅스 - 안드로이드 개발 가능</p>
<p>Node.js, JDK, 안드로이드 스튜디오, 왓치맨, Xcode 추가 설치필요</p>
<p>왓치맨 : 파일 시스템 변경 감지 도구</p>
<h3 id="ios-시뮬레이터를-실행">iOS 시뮬레이터를 실행</h3>
<p>Xcode 메뉴의 Open Developer Tool 메뉴에서 Simulator를 실행하는 방법
맥의 Spotlight 검색 기능에서 Simulator.app</p>
<p>File -&gt; Open Device -&gt; 기기 선택 가능하다.</p>
<ul>
<li><p>project 설치
npx @react-native-community/cli init MyFirstApp(폴더명)</p>
</li>
<li><p>react native 프로젝트 생성하는 방법
1) Expo 
npx expo login
2) Expo 프로젝트 생성
expo init my-first-expo
항상 첫번째 blank 선택
3) cd my-first-expo
npm start
4) app.json 에서 package 추가하기
  &quot;android&quot;: {</p>
<pre><code>&quot;adaptiveIcon&quot;: {
  &quot;backgroundColor&quot;: &quot;#E6F4FE&quot;,
  &quot;foregroundImage&quot;: &quot;./assets/android-icon-foreground.png&quot;,
  &quot;backgroundImage&quot;: &quot;./assets/android-icon-background.png&quot;,
  &quot;monochromeImage&quot;: &quot;./assets/android-icon-monochrome.png&quot;
}</code></pre><p>  },
  &quot;package&quot;: &quot;com.stella03.my-first-expo&quot;,
  &quot;web&quot;: {</p>
<pre><code>&quot;favicon&quot;: &quot;./assets/favicon.png&quot;</code></pre><p>  }
}
}
5) terminal에서 a를 누르면 실행된다.
cmd+control+z 와 cmd+m를 이용해서 메뉴를 열 수 있다.</p>
</li>
</ul>
<p>6) 로그 확인하기
vscode 에서 그냥 npx expo start 해서 
a누르고, 로그 확인 가능</p>
<ul>
<li>리액트 네이티브 CLI 
expo의 단점을 극복한다. expo와 반대로 네이티브 CLI에서 필요한 기능이 있을 경우 모듈을 직접 만들어 사용할 수 있다. 배포가 불편하고, 어렵다.</li>
</ul>
<p>npx react-native init 폴더이름
npm run ios
npm run android 실행할 수 있다.
명령 프롬프트 창 하나가 추가로 열리고 Metro가 실행되는 것을 볼 수 있다.
네이티브를 위한 자바스크립트 번들러이다. 실행될 때마다 단일 파일로 컴파일하는 역할을 한다.</p>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/288d828c-6a89-45a2-9eff-b78418dd6d2f/image.png" alt=""></p>
<h3 id="-90-이상이-expo를-이용하여-진행한다">= 90% 이상이 expo를 이용하여 진행한다.</h3>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/df165010-802b-4c50-856e-9dbac64a6892/image.png" alt=""></p>
<h2 id="3장-component">3장 Component</h2>
<p>View : ui를 구성하는 가장 기본적인 요소, = div와 같은 역할이다.
Fragment : 여러 개의 컴포넌트를 반환하고 싶은 경우 = &lt;&gt; 빈 태그로 사용해도 된다.</p>
<p>jsx의 경우 Null은 가능하지만 undefined는 오류가 발생한다.
{/* 주석 */} </p>
<ul>
<li><p>스타일링</p>
<pre><code>&lt;View style={{ flex: 1, backgroundColor: &#39;#fff&#39;, alignItems: &#39;center&#39;, justifyContent: &#39;center&#39;, }}&gt;
&lt;/View&gt;</code></pre><p>카멜케이스로 작성해야한다.</p>
</li>
<li><p>내장 컴포넌트
리액트 네이티브 컴포넌트 
<a href="https://reactnative.dev/docs/components-and-apis">https://reactnative.dev/docs/components-and-apis</a></p>
</li>
</ul>
<h3 id="커스텀-컴포넌트">커스텀 컴포넌트</h3>
<p>react와 동일하게 components/MyButton.js 생성한다.
TouchableOpacity 컴포넌트 = 버튼 만들 때 사용한다.</p>
<pre><code>import { TouchableOpacity } from &quot;react-native&quot;


export default MyButton = () =&gt; {
    return (
        &lt;TouchableOpacity&gt;
            &lt;Text style={{ fontSize: 24}}&gt;My Button&lt;/Text&gt;
        &lt;/TouchableOpacity&gt;
    )
}</code></pre><h4 id="props와-state">props와 state</h4>
<p>1) props : 부모 컴포넌트로부터 전달된 속성값 혹은 상속받은 속성값을 말한다.</p>
<pre><code>&lt;Button title=&quot;Button&quot;/&gt; 여기서 속성을 지정한다. 부모에서</code></pre><p>자식 컴포넌트인 MyButton.js에서</p>
<pre><code>const MyButton = props =&gt; {
    console.log(props);
    return (...);
}; 이렇게 사용 가능하다.</code></pre><p>3) propTypes OR TypeScript 사용한다.
잘못된 타입을 전달, 필수로 전달해야 하는 값을 전달하지 않아서 문제, 잘못된 props가 전달되었다는 것을 경고 메시지를 통해 알리는 방법</p>
<pre><code>npm install prop-types
설치 하고 

d@Dui-MacBookAir react-native-component % npx react-native start

⚠️ react-native depends on @react-native-community/cli for cli commands. To fix update your package.json to include:


  &quot;devDependencies&quot;: {
    &quot;@react-native-community/cli&quot;: &quot;latest&quot;,
  }</code></pre><p>npx react-native start 하면 런타임 경고를 확인할 수 있다.</p>
<p>PropTypes.string.isRequired, 필수 여부를 지정하는 방법이다.</p>
<pre><code>MyButton.propTypes = {
  title: PropTypes.string.isRequired,
  name: PropTypes.string.isRequired,
};

MyButton.propTypes = {
  title: PropTypes.string.isRequired,
  onPress: PropTypes.func.isRequired,
};</code></pre><p>onPress가 필수로 설정되었기 때문에 MyButton 컴포넌트를 사용할 때 onPress를 전달하도록 수정</p>
<p>4) press 이벤트
특정 DOM을 클릭했을 때 호출되는 onClick 이벤트 </p>
<p>onPressIn : 터치가 시작될 때 항상 호출
onPressOut : 터치가 해제될 때
onPress : 터치 해제될 때 onPressOut 이후 호출
onLongPress : 터치가 일정 시간 이상 지속되면 호출
delayLongPress=[3000] 호출되는 시간 조절 가능</p>
<p>5) change 이벤트</p>
<pre><code>const EventInput = () =&gt; {
    const [text, setText] = useState(&#39;&#39;);
    const _onChange = event =&gt; setText(event.nativeEvent.text);

    return (
        &lt;View&gt;
            &lt;Text style={{ margin: 10, fontSize:30 }}&gt;text: {text}&lt;/Text&gt;
            &lt;TextInput
                style={{ borderWidth: 1, padding: 10, fontSize: 20 }}
                placeholder=&quot;Enter a text...&quot;
                onChange={_onChange}
            /&gt;
        &lt;/View&gt;
    )
}</code></pre><p>onChange를 통해 변화된 텍스트를 전달할 수 있다.
onChangeText는 컴포넌트 텍스트가 변경되었을 때 변경된 텍스트만 인수로 전달하여 호출</p>
<p>6) Pressable 컴포넌트
TouchableOpacity 컴포넌트를 대체한다. 기존보다 다양한 기능을 제공한다.
HitRect, PressRect이다. (정확하게 클릭해야 하기 때문에)<img src="https://velog.velcdn.com/images/seonguul_2/post/c379047c-91a6-43b8-bdbb-9014a8a1c19b/image.png" alt=""></p>
<p>누른 상태에서 벗어나도록 pressRect을 조절한다.</p>
<pre><code>const Button = (props) =&gt; {
    return (
        &lt;Pressable 
            pressRetnetionOffset={{ bottom: 50, left: 50, right: 50, top: 50}}
            hitSlop={50}
        &lt;/Pressable&gt;
    )
};</code></pre><p>PressRect 범위는 HitRect의 범위끝에서 시작되기 때문에 hitSlop의 값에 따라 PressRect의 범위가 달라진다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Typescript - 클래스]]></title>
            <link>https://velog.io/@seonguul_2/Typescript-%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@seonguul_2/Typescript-%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Sat, 10 Jan 2026 12:14:12 GMT</pubDate>
            <description><![CDATA[<h3 id="클래스">클래스</h3>
<p>Typescript에서 클래스 기능은 c#에서 유래된 것이 많다.
컴파일 후에 사라진다. </p>
<p>항상 대문자로 시작
class PersonContext {
  name: string = &quot;&quot;;
  age: number = 0;
  readonly location: string = &quot;Korea&quot;; // readonly 수정 불가능하게 한다.</p>
<h4 id="생성자">생성자</h4>
<p>초기화를 담당한다. -&gt; 수정 가능하다.
 constructor(name: string, age: number) { 인스턴스를 셋팅하고 초기화한다.
    this.name = name // this가 생성될 인스턴스를 바라본다.
    this.age = age
    }
}</p>
<p>const p1 = new PersonContext(‘Jang’, 99); // 인스턴스
Const p2 = new PersonContext(‘Poco’, 100);</p>
<h4 id="인스턴스--실제-건물-유연한-결합">인스턴스 = 실제 건물 (유연한 결합)</h4>
<p>클래스에서 파생된 고유한 것 실제로 생성된 후 메모리에 올라간다.
설계도를 보고 실제로 벽돌을 쌓아 올린 실제 건물이다.
처음부터 끝까지 직접 코드를 짠다. 여러 개를 동시에 가져다 쓸 수 있다.</p>
<h4 id="메서드">메서드</h4>
<p>객체(클래스)에서는 행동을 뜻한다.
함수이기도 하다. 클래스가 가지고 있는 함수
introduce(): string {
    return <code>${this.name}의 나이는 ${this.age}입니다.</code>
} 
console.log(p1.introduce())</p>
<h4 id="getter-setter">Getter, setter</h4>
<p>필드의 접근할 권한을 가진 제어자이다. (외부에서 값이 어떻게 바뀌는지 통제 가능)
getter O / setter X =&gt; 속성은 자동으로 읽기 전용
setter 매개변수의 타입 X / getter의 반환 타입에서 추론
private 속성은 .연산자로 접근할 수 없다.</p>
<p>객체에 속성에 접근할 때 실행되는 함수이다.
get : 값을 읽을 때 실행
set : 값을 쓸 때 실행</p>
<pre><code>class Person {
    name: string;
    private _age: number;

    constructor(name: string, age: number) {
        this.name = name
        this._age = age
    }
    get age() {
        if (this._age === 0) {
            return &#39;설정되지 않았습니다.&#39;
        }
        return `나이는 ${this._age}세로 추정됩니다.`
    }

    set age(age) {
        if (typeof age === &#39;number&#39;) {
            this._age = age    
        }
        this._age = 0;
}

const p = new Person(&#39;Jang&#39; 99); // p가 인스턴스이다. 메모리 어딘가에 값을 담고 메서드를 실행할 수 있는 실체가 생겨난 것이다.
console.log(p.age)
console.log(p.name)</code></pre><h4 id="extends-상속">extends (상속)</h4>
<p>상속, 확장</p>
<pre><code>class 기본 {
    result() {
        return &#39;Base&#39;
    }
}

class 파생 extends 기본 {
    result() {
        return &#39;Derived&#39;
    }
}

const de = new 파생()
console.log(de.result()) // &quot;Drived&quot;</code></pre><h4 id="super">super</h4>
<p>super는 상속받은 부모 클래스를 의미해.
super() → 부모 클래스의 생성자 호출
super.method() → 부모 클래스의 메서드 호출</p>
<pre><code>class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  breed: string;

  constructor(name: string, breed: string) {
    super(name);          // ✅ 부모 생성자 호출 (필수)
    this.breed = breed;
  }
}

const dog = new Dog(&quot;Buddy&quot;, &quot;Poodle&quot;);</code></pre><h4 id="접근-제어자">접근 제어자</h4>
<p>public : 기본값, 어디서든 자유롭게 접근 가능
protected : 해당 클래스와 이를 상속받은 자식 클래스에서만 접근 가능하다.
private : 해당 클래스 내부에서만 접근 가능하다.</p>
<pre><code>class Account {
  public bankName: string = &quot;Hana Bank&quot;; // 누구나 접근 가능
  protected owner: string;              // 자신과 자식 클래스만 가능
  private balance: number;               // 오직 이 클래스 내부에서만

  constructor(owner: string, initialBalance: number) {
    this.owner = owner;
    this.balance = initialBalance;
  }

  public showBalance() {
    console.log(`잔액은 ${this.balance}원입니다.`); // 클래스 내부이므로 접근 가능
  }
}

const myAccount = new Account(&quot;Stella&quot;, 10000);
console.log(myAccount.bankName); // OK
// console.log(myAccount.balance); // Error: private이라서 외부 접근 불가</code></pre><h4 id="static">static</h4>
<p>인스턴스가 아닌, 클래스 자체에 귀속되는 속성이나 메서드이다.
인스턴스 생성하지 않고도 클래스 이름을 통해 바로 호출할 수 있다. 공유해야 하는 고정된 값이나 유틸리티 함수에 사용한다.</p>
<pre><code>class Circle {
  static PI: number = 3.14159; // 모든 원이 공유하는 고정값

  static calculateArea(radius: number) {
    return this.PI * radius * radius;
  }
}

console.log(Circle.PI); // 인스턴스 없이 바로 접근
console.log(Circle.calculateArea(5));</code></pre><h4 id="readonly">readonly</h4>
<p>값을 수정할 수 없게 만드는 제어자이다.</p>
<pre><code>class Person {
  public readonly birthDate: string;

  constructor(date: string) {
    this.birthDate = date; // 초기 할당 가능
  }

  updateBirth() {
    // this.birthDate = &quot;2000-01-01&quot;; // Error: 읽기 전용 속성이라 수정 불가
  }
}</code></pre><h4 id="추상-클래스-abstract--인스턴스-생성-불가능-파생으로-가능">추상 클래스 abstract = 인스턴스 생성 불가능, 파생으로 가능</h4>
<p>abstract를 선언한 클래스로 직접 인스턴스를 생성 불가능하다.
직접 인스턴스화 될 수 없지만 extends후 파생된 클래스를 인스턴스화하도록 유도한다.
추상 클래스는 구현된 메서드를 포함시킬 수 있다.
abstract 선언한 메서드는 파생된 클래스에서 메서드를 구현해야 한다.</p>
<p>= 자식 클래스들이 공통으로 가져야 할 변수나 로직이 있다면 추상 클래스가 훨씬 유리하다.
전체적인 실행 흐름은 같은데, 특정 단계만 다를 때 사용한다.</p>
<pre><code>abstract class GameCharacter {
  constructor(protected hp: number) {}

  // 모든 캐릭터가 공통으로 사용하는 로직
  takeDamage(amount: number) {
    this.hp -= amount;
    console.log(`남은 체력: ${this.hp}`);
  }

  // 공격 방식은 캐릭터마다 다름
  abstract attack(): void;
}</code></pre><pre><code>abstract class Animal {
    // 선언된 메서드
    abstract hello(str: string): string {

    }
    // 구현된 메서드
    run() {
        return this.hello() + &#39;run&#39;
    }
}
// abstract를 붙이면 직접 인스턴스화가 될 수 없다.
const animal = new Animal() 

class Person extends Animal {
// 파생된 클래스는 가능하다.
    hello() {
        return &#39;Person&#39;
    }
}
const person = new Person()  
console.log(person.hello()) // &quot;Person&quot;정상적으로 실행될 수 있다.</code></pre><h4 id="parameter-properties--매개변수를-받는다">Parameter Properties = 매개변수를 받는다.</h4>
<p>typescript에서 필드를 생성하고, 생성자 매개변수를 동일하게 만드는 과정이 귀찮을 때
접근제어자, 생성자 필드 네임, 타입을 생성자 매개변수를 받는 방법</p>
<p>= 매개변수에 다 넣고 this도 생략한다.</p>
<pre><code>class Person {
    constructor(public name: string, private age: number, protected gender: &#39;M&#39; | &#39;F&#39;) { // 매개변수에 넣을 수 있다.
    //this.name = name 
    //this.age = age
    //this.gender = gender
    = this도 생략이 가능하다.
      }

    sayName() { 
        return `이름은 ${this.name} 입니다.`
    }

    protected sayAge() {
        return `나이는 ${this.age}`
        }
    }

</code></pre><h4 id="메서드-오버라이딩">메서드 오버라이딩</h4>
<p>부모 클래스에서 물려받은 메서드를 자신의 용도에 맞게 재정의 하는것이다. 
클래스로 만든것을 확장하더라도 오버라이딩 할 수 있다.</p>
<p>1) extends 사용</p>
<pre><code>class Animal {
    run() {
        return &#39;Animal이 달리다&#39;
    }
}
class Dog extends Animal {
    run() {
        return &#39;Dog이 달리다&#39;
    }
}
class Person extends Animal {
    run() {
        return &#39;Person이 달리다&#39;
    }
}

const p = new Person()
const d = new Dog()

console.log(p.run()) // Animal이 달리다
console.log(d.run())</code></pre><p>2) super 키워드 활용
부모의 기능을 완전히 버리는 것이 아니라, 부모의 기능을 먼저 수행하고, 추가적인 로직을 더하고 싶을 때 super을 사용한다.</p>
<pre><code>class Robot {
  work() {
    console.log(&quot;시스템 가동...&quot;);
  }
}

class CleaningRobot extends Robot {
  work() {
    super.work(); // 부모의 work() 실행 (&quot;시스템 가동...&quot;)
    console.log(&quot;청소를 시작합니다.&quot;); // 자식만의 추가 로직
  }
}</code></pre><h3 id="오버라이딩-vs-오버로딩">오버라이딩 vs 오버로딩</h3>
<ul>
<li><p>오버라이딩 : 상속 관계에서 부모의 메서드를 자신의 입맛에 맞게 재정의한다.
부모의 기능을 무시하거나(덮어쓰기)extends, super를 통해 부모의 기능을 포함하여 확장한다.</p>
</li>
<li><p>오버로딩 : 같은 이름의 메서드를 매개변수만 다르게 여러 방식으로 호출할 수 있게 하는 것이다.
(여러 옵션 쌓기), 매개 변수가 달라야 한다. 다양한 입력 타입에 유연하게 대응한다.</p>
<pre><code>// 1. 오버로드 시그니처 (함수 선언부)
function add(a: string, b: string): string;
function add(a: number, b: number): number;
</code></pre></li>
</ul>
<p>// 2. 함수 구현부 (실제 로직)
function add(a: any, b: any): any {
  return a + b;
}</p>
<p>console.log(add(10, 20));      // 30 (number 타입으로 작동)
console.log(add(&quot;안녕&quot;, &quot;세상&quot;)); // &quot;안녕세상&quot; (string 타입으로 작동)
// console.log(add(10, &quot;안녕&quot;)); // Error: 선언부에 없는 조합은 사용 불가</p>
<pre><code>
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js - Page Router 페이지 라우터]]></title>
            <link>https://velog.io/@seonguul_2/Next.js-Page-Router-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%9D%BC%EC%9A%B0%ED%84%B0</link>
            <guid>https://velog.io/@seonguul_2/Next.js-Page-Router-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%9D%BC%EC%9A%B0%ED%84%B0</guid>
            <pubDate>Thu, 08 Jan 2026 02:01:10 GMT</pubDate>
            <description><![CDATA[<h3 id="페이지-라우터">페이지 라우터</h3>
<p>1) 파일 시스템 기반 라우팅
2) 사전 렌더링 기능
src/pages 폴더 구조 기반 라우팅 </p>
<p>npx <a href="mailto:create-next-app@15.2.3">create-next-app@15.2.3</a> 
즉시 실행, 앱생성, 버전</p>
<ol>
<li>타입스크립트 사용 ? Y</li>
<li>EsLint 사용 ? Y</li>
<li>TailwindCSS 사용 ? Y</li>
<li>JS코드를 src 디렉토리 보관 여부 Y</li>
<li>AppRouter 사용 여부 ? Y/N</li>
<li>Turbopack 사용 여부 ? N</li>
<li>import alias 수정 여부 ? N</li>
</ol>
<p>package.json
dev : Next.js 앱을 개발 모드로 실행
build : Next.js 앱을 빌드한다.
start : 빌드한 Next.js 앱을 실행한다. 프로덕션모드로
lint : EsLint 규칙을 기반으로 한 코드의 품질 검사</p>
<ul>
<li><p>next.config.ts
리액트에서 strictMode 설정 옵션 -&gt; 문제 찾기 위해 컴포넌트 두번 렌더링 -&gt; false</p>
</li>
<li><p>public 폴더 : 정적파일을 보관하는 폴더이다.
src폴더 -&gt; 체계적인 관리를 위해
pages폴더 (라우팅 관리 폴더)</p>
</li>
<li><p>EsLint : 코드의 품지을 유지하고, 코드에 존재하는 잠재 오류를 미리 발견하도록 도와주는 도구
eslint.config.mjs</p>
<pre><code>...compat.extends(&quot;next/core-web-vitals&quot;, &quot;next/typescript&quot;),
{
  rules: {
      &quot;@typescript-eslint/no-unused-vars&quot;:&quot;off&quot;, (사용 x 변수를 오류로 판단)
      &quot;@typescript-eslint/no-explicit-any&quot;:&quot;warn&quot;, -&gt; any명시적으로 사용 못하도록
  }
}</code></pre></li>
</ul>
<h3 id="개발-모드로-실행-npm-run-dev">개발 모드로 실행 npm run dev</h3>
<pre><code>&quot;script&quot;: {
    &quot;dev&quot; : &quot;next dev&quot;, // 앱을 개발 모드로 실행
    &quot;build&quot; : &quot;next build&quot; // 앱을 빌드한다.
    &quot;start&quot; : &quot;next start&quot; // 빌드한 앱을 실행한다.
    &quot;lint&quot; : &quot;next lint&quot; // 코드 품질을 검사한다.
}</code></pre><h3 id="프로덕션-모드로-실행하기-npm-run-build">프로덕션 모드로 실행하기 npm run build</h3>
<ul>
<li>개발 모드와 프로덕션 모드의 차이점
개발 모드 : 핫 리로딩 활성화 -&gt; 자동 새로고침 기능
프로덕션 모드 : 실제 사용자에게 배포할 때 사용 -&gt; 코드 최소화, 불필요한 코드 제거</li>
</ul>
<p>npm run build -&gt; npm run start</p>
<h3 id="라우팅-설정하기">라우팅 설정하기</h3>
<p>브라우저가 요청하는 URL경로에 따라 적절한 페이지를 화면에 렌더링하는 과정이다.</p>
<p>1) 불필요한 CSS 파일 제거 styles/global.css
2) npm run dev
3) 파일시스템 기반 페이지 라우팅 (자동 매핑)
pages/index.tsx // Home 컴포넌트 -&gt; 인덱스 페이지
pages/search.tsx -&gt; search/index.tsx로 변경해서 라우팅 설정 가능하다.
pages/book/index.tsx</p>
<h3 id="eslint-옵션-파일">EsLint 옵션 파일</h3>
<p>코드 품질을 유지하고, 코드에 존재하는 잠재 오류를 미리 발견하도록</p>
<p>_app.tsx 
_document.tsx </p>
<h4 id="1-글로벌-설정을-담당하는-파일-html-문서의-기본-틀">1) 글로벌 설정을 담당하는 파일 HTML 문서의 기본 틀</h4>
<p>Html lang = &quot;ko-KR&quot; 공통 설정 가능하다.</p>
<h4 id="2-_apptsx">2) _app.tsx</h4>
<p>Next.js 앱에서 루트 컴포넌트 역할을 수행한다. 글로벌 설정(공통 제어)</p>
<p>export default function App ({ Component, pageProps }: AppProps ) 현재 page컴포넌트, 페이지 컴포넌트에 제공할 모든 Props가 전달된다. index.tsx</p>
<p>글로벌 헤더 -&gt; _app.tsx에서 생성하기
book/[id].tsx 파일을 생성한다. (경로가 수시로 변경될 경우)
= 옵셔널 캐치올 세그먼트</p>
<h3 id="쿼리스트링과-url파라미터-사용하기">쿼리스트링과 URL파라미터 사용하기</h3>
<h4 id="--쿼리-스트링">- 쿼리 스트링</h4>
<p>경로에서 물음표(?)와 함께 표현, 검색어, 필터링 조건을 전달한다. /search?q=한입</p>
<pre><code>export default function Page() {
    const router = useRouter();
    return (&lt;div&gt;&lt;h1&gt;검색: {router.query.q}&lt;/h1&gt;);
}</code></pre><h4 id="--url-파라미터">- URL 파라미터</h4>
<pre><code>localhost:3000/book/123
export default function Page(){
    const router = useRouter();
    return ( &lt;div&gt;&lt;h1&gt;{touer.query.id}도서 상세 페이지&lt;/h1&gt;&lt;/div&gt;)
}</code></pre><h4 id="--400-500-특수-페이지-라우팅">- 400, 500 특수 페이지 라우팅</h4>
<p>pages/404.tsx 생성 -&gt; 컴포넌트 생성한다.
pages/500일 경우 -&gt; _error.tsx 파일 생성 후 내보낸다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js - 최적화(이미지, 검색 엔진, 사이트맵 생성)]]></title>
            <link>https://velog.io/@seonguul_2/Next.js-%EC%B5%9C%EC%A0%81%ED%99%94%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B2%80%EC%83%89-%EC%97%94%EC%A7%84-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@seonguul_2/Next.js-%EC%B5%9C%EC%A0%81%ED%99%94%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B2%80%EC%83%89-%EC%97%94%EC%A7%84-%EC%82%AC%EC%9D%B4%ED%8A%B8%EB%A7%B5-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Wed, 07 Jan 2026 11:55:30 GMT</pubDate>
            <description><![CDATA[<h3 id="1-이미지-최적화">1. 이미지 최적화</h3>
<p>많은 용량 차지 페이지 로딩 속도 + 사용자 경험</p>
<p>Webp, AVIF 등 차세대 이미지 포맷으로 변환 (브라우저 지원 확인)</p>
<p>사용자 화면에 맞게 이미지 크기 조절 불필요한 데이터 로드 최소화</p>
<h4 id="--이미지-레이지-로딩-적용">- 이미지 레이지 로딩 적용</h4>
<p>스크롤을 올리거나 내려야 보이는 이미지는 나중에 필요할 때 불러오는 기법</p>
<h4 id="--이미지-최적화-올인원-nextimage-컴포넌트">- 이미지 최적화 올인원 next/Image 컴포넌트</h4>
<p>최적화 과정을 자동으로 처리하는 Image 컴포넌트, 기존의 이미지 태그와 유사</p>
<pre><code>export default function Page() {
    return (
        &lt;Image src=&quot;/profile.png&quot; width={500} heigth={500} alt=&quot;Picture of the author&quot;/&gt;
     )
}</code></pre><ul>
<li>개발자도구 network/img탭/preview 탭 하단 이미지 크기 보면 비효율적인 크기 확인
Image 컴포넌트로 대체해서 크기 조절</li>
</ul>
<h4 id="--반응형-이미지">- 반응형 이미지</h4>
<p>Image 컴포넌트에 fill Prop을 설정하면 자동으로 부모 요소를 꽉 채우도록 크기를 조정한다. (부모요소 크기가 설정)</p>
<pre><code>export default function Example() {
    return (
        &lt;div style={{ position: &quot;relative&quot;, width: &quot;100%&quot;, height: &quot;300px&quot; }}&gt;
            &lt;Image src=&quot;/example.jjpg&quot; alt=&quot;Example Image&quot; fill /&gt;
        &lt;/div&gt;
    );
}</code></pre><h4 id="-nextconfigts">-next.config.ts</h4>
<p>외부 서버에서 제공하는 이미지는 허용하도록 설정하기
images.remotePatterns옵션을 추가로 설정</p>
<pre><code>const nextConfig: NextConfig = {
  /* config options here */
  reactStrictMode: false,
  logging: {
    fetches: {
      fullUrl: true,
    }
  },
  images: {
    remotePatterns: [
      {
        protocol: &quot;https&quot;,
        hostname: &quot;shopping-phinf.pstatic.net&quot;,
      }
    ]
  }
};</code></pre><h3 id="2-검색-엔진-최적화-앱-라우터-버전">2. 검색 엔진 최적화 (앱 라우터 버전)</h3>
<p>앱 라우터 버전에서 검색 엔진 최적화(SEO)를 위해 파비콘, 메타 태그, 사이트맵 등 설정 방법을 살펴본다.</p>
<h4 id="썸네일-및-파비콘-이미지-준비">썸네일 및 파비콘 이미지 준비</h4>
<p>favicon.ico, thumbnail.png 파일을 public폴더로 옮긴다.</p>
<h4 id="파비콘-설정">파비콘 설정</h4>
<p>src폴더에 있던 favicon.ico파일 대체하기</p>
<h4 id="인덱스-페이지-메타-태그-설정하기">인덱스 페이지 메타 태그 설정하기</h4>
<pre><code>export const metadata; Metadata = {
    title: &quot;한입북스&quot;,
    description: &quot;한입북스에 등록된 도서를 만나보세요.&quot;,
    openGraph: {
        title: &quot;한입북스&quot;,
        description: &quot;한입북스에 등록된 도서를 만나보세요.&quot;,
        images: [&quot;/thumnail.png&quot;],
    },
 };</code></pre><p>metadata라는 변수를 선언, title, description, openGraph를 메타 데이터로 설정하면 자동 적용
개발자도구 Elements 탭에서 head 태그에 추가 된 메타 태그를 확인할 수 있다.</p>
<h4 id="검색-페이지의-메타-태그-설정">검색 페이지의 메타 태그 설정</h4>
<p>사용자 요청에 따라 실시간으로 바뀌는 쿼리 스트링 값은 반영할 수 없다.
동적 메타 데이터를 생성하는 generateMetadata라는 함수를 활용한다.</p>
<p>page.tsx에서 generateMetadata함수를 선언하고 내보내면 Next.js가 자동으로 함수의 반환값을 해당 페이지의 메타 태그로 설정한다.</p>
<pre><code>export async function generateMetadata({ searchParams, }: {
    searchParams: Promise&lt;{ q?: string }&gt;;
    }) { const {q} = await searchParams;
}    </code></pre><p>검색 페이지의 페이지 컴포넌트를 위와 같이 수정한다.</p>
<h3 id="3-사이트맵-생성하기">3. 사이트맵 생성하기</h3>
<p>구글이나 네이버의 검색 엔진 크롤러가 웹사이트 페이지, 동영상, 이미지 등 콘텐츠를 효율적으로 수집하도록 도와주는 파일이다.</p>
<p>= 웹사이트 페이지 구조를 검색 엔진에 알리는 용도로 사용된다.
사이트맵은 검색 엔진이 웹사이트의 콘텐츠를 더 빠르고 정확하게 크롤링하도록 도와준다.</p>
<p><a href="https://winterload.com/sitemap.xml%EB%A1%9C">https://winterload.com/sitemap.xml로</a> 설정된다</p>
<p>sitemap 함수로 현재 데이터베이스에 등록된 모든 도서 목록 정보를 동적으로 불러온다.</p>
<pre><code>import { BookData } from &quot;@/types&quot;;
import { url } from &quot;inspector&quot;;
import { MetadataRoute } from &quot;next&quot;;

export default async function sitemap(): Promise&lt;MetadataRoute.Sitemap&gt;{
    const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book`, {
        cache: &quot;force-cache&quot;,
    });
    if (!response.ok) throw new Error(response.statusText);

    const allBooks: BookData[] = await response.json();
    return [
        {
            url: &quot;http://localhost:3000&quot;,
            lastModified: new Date(),
        },
        {
            url: &quot;http://localhost:3000/search&quot;,
            lastModified: new Date(),
        },
        ...allBooks.map((book) =&gt; ({
            url: `http://localhost:3000/book/${book.id}`,
            lastModified: new Date(),
        })),
    ]
}</code></pre><p>url : 페이지 주소
lastModified : 페이지를 마지막으로 수정한 날짜 지정
changefreq : 페이지의 변경 빈도 (daily, weekly, monthly)
priority : 상대적 중요도 0.0 ~ 1.0 사이의 값</p>
<p>나중에 배포하기 위해 
local.env에 BASE_URL을 등록하고 sitemap url에 BASE_URL을 넣기!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js - 고급 라우트 기법 (병렬 라우트, 가로채기 라우트)]]></title>
            <link>https://velog.io/@seonguul_2/Next.js-%EA%B3%A0%EA%B8%89-%EB%9D%BC%EC%9A%B0%ED%8A%B8-%EA%B8%B0%EB%B2%95-%EB%B3%91%EB%A0%AC-%EB%9D%BC%EC%9A%B0%ED%8A%B8-%EA%B0%80%EB%A1%9C%EC%B1%84%EA%B8%B0-%EB%9D%BC%EC%9A%B0%ED%8A%B8</link>
            <guid>https://velog.io/@seonguul_2/Next.js-%EA%B3%A0%EA%B8%89-%EB%9D%BC%EC%9A%B0%ED%8A%B8-%EA%B8%B0%EB%B2%95-%EB%B3%91%EB%A0%AC-%EB%9D%BC%EC%9A%B0%ED%8A%B8-%EA%B0%80%EB%A1%9C%EC%B1%84%EA%B8%B0-%EB%9D%BC%EC%9A%B0%ED%8A%B8</guid>
            <pubDate>Wed, 07 Jan 2026 11:12:41 GMT</pubDate>
            <description><![CDATA[<p>병렬 라우트, 가로채기 라우트라는 고급 라우트 기법</p>
<h3 id="병렬-라우트">병렬 라우트</h3>
<p>하나의 화면에서 여러 페이지를 병렬로 렌더링하는 기능이다.
레이아웃이 복잡하거나 멀티태스킹이 필요한 UI를 구현할 때 매우 유용하다.</p>
<p>하나의 화면에 여러 개의 페이지 컴포넌트를 동시에 렌더링한다.</p>
<h4 id="기존-렌더링-방식의-문제점">기존 렌더링 방식의 문제점</h4>
<p>기존의 렌더링 방식 단순히 컴포넌트로 구현하면 특정 섹션에서 오류가 발생,
렌더링에 문제가 생길 경우 페이지 컴포넌트 전체에 예외가 발생한다.</p>
<p>= 오류가 없는 섹션에도 영향을 미치는 상황 발생
try-catch문을 사용할 수 있지만 유지보수성이 떨어진다.
예외처리 로직 중복 사용하면 일관성이 저하, 하위 탐색이 불가능한 경우가 많다. (복잡성)</p>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/5600388b-b584-4239-a2ce-9555f7fa3ef1/image.png" alt=""></p>
<h4 id="병렬-라우트-대시보드-ui구현">병렬 라우트 대시보드 UI구현</h4>
<pre><code>import { ReactNode } from &quot;react&quot;;

export default function Layout({ children }: { children: ReactNode }) {
    return &lt;div&gt;{children}&lt;/div&gt;;
}

// page.tsx
export default function Page() {
    return &lt;div&gt;관리자 페이지&lt;/div&gt;
}</code></pre><p>/admin 페이지로 접속하여 관리자 페이지를 확인한다.</p>
<h4 id="--slot-폴더-생성--props의-key">- @slot 폴더 생성 = Props의 Key</h4>
<p>병렬로 렌더링할 페이지 컴포넌트를 보관하는 폴더이다. @기호를 붙여 특정 폴더를 슬롯으로 지정할 수 있다.</p>
<p>src/app/admin/@notification/page.tsx</p>
<pre><code>export default function Page() {
    return &lt;div&gt;@notification&lt;/div&gt;;
}</code></pre><p>src/app/admin/@user</p>
<pre><code>export default function Page() {
    return &lt;div&gt;@user&lt;/div&gt;;
}</code></pre><p>각각의 페이지 컴포넌트는 Next.js가 자동으로 레이아웃 컴포넌트에 Props로 전달한다.
슬롯 이름이 Props의 Key가 된다.</p>
<h4 id="--layout-컴포넌트에서-props를-사용해-병렬로-렌더링">- layout 컴포넌트에서 props를 사용해 병렬로 렌더링</h4>
<pre><code>import { ReactNode } from &quot;react&quot;;

export default function Layout({
    children,
    notification,
    user,    
}: { children: ReactNode;
    notification: ReactNode;
    user: ReactNode;
}){
    return (
        &lt;div&gt;
            {children}
            {notification}
            {user}
        &lt;/div&gt;
    )
}</code></pre><p>props로 전달한다.
페이지 컴포넌트가 제공되므로 ReactNode타입으로 정의한다. {notification}{user} 등 페이지 컴포넌트를 렌더링한다.</p>
<h4 id="--예외-처리하기">- 예외 처리하기</h4>
<p>@user 슬롯 아래에 error.tsx파일을 생성, 에러가 발생하면 페이지 컴포넌트 대신 렌더링할 컴포넌트를 작성한다.</p>
<pre><code>&quot;use client&quot;;

export default function Error() {
    return &lt;div&gt;오류 발생!&lt;/div&gt;;
}</code></pre><p>해당 슬롯의 페이지 컴포넌트만 마비될 뿐 다른 슬롯이나 레이아웃에는 영향을 미치지 않는다.
오류가 발생한 섹션만 별도로 처리되며, 나머지 섹션과 페이지는 정상적으로 렌더링 된다.</p>
<pre><code>export default function Page() {
    throw new Error();

    return &lt;div&gt;@notification&lt;/div&gt;;
}</code></pre><h4 id="--섹션별로-하위-탐색-구현하기">- 섹션별로 하위 탐색 구현하기</h4>
<p>병렬 라우트를 활용하여 섹션별로 하위 탐색을 구현할 수 있따.
/admin : 대시보드 페이지, 회원 관리 섹션과 알림 섹션을 렌더링
/admin/archived : 대시보드 페이지로 회원 관리 섹션과 보관된 알림 섹션을 렌더링
<img src="https://velog.velcdn.com/images/seonguul_2/post/b7933a18-40da-4f20-a96a-c2c73a7534ef/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/ae4d670f-58fd-456d-a25b-371a688d9a9c/image.png" alt=""></p>
<p>알림을 렌더링하는 페이지 컴포넌트를 정의한다.</p>
<p>현재 접속 주소 ~/admin/archived
슬롯별 페이지 컴포넌트
@notification 슬롯 : ~/admin/@notification/archived/page.tsx
@user 슬롯 : ~/admin/@user/archived/page.tsx
children 슬롯 : ~/admin/archived/page.tsx</p>
<p>다른 슬롯에 불필요한 폴더나 페이지를 추가할 필요 없이 문제를 해결할 수 있다.</p>
<h4 id="--default-컴포넌트-추가하기">- default 컴포넌트 추가하기</h4>
<p>슬롯 폴더에 default.tsx 파일을 만들고 컴포넌트를 정의하면 된다.</p>
<h4 id="--섹션별로-하위-탐색할-때-주의할-사항">- 섹션별로 하위 탐색할 때 주의할 사항</h4>
<p>하나의 슬롯이라도 해당 경로에 렌더링할 페이지 컴포넌트가 없으면 404페이지가 나타난다.
이를 방지하기 위해 경로가 없는 슬롯에는 default 컴포넌트를 정의해야 한다.</p>
<p>= 하드 네비게이션 방식</p>
<p>1) 하드 네비게이션 (SSR) 환경에서 일관된 초기 상태 보장
브라우저의 주소 표시줄에서 URL을 직접 입력하거나 페이지를 새로고침해 이동하는 방식이다.
서버로부터 새로운 페이지를 다시 로드하고 기존의 상태나 UI를 초기화한다.</p>
<p>= 모든 슬롯에서 페이지 컴포넌트 or Default 컴포넌트가 필요하다.
페이지 컴포넌트가 없으면 Default 아니면 404페이지가 나타난다.</p>
<p>2) 소프트 네비게이션
Link컴포넌트 또는 라우터 객체의 navigate메서드 등 클라이언트 사이드 렌더링으로 페이지를 이동하는 방식
기존의 상태를 유지 + 필요한 부분만 업데이트하므로 페이지 이동이 빠르다.</p>
<p>= 현재 경로에 맞는 페이지 컴포넌트가 존재하는 슬롯만 업데이트되며 페이지 컴포넌트가 없는 슬롯은 이전 상태를 그대로 유지한다. 
/admin 페이지의 레이아웃 컴포넌트에서 소프트 네비게이션 방식을 사용한다.</p>
<pre><code>import Link from &quot;next/link&quot;;
import { ReactNode } from &quot;react&quot;;

export default function Layout({
    children,
    notification,
    user,    
}: { children: ReactNode;
    notification: ReactNode;
    user: ReactNode;
}){
    return (
        &lt;div&gt;
            &lt;header style={{ display: &quot;flex&quot;, gap: &quot;10px&quot; }}&gt;
                &lt;Link href={&quot;/admin&quot;} style={{ color: &quot;blue&quot;}}&gt;
                    /admin
                &lt;/Link&gt;
                &lt;Link href={&quot;/admin/archived&quot;} style={{ color: &quot;blue&quot;}}&gt;
                    /admin/archived
                &lt;/Link&gt;
            &lt;/header&gt;
            &lt;br /&gt;
            {children}
            {notification}
            {user}
        &lt;/div&gt;
    );
}</code></pre><h3 id="가로채기-라우트-하드-or-소프트-고르기">가로채기 라우트 (하드 or 소프트) 고르기</h3>
<p>특정 경로에 사용자가 소프트 네비게이션 방식으로 접근했을 때, 해당 요청을 가로채 원래 렌더링할 페이지 컴포넌트 대신 다른 페이지 컴포넌트를 렌더링하는 기능이다.</p>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/64970b8c-f735-4bbe-9c20-7b7ee2a2de21/image.png" alt=""></p>
<p>동일한 페이지라도 사용자의 접근 방식에 따라 다른 UI제공할 수 있다.
피드 형식의 SNS 서비스에서 자주 활용된다.</p>
<h4 id="가로채기-라우트-적용-방법">가로채기 라우트 적용 방법</h4>
<p>app 폴더에 가로채는 폴더 생성 -&gt; page.tsx 생성 -&gt; 원래 페이지 컴포넌트 대신 화면에 렌더링할 페이지 컴포넌트를 만들어야 한다.</p>
<p>= (.)폴더는 가로채기 폴더로 인식</p>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/1ee19f67-9f57-4b2d-9faa-1460f92fe7e1/image.png" alt=""></p>
<ul>
<li><p>소괄호 : 가로채기 라우트를 위한 폴더임 명시
photo/[id]/page.tsx
(.)photo/[id]/page.tsx</p>
</li>
<li><p>마침표(.) : 동일한 경로에 있는 폴더의 가로채기임을 명시한다.
(..)photo/ 한단계 위에 있는 app 폴더를 가로채기 하는 방법
(..)(..)photo/ 두 단계 위에 위치한 페이지를 가로채기 하는 방법
(...)photo/ 루트 폴더를 기준으로 가로채기 할 수 있다.</p>
</li>
</ul>
<h4 id="도서-상세-페이지에-가로채기-라우트">도서 상세 페이지에 가로채기 라우트</h4>
<p>book/[id] 경로의 page.tsx를 가로채기 위해
src/app/(.)book/[id]/page.tsx 로 도서 상세페이지에 접근한다.</p>
<h4 id="모달-구현하기">모달 구현하기</h4>
<p>가로채기 라우트가 동작했을 때 기존 페이지 컴포넌트를 모달 형태로 렌더링한다.</p>
<p>Modal 컴포넌트는 일반적으로 독립적이기 때문에 최상단에 렌더링하는 것이 바람직한다. 루트 레이아웃에서 렌더링하면 레이아웃 스타일에 영향을 받거나 의도치 않은 스타일 충돌을 일으킬 수 있다.</p>
<h4 id="--createportal-메서드를-이용">- createPortal 메서드를 이용</h4>
<p>특정 컴포넌트를 트리에섯 분리 HTML구조의 원하는 위치에 렌더링한다.
app/layout.tsx</p>
<pre><code>return createPortal (
        &lt;div className={style.backdrop} onClick={(e) =&gt; {
            if (e.target === e.currentTarget) {
                router.back();
            }
        }}&gt;
            &lt;div className={style.modal}&gt;{children}&lt;/div&gt;
        &lt;/div&gt;,
        document.getElementById(&quot;modal-root&quot;) as HTMLElement
);</code></pre><p>createPortal 메서드를 불러온다. 첫 번째 인수 : 렌더링하려는 UI 요소
두 번째 인수 : 첫 번째 인수로 전달하는 UI 요소를 렌더링 할 위치 (modal-root)</p>
<h3 id="가로채기-라우트와-병렬-라우트-함께-사용하기">가로채기 라우트와 병렬 라우트 함께 사용하기</h3>
<p>모달을 렌더링해도 종전에 탐색하던 페이지를 뒷배경에 나오도록 구현하려면 모달과 기존 페이지를 동시에 렌더링하는 방식으로 구조를 수정해야 한다. </p>
<p>= 두 개 이상의 페이지를 동시에 렌더링하도록 설정해야 함</p>
<p>도서 상세 페이지 @modal
종전 페이지 children슬롯으로 설정해 하나의 페이지에서 동시에 렌더링 가능</p>
<p>src/app/@modal/(.)book/[id]/page.tsx
@modal에서 보여줄 default 컴포넌트 생성</p>
<p>src/app/book/[id]/page.tsx</p>
<ul>
<li>app/layout.tsx
children, modal이라는 두 슬롯의 페이지 컴포넌트가 props으로 제공된다. 
{modal} children 슬롯의 페이지 컴포넌트와 함께 렌더링한다.</li>
</ul>
<h4 id="도서-상세-페이지-로딩-ui설정">도서 상세 페이지 로딩 UI설정</h4>
<p>로딩중 표시하는 로딩 UI를 추가하고 스트리밍 방식으로 페이지를 로딩하는 동안 사용자에게 로딩 UI를 보여 주도록 설정한다.</p>
<p>도서 상세 페이지를 Suspense컴포넌트로 감싸고, fallback Prop의 값으로 &#39;로딩 중&#39;이라는 div태그 전달</p>
<pre><code>import Modal from &quot;@/components/modal&quot;;
import BookPage from &quot;@/app/book/[id]/page&quot;;
import { Suspense } from &quot;react&quot;;

export default function Page(props: any) {
    return (
        &lt;Modal&gt;
            &lt;Suspense fallback={&lt;div&gt;로딩 중&lt;/div&gt;}&gt;
                &lt;BookPage {...props}/&gt;;
            &lt;/Suspense&gt;
        &lt;/Modal&gt;
    )
}</code></pre><h4 id="--skeleton-ui-설정하기">- skeleton UI 설정하기</h4>
<p>fallback prop의 값을 BookPageSkeleton 컴포넌트로 설정한다.</p>
<pre><code>&lt;Suspense fallback={&lt;BookPageSkeleton /&gt;}&gt;
    &lt;BookPage {...props}/&gt;
&lt;/Suspense&gt;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js - 서버 액션]]></title>
            <link>https://velog.io/@seonguul_2/Next.js-6.-%EC%84%9C%EB%B2%84-%EC%95%A1%EC%85%98</link>
            <guid>https://velog.io/@seonguul_2/Next.js-6.-%EC%84%9C%EB%B2%84-%EC%95%A1%EC%85%98</guid>
            <pubDate>Tue, 06 Jan 2026 10:26:23 GMT</pubDate>
            <description><![CDATA[<h3 id="서버-액션이란">서버 액션이란?</h3>
<p>로그인, 회원 가입, 로그아웃 등 서버에서 이루어지는 작업을 쉽고 간단하게 구현할 수 있다.
서버에서 실행되는 비동기 함수 가운데 클라이언트가 직접 호출할 수 있는 함수를 말한다.</p>
<p>로그인, 회원 가입, 데이터 생성처럼 서버에서 이루어지는 작업을 쉽고 간단하게 구현할 수 있다.</p>
<pre><code>export default function Page() {
    const loginAction = async () =&gt; {
        &quot;use server&quot;
        console.log(&quot;서버 액션 loginAction 호출!&quot;);
    };

    return (
        &lt;form action={loginAction}&gt;
            &lt;input type=&quot;text&quot; name=&quot;id&quot; /&gt;
            &lt;input type=&quot;password&quot; name=&quot;password&quot; /&gt;
            &lt;button type=&quot;submit&quot;&gt;로그인&lt;/button&gt;
        &lt;/form&gt;
    )
}

// 비동기 함수 loginAction을 만든다. -&gt; use server라는 지시자를 작성 (서버액션)으로 설정
// 폼을 제출했을 때 loginAction을 호출하도록 설정한다.</code></pre><p>사용자가 폼에 작성한 아이디와 패스워드는 서버 액션에 폼 데이터 형태의 인수로 전달된다. (폼 데이터)
따라서 서버 액션에서 매개변수로 사용자가 입력한 값을 꺼내 사용할 수 있다.</p>
<pre><code>const loginAction = async (formData: FormData) =&gt; {
        &quot;use server&quot;

        const id = formData.get(&quot;id&quot;);
        const password = formData.get(&quot;password&quot;);

        console.log({ id, password });

    };</code></pre><p>서버 액션은 브라우저에서 호출할 수 있는 Next.js 서버의 비동기 함수이다.
로그인이나 회원 가입 등과 같은 서버 동작을 쉽고 간단하게 구현할 수 있다.</p>
<p>= 복잡한 과정을 줄일 수 있다. 개발 생산성이 높아진다. 브라우저에 코드가 노출되지 않아 보안이 중요한 작업에 적합하다.</p>
<p>로직이 복잡하거나 대규모 통합 작업이 필요한 경우, 전통적인 API 설계 방식이 적합할 수 있다.
서버 액션은 간단한 서버 동작 or 클아이언트와 서버의 상호작용을 빠르게 구현할 때 유용한 선택이 될 수 있다.</p>
<ul>
<li>DB와 연결해 SQL문을 직접 실행<pre><code>// SQL문을 직접 호출하는 액션 예시
const createPostAction = async(formData:FormData) =&gt; {
  &#39;use server&#39;
  const content = formData.get(&quot;content&quot;);
  await sql &#39;INSERT INTO BOARD (content) VALUES (content)&#39;;
}</code></pre><h3 id="서버-액션-동작">서버 액션 동작</h3>
Next.js의 서버 액션은 클라이언트가 POST메서드를 이용해 Next.js서버에게 HTTP요청을 보내는 방식으로 동작한다.</li>
</ul>
<p>개발자도구 -&gt; 네트워크탭 -&gt; Fetch/XHR필터 활성화 -&gt; 폼 제출</p>
<p>Headers탭 -&gt; Request Headers에서 Next-Action에</p>
<p>next-action (호출하려는 서버 액션의 고유 아이디) Next.js가 자동생성
407ca5dd41991d4004ed0cd806aef03b2d052310c2</p>
<p>클라이언트가 서버에 요청할 때 함께 전송되어 어떤 서버 액션을 실행할지 Next.js 서버가 정확히 판단할 수 있도록 한다.</p>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/419a6a1c-4389-46e7-a2a4-87d1fde2a01a/image.png" alt=""></p>
<p>복잡한 HTTP요청 코드를 직접 작성하지 않고 마치 일반 함수처럼 서버 액션 선언 및 호출 -&gt; 비동기 로직 손쉽게 실행할 수 있다.</p>
<h3 id="서버-액션에서-주의할-사항">서버 액션에서 주의할 사항</h3>
<p>1) 꼭 form태그를 이용해야 하는 것은 아니다.
서버 액션을 별도의 파일로 분리, 페이지 컴포넌트를 클라이언트 컴포넌트로 설정한다.
이벤트 핸들러를 사용해 서버 액션 호출</p>
<pre><code>&quot;use client&quot;;

import { useRef } from &quot;react&quot;;
import { loginAction } from &quot;./login.action&quot;;

export default function Page() {
    const idRef = useRef&lt;HTMLInputElement&gt;(null);
    const pwRef = useRef&lt;HTMLInputElement&gt;(null);

    const onCLickLogin = async () =&gt; {
        if (!idRef.current || !pwRef.current) return;
        const id = idRef.current.value;
        const password = pwRef.current.value;

        const formData = new FormData();
        formData.set(&quot;id&quot;, id);
        formData.set(&quot;password&quot;, password);
        await loginAction(formData);
    };

    return (
        &lt;div&gt;
            &lt;input ref={idRef} type=&quot;text&quot; name=&quot;id&quot; /&gt;
            &lt;input ref={pwRef} type=&quot;password&quot; name=&quot;password&quot; /&gt;
            &lt;button onClick={onCLickLogin}&gt;로그인&lt;/button&gt;
        &lt;/div&gt;
    )
}</code></pre><p>아이디와 패스워드를 받은 input태그를 idRef와 pwRef가 참조하도록 설정,</p>
<p>2) 비동기 함수로 만들어야 한다.
서버 액션은 반드시 비동기 함수로 구현해야 한다. 안그러면 오류 발생!!</p>
<h3 id="서버-액션으로-리뷰-기능-구현">서버 액션으로 리뷰 기능 구현</h3>
<p>특정 도서의 리뷰를 작성하는 기능 구현
도서 상세 페이지 컴포넌트에서 백엔드 서버 도서 데이터를 불러와 렌더링하는 기능을 별도의 컴포넌트로 분리
기능을 분리하면 이후에 리뷰를 손쉽게 추가하고 관리할 수 있다.</p>
<pre><code>// 리뷰를 작성하는 컴포넌트, 서버 액션을 사용해 새로운 리뷰 추가 
function ReviewEditor() {
  const createReviewAction = async (formData: FormData) =&gt; {
    &quot;use server&quot;;

    const content = formData.get(&quot;content&quot;);
    const author = formData.get(&quot;author&quot;);

    console.log({ content, author });

  }
  return (
    &lt;section&gt;
      &lt;form action={createReviewAction}&gt;
      &lt;input name=&quot;bookId&quot; value={bookId} type=&quot;hidden&quot; readOnly /&gt;
        &lt;input required name=&quot;content&quot; placeholder=&quot;리뷰 내용&quot; /&gt;
        &lt;input required name=&quot;author&quot; placeholder=&quot;작성자&quot; /&gt;
        &lt;button type=&quot;submit&quot;&gt;작성하기&lt;/button&gt;
      &lt;/form&gt;
    &lt;/section&gt;
  )
}</code></pre><p>비동기 함수 createReviewAction을 만들고 함수의 최상단에 &quot;use server&quot;지시자를 작성해 함수를 서버 액션으로 설정한다.
input태그에 required속성을 추가하면 내용이 없으면 폼을 제출하지 못한다.</p>
<p>localhost:8080/api로 접속 API문서를 살펴보면
/review탭에서 새 리뷰를 생성하는 API가 있음을 확인할 수 있다.</p>
<p>HTTP메서드 : POST
요청 Body필드 : bookId(도서아이디), content(리뷰 내용), author(리뷰 작성자 이름)</p>
<h4 id="--서버-액션-도서-아이디를-서버-액션에-포함된-폼-데이터와-함께-전달">- 서버 액션, 도서 아이디를 서버 액션에 포함된 폼 데이터와 함께 전달</h4>
<pre><code>&lt;input name=&quot;bookId&quot; value={bookId} type=&quot;hidden&quot; readOnly /&gt; </code></pre><p>페이지에서 보이지 않도록 hidden으로 설정한다. readOnly속성으로 값이 수정되지 않도록 한다.</p>
<pre><code>{ content: &#39;아 좋타&#39;, author: &#39;stella&#39;, bookId: &#39;1&#39; } </code></pre><p>form태그에 추가해서 서버 액션에 도서 아이디 값을 전달하는지 확인</p>
<h4 id="--db에-새로운-리뷰를-추가한다">- DB에 새로운 리뷰를 추가한다.</h4>
<p>API에러 예외상황 처리 try-catch문 작성JSON으로 직렬화 전송한다.</p>
<pre><code>try {
      if (!bookId || !content || !author) throw new Error(&quot;잘못된 요청입니다.&quot;);
      const response = await fetch(
        `${process.env.NEXT_PUBLIC_API_URL}/review`, {
          method: &quot;POST&quot;,
          body: JSON.stringify({ bookId, content, author}),
        })
        if(!response.ok) throw new Error(response.statusText);
    } catch (e) {
      console.error(e);
    }</code></pre><p>server에서 npx prisma studio에서 확인 가능 
createdAt 두번 클릭하면 최신순 정렬</p>
<h3 id="리뷰-조회-및-갱신-기능-구현">리뷰 조회 및 갱신 기능 구현</h3>
<p>DB에 등록한 리뷰를 불러와 리스트로 렌더링 -&gt; 컴포넌트의 자식으로 배치
HTTP 메서드 : GET
요청주소 /review/book/[도서 아이디]
응답 데이터 </p>
<pre><code>[
  {
    &quot;id&quot;: 0,
    &quot;content&quot;: &quot;string&quot;,
    &quot;author&quot;: &quot;string&quot;,
    &quot;createdAt&quot;: &quot;2026-01-06T05:21:08.879Z&quot;,
    &quot;bookId&quot;: 0
  }
]</code></pre><p>types.ts에 타입정의한 뒤 ReviewList에서 review data를 불러오는 기능 추가</p>
<pre><code>async function ReviewList({ bookId }: { bookId: string }) {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_URL}/review/book/${bookId}`);
    if (!response.ok) throw new Error(response.statusText);

    const reviews: ReviewData[] = await response.json();
    console.log(reviews);

  return &lt;section&gt;&lt;/section&gt;;
}</code></pre><p>~/book/1로 접속하면 백엔드 서버에서 해당 도서의 모든 리뷰 데이터를 성공적으로 가져온다.</p>
<h4 id="리뷰-리스트-렌더링하기">리뷰 리스트 렌더링하기</h4>
<ul>
<li><p>reviewItem 컴포넌트 만들기</p>
<pre><code>export default function ReviewItem({
  id,
  content,
  author,
  createdAt,
  bookId,
}: ReviewData) {
  return (
      &lt;div className={style.container}&gt;
          &lt;div className={style.author}&gt;{author}&lt;/div&gt;
          &lt;div className={style.content}&gt;{content}&lt;/div&gt;
          &lt;div className={style.bottom_container}&gt;
              &lt;div className={style.date}&gt;{new Date(createdAt).toLocaleString()}&lt;/div&gt;
          &lt;/div&gt;
          &lt;div className={style.delete_btn}&gt;삭제하기&lt;/div&gt;
      &lt;/div&gt;
  )
}</code></pre></li>
<li><p>reviewItem 컴포넌트 reviewList에 불러오기</p>
<pre><code>return (
  &lt;section&gt;
    {reviews.map((review) =&gt; (
      &lt;ReviewItem key={`review-item-${review.id}`} {...review}/&gt;
    ))}
  &lt;/section&gt;
)</code></pre></li>
</ul>
<h3 id="리뷰-갱신-기능-구현하기">리뷰 갱신 기능 구현하기</h3>
<p>새로운 리뷰를 추가해도 실시간으로 데이터를 페이지에 반영하지 않는다.  -&gt; 새로고침 필요
ReviewEditor 컴포넌트에서 호출한 서버 액션이 수행 -&gt; DB에 리뷰가 추가되면 페이지 컴포넌트 Next.js서버 
-&gt; 다시 렌더링해서 데이터 반영 -&gt; 화면 갱신</p>
<p>= 서버 컴포넌트이기 때문에</p>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/daaf76aa-1efd-4325-a0d1-edfdfb91ac2c/image.png" alt=""></p>
<h4 id="--revalidatepath">- revalidatePath</h4>
<p>특정 경로의 캐시 데이터를 모두 제거하는 기능 = 풀 라우트 캐시와 데이터 캐시를 무효화 한다.
주로 Next.js 서버에서만 호출할 수 있다.</p>
<pre><code>import { revalidatePath } from &quot;next/cache&quot;;

revalidatePath(&quot;/&quot;) // &quot;/&quot;경로에 해당하는 인덱스 페이지의 풀 라우트 캐시와 데이터 캐시를 무효화</code></pre><p>동적 경로의 모든 캐시 또는 앱 전체의 모든 캐시를 무효화하는 확장 기능도 제공한다.</p>
<h4 id="--revalidatepathpath-type--특정-경로의-모든-캐시">- revalidatePath(path, type) = 특정 경로의 모든 캐시</h4>
<p>캐시 무효화의 범위를 설정하는 두 번째 인수 type
revalidatePath(&quot;book/[id]&quot;, &quot;page&quot;) // &quot;book/[id]&quot; 경로의 캐시 무효화
첫번째 경로에 포함되는</p>
<pre><code>revalidatePath(&#39;/(with-searchbar)&#39;, &#39;layout&#39;)
// (with-searchbar)/layout.tsx 파일의 레이아웃 컴포넌트가 적용되는
// 모든 경로의 캐시를 무효화한다.</code></pre><p>/search 페이지의 캐시도 모두 무효화된다.</p>
<pre><code>revalidatePath(&#39;.&#39;, &#39;layout&#39;)
// Next.js 앱의 캐시를 모두 무효화한다.</code></pre><h4 id="--revalidatetag--특정-태그가-있는-데이터-캐시">- revalidateTag = 특정 태그가 있는 데이터 캐시</h4>
<p>특정 태그가 있는 데이터 캐시를 무효화한다.</p>
<pre><code>fetch(url, { next: {tags: [&quot;a&quot;]} });
// &quot;a&quot; 태그를 설정
revalidateTag(&quot;a&quot;) // a태그 데이터 캐시를 모두 무효화</code></pre><h3 id="리뷰-갱신-기능-업그레이드">리뷰 갱신 기능 업그레이드</h3>
<ol>
<li>BookDetail 컴포넌트 : 도서의 상세 정보를 불러오는 fetch 메서드
변경되지 않으므로 캐시 무효화 불필요</li>
<li>ReviewList 컴포넌트 : 리뷰 데이터를 불러오는 fetch 메서드
새 리뷰를 추가했을 때 무효화할 캐시 -&gt; 리뷰 데이터 캐시</li>
</ol>
<p>둘 중 하나만 캐시를 무효화하기 위해 revalidateTag를 사용
reviewList 컴포넌트의 fetch 메서드에서 캐시 옵션 추가 </p>
<pre><code>async function ReviewList({ bookId }: { bookId: string }) {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_API_URL}/review/book/${bookId}`,
    { next: { tags: [`review`]}}); // 리뷰 캐시의 데이터 옵션 </code></pre><p>reviewEditor에 revalidateTag 추가</p>
<pre><code>revalidateTag(`review-${bookId}`); </code></pre><p>review-${bookId}태그가 있는 데이터 캐시를 무효화하도록 변경한다. 갱신 범위를 최소화하면서 필요한 부분은 정확히 갱신한다.</p>
<h3 id="리뷰-추가-및-갱신-기능-업그레이드-로딩-오류-출력">리뷰 추가 및 갱신 기능 업그레이드 (로딩, 오류 출력)</h3>
<h4 id="revieweditor-컴포넌트---클라이언트-컴포넌트로-전환">ReviewEditor 컴포넌트 -&gt; 클라이언트 컴포넌트로 전환</h4>
<p>서버 컴포넌트 -&gt; client 컴포넌트 전환 -&gt; 서버 액션 별도의 파일로 분리</p>
<p>create-review.action.js</p>
<pre><code>&quot;use server&quot;;
import { revalidateTag } from &quot;next/cache&quot;;

const createReviewAction = async (formData: FormData) =&gt; {

    const bookId = formData.get(&quot;bookId&quot;);
    const content = formData.get(&quot;content&quot;);
    const author = formData.get(&quot;author&quot;);

    // api를 호출 DB에 리뷰 추가
    try {
      if (!bookId || !content || !author) throw new Error(&quot;잘못된 요청입니다.&quot;);
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/review`,
        {
          method: &quot;POST&quot;,
          body: JSON.stringify({ bookId, content, author }),
        }
      );
      if (!response.ok) throw new Error(response.statusText);
      revalidateTag(`review-${bookId}`); // 특정 경로의 캐시 데이터 제거
    } catch (e) {
      console.error(e);
    }
  };

export default createReviewAction;</code></pre><p>client 컴포넌트로 전환하기 위해 서버 액션 createReviewAction을 별도의 파일로 분리했다.</p>
<h4 id="useactionstate를-이용해-form-액션-상태-관리">useActionState를 이용해 form 액션 상태 관리</h4>
<p>ReviewEditor 컴포넌트를 클라이언트 컴포넌트로 전환 
서버 액션 결과에 따라 사용자에게 적절한 피드백을 제공한다.</p>
<p>ReviewEditor 컴포넌트에서 서버 액션의 결괏값을 추적할 수 있어야 한다.
useActionState훅을 사용해 구한다. form 액션의 실행 결과, 로딩 상태 State로 불러와 사용 가능</p>
<h4 id="useactionstate">useActionState</h4>
<pre><code>const [state, action, isPending] = useActionState(fn, initialState, permalink?);
</code></pre><p>fn : function 액션 함수 전달
initialState : 액션 상태의 초깃값 전달
permalink : form 제출 또는 액션 실행 이후 이동하려는 URL을 전달한다.</p>
<p>state : 첫 번째 요소 form 액션의 결괏값을 저장하는 상태
action : 액션을 발생시키는 함수이다. 전달한 액션 함수(fn)을 실행하여 결괏값 추적
isPending : 현재 액션의 진행 여부 로딩 상태 반환</p>
<pre><code>const [state, action, isPending] = useActionState(createReviewAction, null);
    // state : 현재 상태, action 발생시키는 함수, isPending : 액션의 로딩 상태</code></pre><p>prevState: unknown 이전 상태를 나타내는 값, 특정 로직에서 활용 하지 않으면 unknown 타입으로 설정</p>
<pre><code>&quot;use server&quot;;
import { revalidateTag } from &quot;next/cache&quot;;

const createReviewAction = async (prevState: unknown, formData: FormData) =&gt; {

    const bookId = formData.get(&quot;bookId&quot;);
    const content = formData.get(&quot;content&quot;);
    const author = formData.get(&quot;author&quot;);

    // api를 호출 DB에 리뷰 추가
    try {
      if (!bookId || !content || !author) throw new Error(&quot;잘못된 요청입니다.&quot;);
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/review`,
        {
          method: &quot;POST&quot;,
          body: JSON.stringify({ bookId, content, author }),
        }
      );
      if (!response.ok) throw new Error(response.statusText);
      revalidateTag(`review-${bookId}`); // 특정 경로의 캐시 데이터 제거
    } catch (e) {
      console.error(e);
    }
  };

export default createReviewAction;</code></pre><h4 id="서버-액션의-결괏값-이용하기">서버 액션의 결괏값 이용하기</h4>
<p>서버 액션의 결괏값이 State에 저장되는지 확인 </p>
<pre><code>&quot;use server&quot;;
import { revalidateTag } from &quot;next/cache&quot;;

const createReviewAction = async (prevState: unknown, formData: FormData) =&gt; {

    const bookId = formData.get(&quot;bookId&quot;);
    const content = formData.get(&quot;content&quot;);
    const author = formData.get(&quot;author&quot;);

    // api를 호출 DB에 리뷰 추가
    try {
      if (!bookId || !content || !author) throw new Error(&quot;잘못된 요청입니다.&quot;);
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/review`,
        {
          method: &quot;POST&quot;,
          body: JSON.stringify({ bookId, content, author }),
        }
      );
      if (!response.ok) throw new Error(response.statusText);
      revalidateTag(`review-${bookId}`); // 특정 경로의 캐시 데이터 제거

      return {
        status: true,
        message: &quot;리뷰를 성공적으로 추가했습니다.&quot;,
      };
    } catch (e) {
      return {
        status: false,
        message: `새로운 리뷰를 추가하지 못했습니다: ${e}`,
      };
    }
  };

export default createReviewAction;
</code></pre><p>review-editor.tsx에서 
useEffect를 사용하여 state의 값이 변경될 때마다 브라우저 콘솔에 출력하도록 설정한다.</p>
<pre><code>&quot;use client&quot;;

import { useActionState, useEffect } from &quot;react&quot;;
import style from &quot;./review-editor.module.css&quot;;
import createReviewAction from &quot;@/actions/create-review.action&quot;;

export default function ReviewEditor({ bookId }: { bookId: string }) {
    // useActionState 사용
    const [state, action, isPending] = useActionState(createReviewAction, null);
    // state : 현재 상태, action 발생시키는 함수, isPending : 액션의 로딩 상태

    useEffect(() =&gt; {
        console.log(state);

    }, [state]); // state값이 변경될 때마다 콘솔에 출력
    return (
    &lt;section className={style.container}&gt;
      &lt;form action={action}&gt;
        &lt;input name=&quot;bookId&quot; value={bookId} type=&quot;hidden&quot; readOnly /&gt;
        &lt;textarea required name=&quot;content&quot; placeholder=&quot;리뷰 내용&quot; /&gt;
        {/* bookId값으로 설정 =&gt; 서버 액션에 폼을 제출할 때 도서 아이디를 bookId로 전달 가능 */}
        &lt;div className={style.submit_container}&gt;
          &lt;input required name=&quot;author&quot; placeholder=&quot;작성자&quot; /&gt;
          &lt;button type=&quot;submit&quot;&gt;작성하기&lt;/button&gt;
        &lt;/div&gt;
      &lt;/form&gt;
    &lt;/section&gt;
  );
}
</code></pre><h4 id="로딩-ui-설정하기">로딩 UI 설정하기</h4>
<p>useActionState의 isPending에는 현재 액션의 로딩 상태를 저장한다.
isPending을 이용해 로딩 UI를 설정할 수 있다.</p>
<pre><code>&lt;textarea required={isPending} name=&quot;content&quot; placeholder=&quot;리뷰 내용&quot; /&gt;
  {/* bookId값으로 설정 =&gt; 서버 액션에 폼을 제출할 때 도서 아이디를 bookId로 전달 가능 */}
    &lt;div className={style.submit_container}&gt;
    &lt;input required={isPending} name=&quot;author&quot; placeholder=&quot;작성자&quot; /&gt;
&lt;button disabled={isPending}type=&quot;submit&quot;&gt;작성하기&lt;/button&gt;</code></pre><p>리뷰 내용, 작성자, 작성하기 disabled={isPending}으로 설정</p>
<h4 id="리뷰-삭제-기능-구현하기">리뷰 삭제 기능 구현하기</h4>
<p>서버 액션을 호출해 DB에서 특정 리뷰 데이터를 삭제하도록 설정</p>
<pre><code>&lt;form&gt;
    &lt;div className={style.delete_btn}&gt;삭제하기&lt;/div&gt;
&lt;/form&gt;</code></pre><p>form 태그로 감싸야 useActionState와 같은 훅으로 서버 액션의 결과를 추적하고 로딩 상태를 효율적으로 관리</p>
<p>= 삭제하기 버튼만 클라이언트 컴포넌트로 전환하는 게 효율적인 문제 해결 방법이다.</p>
<pre><code>&quot;use client&quot;;

export default function DeleteReviewItemButton() {
    return (
        &lt;form&gt;
            &lt;div&gt;삭제하기&lt;/div&gt;
        &lt;/form&gt;
    );
}</code></pre><ul>
<li>DB리뷰 삭제할 서버 액션<pre><code>&quot;use server&quot;;
</code></pre></li>
</ul>
<p>import { revalidateTag } from &quot;next/cache&quot;;</p>
<p>const deleteReviewAction = async (prevState: unknown, formData: FormData) =&gt; {
    const reviewId = formData.get(&quot;reviewId&quot;);
    const bookId = formData.get(&quot;bookId&quot;);
    // 삭제할 아이디, 도서 아이디 추출 </p>
<pre><code>if (!reviewId) {
    return {
        status: false,
        message: &quot;삭제할 리뷰가 없습니다.&quot;,
    };
}

try {
    const response = await fetch(
        `${process.env.NEXT_PUBLIC_API_URL}/review/${reviewId}`, // 실제 리뷰 삭제 API
        {
            method: &quot;DELETE&quot;,
        }
    );

    if(!response.ok){ throw new Error(response.statusText);

    }

    revalidateTag(`review-${bookId}`); // 페이지에 반영 review-${bookId} 태그가 있는 데이터 캐시 무효화
    return {
        status: true,
        message: &quot;&quot;,
    };
} catch(err) {
    return {
        status: false,
        message: `리뷰 삭제에 실패했습니다: ${err}`,
    };
}</code></pre><p>};</p>
<p>export default deleteReviewAction;</p>
<pre><code>
- delete-review-item-button에
도서 아이디와 리뷰 아이디를 props로 모두 받아온다.</code></pre><p>&quot;use client&quot;;</p>
<p>export default function DeleteReviewItemButton({
    bookId,
    reviewId,
}: {
    bookId: number;
    reviewId: number;
}) {
    return (
        <form>
            <div>삭제하기</div>
            <input name="bookId" value={bookId} type="hidden" readOnly />
            <input name="reviewId" value={reviewId} type="hidden" readOnly />
        </form>
    )
}</p>
<pre><code>서버 액션을 호출할 때 formData에 포함시켜 전달할 값을 설정할 수 있다.

- div 태그를 유지해야 하는 경우 
div를 클릭했을 때 form 태그를 프로그래매틱하게 제출할 수 있다.
</code></pre><pre><code>const formRef = useRef&lt;HTMLFormElement&gt;(null);
return (
    &lt;form ref={formRef}&gt;
        &lt;div onClick={() =&gt; {
            if (formRef.current) formRef.current.requestSubmit(); // useRef를 불러온다. 객체를 생성하고 변수에 저장한다.
        }}&gt;삭제하기&lt;/div&gt;</code></pre><pre><code>
- requestSubmit 
사용자가 submit 버튼을 클릭한 것과 동일하게 동작, 유효성 검사도 정상적으로 수행한다.

- form 태그를 제출할 때 서버 액션을 호출하도록 설정 
useActionState를 사용하여 서버 액션을 호출해 리뷰를 삭제한다. 이 과정에서 로딩 상태와 에러 처리도 함께 구현한다.
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js - 스트리밍, 스켈레톤 UI]]></title>
            <link>https://velog.io/@seonguul_2/Next.js-%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D-%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4-UI</link>
            <guid>https://velog.io/@seonguul_2/Next.js-%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D-%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4-UI</guid>
            <pubDate>Mon, 05 Jan 2026 07:41:58 GMT</pubDate>
            <description><![CDATA[<p>!!한 입 크기로 잘라 먹는 Next.js 책 스터디 내용</p>
<h3 id="스트리밍이란">스트리밍이란?</h3>
<p>특정 UI요소를 스트리밍 방식으로 렌더링하는 기능을 제공, 렌더링 가능한 요소부터 먼저 표시하기 때문에 사용자를 필요 이상으로 오래 기다리지 않게 할 수 있다.</p>
<p>용량이 크거나 준비하는 데 오랜 시간이 걸리는 데이터를 보다 효과적으로 전송하기 위해 고안된 방식이다.
데이터를 여러 개로 나누어 조각 별로 실시간으로 전송한다.</p>
<h4 id="웹-서비스의-스트리밍">웹 서비스의 스트리밍</h4>
<p>오래 걸리는 부분과, 바로 준비되는 부분을 구분한다.
오래 걸리는 요소 -&gt; 로딩 상태 (&#39;로딩 중&#39;)으로 처리한다.</p>
<p>초기 로딩 시간을 줄인다. 필요한 정보를 빠르게 확인할 수 있다. </p>
<h4 id="nextjs의-스트리밍-활용-사례">Next.js의 스트리밍 활용 사례</h4>
<p>앱 라우터 버전은 다이나믹 페이지를 최적화하기 위해, 사용자 경험을 향상하기 위해 스트리밍을 사용한다.
브라우저의 접속 요청에 따라 실시간으로 서버에서 생성하므로 데이터를 불러오거나 UI요소를 렌더링하는 데 시간이 걸릴 수 있다.</p>
<p>다이나믹 페이지 + 스트리밍 기법 
준비된 UI요소부터 사용자에게 표시, 나머지 요소는 준비되는 대로 페이지에 점진적으로 추가</p>
<p>페이지 컴포넌트를 제외하고 레이아웃 및 검색 폼 컴포넌트는 브라우저가 접속 요청 시 바로 렌더링 가능하다.
루트 layout, 검색 폼 layout, 검색 폼 component 이후에 페이지 컴포넌트가 렌더링 = 지연 발생</p>
<h3 id="스트리밍-설정하기">스트리밍 설정하기</h3>
<p>src/app 폴더에 loading.tsx를 생성하면 이 파일의 경로 폴더를 포함해 아래 경로의 페이지는 모두 자동으로 스트리밍 할 수 있다.</p>
<p>src/util/delay.ts</p>
<pre><code>export async function delay(ms: number){
    return new Promise((resolve) =&gt; setTimeout(resolve, ms));
} // 인위적으로 delay함수를 작성한다.</code></pre><p>await delay(3000); 호출해서 컴포넌트의 실행을 3초간 지연한다.
스트리밍은 클라이언트의 접속 요청에 실시간으로 페이지를 생성해야 하는 다이나믹 페이지에 유용하다. </p>
<h3 id="loadingtsx파일을-이용해-스트리밍을-설정할-때-주의할-점">loading.tsx파일을 이용해 스트리밍을 설정할 때 주의할 점</h3>
<h4 id="--페이지-컴포넌트만-점진적으로-렌더링할-수-있다">- 페이지 컴포넌트만 점진적으로 렌더링할 수 있다.</h4>
<p>loading.tsx를 이용해 스트리밍을 설정하면 페이지 컴포넌트만 점진적으로 렌더링
페이지 컴포넌트 이외의 컴포넌트(검색 폼 컴포넌트 등)에서 비동기 데이터 호출하더라도 이 컴포넌트를 점진적으로 렌더링할 수 없다.</p>
<p>= Suspense 컴포넌트를 사용하면 특정 컴포넌트 단위로 로딩 상태를 처리</p>
<h4 id="--쿼리-스트링-변경은-스트리밍을-다시-유발하지-않는다">- 쿼리 스트링 변경은 스트리밍을 다시 유발하지 않는다.</h4>
<p>검색 페이지에서 새로운 검색어를 입력하면 스트리밍이 다시 동작 X
이미 검색 페이지에 접속한 상태라면 검색어를 변경하더라도 스트리밍이 동작하지 않아 페이지 컴포넌트의 로딩 UI가 다시 표시되지 않는다.</p>
<p>Next.js의 스트리밍은 초기 접속 페이지나 페이지를 이동할 때만 활성화되므로 쿼리 스트링 변경처럼 클라이언트의 상태 업데이트에는 반응하지 않는다. = 새 검색어를 입력해도 로딩 UI는 표시되지 않은 채 준비시간이 길어지면 사용자는 불만이 생길 수 있다.</p>
<p>= Suspense 컴포넌트를 사용하면 데이터로드가 다시 발생할 때 로딩 상태를 효과적으로 처리할 수 있다.</p>
<h3 id="suspense를-이용한-스트리밍-설정">Suspense를 이용한 스트리밍 설정</h3>
<p>특정 컴포넌트를 완전히 로드하기 전까지 페이지에 대체 UI를 표시하도록 한다.
이 컴포넌트를 사용하면 비동기 데이터나 외부 리소스를 불러오는 동안 &#39;로딩 중&#39;임을 나타내는 UI를 보여줄 수 있다.</p>
<h4 id="1-전체-페이지에-적용">1) 전체 페이지에 적용</h4>
<pre><code>&lt;Suspense fallback=&lt;div&gt;로딩 중입니다.&lt;/div&gt;
    &lt;Child/&gt; // child컴포넌트를 점진적으로 렌더링하도록 설정한다.
&lt;/Suspense&gt;</code></pre><p>Child컴포넌트가 완전히 로드되기 전까지 대체 UI로 화면에 표시된다.
앱에서 페이지뿐만 아니라 다른 컴포넌트도 점진적으로 렌더링할 수 있어 로딩 UI를 세밀하게 구현할 수 있다.</p>
<h4 id="2-인덱스-페이지---다이나믹-페이지로-설정-일부-컴포넌트에는-스트리밍-기능">2) 인덱스 페이지 -&gt; 다이나믹 페이지로 설정, 일부 컴포넌트에는 스트리밍 기능</h4>
<ol>
<li><p>export const dynamic = &quot;force-dynamic&quot;;</p>
</li>
<li><p>인덱스 페이지에서 AllBooks와 RecoBooks 컴포넌트를 
Suspense로 감싸 점진적으로 렌더링하도록 설정한다.</p>
<pre><code>export default function Page() {
return (
 &lt;div className={style.container}&gt;
   &lt;section&gt;
     &lt;h3&gt;지금 추천하는 도서&lt;/h3&gt;
     &lt;Suspense fallback={&lt;div&gt;로딩 중 입니다...&lt;/div&gt;}&gt;
       &lt;RecoBooks /&gt;
     &lt;/Suspense&gt;
   &lt;/section&gt;
   &lt;section&gt;
     &lt;h3&gt;등록된 모든 도서&lt;/h3&gt;
     &lt;Suspense fallback={&lt;div&gt;로딩 중 입니다...&lt;/div&gt;}&gt;
       &lt;AllBooks /&gt;
     &lt;/Suspense&gt;
   &lt;/section&gt;
 &lt;/div&gt;
);
}</code></pre><p>컴포넌트 단위로 점진적인 렌더링 설정이 가능해 스트리밍을 정교하게 구현할 수 있다.</p>
</li>
<li><p>검색 페이지에서 Suspense 컴포넌트를 사용하도록 변경</p>
<pre><code>async function SearchResult({q}: {q: String}){
 await delay(3000);
 const response = await fetch(
   `${process.env.NEXT_PUBLIC_API_URL}/book/search?q=${q}`, // 검색어 q가 달라지면 새로운 캐시 키를 생성하므로 이 요청은 별도의 데이터로 처리된다.
   { cache: &quot;force-cache&quot;},
 );
 if (!response.ok) throw new Error(response.statusText);

 const books: BookData[] = await response.json();

 return (
   &lt;div&gt;
     {books.map((book) =&gt; (
       &lt;BookItem key={book.id} {...book} /&gt;
     ))}
   &lt;/div&gt;
 );
}
</code></pre></li>
</ol>
<p>export default async function Page({
  searchParams,
}: {
  searchParams: Promise&lt;{
    q?: string;
  }&gt;;
}) {
  const { q } = await searchParams;</p>
<p>  return (
    &lt;Suspense fallback={<div>검색 결과를 불러오는 중입니다...</div>}&gt;
      &lt;SearchResult q={q || &quot;&quot;} /&gt;
    </Suspense>
  )
}</p>
<pre><code>
SearchResult 컴포넌트를 만든다. 비동기 동작을 하는 컴포넌트를 감싸기 위해
로딩 UI를 불러오도록 텍스트를 렌더링한다.
쿼리 스트링을 변경하면 로딩 UI가 표시되지 않는다. = 기본적으로 페이지 이동과 초기 페이지의 접속에서만 동작하기 때문이다.

#### - Suspense 컴포넌트의 key Prop으로 key값 적용
key Prop은 리액트에서 특정 컴포넌트를 식별하기 위한 식별자
쿼리 스트링을 변경할 때마다 key값이 달라지므로 컴포넌트를 강제로 다시 렌더링</code></pre><p>&lt;Suspense key={q || &quot;&quot;} fallback={<div>검색 결과를 불러오는 중입니다...</div>}&gt;</p>
<pre><code>
= 쿼리 스트링이 변경될 때마다 기존의 Suspense를 제거하고 새로 생성하는 과정에서 로딩 UI를 다시 표시

### 스트리밍과 검색 엔진 최적화(SEO)
검색 엔진 최적화에 영향을 미치지 않는다. 검색 엔진 크롤러의 동작에서 찾을 수 있다.
점진적으로 수신된 데이터는 처리하지 않는다. 크롤러는 서버에서 모든 HTML 데이터 수신할 때까지 대기 -&gt; 한꺼번에 처리

### 스켈레톤 UI
콘텐츠를 점진적으로 렌더링하는 동안 사용하는 스켈레톤UI 렌더링할 콘텐츠의 최종 모습을 간략히 보여 주는 로딩 UI를 말한다. = 적극적 활용

- 스켈레톤 UI 구현하기
도서 아이템의 스켈레톤 컴포넌트를 생성,</code></pre><p>import style from &quot;./book-item-skeleton.module.css&quot;;
export default function BookItemSkeleton() {
  return (
    <div className={style.container}>
      <div className={style.cover_img}></div>
        <div className={style.info_container}>
          <div className={style.title}></div>
          <br />
            <div className={style.subtitle}></div>
            </div>
          </div>
  );
}</p>
<pre><code>
- BookItemSkeleton 컴포넌트 불러오기</code></pre><p>export default function Page() {
  return (
    <div className={style.container}>
      <section>
        <h3>지금 추천하는 도서</h3>
        {/* &lt;Suspense fallback={<div>로딩 중 입니다...</div>}&gt; <em>/}
        &lt;Suspense fallback={new Array(3).fill(0).map((<em>, idx) =&gt; ( // 도서 아이템 스켈레톤 컴포넌트 3개 불러오도록 설정
          &lt;BookItemSkeleton key={<code>reco-book-skeleton-${idx}</code>} /&gt;
        ))}&gt;
          <RecoBooks />
        </Suspense>
      </section>
      <section>
        <h3>등록된 모든 도서</h3>
        &lt;Suspense fallback={new Array(5).fill(0).map((</em>, idx) =&gt; (
          &lt;BookItemSkeleton key={<code>all-book-skeleton-${idx}</code>} /&gt;
        ))}&gt; 
          <AllBooks />
        </Suspense>
        {/</em> 5개 렌더링하도록 설정한다. */}
      </section>
    </div>
  );
}</p>
<pre><code>
= 확인을 마쳤다면 delay함수를 모두 제거하기 (테스트 용도)






</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js - 페이지 캐시 (Server Page Cache, Client Page Cache)]]></title>
            <link>https://velog.io/@seonguul_2/Next.js-6%EC%9E%A5-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%BA%90%EC%8B%9C-Server-Page-Cache-Client-Page-Cache</link>
            <guid>https://velog.io/@seonguul_2/Next.js-6%EC%9E%A5-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%BA%90%EC%8B%9C-Server-Page-Cache-Client-Page-Cache</guid>
            <pubDate>Mon, 05 Jan 2026 05:38:22 GMT</pubDate>
            <description><![CDATA[<ul>
<li>한 입 크기로 잘라 먹는 Next.js 책 스터디 내용</li>
</ul>
<h3 id="6장-페이지-캐시">6장 페이지 캐시</h3>
<p>앱 라우터 버전에서 페이지를 캐시해 성능을 최적화하고 사용자의 요청에 빠르게 응답하는 방법을 살펴본다.</p>
<h3 id="--서버의-페이지-캐시-풀-라우트-캐시">- 서버의 페이지 캐시 (풀 라우트 캐시)</h3>
<p>서버와 클라이언트에서 페이지를 각각 캐시한다. </p>
<ul>
<li>풀 라우트 캐시란?
next.js 서버에서 사전 렌더링 결과로 생성한 html페이지를 캐시하는 기능이다. 
이 기능을 이용하면 브라우저의 접속 요청에 서버가 페이지를 다시 생성할 필요 없이 캐시 페이지로 바로 응답할 수 있어 
응답 속도가 크게 향상된다.
<img src="https://velog.velcdn.com/images/seonguul_2/post/390a43fa-c1e9-4aa6-ad79-3f4bf20cb2d1/image.png" alt=""></li>
</ul>
<p>빌드 타임에 저장, 미리 인덱스 페이지의 생성을 요청, 인덱스 페이지에 필요한 데이터를 페칭한다. 
생성이 완료된 페이지를 풀 라우트 캐시에 저장한다.
빌드 종료 후 next.js 서버가 가동되면 브라우저가 next.js 서버에서 인덱스 페이지를 요청한다.
사전 렌더링이 풀 라우트 캐시에 보관되어 있으므로 캐시가 hit 된다. next.js 서버는 페이지를 새로 생성하지 않고 캐시 페이지로 즉시 응답한다. = 응답 속도 빠름</p>
<h4 id="동적-데이터를-포함하지-않는-페이지는-자동으로-빌드-타임에-생성하고-풀-라우트-캐시에-보관한다">동적 데이터를 포함하지 않는 페이지는 자동으로 빌드 타임에 생성하고, 풀 라우트 캐시에 보관한다.</h4>
<p>조건 1 : 캐시되지 않는 데이터 페칭을 포함하는 페이지
데이터 캐시 옵션 cache: “no-store” 또는 next: {revalidate: 0}으로 설정된 데이터 페칭을 포함하는 페이지들이다.
요청마다 실시간으로 데이터를 불러오기 때문에 빌드 타임에 미리 캐시할 수 없다.</p>
<p>조건 2 : 동적 API를 사용하는 페이지
브라우저 접속 요청과 함께 전달되는 값인 헤더, 쿠키, 쿼리 스트링을 불러오는 메서드이다. 
빌드 타임에 미리 캐시할 수 없다.</p>
<p>// 동적 데이터를 사용하는 컴포넌트가 하나도 없는 static page -&gt; 빌드 타임에 미리 생성 풀 라우트 캐시에 보관
그 페이지는 풀 라우트 캐시에 보관하는 페이지(Static Page) 
그렇지 않은 페이지는 (Dynamic Page)라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/seonguul_2/post/0841fd55-6130-4181-80af-7d0ce72421e5/image.png" alt=""></p>
<p>데이터 캐시나 리퀘스트 메모이제이션을 이용하면 빠른 속도로 페이지를 제공한다.
페이지 라우터와 다르게 캐시가 독립적으로 동작한다는 점이 특징이다.
내부의 특정 컴포넌트나 api요청 결과까지 개별적으로 유연하게 최적화할 수 있다.</p>
<h4 id="스태틱-페이지와-풀-라우트-캐시-확인">스태틱 페이지와 풀 라우트 캐시 확인</h4>
<p>인덱스 페이지는 static page (루트 레이아웃, 검색 폼, 페이지 컴포넌트) : 동적 api 사용 X
dynamic page(검색, 도서상세) : 동적 api를 사용하기 때문이다.
searchParams를 받고 쿼리 스트링 q에서 값을 꺼내 사용 -&gt; 실시간 처리 동적 api 간주</p>
<p>모든 페이지에서 인덱스 페이지만이 스태틱 페이지로 설정된다. 스태틱 페이지로 설정된 페이지는
빌드 타임에 미리 사전 렌더링을 진행하고, 그 결과 html 페이지를 풀 라우트 캐시에 보관한다.
.next/server/app 폴더의 index.html을 클릭하면 확인할 수 있다.</p>
<p>Cache: “no-store”로 캐시 옵션을 설정한다. (다이나믹 페이지)
next: { revalidate: 3} -&gt; (스태틱 페이지)</p>
<h4 id="풀-라우트-캐시-갱신하기">풀 라우트 캐시 갱신하기</h4>
<p>풀 라우트 캐시에 보관한 페이지도 자동으로 갱신할 수 있다. 
데이터 캐시를 갱신하면 해당 데이터를 사용하는 페이지도 자동으로 갱신된다.
{next: { revalidate: 3}, } 을 사용하여 페이지를 Stale(상한) 상태로 설정하면 된다.</p>
<h4 id="스태틱-페이지로-설정할-수-없다면-데이터-캐시라도-적용하기">스태틱 페이지로 설정할 수 없다면 데이터 캐시라도 적용하기</h4>
<p>검색 페이지는 동적 API를 사용하므로 다이나믹 페이지로 설정된다.
풀 라우트 캐시는 사용하지 못하더라도 데이터 캐시를 최대한 활용하여 성능을 최적화하는 것이 좋다.</p>
<pre><code>const response = await searchParams;
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book/search?q=${q}`,
    { cache: &quot;force-cache&quot; } // 영구적으로 캐시
    );
}</code></pre><h4 id="동적-경로가-있는-페이지를-스태틱-페이지로-설정하기">동적 경로가 있는 페이지를 스태틱 페이지로 설정하기</h4>
<p>동적 경로가 있는 도서 상세 페이지를 스태틱 페이지로 설정 풀 라우트 캐시에 보관한다. 
Export function generateStaticParams() {
    return [{ id: “1” }, { id: “2” }, { id: “3” }]; // 반드시 문자열로 작성!
} // 약속된 이름의 함수를 사용해 URL파라미터 값을 미리 정의하기 위해 사용</p>
<p>이 함수에서 내보낸 URL파라미터와 일치하는 페이지를 Static Page로 생성 -&gt; 풀 라우트 캐시에 보관
(페이지 라우터 버전의 getStaticPaths함수와 유사한 기능을 수행)</p>
<p>build하면 (SSG) Static Site Generation로 뜨는 것을 확인할 수 있다.
풀 라우트 캐시에 보관 next/server/app/book 폴더에 저장된다.</p>
<p>경로 이외의 페이지를 없는 페이지로 취급하려면 dynamicParams라는 지정된 이름의 변수를 사용해야 한다.</p>
<pre><code>export const dynamicParams = false;</code></pre><h4 id="--라우트-세그먼트-컨픽route-seg-ment-config">- 라우트 세그먼트 컨픽(Route Seg-ment Config)</h4>
<p>dynamicParams : 처럼 페이지 설정을 조절하는 변수
dynamic : 페이지의 유형을 결정
revalidate : 페이지의 갱신 주기를 설정</p>
<p>generateStaticParams 함수는 서버의 사전 렌더링 과정에서 딱 한 번 동작한다.
동적 경로를 사용하는 페이지라도 빌드 타임에 미리 스태틱 페이지로 생성할 수 있다.</p>
<pre><code>export async function generateStaticParams() {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book`);
  if (!response.ok) throw new Error(response.statusText);

  const books: BookData[] = await response.json();
  return books.map((book) =&gt; ({
    id: String(book.id),
  }));
}</code></pre><p>1) /book 주소로 API를 호출, 백엔드 서버에서 도서 데이터를 모두 불러온다.
2) 문제가 있으면 예외가 발생하도록 설정한다. 
3) 백엔드 서버에서 받은 응답을 JSON 형태로 변환
4) 각 도서의 id값을 문자열로 변환 -&gt; { id: &quot;도서 아이디&quot; } 형태의 객체 배열 반환</p>
<p>// 추가로 8개의 페이지 생성</p>
<h3 id="클라이언트의-페이지-캐시---라우터-캐시">클라이언트의 페이지 캐시 - 라우터 캐시</h3>
<h4 id="클라이언트-라우터-캐시란">클라이언트 라우터 캐시란?</h4>
<p>클라이언트인 브라우저에서 페이지의 데이터를 직접 캐시하는 기능
사용자가 브라우저에서 페이지를 이동 할 때 불필요한 데이터 요청을 방지, 페이지를 빠르게 이동 할 때 불필요한 데이터 요청을 방지해 페이지를 빠르게 이동</p>
<ul>
<li>client router cache의 주요 목적
브라우저에서 페이지를 이동할 때 두 페이지가 동일한 레이아웃 컴포넌트 사용하는 경우, 중복 요청을 방지
RSC Payload를 캐시해 보관하는 방법
<img src="https://velog.velcdn.com/images/seonguul_2/post/0d932afb-776a-4e03-b064-ce4c993ffc18/image.png" alt=""></li>
</ul>
<p>레이아웃과 페이지 단위로 나누어 저장한다. </p>
<h4 id="1-레이아웃-컴포넌트-rsc-payload">1) 레이아웃 컴포넌트 RSC Payload</h4>
<p>새로고침이 이루어지기 전까지 계속 재사용 된다. 동일한 레이아웃을 사용하는 페이지를 이동할 때 중복 요청을 방지한다.</p>
<h4 id="2-페이지-컴포넌트-rsc-payload">2) 페이지 컴포넌트 RSC Payload</h4>
<p>뒤로가기, 앞으로 가기 동작에 사용한다. 페이지 이동 과정에서 최신 데이터를 갱신할 때 사용한다.</p>
<h3 id="클라이언트-라우터-캐시-확인하기">클라이언트 라우터 캐시 확인하기</h3>
<div>{new Date().toLocaleTimeString()}</div>

<p>인덱스 페이지와 검색 페이지 둘 다 같은 시간으로 확인이 된다. 
페이지를 이동할 때 레이아웃 컴포넌트의 불필요한 중복 호출을 방지하기 위한 목적으로 사용</p>
<h4 id="프리페칭과-클라이언트-라우터-캐시">프리페칭과 클라이언트 라우터 캐시</h4>
<p>프리페칭한 데이터도 저장할 수 있다. 앱 라우터 버전도 페이지 라우터 버전과 마찬가지로 프리페칭 기능을 제공한다.
앱 라우터 버전에서는 JS Bundle, RSC Payload도 함께 프리페칭한다.
클라이언트 라우터 캐시에는 RSC Payload만 저장된다. 
= 페이지를 전환할 때 서버 요청을 최소화, 더 빠른 렌더링을 가능하게 한다.</p>
<h3 id="라우트-세그먼트-컨픽-동적-경로의-url-파라미터를-제한을-구현">라우트 세그먼트 컨픽 (동적 경로의 URL 파라미터를 제한을 구현)</h3>
<p>특정 페이지의 동작을 명시적으로 설정하는 라우트 세그먼트 컨픽(Route Segment Config)기능을 제공한다.</p>
<h4 id="라우트-세그먼트-컨픽이란">라우트 세그먼트 컨픽이란?</h4>
<p>특정 페이지의 캐시 유무나 갱신 등의 동작을 강제로 설정하는 Next.js의 한 기능으로 약속된 이름의 변수를 레이아웃이나 페이지 파일에서 내보내는 방식으로 설정한다.</p>
<ul>
<li>해당 페이지는 다이나믹 페이지로 강제 설정된다.<pre><code>export const dynamic = &quot;force-dynamic&quot;
// 다이나믹 페이지로 설정된다.</code></pre></li>
<li>dynamic : 해당 페이지는 다이나믹 페이지로 강제 설정</li>
<li>dynamicParams : false로 설정하면 페이지의 URL 파라미터가 동적으로 생성되지 않도록 제한한다.
해당 페이지를 404에러 페이지로 반환한다.</li>
</ul>
<h4 id="dynamic-옵션">dynamic 옵션</h4>
<ol>
<li><p>dynamic=&quot;auto&quot;
기본값, 아무것도 설정하지 않음
Next.js가 자체적으로 판단한다. </p>
</li>
<li><p>dynamic=&quot;force-dynamic&quot;
페이지를 강제로 다이나믹 페이지로 설정한다.
동적 API나 캐시되지 않는 데이터 페칭이 없어도, 특정 페이지를 다이나믹 페이지로 설정하고 싶을 때 사ㅛㅇㅇ</p>
</li>
<li><p>dynamic=&quot;force-static&quot;
페이지를 강제로 스태틱 페이지로 설정
동적 API(쿠키, 헤더, 쿼리 스트링 등)을 사용하면 undefined로 처리된다.</p>
</li>
<li><p>dynamic=&quot;error&quot;
페이지를 스태틱 페이지로 설정
페이지에서 동적 API를 사용, 캐시되지 않는 데이터 페칭을 사용하면 빌드 에러가 발생</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js - 앱 라우터 버전의 데이터 페칭]]></title>
            <link>https://velog.io/@seonguul_2/Next.js-%EC%95%B1-%EB%9D%BC%EC%9A%B0%ED%84%B0-%EB%B2%84%EC%A0%84%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8E%98%EC%B9%AD</link>
            <guid>https://velog.io/@seonguul_2/Next.js-%EC%95%B1-%EB%9D%BC%EC%9A%B0%ED%84%B0-%EB%B2%84%EC%A0%84%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8E%98%EC%B9%AD</guid>
            <pubDate>Mon, 05 Jan 2026 02:13:59 GMT</pubDate>
            <description><![CDATA[<h3 id="--앱-라우터-버전의-데이터-페칭">- 앱 라우터 버전의 데이터 페칭</h3>
<p>서버 컴포넌트 도입으로 변경된 데이터 페칭 방식
라우터 버전이 갖는 한계를 극복하기 위한 것</p>
<p>SSR : getServerSideProps
SSG, ISR : getStaticProps</p>
<pre><code> Export async function getServerSideProps() {
    // 데이터 페칭 코드
    return { props: { … } };
}

 Export async function getStaticProps() {
    // 데이터 페칭 코드
    return { props: { … } };
}

= 불러온 데이터를 페이지 컴포넌트에 Props로 전달된다.

// 서버에서만 실행되는 코드
Export async function getServerSideProps() {
    // 데이터 페칭 코드
    return { props: { … } };
}

// 서버와 클라이언트에서 모두 실행되는 코드
 Export function Page(props) {
    return &lt;div&gt; … &lt;/div&gt;;
}</code></pre><h4 id="-이러한-props로-전달하는-방식은-문제를-만든다">= 이러한 Props로 전달하는 방식은 문제를 만든다.</h4>
<h4 id="페이지를-구성하는-컴포넌트-간-데이터를-전달하는-과정이-불필요하게-복잡해진다-props-drilling문제">페이지를 구성하는 컴포넌트 간 데이터를 전달하는 과정이 불필요하게 복잡해진다. Props Drilling문제</h4>
<ul>
<li>서버 컴포넌트 사용하여 문제 해결<pre><code>Async function Page() {
  const dataForPage = await fetch(“…”); // 페이지 컴포넌트에 필요한 데이터 페칭
  return &lt;Child/&gt;;
}
</code></pre></li>
</ul>
<p>Async function Child() {
    const dataForChild = await fetch(“…”); // Child 컴포넌트에 필요한 데이터 페칭
    return <div>…</div>
}</p>
<pre><code>
서버 컴포넌트이기 때문에 async/await 으로 비동기적으로 가져온다.
백엔드 서버에서 


### 앱 라우터 버전에서 컴포넌트가 직접 서버에서 데이터를 가져온다.
getServerSideProps와 같은 함수를 사용할 필요 없이 컴포넌트가 필요한 데이터를 직접 가져올 수 있어 데이터 전달이 단순

### 데이터 요청을 영구적으로 보관하는 데이터 캐시

1. 데이터 캐시 : fetch  메서드로 데이터를 불러올 때 사용, fetch 메서드에서 두번째 인수로 옵션 추가 </code></pre><p>async function RecoBooks() {
  try {
    const response = await fetch(<code>${process.env.NEXT_PUBLIC_API_URL}/book/random</code>,
      { cache: &quot;force-cache&quot;} // fetch 메서드로 불러온 데이터는 무조건 캐시 (영구적)
    );</p>
<pre><code>-&gt; 로깅 기능을 사용하면 캐시 동작을 직관적으로 확인 가능 (cache skip)

#### - 캐시 옵션 
cache: “force-cache”  :  영구적으로 캐시하는 옵션 (주로 정적 데이터에 사용)
= 첫 번째 접속 요청일 경우 ( 캐시 miss) 가 발생한다. -&gt; 백엔드 서버에 데이터를 요청하기 전 데이터가 있는지 확인 (cache hit)

cache: “no-store” :  요청으로 불러온 데이터를 절대 캐시하지 않고, 캐시된 데이터도 사용하지 않는다. (매번 백엔드 서버에 요청) = 매번 새로운 데이터에 사용
Next.js 15버전부터 캐시 옵션을 설정하지 않아도 된다.

next: { revalidate: 시간 } : 3으로 설정하면 3초 주기로 갱신

Next: { tags: [] } : 특정 시점의 요청 기반으로 데이터 캐시 무효화 
설정한 태그값을 이용해 특정 시점에 캐시 데이터를 갱신할 수 있다.

Ex. Next: { tags: [“a”] } “a”라는 태그를 활용해 특정 시점 데이터를 갱신, 하나의 페칭에 여러 태그 설정 가능
{ next: { revalidate: 3, tags: [“a”] }} : 특정 주기로 데이터 갱신, “a” 태그로 갱신
{ cache: “force-cache”, next: { tags: [“a”] }} : 영구적으로 캐싱된 데이터, “a”태그로 갱신
</code></pre><p>Export async function GET() {
    await revalidateTag(“a”);
} // revalidateTag “a” 태그와 관련된 데이터 캐시는 무효화(purge)</p>
<pre><code>
### - 요청 주소, 쿼리 파라미터, 헤더, 캐시 옵션 기준으로 구분
fetch(‘/book/‘);
fetch(‘/book/search?q=123’); // 요청 주소, 쿼리 파라미터 기준으로 별도의 캐시로 저장
fetch(“/book”, { headers: { Authorization: “Bearer token-1” }, }); 
= 데이터 캐시 확인하려면 .next에서 cache/fetch-cache 폴더 확인 

#### - Axios, ky
http 요청을 보내기 위한 라이브러리, 백엔드 서버에서 데이터 요청, 보낼 때 사용한다.
Axios : 직관적인 기능(인터셉터, 에러 처리) http요청
ky: fetch API를 기반으로 만든 가볍고 모던한 라이브러리이다. 코드가 간겨라면서 구조적이어서 읽기 쉽다.

#### - 서드파티(Firebase, Supabase, Prisma 등) 사용하는 경우
fetch메서드를 사용할 수 없는 상황이 존재 서드파티가 제공하는 메서드를 이용해 데이터를 불러와야 하기 때문
=&gt; cache 메서드를 사용해 데이터 요청을 수동으로 메모이제이션 할 수 있다.
</code></pre><p>Import { cache } from “react”;
Export const getPosts = cache(async () =&gt; {
    const { data: posts } = await supabase.from(“posts”).select();
    return posts;
});</p>
<pre><code>Cache 인수로 함수를 받아 함수의 반환값을 캐시한다. 전달된 인수를 기준으로 이루어지며 프로그램이 종료되면 자동으로 소멸된다. 

### - 페이지를 생성할 때 중복 요청을 방지하는 리퀘스트 메모이제이션
Request Memoization 데이터 페칭 최적화 기능 
서버에서 특정 페이지를 사전 렌더링할 때 페이지에 필요한 데이터를 불러오는 요청이 중복 수행되지 않도록 최적화한다.
= 페이지를 사전 렌더링할 때만 사용된다. (중복 요청 방지)

cache: “no-store” 로 변경한다. =&gt; 새로운 데이터 불러온다.  (서버에서 확인 가능 request )

### 에러 처리
Try-catch 문 외에도 error.tsx 파일을 사용하여 에러를 감지하고 처리할 수 있다.
App폴더에 error.tsx생성 단계별로 에러 처리 (계층적 에러 처리 방식)
“Use client” 컴포넌트가 오류를 복구할 때 브라우저가 제공하는 기능 사용

force-cache를 사용하면 try-catch 예외처리 해도 error 페이지에 렌더링되지 않는다. 
// try-catch 문 지우면 가능

#### 앱 라우터 버전 -&gt; error.tsx 파일로 발생한 오류를 일괄처리 한다. 
error를 핸들링할 때 하위의 레이아웃 파일은 모두 무시된다.
검색 폼 레이아웃도 같이 렌더링하려면 (with-searchbar) 폴더에 별도 error.tsx 파일 생성

#### 에러 메시지 확인</code></pre><p>&quot;use client&quot;;</p>
<p>interface Props {
    error: Error &amp; { digest?: string};
}</p>
<p>export default function Error({error}: Props) {
    console.log(error);</p>
<pre><code>return (
    &lt;div&gt;오류가 발생했습니다.&lt;/div&gt;
)</code></pre><p>} // props의 타입을 정의 -&gt; error 프로퍼티 포함, props로 받은 error 객체를 콘솔에 출력한다.</p>
<pre><code>
#### 에러 복구하기 
reset에 페이지에서 발생한 오류를 복구할 수 있는 함수가 있다.</code></pre><p>&quot;use client&quot;;</p>
<p>interface Props {
    error: Error &amp; { digest?: string};
    reset: () =&gt; void;
}</p>
<p>export default function Error({error, reset}: Props) {
    console.log(error);</p>
<pre><code>return (
    &lt;&gt;
        &lt;div&gt;오류가 발생했습니다.&lt;/div&gt;
        &lt;button onClick={() =&gt; reset()}&gt;다시 시도&lt;/button&gt;
    &lt;/&gt;
)</code></pre><p>} // 똑같은 오류 메시지만 계속 출력된다. </p>
<pre><code>
#### 해결 방법
- 브라우저의 새로고침 기능을 이용</code></pre><p>onClick = {() =&gt; {
    window.location.reload(); // 새로고침 하면 서버에게 초기 접속 요청을 다시 보내며, 오류 해결
}}</p>
<pre><code>- 라우터 객체를 이용 (추천)
새로고침 해도 정보가 다 날라가지 않도록
useRouter 훅 next/navigation 패키지에서 불러온다. 
Const router = useRouter();
router.refresh(); // 서버 컴포넌트 다시 실행하도록 
reset(); // 다시 시도

= 다시시도를 먼저 가동 후 서버 컴포넌트가 실행</code></pre><p>&lt;button onClick={() =&gt; { 
    startTransition(() =&gt; { // startTransition 메서드를 사용하여 간단히 해결할 수 있다.
        router.refresh(); // 서버 컴포넌트 다시 실행
        reset(); // 다시 렌더링</p>
<pre><code>- startTransition : UI를 멈추지 않고 여유있게 업데이트 하는 기능



</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[React - 폰트 설정, Navigation 설정]]></title>
            <link>https://velog.io/@seonguul_2/React-%ED%8F%B0%ED%8A%B8-%EC%84%A4%EC%A0%95-Navigation-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@seonguul_2/React-%ED%8F%B0%ED%8A%B8-%EC%84%A4%EC%A0%95-Navigation-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Tue, 04 Nov 2025 06:36:30 GMT</pubDate>
            <description><![CDATA[<h3 id="--react-에서-폰트-적용">- react 에서 폰트 적용</h3>
<p>  1) 폰트 파일 프로젝트에 추가
  .ttf 
  src/assets/fonts 폴더에 저장한다.</p>
<p>  2) CSS에서 폰트 불러오기
  @font-face사용</p>
<pre><code>  @font-face {
  font-family: &quot;SummerFont&quot;;
  src: url(&quot;./assets/fonts/SummerFont.ttf&quot;) format(&quot;truetype&quot;);
  font-weight: normal;
  font-style: normal;
}</code></pre><p>  3) 컴포넌트에서 사용하기
  style={{ fontFamily: &#39;SummerFont, sans-serif&#39; }}</p>
<p>  4) 전역 스타일에 적용하기</p>
<pre><code>  body {
      font-family: &quot;SummerFont&quot;, sans-serif;
  }</code></pre><h3 id="--react-router설정">- React Router설정</h3>
<p>  1) 설치
  npm install react-router-dom</p>
<p>  2) 설정
  App.js or 라우트 설정 파일에 라우터 구성</p>
<pre><code>  // App.js
import { Routes, Route} from &quot;react-router-dom&quot;;
import Home from &quot;./pages/Home&quot;;
import About from &quot;./pages/About&quot;;

function App() {
  return (
      &lt;Routes&gt;
        &lt;Route path=&quot;/&quot; element={&lt;Home /&gt;} /&gt;
        &lt;Route path=&quot;/about&quot; element={&lt;About /&gt;} /&gt;
      &lt;/Routes&gt;
  );
}

export default App;</code></pre><p>3) pages 폴더
각 페이지 생성 </p>
<p>4) components 폴더</p>
<pre><code>import { Link } from &quot;react-router-dom&quot;

export default function Navbar() {
    return (
        &lt;nav&gt;
            &lt;Link to=&quot;/&quot;&gt;Home&lt;/Link&gt;
            &lt;Link to=&quot;/about&quot;&gt;About&lt;/Link&gt;
        &lt;/nav&gt;
    )
}</code></pre><pre><code>&lt;App&gt;

/ 👉 &lt;Home&gt;

/products 👉 &lt;AllProducts&gt;

/products/new 👉 &lt;NewProduct&gt;

/products/:id 👉 &lt;ProductDetail&gt;

/carts 👉 &lt;MyCart&gt;</code></pre><p>5) index.js 에 BrowserRouter로 감싸주기</p>
<pre><code>import React from &#39;react&#39;;
import ReactDOM from &#39;react-dom/client&#39;;
import &#39;./index.css&#39;;
import App from &#39;./App&#39;;
import { BrowserRouter } from &#39;react-router-dom&#39;;

const root = ReactDOM.createRoot(document.getElementById(&#39;root&#39;));
root.render(
  &lt;BrowserRouter&gt;
    &lt;App /&gt;
  &lt;/BrowserRouter&gt;
);
</code></pre><h3 id="--tailwindcss-추가">- tailwindcss 추가</h3>
<p>npm install tailwindcss
yarn add -D tailwindcss</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TypeScript - 기본 제네릭, 유틸리티 유형, 키오프, null!]]></title>
            <link>https://velog.io/@seonguul_2/TypeScript-%EA%B8%B0%EB%B3%B8-%EC%A0%9C%EB%84%A4%EB%A6%AD-%EC%9C%A0%ED%8B%B8%EB%A6%AC%ED%8B%B0-%EC%9C%A0%ED%98%95-%ED%82%A4%EC%98%A4%ED%94%84-null</link>
            <guid>https://velog.io/@seonguul_2/TypeScript-%EA%B8%B0%EB%B3%B8-%EC%A0%9C%EB%84%A4%EB%A6%AD-%EC%9C%A0%ED%8B%B8%EB%A6%AC%ED%8B%B0-%EC%9C%A0%ED%98%95-%ED%82%A4%EC%98%A4%ED%94%84-null</guid>
            <pubDate>Fri, 31 Oct 2025 05:38:32 GMT</pubDate>
            <description><![CDATA[<h3 id="기본-제네릭">기본 제네릭</h3>
<p>타입을 변수처럼 다루는 기능이다. 값이 아니라 타입을 나중에 정할 수 있게 해주는 문법</p>
<pre><code>type Example&lt;T = string&gt; = {
// T의 기본값은 string이다. 
    value: T;
};

const a: Example = { value: &quot;hello&quot; };
const b: Example&lt;number&gt; = {value:42};</code></pre><h3 id="제네릭을-사용하는-이유">제네릭을 사용하는 이유</h3>
<p>타입을 나중에 주입할 수 있는 재사용 가능한 코드를 만드는 것이다. 코드를 한 번 작성, 다양한 타입과 함께 안전하게 사용할 수 있도록 하기 위해서이다.</p>
<ul>
<li>제네릭이 없을 때<pre><code>function getFirstString(arr: string[]): string {
  return arr[0];
}
</code></pre></li>
</ul>
<p>function getFirstNumber(arr: number[]): number {
    return arr[0];
}</p>
<pre><code>= 중복이 많고 유지보수가 힘들다.

- 제네릭으로 계산</code></pre><p>function getFirst<T>(arr: T[]): T {
    return arr[0];
}</p>
<p>getFirst<string>([&quot;a&quot;, &quot;b&quot;]); // string
getFirst<number>([1, 2, 3]); // number</p>
<pre><code>= 타입을 나중에 넘길 수 있음 (타입 안전한 코드 생성)


### 함수에서 기본 제네릭</code></pre><p>function identity&lt;T = string&gt;(value: T): T {
  return value;
}</p>
<p>// 제네릭을 생략 → T는 string으로 추론됨
const x = identity(&quot;hi&quot;); // T = string</p>
<p>// 명시적으로 지정 가능
const y = identity<number>(123); // T = number</p>
<pre><code>### 클래스에서 가능</code></pre><p>class Box&lt;T = string&gt; {
  content: T;
  constructor(content: T) {
    this.content = content;
  }
}</p>
<p>const strBox = new Box(&quot;Hello&quot;);  // T는 string
const numBox = new Box<number>(123); // T는 number</p>
<pre><code>
### API에서 사용</code></pre><p>async function fetchData<T>(url: string): Promise<T> {
  const res = await fetch(url);
  return res.json();
}</p>
<p>interface User {
  id: number;
  name: string;
}</p>
<p>interface Post {
  id: number;
  title: string;
}</p>
<p>const user = await fetchData<User>(&quot;/api/user/1&quot;);
const post = await fetchData<Post>(&quot;/api/post/1&quot;);</p>
<pre><code>
### TypeScript 유틸리티 유형</code></pre><p>let myCar: Required<Car> = {} // 객체의 모든 속성을 필수로 변경
Record : 특정 키 유형과 값 유형을 사용하여 객체 유형을 정의하는 단축키
const nameAgeMap: Record&lt;string, number&gt; = {} // 기록 객체 유형 정의
const bob: Omit&lt;Person, &#39;age&#39; | &#39;location&#39;&gt; = { // 객체 유형에서 키를 제거
  name: &#39;Bob&#39;
  // <code>Omit</code> has removed age and location from the type and they can&#39;t be defined here
};
const bob: Pick&lt;Person, &#39;name&#39;&gt; = {} //Pick 지정된 키를 제외한 모든 키를 객체 유형에서 제거
const value: Exclude&lt;Primitive, string&gt; = true; // Exclude 공용체에서 유형을 제거한다.
const point: ReturnType<PointGenerator> = {} // ReturnType 반환 유형 
const point: Parameters<PointPrinter>[0] = {} // 매개변수 유형을 배열로 추출한다.
const person: Readonly<Person> = {} // Readonly읽기 전용 </p>
<pre><code>
### 키오프
객체 유형에서 키 유형을 추출하는 데 사용된다.
type PersonKeys = keyof Person;
// 결과: &quot;name&quot; | &quot;age&quot;

- 다른 타입 도구와 함께 사용된다.</code></pre><p>const person = { name: &quot;Alice&quot;, age: 25 };</p>
<p>// typeof로 타입 추출
type Person = typeof person;</p>
<p>// keyof로 키 추출
type PersonKeys = keyof Person; // &quot;name&quot; | &quot;age&quot;</p>
<p>// 인덱싱으로 값 타입 추출
type PersonValueTypes = Person[PersonKeys]; // string | number</p>
<pre><code>


### Null 및 Undefined
null TypeScript는 값을 처리하는 강력한 시스템을 갖고 있다. undefined</code></pre><p>let value: string | undefined | null = null;
value = &#39;hello&#39;;
value = undefined;</p>
<pre><code>기본 유형이므로 다른 유형처럼 사용할 수 있다.

### 무효화 병합
function printMileage(mileage: number | null | undefined) 
?? 표현식에서 연산자와 함께 사용 가능하며 &amp;&amp; 연산자를 사용하는 것과 유사하다.

### null!
null검사 오류를 강제로 무시한다.
el!.innerText = &quot;Hello&quot;;

### 템플릿 리터럴 타입
문자열 리터럴 타입을 문자열 조합으로 만들어내는 타입 문법이다.</code></pre><p>type Color = &quot;red&quot; | &quot;green&quot; | &quot;blue&quot;;</p>
<p>type HexColor<T extends Color> = <code>#${string}</code>;</p>
<pre><code>
### 인덱스 서명 레이블
인덱스 서명에 레이블을 지정할 수 있다.




</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[TypeScript - Type, interface, 함수, TypeCasting, 클래스]]></title>
            <link>https://velog.io/@seonguul_2/Type-interface-%ED%95%A8%EC%88%98-TypeCasting-%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@seonguul_2/Type-interface-%ED%95%A8%EC%88%98-TypeCasting-%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Fri, 31 Oct 2025 02:05:55 GMT</pubDate>
            <description><![CDATA[<ul>
<li>숫자 enum 열거형
열거형은 첫 번째 값을 초기화 0하고 각 추가 값에 1을 더한다.
각 열거형 값에 고유한 숫자 값을 할당할 수 있다.
값이 자동으로 증가하지 않는다.</li>
</ul>
<pre><code>enum Status {
    NotFound = 404, 
    Success = 200,
} </code></pre><p>숫자 대신 이름으로 의미를 표현할 수 있다.</p>
<h3 id="type">Type</h3>
<p>사용자 정의 이름으로 유형을 정의할 수 있다.</p>
<pre><code>type CarYear = number
type CarType = string
type CarModel = string
type Car = {
    yaer: CarYear,
    type: CarType,
    model: CarModel
}
const carYear: CarYear = 2001
const carType: CarType = &quot;Toyota&quot;
const carModel: CarModel = &quot;Corolla&quot;;;;</code></pre><h3 id="인터페이스">인터페이스</h3>
<p>객체의 모양을 정의하기 위해 사용된다. 객체가 어떤 속성과 타입을 가져야 하는지 약속</p>
<pre><code>interface User {
    name: string;
    age: number;
}

const user:User = {
    name: &quot;Alice&quot;,
    age: 25
};</code></pre><p>= 타입 안정성, 가독성 향상, 재사용성, 확장성
인터페이스틑 서로 상속할 수 있다. </p>
<h3 id="interface-vs-type">interface vs type</h3>
<p>1) interface : 객체의 구조, 클래스 설계, API응답 형태 등을 표현할 때
여러 인터페이스를 상속해서 조합할 때, 외부 모듈 사용
2) 합집합, 교집합, 기본형 등을 표현할 때, 조건부 타입을 쓸 때
type ID = string | number;  // 합집합
type Point = { x: number } &amp; { y: number }; // 교집합</p>
<pre><code>type ID = string | number;  // 합집합
type Point = { x: number } &amp; { y: number }; // 교집합</code></pre><h3 id="union-합집합">union (합집합)</h3>
<pre><code>function printStatusCode(code: string | number) {
    console.log(`My status code is ${code}.`)
}
printStatusCode(404);
printStatusCode(&#39;404&#39;);</code></pre><h2 id="함수">함수</h2>
<pre><code>function getTime(): number {
    return new Date().getTime();
}</code></pre><h3 id="void-반환-유형">void 반환 유형</h3>
<p>void함수가 아무 값도 반환하지 않음을 나타내는 데 사용할 수 있다.</p>
<pre><code>function printHello(): void {
    console.log(&#39;Hello!&#39;);
}</code></pre><h3 id="매개변수">매개변수</h3>
<pre><code>function multiply(a: number, b: number) {
    return a * b;
}</code></pre><h3 id="기본-매개변수">기본 매개변수</h3>
<pre><code>function pow(value: number, exponent: number = 10) {
  return value ** exponent;
}</code></pre><h3 id="나머지-매개변수">나머지 매개변수</h3>
<pre><code>function add(a: number, b: number, ...rest: number[]) {
  return a + b + rest.reduce((p, c) =&gt; p + c, 0);
}</code></pre><h3 id="typecasting">TypeCasting</h3>
<p>변수의 유형을 재정의 해야 하는 경우</p>
<pre><code>let x: unknown = &#39;hello&#39;;
console.log((x as string).length);</code></pre><ul>
<li><p>캐스팅&lt;&gt;</p>
<pre><code>let x: unknown = &#39;hello&#39;;
console.log((&lt;string&gt;x).length);</code></pre></li>
<li><p>강제 캐스팅</p>
<pre><code>let somwValue: unknown = &quot;hello world&quot;;
let strLength: number = (someValue as string).length;</code></pre><h2 id="typescript-클래스">TypeScript 클래스</h2>
<pre><code>class Person {
  name: string;
}
const person = new Person();
person.name = &quot;Jane&quot;;</code></pre></li>
</ul>
<p>public- (기본값) 어디에서나 클래스 멤버에 액세스할 수 있습니다.
private- 클래스 내부에서만 클래스 멤버에 대한 액세스를 허용합니다.
protected- 상속 섹션에서 다루는 내용에 따라 자체 및 이를 상속하는 모든 클래스에서 클래스 멤버에 대한 액세스를 허용합니다.</p>
<h3 id="매개변수-속성">매개변수 속성</h3>
<pre><code>class Person {
  // name is a private member variable
  public constructor(private name: string) {}

  public getName(): string {
    return this.name;
  }
}

const person = new Person(&quot;Jane&quot;);
console.log(person.getName());</code></pre><h3 id="읽기-전용-readonly">읽기 전용 readonly</h3>
<p>클래스 멤버가 변경되는 것을 방지 가능</p>
<h3 id="implements-구현한다">implements 구현한다</h3>
<p>클래스와 인터페이스
인터페이스의 형태를 따라감(계약을 지킴)</p>
<pre><code>interface Shape {
  getArea: () =&gt; number;
}

class Rectangle implements Shape {
  public constructor(protected readonly width: number, protected readonly height: number) {}

  public getArea(): number {
    return this.width * this.height;
  }
}</code></pre><h3 id="extends-상속한다">extends 상속한다.</h3>
<p>클래스는 클래스와 인터페이스는 인터페이스 부모의 속성과 메서드를 물려받는다.
클래스는 키워드를 통해 서로를 확장할 수 있다. extends</p>
<pre><code>class Animal {
  move() {
    console.log(&quot;동물이 움직입니다&quot;);
  }
}

class Dog extends Animal {
  bark() {
    console.log(&quot;멍멍!&quot;);
  }
}

const d = new Dog();
d.move(); // 부모 클래스의 메서드 사용 가능 ✅
d.bark();</code></pre><h3 id="override">override</h3>
<p>클래스가 다른 클래스를 확장하는 경우, 같은 이름을 가진 부모 클래스의 멤버를 대체할 수 있다.</p>
<pre><code>// 이미 Rectangle 클래스가 있다고 가정
class Square extends Rectangle {
    public constructor(width: number) {
        super(width, width);
    }

    public override toString(): string {
        return `Square[width=${this.width}]`; // 대체
   }
}</code></pre><h3 id="abstract">abstract</h3>
<p>모든 멤버를 구현하지 않고 다른 클래스의 기본 클래스로 사용될 수 있도록 작성 가능</p>
<pre><code>abstract class Polygon {
  public abstract getArea(): number;

  public toString(): string {
    return `Polygon[area=${this.getArea()}]`;
  }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[TypeScript - 명시적 및 추론, 널리쉬 콜레싱, 옵셔널 체이닝 Array, Tuple]]></title>
            <link>https://velog.io/@seonguul_2/TypeScript-%EB%AA%85%EC%8B%9C%EC%A0%81-%EB%B0%8F-%EC%B6%94%EB%A1%A0-%EB%84%90%EB%A6%AC%EC%89%AC-%EC%BD%9C%EB%A0%88%EC%8B%B1-%EC%98%B5%EC%85%94%EB%84%90-%EC%B2%B4%EC%9D%B4%EB%8B%9D-Array-Tuple</link>
            <guid>https://velog.io/@seonguul_2/TypeScript-%EB%AA%85%EC%8B%9C%EC%A0%81-%EB%B0%8F-%EC%B6%94%EB%A1%A0-%EB%84%90%EB%A6%AC%EC%89%AC-%EC%BD%9C%EB%A0%88%EC%8B%B1-%EC%98%B5%EC%85%94%EB%84%90-%EC%B2%B4%EC%9D%B4%EB%8B%9D-Array-Tuple</guid>
            <pubDate>Tue, 28 Oct 2025 06:28:48 GMT</pubDate>
            <description><![CDATA[<p>T(Type) : 가장 일반적으로 사용
U, V : T외에 추가적인 타입이 필요할 때
K(Key) : 객체의 키 타입을 나타낼 때
E(Element) : 리스트나 배열의 요소를 나타낼 때</p>
<h3 id="typescript--정적-타이핑을-추가한-javascript의-구문적-상위-집합니다">TypeScript? : 정적 타이핑을 추가한 JavaScript의 구문적 상위 집합니다.</h3>
<p>어떤 유형의 데이터가 전달되는지 이해 어려움 -&gt; 함수 매개변수와 변수에 아무런 정보가 없음
코드 내에서 전달되는 데이터 유형을 지정할 수 있다. 유형이 일치하지 않을 때 오류 보고 가능</p>
<p>= 코드 실행 전에 지정된 유형이 일치하는지 확인</p>
<p>string = &quot;Hello, TypeScript!&quot;;</p>
<ul>
<li><p>컴파일러 : npm을 통해 설치 가능
npm install typescript --save-dev
npx tsc로 실행 가능</p>
</li>
<li><p>컴파일러 구성 : npx tsc --init</p>
<pre><code>function greet(name: string): string {
  return `Hello ${name}!`;
</code></pre></li>
</ul>
<p>const message string = greet(&quot;world&quot;);</p>
<pre><code>
- 코드 컴파일 : npx tsc hello.ts -&gt; node hello.js 실행

### 원시형
1) boolean 
let isActive: boolean = true;
let hasPermission = false;

2) number 
3) string
let color: string = &quot;blue&quot;;
let fullName: string = &#39;John Doe&#39;;
4) bigint
5) symbol : 중복되지 않는 고유한 값
ex. 객체의 속성 이름이 겹치지 않게 하기 위해 주로 사용한다.
const ID = Symbol(&#39;id&#39;);
const user = { name: &#39;Alice&#39;, [ID]: 123, };
console.log(user[ID]); // 123

### 명시적 유형 및 추론

1) 명시적 타이핑 : 변수의 유형을 명시적으로 선언한다.
</code></pre><p>// String
greeting: string = &quot;Hello, TypeScript!&quot;;</p>
<p>// Number
userCount: number = 42;</p>
<p>// Boolean
isLoading: boolean = true;</p>
<p>// Array of numbers
scores: number[] = [100, 95, 98];</p>
<pre><code>
2) 유형 추론 : TypeScript는 할당된 
초기값을 기반으로 변수의 유형을 자동으로 결정(추론)할 수 있다.

let username = &quot;alice&quot;; infers &#39;string&#39;
let score = 100; // infers &#39;number&#39;
</code></pre><p>// TypeScript infers the shape of the object
const user = {
name: &quot;Alice&quot;,
age: 30,
isAdmin: true
};</p>
<p>// TypeScript knows these properties exist
console.log(user.name);  // OK
console.log(user.email); // Error: Property &#39;email&#39; does not exist</p>
<pre><code>= JavaScript는 오류 없이 실행되지만 버그가 발생할 가능성이 있다.</code></pre><p>function add(a: number, b:number): number {
    return a + b;
}</p>
<pre><code>- TypeScript가 유형을 유추할 수 없는 경우 
any유형 검사를 비활성화한다.
let something; // type is &#39;any&#39;
something = &#39;hello&#39;;
something = 42;

any를 사용하면 TypeScript의 유형 검사가 비활성화된다.

### TypeScript 특수 유형

- any
TypeScript에서 가장 유연한 유형이다. 컴파일러에게 특정 변수에 대한 유형 검사를 건너뛰라고 지시하는 것이다.

1) JavaScript코드를 TypeScript로 마이그레이션할 때
2) 유형이 알려지지 않은 동적 콘텐츠로 작업할 때
3) 특정 사례에 대한 유형 검사를 거부해야 하는 경우
</code></pre><p>let v:any = true;
v = &quot;string&quot;;
Math.round(v); // any타입으로 선언하면 에러가 발생하지 않는다.</p>
<pre><code>
- unknown 
any보다 더 안전한 **모든 값의 상위 타입이다.** 뭐가 들어올지 모르겠지만, 안전하게 다뤄야 하는 값이다.
이것은 무엇이든 될 수 있으므로 사용하기 전에 어떤 유형의 검사를 수행해야 한다.
무엇이든 될 수 있으므로 사용하기 전에 어떤 유형의 검사를 수행해야 합니다 라고 말하는 안전한 방법이다.

let value: unknown;
value = 123; //ok
value = &quot;hello&quot;;
</code></pre><p>let a: any = 10;
let b: unknown = 10;</p>
<p>a.toFixed(); // ✅ OK (위험함)
b.toFixed(); // ❌ Error — b의 타입을 아직 모름</p>
<pre><code>#### 타입을 명시적으로 확인한 다음에 접근 가능하다.

= API 응답 데이터(JSON) 타입을 아직 모를 때
try/catch로 잡은 에러의 타입을 알 수 없을 때
제너릭 함수의 입력 타입이 명확하지 않을 때
</code></pre><p>function parseJSON(input: string): unknown {
  return JSON.parse(input);
}</p>
<p>const data = parseJSON(&#39;{&quot;name&quot;: &quot;Alice&quot;}&#39;);</p>
<p>// data.name; ❌ 에러 (아직 unknown)
if (typeof data === &quot;object&quot; &amp;&amp; data !== null &amp;&amp; &quot;name&quot; in data) {
  console.log((data as any).name); // ✅ 안전하게 접근
}</p>
<pre><code>

#### = 라이브러리나 유틸 함수에서 타입을 미리 알 수 없을 때 유용하다. 

- never
발생하지 않는 값의 유형을 나타낸다.
반환하지 않는 함수(항상 오류를 발생시키거나 무한 루프에 진입함)
유형 검사를 통과하지 못하는 유형 가드
차별받는 노조의 철저성 검사

function throwError(message: string): never {
    throw new Error(message);
}

- undefined &amp;&amp; null
undefined변수가 선언 -&gt; 값 할당 x
null값이나 객체 나타내지 않는 명시적 할당
활성화 -&gt; strictNullChecks 유형 명시적으로 처리
</code></pre><p>function greet(name?: string) {
  return <code>Hello, ${name || &#39;stranger&#39;}</code>;
}</p>
<p>// Optional property in an interface
interface User {
  name: string;
  age?: number; // Same as <code>number | undefined</code> }</p>
<pre><code>

- Nullish Coalescing(??) 널리쉬 콜레싱 : 주로 변수에 적용되어 사용된다.
const value = something ?? defaultValue;
something이 null 또는 undefined -&gt; defaultValue 사용한다.
falsy값은 그대로 유지</code></pre><p>function getPrice(price?: number) {
    return price ?? 100; // price가 null/undefined이면 기본값 100
}</p>
<pre><code>
- Optional Chaining : 선택적 체이닝 주로 객체에 적용되어 사용
?.는 객체의 속성이 없거나 undefined여도 에러 없이 안전하게 접근할 수 있게 해주는 연산자이다.
null또는 undefined이면 -&gt; undefined 반환
그렇지 않으면 -&gt; 정상적으로 오른쪽 속성에 접근
</code></pre><p>console.log(user?.address?.city);
const user = {
  name: &quot;Alice&quot;,
  settings: null
};</p>
<p>const theme = user.settings?.theme ?? &quot;light&quot;;
console.log(theme); // &quot;light&quot;</p>
<pre><code>
### Array
배열을 입력하기 위한 특정 구문
const names: string[] = [];
names.push(&quot;Dylan&quot;);

- readonly 배열이 변경되는 것을 방지
const names: readonly string[] = [&quot;Dylan&quot;]; // push를 할 수 없다.

- TypeScript는 값이 있는 경우 배열의 유형을 추론할 수 있다.

### Tuple
각 인덱스의 길이와 유형이 미리 정의된 형식화된 배열이다.
배열의 요소가 알려진 유형의 값이 될 수 있기 때문에 유용하다.</code></pre><p>// define our tuple
let ourTuple: [number, boolean, string];</p>
<p>// initialize correctly
ourTuple = [false, &#39;Coding God was here&#39;, 5];</p>
<pre><code>
boolean, string이 있더라도 순서가 중요하므로 오류가 발생한다. number

- readonly 초기 값에 대해서만 강력하게 정의된 유형을 갖는다.
튜플을 만드는 것이다.
</code></pre><p>const ourReadonlyTuple: readonly [number, boolean, string] = [5, true, &#39;The Real Coding God&#39;];
// throws error as it is readonly.
ourReadonlyTuple.push(&#39;Coding God took a day off&#39;);</p>
<pre><code>
- 명명된 튜플
각 인덱스의 값에 대한 컨텍스트 제공
const graph: [x: number, y: number] = [55.2, 41.3];

- 튜플 구조 분해
const graph: [number, number] = [55.2, 41.3];
const [x, y] = graph;








</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Next.js tailwind 설치 권한 문제]]></title>
            <link>https://velog.io/@seonguul_2/Next.js-tailwind-%EC%84%A4%EC%B9%98-%EA%B6%8C%ED%95%9C-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@seonguul_2/Next.js-tailwind-%EC%84%A4%EC%B9%98-%EA%B6%8C%ED%95%9C-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Sat, 13 Sep 2025 00:11:04 GMT</pubDate>
            <description><![CDATA[<p>1.
<img src="https://velog.velcdn.com/images/seonguul_2/post/11e24a4d-5396-4eb9-8eea-dd015dc00b03/image.png" alt=""></p>
<ol start="2">
<li><img src="https://velog.velcdn.com/images/seonguul_2/post/37439e05-3b01-4868-8de2-70c70e918b98/image.png" alt=""></li>
</ol>
<p>두가지 방법대로 하면 설치가 완료된다.</p>
<p>init -p는 굳이 안해도 된다.
css파일에 @import &quot;tailwindcss&quot;; 만 선언하고 사용해도 됌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Readme, boxshadow]]></title>
            <link>https://velog.io/@seonguul_2/Readme-%EC%9E%91%EC%84%B1-%EC%82%AC%EC%9D%B4%ED%8A%B8</link>
            <guid>https://velog.io/@seonguul_2/Readme-%EC%9E%91%EC%84%B1-%EC%82%AC%EC%9D%B4%ED%8A%B8</guid>
            <pubDate>Thu, 04 Sep 2025 01:26:44 GMT</pubDate>
            <description><![CDATA[<p>1) readme.so
<a href="https://readme.so/editor">https://readme.so/editor</a></p>
<p>2)<a href="https://github-profile-readme-editor.netlify.app/">https://github-profile-readme-editor.netlify.app/</a></p>
<p>3) <a href="https://www.easy-me.com/d">https://www.easy-me.com/d</a> </p>
<p>easy-me</p>
<ul>
<li>boxshadow
<a href="https://cssgenerator.org/box-shadow-css-generator.html">https://cssgenerator.org/box-shadow-css-generator.html</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>