<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>forest_xox</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 25 Mar 2025 07:56:05 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>forest_xox</title>
            <url>https://velog.velcdn.com/images/forest_xox/profile/beced2f0-c1b2-4761-b0ab-6da6d4000af2/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. forest_xox. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/forest_xox" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Nuxt] Nuxt+Vitest 초기 세팅]]></title>
            <link>https://velog.io/@forest_xox/Nuxt-NuxtVitest-%EC%B4%88%EA%B8%B0-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@forest_xox/Nuxt-NuxtVitest-%EC%B4%88%EA%B8%B0-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Tue, 25 Mar 2025 07:56:05 GMT</pubDate>
            <description><![CDATA[<h2 id="1-테스트-관련-라이브러리">1. 테스트 관련 라이브러리</h2>
<h3 id="기본-테스트-라이브러리">기본 테스트 라이브러리</h3>
<ul>
<li><strong>vitest</strong>: 빠르고 가벼운 JavaScript 테스트 프레임워크</li>
<li><strong>eslint-plugin-vitest</strong>: Vitest 관련 ESLint 규칙 제공</li>
<li><strong>happy-dom</strong>: 가벼운 가상 DOM 구현체 (GUI 없는 브라우저 환경 제공)<ul>
<li>테스트, 웹 스크래핑, SSR 환경에서 활용 가능</li>
</ul>
</li>
<li><strong>@testing-library/user-event</strong>: 사용자의 실제 브라우저 이벤트를 시뮬레이션하는 라이브러리</li>
<li><strong>@testing-library/jest-dom</strong>: Jest 전용 매처 제공</li>
<li>Vitest에서도 Jest 매처를 지원하므로, 이를 활용해 직관적인 테스트 코드 작성 가능</li>
</ul>
<h3 id="p-stylecolorgreennuxtvue-테스트-관련-라이브러리p"><p style="color:green">Nuxt/Vue 테스트 관련 라이브러리<p></h3>
<ul>
<li><strong>@testing-library/vue</strong>: Vue 컴포넌트 테스트를 쉽게 작성할 수 있도록 지원 (@vue/test-utils 기반)<ul>
<li>내부적으로 @testing-library/dom을 사용해 DOM 요소 탐색 및 테스트 가능 (별도 설치 필요 없음)</li>
</ul>
</li>
<li><strong>@vue/test-utils</strong>: Vue 3 컴포넌트 테스트를 위한 유틸리티 제공</li>
<li><strong>@nuxt/test-utils</strong>: Nuxt 앱의 테스트 환경을 설정하고 실행할 수 있도록 지원</li>
</ul>
<hr>
<h2 id="2-nuxt-런타임-환경-설정"><strong>2. Nuxt 런타임 환경 설정</strong></h2>
<p>Nuxt 환경을 추가해야 아래의 요소들을 정상적으로 테스트할 수 있다.</p>
<ul>
<li><strong>Nuxt 전용 API</strong>: useNuxtApp(),useFetch() 등</li>
<li><strong>Nuxt 플러그인,미들웨어,자동 주입된 함수</strong>: useRoute(),useCookie() 등</li>
<li><strong>Nuxt 파일 기반 구조</strong><ul>
<li><strong>파일 기반 라우팅</strong>: pages/ 디렉터리를 통한 자동 라우팅</li>
<li><strong>기타 디렉터리 자동 처리</strong>: layouts/, server/ 등</li>
</ul>
</li>
</ul>
<h3 id="nuxt-환경-활성화">Nuxt 환경 활성화</h3>
<p><strong>1. 부분적 Nuxt환경 활성화하는 방법 2가지</strong></p>
<ol>
<li><code>.nuxt.</code>가 포함된 파일명 ex) my-file.nuxt.test.ts</li>
<li>테스트 파일에 <code>// @vitest-environment nuxt</code> 주석 추가</li>
</ol>
<p><strong>2. 모든 테스트 Nuxt 환경 기본 설정하는 방법</strong></p>
<p>vitest.config.ts에서 environment: &#39;nuxt’ 설정</p>
<pre><code class="language-jsx">// vitest.config.ts
import { defineVitestConfig } from &#39;@nuxt/test-utils/config&#39;

export default defineVitestConfig({
  test: { environment: &#39;nuxt&#39; }
})</code></pre>
<p><strong>3. 특정 테스트 파일에서 Nuxt 환경을 제외하는 방법</strong></p>
<p><code>// @vitest-environment node</code> 주석 추가해 Nuxt환경 비활성화 </p>
<hr>
<h2 id="3-nuxt-dev-tools에서-테스트-선택-사항">3. Nuxt Dev Tools에서 테스트 (선택 사항)</h2>
<pre><code class="language-jsx">export default defineNuxtConfig({
  modules: [&#39;@nuxt/test-utils/module&#39;]
})</code></pre>
<p>nuxt.config 파일에 @nuxt/test-utils/module을 추가하면 Nuxt DevTools에서 단위 테스트를 실행할 수 있으며, 추가적으로 <code>@vitest/ui</code> 가 필요하다. </p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Nuxt] SSR에서 $fetch onResponse의 Nuxt 컨텍스트 오류 해결]]></title>
            <link>https://velog.io/@forest_xox/Nuxt-SSR%EC%97%90%EC%84%9C-fetch-onResponse%EC%9D%98-Nuxt-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@forest_xox/Nuxt-SSR%EC%97%90%EC%84%9C-fetch-onResponse%EC%9D%98-Nuxt-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Tue, 25 Mar 2025 07:32:29 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제">1. 문제</h2>
<p>로딩 표시 최적화를 위해 로딩이 1초 이상일 때만 <code>useLoadingStore().show()</code>로 로딩 스피너를 표시하였다.</p>
<p>문제는 새로고침을 하면(SSR) Nuxt의 $fetch API에 문제가 생겨서 데이터 요청을 하지 않는다.
⬇️에러 메시지⬇️</p>
<pre><code>[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.</code></pre><h2 id="2-디버깅-과정">2. 디버깅 과정</h2>
<p>최적화 후 문제가 생겨서 이전 코드와 비교를 진행했다. 전에는 onRequest에서 스토어 초기화 및 실행을 반드시 진행하였다. </p>
<pre><code class="language-tsx">// before
onRequest({ options }) {        
 useLoadingStore().show() 🚩
 options.headers = {생략}      
},      
onResponse() {        
 useLoadingStore().hide()      
},</code></pre>
<pre><code class="language-tsx">// after
onRequest({ options }) {        
    options.headers = {생략}
    if (timer) clearTimeout(timer)        
    timer = setTimeout(() =&gt; {          
        useLoadingStore().show() 🚩   
    }, 1000)      
},      
onResponse() {        
    if (timer) clearTimeout(timer)        
    useLoadingStore().hide()      
},</code></pre>
<p>하지만 현재 코드는 요청이 1초가 지나지 않으면 스토어 초기화 및 실행을 하지 않는다. onResponse에서 초기화가 되면 문제가 생기는 것이였다.</p>
<ul>
<li>useLoadingStore() 호출: 스토어 인스턴스 생성 혹은 가져옴</li>
<li>SSR) onResponse에서는 인스턴스 가져오기 가능, 생성은 불가</li>
</ul>
<p><strong>$fetch 외부</strong>와 <strong>onRequest에서 초기화</strong>하니 정상 동작하였다. 또는 <strong>runWithContext</strong>를 사용해 Nuxt 컨텍스트를 복원할 수 있다.</p>
<p>서버에서 사용할 필요가 없는 경우 클라이언트에서만 스토어에 접근하게 하면 된다.</p>
<p><strong>onResponse에서 Nuxt 컨텍스트 사용하는 방법 4가지</strong></p>
<ol>
<li>$fetch 외부에서 초기화</li>
<li>onRequset에서 초기화</li>
<li>runWithContext 사용</li>
<li>서버에서 렌더링 필요 없으면) import.meta.client로 분기처리</li>
</ol>
<p>로딩은 서버에서 렌더링될 필요가 없어서 분기처리 하면 해결된다. 하지만 서버 렌더링에서 Nuxt컨텍스트가 필요하다면 1~3번 방식을 이용하면 된다. </p>
<h2 id="3-결론">3. 결론</h2>
<p>onResponse에서 Nuxt 컨텍스트와 Nuxt  컴포저블 참조는 가능하나, 이를 생성하면 SSR 환경에서 오류가 발생할 수 있다.</p>
<p>setup(), useFetch(), useAsyncData() 등은 클라이언트와 서버에서 실행될 때 Nuxt 컨텍스트에 접근할 수 있다. 하지만 Nitro 미들웨어(onRequest, onResponse 등)에서는 SSR 환경에서 실행될 때 Nuxt 컨텍스트를 유지하지 않는다.</p>
<ul>
<li>Nitro 미들웨어는 Nuxt가 아닌 Nitro 실행 컨텍스트에서 동작하기 때문에 Nuxt 컨텍스트에 접근할 수 없다.</li>
</ul>
<p>따라서 useFetch() 내부에서 인터셉터(onRequest, onResponse…)를 사용할 경우, 클라이언트와 서버에서 동작 방식이 다르다는 것을 인지하고 개발해야 한다.</p>
<blockquote>
<p>useNuxtApp은 클라이언트 및 서버 측에서 모두 사용할 수 있는(Nitro 라우트 내에서는 사용할 수 없음) Nuxt의 공유 런타임 컨텍스트에 액세스하는 방법을 제공하는 내장 컴포저블입니다(Nuxt 컨텍스트라고도 함). Vue 앱 인스턴스, 런타임 훅, 런타임 구성 변수 및 내부 상태(예: ssrContext 및 페이로드)에 액세스하는 데 도움이 됩니다.</p>
<p><a href="https://nuxt.com/docs/api/composables/use-nuxt-app">https://nuxt.com/docs/api/composables/use-nuxt-app</a></p>
</blockquote>
<hr>
<pre><code class="language-tsx">const $customFetch = async (url, opts) =&gt; {
  const loadingStroe = useLoadingStore() 🚩
  let timer: NodeJS.Timeout | null = null

  return await $fetch({생략}, {
    ...opts,
    onRequest() {
        if(import.meta.client){ 🚩
        if (timer) clearTimeout(timer)
        timer = setTimeout(() =&gt; useLoadingStore().show(), 1000)}
    },
    onResponse() {
      if(import.meta.client){ 🚩
        if (timer) clearTimeout(timer)
        useLoadingStore().hide()
      }
    },
  })
}
</code></pre>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://github.com/nuxt/nuxt/discussions/22615">https://github.com/nuxt/nuxt/discussions/22615</a></li>
<li><a href="https://nuxt.com/docs/api/composables/use-nuxt-app#runwithcontext">https://nuxt.com/docs/api/composables/use-nuxt-app#runwithcontext</a></li>
<li><a href="https://github.com/nuxt/nuxt/discussions?discussions_q=is%3Aopen+onResponse+Vue+instance+">https://github.com/nuxt/nuxt/discussions?discussions_q=is%3Aopen+onResponse+Vue+instance+</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tanstack Query] 9. Query Key 네이밍 가이드]]></title>
            <link>https://velog.io/@forest_xox/Tanstack-Query-9.-Query-Key-%EB%84%A4%EC%9D%B4%EB%B0%8D-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@forest_xox/Tanstack-Query-9.-Query-Key-%EB%84%A4%EC%9D%B4%EB%B0%8D-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Sun, 16 Feb 2025 10:16:20 GMT</pubDate>
            <description><![CDATA[<h2 id="1-쿼리-키란">1. 쿼리 키란?</h2>
<p>쿼리 키는 각 쿼리를 식별하는 역할을 하며 캐싱, 데이터 동기화, 리페칭에 사용된다.</p>
<p>배열 또는 문자열로 정의하며, 같은 키를 가진 쿼리는 같은 데이터로 인식된다.</p>
<p>아래의 쿼리는 다른 키를 가지고 있어 따로 관리된다.</p>
<pre><code class="language-tsx">useQuery({ queryKey: [&#39;todos&#39;], queryFn: fetchTodos })
useQuery({ queryKey: [&#39;todos&#39;, userId], queryFn: fetchUserTodos })</code></pre>
<h2 id="2-쿼리-키-네이밍-패턴">2. 쿼리 키 네이밍 패턴</h2>
<h3 id="2-1-기본적으로-배열-사용">2-1. 기본적으로 배열 사용</h3>
<p>쿼리 키는 배열로 작성하는 것이 권장된다. </p>
<p>이유는 계층적 구조를 가져 더 세밀한 관리가 가능하다. 또 검색 및 필터링에 유리하다.</p>
<pre><code class="language-tsx">useQuery({ queryKey: [&#39;todos&#39;, {userId: 1}], queryFn: fetchUserTodos })</code></pre>
<p><code>queryKey: [&#39;todos&#39;, {userId: 1}]</code> 를 사용하면 특정 사용자만 조회하는 쿼리로 관리가 가능하다.</p>
<h3 id="2-2-객체는-마지막에-넣기">2-2 객체는 마지막에 넣기</h3>
<p>쿼리 키에 객체도 포함이 가능하다. 하지만 마지막 요소로 두는 것이 좋다.</p>
<p>이유는 배열의 앞부분을 기준으로 더 쉽게 필터링이 가능하기 때문이다.</p>
<pre><code class="language-tsx">useQuery({ queryKey: [&#39;todos&#39;, { userId: 1, status: &#39;done&#39; }] })</code></pre>
<p>위처럼 하면 [’todos’] 쿼리와 구분이 가능하면서, <code>queryClient.invalidateQueries({ queryKey: [&#39;todos&#39;] })</code>로 모든 todos 쿼리를 한 번에 무효화할 수 있다.</p>
<h3 id="2-3-필터링-정보-포함">2-3 필터링 정보 포함</h3>
<p>쿼리 키에 필터링 정보를 넣어 필터링이 변경될 때마다 새 데이터를 가져오게 할 수 있다.</p>
<pre><code class="language-tsx">useQuery({ queryKey: [&#39;todos&#39;, { userId, status }], queryFn: fetchTodos })
// [&#39;todos&#39;, { userId:1, status:done }] ≠ [&#39;todos&#39;, { userId:1, status:pending }]</code></pre>
<p>status가 done이면 완료된 할 일, pending이면 진행중인 할 일을 가져온다. 상태가 다르면 다른 키로 구분되어 각가 캐시된다.</p>
<h3 id="2-4-객체-사용-주의사항">2-4 객체 사용 주의사항</h3>
<p>키 순서가 달라도 동일한 쿼리로 인식된다.</p>
<pre><code class="language-tsx">useQuery({ queryKey: [&#39;todos&#39;, { status, page }] }) 
useQuery({ queryKey: [&#39;todos&#39;, { page, status }] }) 
useQuery({ queryKey: [&#39;todos&#39;, { page, status, other: undefined }] }) </code></pre>
<p>하지만 객체 내부의 값이 다르면 별개의 쿼리로 취급된다.</p>
<pre><code class="language-tsx">useQuery({ queryKey: [&#39;todos&#39;, { page: 1 }] }) 
useQuery({ queryKey: [&#39;todos&#39;, { page: 2 }] }) </code></pre>
<h2 id="3-쿼리-키-관리-예제">3. <strong>쿼리 키 관리 예제</strong></h2>
<pre><code class="language-tsx">// Todo 목록 조회
useQuery({ queryKey: [&#39;todos&#39;], queryFn: fetchTodos })
// 특정 사용자 Todo 조회
useQuery({ queryKey: [&#39;todos&#39;, { userId }], queryFn: fetchUserTodos })
// 특정 필터 적용된 Todo 조회
useQuery({ queryKey: [&#39;todos&#39;, { userId, status: &#39;done&#39; }], queryFn: fetchUserTodos })
// 모든 Todo 캐시 무효화
queryClient.invalidateQueries({ queryKey: [&#39;todos&#39;] })
// 특정 유저의 Todo 캐시 무효화
queryClient.invalidateQueries({ queryKey: [&#39;todos&#39;, { userId }] })</code></pre>
<h2 id="4-todos-1-vs-todos--userid-1-">4. [&#39;todos&#39;, 1] VS [&#39;todos&#39;, { userId: 1 }]</h2>
<p>두 방식은 모두 특정 데이터를 조회할 때 사용하는 패턴이지만, 목적과 사용 범위가 다르다.</p>
<h3 id="todos-1">[&#39;todos&#39;, 1]</h3>
<p>이 패턴은 특정 단일 항목을 조회할 때 사용한다. 보통 GET /todos/1 같은 요청에 해당한다.</p>
<p>예를 들면 특정 할 일의 상세정보, 특정 게시글 조회 등이 있다.</p>
<h3 id="todos--userid-1-">[&#39;todos&#39;, { userId: 1 }]</h3>
<p>이 패턴은 여러 데이터를 조회할 때 사용한다. 보통 GET /todos?userId=1 같은 요청에 해당한다.</p>
<p>예를 들면 특정 사용자의 모든 할 일 목록, 특정 카테고리의 게시글 목록 등이 있다.</p>
<p>정리하면 [&#39;todos&#39;, id]는 단일 리소스 조회, [&#39;todos&#39;, { 필터 }]는 조건부 목록 조회에 활용된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tanstack Query] 8. 쿼리 캐시와 데이터 프리페칭]]></title>
            <link>https://velog.io/@forest_xox/Tanstack-Query-8.-%EC%BF%BC%EB%A6%AC-%EC%BA%90%EC%8B%9C%EC%99%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%94%84%EB%A6%AC%ED%8E%98%EC%B9%AD</link>
            <guid>https://velog.io/@forest_xox/Tanstack-Query-8.-%EC%BF%BC%EB%A6%AC-%EC%BA%90%EC%8B%9C%EC%99%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%94%84%EB%A6%AC%ED%8E%98%EC%B9%AD</guid>
            <pubDate>Sun, 16 Feb 2025 07:20:11 GMT</pubDate>
            <description><![CDATA[<h2 id="1-쿼리-캐시">1. 쿼리 캐시</h2>
<p>쿼리 캐시는 이전에 가져온 데이터를 저장하여, 동일한 요청이 발생할 때 네트워크 요청 없이 빠르게 반환할 수 있도록 하는 기능이다.</p>
<p>TanStack Query는 기본적으로 쿼리 키를 기준으로 데이터를 캐싱한다.</p>
<h3 id="특징">특징</h3>
<p><strong>1. staleTime (신선도 시간)</strong></p>
<p>이 시간이 지나기 전까지는 캐시된 데이터를 신선한(fresh) 상태로 간주한다.</p>
<p>stale상태는 새로운 데이터를 가져와도 되는 상태로 이때 리패치가 발생하면 새로 요청한다.</p>
<p>별다른 설정을 하지 않았다면 컴포넌트 마운트, 윈도우 포커스, 네트워크 재연결 등 발생하면 자동 리패치가 일어난다.</p>
<ul>
<li>기본값: 0ms (즉시 stale 상태가 됨)</li>
</ul>
<p><strong>2. gcTime (가비지 컬렉션 시간)</strong></p>
<p>캐시 데이터가 메모리에서 삭제되기까지의 시간이다.</p>
<ul>
<li>기본값: 5분</li>
</ul>
<p><strong>3. 캐시된 데이터를 바로 반환</strong></p>
<p>쿼리 요청 시 캐시 데이터가 있다면 바로 반환을 한다.</p>
<p>staleTime이 지나지 않았다면 추가 요청 없이 그대로 사용한다.</p>
<p><strong>4. 쿼리 키가 동일하면 캐시 공유</strong></p>
<p>동일한 쿼리 키를 가진 useQuery들은 같은 캐시 데이터를 참조한다.</p>
<h2 id="2-queryclient">2. queryClient</h2>
<p>쿼리 캐시는 queryClient로 관리할 수 있고, useQueryClient()로 접근 가능하다.</p>
<p>기본적으로 특정 쿼리 키의 캐시 데이터 가져오기, 삭제, 갱신이 가능하다.</p>
<pre><code class="language-tsx">import { useQueryClient } from &#39;@tanstack/vue-query&#39;
const queryClient = useQueryClient()

// 캐시 데이터 가져오기
const userCache = queryClient.getQueryData([&#39;user&#39;, userId])
// 캐시 삭제
queryClient.removeQueries([&#39;user&#39;, userId])
// 데이터 강제 갱신 (리페치)
queryClient.invalidateQueries([&#39;user&#39;, userId])</code></pre>
<p><strong>추가 메서드 소개</strong></p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>getQueryData(queryKey)</code></td>
<td>특정 queryKey에 해당하는 캐시된 데이터 가져오기</td>
</tr>
<tr>
<td><code>setQueryData(queryKey, data)</code></td>
<td>특정 queryKey의 캐시 데이터를 강제로 설정</td>
</tr>
<tr>
<td><code>invalidateQueries(queryKey?)</code></td>
<td>특정 queryKey(또는 전체)의 캐시를 무효화하고 다시 요청</td>
</tr>
<tr>
<td><code>removeQueries(queryKey?)</code></td>
<td>특정 queryKey(또는 전체)의 캐시 데이터를 삭제</td>
</tr>
<tr>
<td><code>fetchQuery(queryKey, queryFn)</code></td>
<td>특정 쿼리를 실행하고 결과를 캐시에 저장 (프리페칭)</td>
</tr>
<tr>
<td><code>prefetchQuery(queryKey, queryFn)</code></td>
<td>특정 쿼리를 미리 프리페칭 하지만, 기존 캐시가 유효하면 요청 안 함</td>
</tr>
<tr>
<td><code>cancelQueries(queryKey?)</code></td>
<td>특정 queryKey의 요청을 취소</td>
</tr>
<tr>
<td><code>refetchQueries(queryKey?)</code></td>
<td>특정 queryKey의 데이터를 다시 불러오기</td>
</tr>
<tr>
<td><code>getQueryState(queryKey)</code></td>
<td>특정 queryKey의 상태(stale, fetching, inactive 등) 조회</td>
</tr>
</tbody></table>
<p>자세한 사용법은 아래 주소에 있다.</p>
<p><a href="https://tanstack.com/query/v5/docs/reference/QueryClient">https://tanstack.com/query/v5/docs/reference/QueryClient</a> </p>
<h2 id="3--데이터-프리패칭">3.  <strong>데이터 프리패칭</strong></h2>
<p>프리패칭이란 사용자가 요청하기 전에 미리 데이터를 가져와서, 요청 시 더 빠르게 응답하는 기법이다.</p>
<p><code>prefetchQuery</code>와 <code>prefetchInfiniteQuery</code>를 활용해서 미리 데이터를 가져올 수 있다.</p>
<p><strong>queryClient.prefetchQuery</strong>: 특정 queryKey의 데이터를 미리 가져오고, useQuery가 실행될 때 캐시에서 즉시 반환한다.</p>
<p><strong>queryClient.prefetchInfiniteQuery</strong>: useInfiniteQuery에 사용할 데이터를 미리 가져온다.</p>
<p><strong>useQuery + staleTime</strong>: staleTime이 0이면 프리패칭을 해도 상세페이지로 가면 다시 새 데이터를 요청을 한다. 적절한 staleTime 설정을 해야 프리패칭 효과를 얻을 수 있다.</p>
<ul>
<li>useQuery의 staleTime이 5초라면 프리패칭 후 5초 동안 fresh상태다. 그래서 5초 전에 해당 쿼리가 있는 페이지로 이동하면 프리패칭된 데이터를 사용한다. 하지만 5초후에 이동하면 새 데이터를 요청한다.</li>
</ul>
<h3 id="프리패칭">프리패칭</h3>
<pre><code class="language-html">&lt;!-- 사용자 목록 페이지 --&gt;
&lt;script setup&gt;
import { useQueryClient } from &#39;@tanstack/vue-query&#39;
const queryClient = useQueryClient()

const prefetchUserDetail = (userId) =&gt; {
    queryClient.prefetchQuery({
    queryKey: [&#39;user&#39;, userId],
    queryFn: () =&gt; fetchUser(userId),
    gcTime: 1000 * 60 * 1
  })
}
&lt;/script&gt;
&lt;template&gt;
  &lt;ul&gt;
    &lt;li v-for=&quot;user in users&quot; :key=&quot;user.id&quot;&gt;
      &lt;!-- 마우스를 올리면 프리패칭 실행 --&gt;
      &lt;router-link to=&quot;`/user/${user.id}`&quot;
        @mouseenter=&quot;prefetchUserDetail(user.id)&quot;
      &gt;
        {{ user.name }} 상세보기
      &lt;/router-link&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/template&gt;</code></pre>
<h3 id="프리패칭-데이터-사용">프리패칭 데이터 사용</h3>
<pre><code class="language-html">&lt;!-- 사용자 상세보기 페이지 --&gt;
&lt;script setup&gt;
import { useRoute } from &#39;vue-router&#39;
import { useQuery } from &#39;@tanstack/vue-query&#39;
const route = useRoute()
const userId = route.params.id

const { data: user } = useQuery({
    queryKey: [&#39;user&#39;, userId],
    queryFn: () =&gt; fetchUser(userId),
    staleTime: 5000
  })
&lt;/script&gt;
&lt;template&gt;
  &lt;div&gt;
    &lt;h2&gt;{{ user.name }}의 상세 정보&lt;/h2&gt;
    &lt;p&gt;이메일: {{ user.email }}&lt;/p&gt;
    &lt;p&gt;나이: {{ user.age }}&lt;/p&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre>
<h3 id="결과-예상">결과 예상</h3>
<ol>
<li>목록 페이지에서 링크에 마우스를 올리면 프리패칭 된다.</li>
<li>사용자 상세 페이지로 이동하면 useQuery가 실행된다.</li>
<li>데이터 요청 없이 프리패칭한 데이터를 사용한다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tanstack Query] 7. 무한 스크롤과 더 보기]]></title>
            <link>https://velog.io/@forest_xox/Tanstack-Query-7.-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4%EA%B3%BC-%EB%8D%94-%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@forest_xox/Tanstack-Query-7.-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4%EA%B3%BC-%EB%8D%94-%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 16 Feb 2025 07:16:47 GMT</pubDate>
            <description><![CDATA[<h2 id="1-useinfinitequery"><strong>1. useInfiniteQuery</strong></h2>
<p>useInfiniteQuery는 목록 데이터를 점진적으로 불러와 ‘더 보기’ 또는 ‘무한 스크롤’을 구현할 때 사용되는 훅이다. useQuery와 유사하지만, 여러 페이지의 데이터를 효율적으로 관리할 수 있도록 추가 기능을 제공한다.</p>
<pre><code class="language-tsx">const {
  data,
  error,
  fetchNextPage,
  hasNextPage,
  isFetching,
  isFetchingNextPage,
  status,
} = useInfiniteQuery({
  queryKey: [&#39;projects&#39;],
  queryFn: fetchProjects,
  initialPageParam: 0,
  getNextPageParam: (lastPage, pages) =&gt; lastPage.nextCursor,
})</code></pre>
<h2 id="2-useinfinitequery의-추가-기능"><strong>2. useInfiniteQuery의 추가 기능</strong></h2>
<p><strong>데이터 구조 변화</strong></p>
<p>data.pages: 불러온 모든 페이지 데이터를 포함하는 배열
data.pageParams: 각 페이지를 가져오는 데 사용된 매개변수를 포함하는 배열</p>
<p><strong>추가 메서드</strong></p>
<p>fetchNextPage: 다음 페이지 데이터를 불러오는 함수</p>
<p>fetchPreviousPage: 이전 페이지 데이터를 불러오는 함수</p>
<p><strong>추가 옵션</strong></p>
<p>initialPageParam: 초기 페이지 매개변수 (필수)</p>
<p>getNextPageParam: 다음 페이지를 불러올 수 있는 기준 정의</p>
<p>getPreviousPageParam: 이전 페이지를 불러올 수 있는 기준 정의</p>
<p><strong>추가 상태값</strong></p>
<p><strong>hasNextPage</strong>: 다음 페이지가 존재하는지 여부 (getNextPageParam이 null/undefined가 아니면 true)
<strong>hasPreviousPage</strong>: 이전 페이지가 존재하는지 여부 (getPreviousPageParam이 null/undefined가 아니면 true)
<strong>isFetchingNextPage</strong>: 다음 페이지 데이터를 요청 중인지 여부
<strong>isFetchingPreviousPage</strong>: 이전 페이지 데이터를 요청 중인지 여부</p>
<h2 id="3-더-보기-예제">3. ‘더 보기’ 예제</h2>
<p><strong>getNextPageParam</strong></p>
<p>getNextPageParam을 보면 lastPage.nextPage값을 통해 다음 페이지가 있는지 설정한다.</p>
<p><code>{&quot;items&quot;: [...], &quot;nextPage&quot;: 3 }</code> 서버에서 이런 형식의 데이터를 받았을 때의 상황이다.</p>
<p>만약 totalCount 정도만 받는다면 직접 계산을 해서 getNextPageParam에 넣으면 된다.</p>
<p><strong>pageParam</strong></p>
<p>useInfiniteQuery에서 queryFn의 인자로 pageParam를 자동으로 전달해준다.</p>
<pre><code class="language-html">&lt;script setup&gt;
import { useInfiniteQuery } from &#39;@tanstack/vue-query&#39;

const fetchPosts = async ({ pageParam = 0, limit = 10 }) =&gt; {
  const res = await fetch(`/api/posts?page=${pageParam}&amp;limit=${limit}`)
  return res.json()
}

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: [&#39;posts&#39;],
  queryFn: fetchPosts,
  initialPageParam: 1, // 초기 페이지
  getNextPageParam: (lastPage) =&gt; lastPage.nextPage, // 다음 페이지 번호 반환
})
&lt;/script&gt;

&lt;template&gt;
  &lt;div&gt;
    &lt;Post v-for=&quot;post in data?.pages.flatMap((group) =&gt; group.items)&quot; :key=&quot;post.id&quot; :post=&quot;post&quot; /&gt;
    &lt;button v-if=&quot;hasNextPage&quot; @click=&quot;fetchNextPage&quot; :disabled=&quot;isFetchingNextPage&quot;&gt;
      {{ isFetchingNextPage ? &#39;로딩 중...&#39; : &#39;더 보기&#39; }}
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tanstack Query] 6. 쿼리 병합과 의존성 관리]]></title>
            <link>https://velog.io/@forest_xox/Tanstack-Query-6.-%EC%BF%BC%EB%A6%AC-%EB%B3%91%ED%95%A9%EA%B3%BC-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@forest_xox/Tanstack-Query-6.-%EC%BF%BC%EB%A6%AC-%EB%B3%91%ED%95%A9%EA%B3%BC-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Sun, 16 Feb 2025 07:13:35 GMT</pubDate>
            <description><![CDATA[<h2 id="1-쿼리-실행-조건과-의존성-관리--querykey-enabled"><strong>1. 쿼리 실행 조건과 의존성 관리 : queryKey, enabled</strong></h2>
<p>queryKey와 enabled를 사용해 어떤 데이터가 다른 데이터에 의존할 때, 올바른 순서로 요청하고 관리할 수 있다.</p>
<pre><code class="language-tsx">const { data: user } = useQuery({
  queryKey: [&#39;user&#39;, userId],
  queryFn: () =&gt; fetchUser(userId),
  enabled: Boolean(userId), // userId가 있어야 실행
});

const { data: posts } = useQuery({
  queryKey: [&#39;posts&#39;, user?.id],
  queryFn: () =&gt; fetchPosts(user?.id), // user가 있어야 실행
  enabled: Boolean(user), // user 데이터가 존재할 때만 실행
});</code></pre>
<h3 id="결과-예상">결과 예상</h3>
<ol>
<li>userId가 있을 때 user 쿼리가 실행되어 fetchUser(userId)를 호출하고, user 데이터를 가져온다.</li>
<li>user 데이터가 존재하면 posts 쿼리가 실행되어 fetchPosts(user.id)를 호출하고, posts 데이터를 가져온다.</li>
<li>queryKey에 user?.id를 포함했기 때문에, user 데이터가 변경되면 posts 쿼리가 자동 갱신된다. userId가 변경된다면 user 쿼리가 자동 갱신된다.</li>
<li>따라서 userId가 없으면 user 쿼리가 실행되지 않고, user 쿼리가 실행되지 않으면 posts 쿼리도 실행되지 않는다.</li>
</ol>
<h2 id="2-여러개의-api-요청-병합--usequeries"><strong>2. 여러개의 API 요청 병합 : useQueries</strong></h2>
<h3 id="usequeries"><strong>useQueries</strong></h3>
<p>여러개의 useQuery를 한번에 실행시켜주는 훅이다. 각 요청을 병렬로 실행해서 성능 최적화가 가능하다.</p>
<p><strong>기본 사용법</strong></p>
<p>useQueries는 배열로 쿼리들을 받고 배열로 결과를 반환한다.</p>
<p>results는 각 useQuery의 반환값을 포함한 배열이라서 아래처럼 접근할 수 있다.</p>
<p>results[0].data → user 데이터
results[1].data → posts 데이터</p>
<pre><code class="language-tsx">const results = useQueries({
  queries: [
    {
      queryKey: [&#39;user&#39;, userId],
      queryFn: () =&gt; fetchUser(userId)
    },
    {
      queryKey: [&#39;posts&#39;, userId],
      queryFn: () =&gt; fetchPosts(userId)
    },
  ],
});</code></pre>
<h3 id="동적-개수의-usequery-실행하기"><strong>동적 개수의 useQuery 실행하기</strong></h3>
<p>useQueries는 배열을 받기 때문에, 동적인 개수의 쿼리도 실행할 수 있다.</p>
<pre><code class="language-tsx">const userIds = [1, 2, 3]; // 여러 개의 userId

const userQueries = useQueries({
  queries: userIds.map((id) =&gt; ({
    queryKey: [&#39;user&#39;, id],
    queryFn: () =&gt; fetchUser(id),
    enabled: !!id,
  })),
});</code></pre>
<p><strong>응답 상태 다루기</strong></p>
<p>some 메서드로 하나라도 로딩중/에러 라면 isLoading/isError를 true로 설정한다.</p>
<pre><code class="language-tsx">const isLoading = userQueries.some(query =&gt; query.isLoading);
const isError = userQueries.some(query =&gt; query.isError);
const users = userQueries.map(query =&gt; query.data).filter(Boolean);</code></pre>
<h2 id="3-usequeries결과-하나의-객체로-변환--combine">3. useQueries결과 하나의 객체로 변환 : <strong>combine</strong></h2>
<p>useQueries의 combine 옵션으로 여러개의 쿼리 결과를 하나의 객체로 합칠 수 있다.</p>
<p>여러개의 쿼리 데이터를 원하는 형식으로 변환하고, isPending, isError 등의 상태도 원하는 대로 계산할 수 있다.</p>
<pre><code class="language-tsx">const ids = [1, 2, 3];

const combinedQueries = useQueries({
  queries: ids.map(id =&gt; ({
    queryKey: [&#39;post&#39;, id],
    queryFn: () =&gt; fetchPost(id),
  })),
  combine: (results) =&gt; {
    return {
      data: results.map(result =&gt; result.data),  // 각 쿼리의 데이터를 배열로 정리
      pending: results.some(result =&gt; result.isPending), // 하나라도 로딩 중이면 true
      // 모든 데이터를 성공적으로 받아오면 true
      allSuccess: results.every(result =&gt; result.status === &#39;success&#39;) 
    };
  },
});</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tanstack Query] 5. 쿼리 상태 관리와 에러 핸들링]]></title>
            <link>https://velog.io/@forest_xox/Tanstack-Query-5.-%EC%BF%BC%EB%A6%AC-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%EC%99%80-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81</link>
            <guid>https://velog.io/@forest_xox/Tanstack-Query-5.-%EC%BF%BC%EB%A6%AC-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC%EC%99%80-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%A7%81</guid>
            <pubDate>Sun, 16 Feb 2025 07:10:32 GMT</pubDate>
            <description><![CDATA[<h2 id="1-usequery의-상태-값"><strong>1. useQuery의 상태 값</strong></h2>
<p>useQuery는 데이터 요청 상태를 나타내는 여러 값을 제공한다. </p>
<p>아래 예제처럼 요청 상태에 따라 별도 동작을 시킬 수 있고, 템플릿에 사용해 요청 상태에 따른 화면을 표시할 수 있다.</p>
<table>
<thead>
<tr>
<th>상태 값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>isLoading</code></td>
<td>데이터 요청 중 (첫 요청)</td>
</tr>
<tr>
<td><code>isFetching</code></td>
<td>백그라운드에서 데이터 가져오는 중</td>
</tr>
<tr>
<td><code>isError</code></td>
<td>요청 실패했을 때 <code>true</code></td>
</tr>
<tr>
<td><code>error</code></td>
<td>에러 객체 (에러 메시지 포함)</td>
</tr>
<tr>
<td><code>isSuccess</code></td>
<td>데이터가 정상적으로 로드됨</td>
</tr>
</tbody></table>
<pre><code class="language-tsx">const { data, isLoading, isFetching, isError, error } = useQuery({
  queryKey: [&#39;users&#39;],
  queryFn: fetchUsers,
});
if(isLoading) console.log(&#39;데이터 로딩 중...&#39;);
if(isFetching) console.log(&#39;새로운 데이터를 가져오는 중...&#39;);
if(isError) console.error(&#39;에러 발생:&#39;, error);</code></pre>
<h2 id="2-에러-핸들링--onerror--retry"><strong>2. 에러 핸들링 : onError &amp; retry</strong></h2>
<p>onError와 retry로 API 요청이 실패했을 때 에러를 처리할 수 있다.</p>
<p><strong>onError</strong></p>
<pre><code class="language-tsx">const { data } = useQuery({
  queryKey: [&#39;users&#39;],
  queryFn: fetchUsers,
  onError: (error) =&gt; {
    console.error(&#39;에러 발생:&#39;, error);
  },
});</code></pre>
<p><strong>retry</strong></p>
<p>네트워크 오류 등으로 요청이 실패했을 때 자동으로 재요청한다.</p>
<pre><code class="language-tsx">const { data } = useQuery({
  queryKey: [&#39;users&#39;],
  queryFn: fetchUsers,
  retry: 3, // 최대 3번 재시도
  retryDelay: 3000, // 실패 시 3초 후 재시도
});</code></pre>
<h2 id="3-초기-데이터와-placeholder-데이터"><strong>3. 초기 데이터와 Placeholder 데이터</strong></h2>
<p>데이터 로드 전까지 초기 데이터를 표시할 수 있다.
<strong>initialData</strong></p>
<p>쿼리의 초기 데이터를 설정한다.</p>
<pre><code class="language-tsx">const { data, isLoading } = useQuery({
  queryKey: [&#39;userProfile&#39;],
  queryFn: fetchUserProfile, // 사용자 프로필 API 호출
  initialData: {name: &#39;알 수 없음&#39;, age: 0},
  staleTime: 1000 * 60 * 5, // 5분 동안 데이터를 fresh 상태로 유지
});</code></pre>
<p><strong>placeholderData</strong></p>
<p>쿼리의 임시 데이터를 설정한다.</p>
<pre><code class="language-tsx">const { data, isLoading } = useQuery({
  queryKey: [&#39;posts&#39;, page], // 현재 페이지 번호
  queryFn: () =&gt; fetchPosts(page), // 페이지별 게시글 불러오기
  placeholderData: { id: 0, title: &#39;로딩 중...&#39;, content: &#39;게시글을 불러오는 중입니다.&#39;}
});</code></pre>
<h3 id="initialdata와-placeholderdata의-차이">initialData와 placeholderData의 차이</h3>
<p>둘다 로딩 중에 보일 데이터를 설정하는 건 비슷하지만 캐싱 차이가 있다.</p>
<p>initialData는 값이 캐싱되고, placeholderData는 캐싱되지 않는다.</p>
<p>그래서 staleTime이 0이면 initialData도 placeholderData처럼 동작하는 것처럼 보인다.</p>
<p><strong>initialData</strong></p>
<p>staleTime 동안 새 데이터를 불러오지 않고 initialData를 사용한다.</p>
<p>이후 staleTime이 지나 새 데이터를 받으면 새 데이터가 캐싱된다.</p>
<p>gcTime이 지나면 캐시가 삭제되기 때문에, 다시 initialData를 캐싱한다.</p>
<p><strong>placeholderData</strong></p>
<p>캐싱되지 않기 때문에 staleTime과 관계없이 새 데이터를 불러온다.</p>
<p>캐시 데이터가 없는 상태에서 로딩중일 때만 보여진다.</p>
<p><strong>정리</strong></p>
<p>초기값이 “진짜 데이터”여야 하고, 캐싱이 필요하다면 → initialData</p>
<p>초기값이 “임시 데이터”여도 되고, 캐싱이 필요 없다면 → placeholderData</p>
<hr>
<h2 id="아하">아하!</h2>
<p>브라우저 새로고침 시 설정한 placeholderData가 계속 나와서 찾아봤는데, 탄스택 쿼리는 새로고침을 하면 캐시와 stale상태의 데이터가 초기화된다고 한다. 새로고침을 해도 캐시가 남아서 캐시가 없는 처음에만 placeholderData가 보일 거라 생각했다.</p>
<hr>
<h2 id="참고">참고</h2>
<p><a href="https://careerly.co.kr/qnas/1034">https://careerly.co.kr/qnas/1034</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tanstack Query] 4. 쿼리 리페치와 데이터 동기화 ]]></title>
            <link>https://velog.io/@forest_xox/Tanstack-Query-4.-%EC%BF%BC%EB%A6%AC-%EB%A6%AC%ED%8E%98%EC%B9%98%EC%99%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8F%99%EA%B8%B0%ED%99%94</link>
            <guid>https://velog.io/@forest_xox/Tanstack-Query-4.-%EC%BF%BC%EB%A6%AC-%EB%A6%AC%ED%8E%98%EC%B9%98%EC%99%80-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8F%99%EA%B8%B0%ED%99%94</guid>
            <pubDate>Sun, 16 Feb 2025 07:06:36 GMT</pubDate>
            <description><![CDATA[<h2 id="리페치">리페치</h2>
<p>리페치는 <strong>이미 존재하는 쿼리의 데이터를 다시 가져오는 것</strong>을 의미한다.</p>
<p>invalidateQueries(): 쿼리를 무효화 </p>
<p>refetch(): 즉시 데이터 다시 불러오기</p>
<h2 id="1-수동-데이터-갱신--refetch">1. <strong>수동 데이터 갱신 : refetch()</strong></h2>
<p>useQuery의 refetch 메서드를 사용해 원하는 시점에 데이터를 갱신할 수 있다.</p>
<pre><code class="language-tsx">const { data, refetch } = useQuery({
  queryKey: [&#39;users&#39;],
  queryFn: fetchUsers,
})
// 버튼 클릭 시 리페치 실행
const handleRefresh = () =&gt; {
  refetch()
}</code></pre>
<h2 id="2-특정-조건에-데이터-가져오기--enabled">2. 특정 조건에 데이터 가져오기 : enabled</h2>
<p>useQuery는 일반적으로 마운트 될 때 자동으로 실행된다. </p>
<p>하지만 <code>enabled옵션</code>을 설정해 특정 조건에서만 실행 시킬 수 있다.</p>
<pre><code class="language-tsx">const isReady = ref(false)

const { data } = useQuery({
  queryKey: [&#39;users&#39;],
  queryFn: fetchUsers,
  enabled: isReady, // isReady가 true일 때만 실행
})
// 버튼 클릭 시 쿼리 활성화
const enableFetching = () =&gt; {
  isReady.value = true
}</code></pre>
<h2 id="3--브라우저-포커스-시-자동-리페치--refetchonwindowfocus">3.  <strong>브라우저 포커스 시 자동 리페치 : refetchOnWindowFocus</strong></h2>
<p><code>refetchOnWindowFocus 옵션</code>을 설정해 <strong>사용자가 브라우저 탭을 다시 활성화할 때</strong> 자동으로 데이터를 다시 가져온다.</p>
<p>다른 작업 후 복귀 시 최신 데이터를 보여줄 수 있다.</p>
<pre><code class="language-tsx">const { data } = useQuery({
  queryKey: [&#39;users&#39;],
  queryFn: fetchUsers,
  refetchOnWindowFocus: true, // 기본값: true
})</code></pre>
<h2 id="4-일정-시간마다-데이터-새로고침--refetchinterval">4. <strong>일정 시간마다 데이터 새로고침 : refetchInterval</strong></h2>
<p><code>refetchInterval옵션</code>을 설정하면 <strong>일정 주기마다 자동으로 데이터를 갱신할 수 있다.</strong> </p>
<p>그래서 실시간 데이터를 보여줄 때 유용하다.</p>
<pre><code class="language-tsx">const { data } = useQuery({
  queryKey: [&#39;users&#39;],
  queryFn: fetchUsers,
  refetchInterval: 5000, // 5초마다 자동 리페치
})</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tanstack Query] 3. 데이터 변경하기와 옵티미스틱 업데이트]]></title>
            <link>https://velog.io/@forest_xox/Tanstack-Query-3.-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0%EC%99%80-%EC%98%B5%ED%8B%B0%EB%AF%B8%EC%8A%A4%ED%8B%B1-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</link>
            <guid>https://velog.io/@forest_xox/Tanstack-Query-3.-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0%EC%99%80-%EC%98%B5%ED%8B%B0%EB%AF%B8%EC%8A%A4%ED%8B%B1-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</guid>
            <pubDate>Sun, 16 Feb 2025 07:03:07 GMT</pubDate>
            <description><![CDATA[<p>useQuery는 데이터를 <strong>읽어오는 용도</strong>, useMutation은 데이터를 <strong>변경하는 용도</strong>이다.</p>
<ul>
<li>useMutation을 사용해 데이터를 추가,수정,삭제를 할 수 있다.</li>
</ul>
<h2 id="1-mutation-기본-개념">1. Mutation 기본 개념</h2>
<p>mutationFn: 실행할 API 요청 (POST, PUT, DELETE)
onSuccess: 요청 성공 시 실행할 로직
onError: 요청 실패 시 실행할 로직</p>
<h2 id="2-기본-사용">2. 기본 사용</h2>
<pre><code class="language-tsx">const addPost = async (newPost) =&gt; {
  const { data } = await axios.post(`${api_url}/posts`, newPost);
  return data;
};

const mutation = useMutation({
  mutationFn: addPost,
  onSuccess: (data) =&gt; {
    console.log(&quot;✅ 게시글 추가 성공:&quot;, data);
  },
  onError: (error) =&gt; {
    console.error(&quot;❌ 게시글 추가 실패:&quot;, error);
  },
});

// 버튼 클릭 시 Mutation 실행 
const addPost = () =&gt; {
  mutation.mutate(newPost);
};</code></pre>
<h2 id="3-mutation-후-데이터-갱신">3. Mutation 후 데이터 갱신</h2>
<p>데이터를 추가/수정/삭제 했다면 기존 데이터를 최신 상태로 유지해야 한다.</p>
<p><code>queryClient.invalidateQueries()</code>를 사용해 자동으로 쿼리를 재실행해 최신 데이터를 유지한다.</p>
<pre><code class="language-tsx">const queryClient = useQueryClient();
const mutation = useMutation({
  mutationFn: addPost,
  onSuccess: () =&gt; {
    queryClient.invalidateQueries({queryKey: [&quot;posts&quot;]}); // 게시글 목록 갱신
  },
});</code></pre>
<ul>
<li><strong>invalidateQueries:</strong>  v5에서는 객체 형태로 전달해야 함</li>
</ul>
<h3 id="결과-예상">결과 예상</h3>
<ol>
<li>새 게시글이 추가되면 [&quot;posts&quot;] 키를 가진 쿼리를 무효화 (invalidateQueries)</li>
<li>기존 게시글 목록이 자동으로 최신 데이터로 갱신된다.</li>
</ol>
<p><strong>쿼리 무효화</strong>: 특정 쿼리의 캐시가 유효하지 않다고 선언하는 것 </p>
<p>무효화된 쿼리는 다음에 자동으로 최신 데이터를 가져오게 된다.</p>
<h2 id="4--옵티미스틱-업데이트">4.  옵티미스틱 업데이트</h2>
<p>서버 응답을 기다리지 않고, UI를 먼저 업데이트하는 걸 옵티미스틱 업데이트라고 한다.</p>
<p>이를 통해 더 나은 UX를 제공할 수 있다. 하지만 관리가 어렵기 때문에  <strong>UI 반응성을 높여야 할 곳</strong> 일부에 사용하는 것이 좋다.</p>
<p><strong>queryClient.setQueryData</strong>: 쿼리의 캐시 데이터를 직접 수정하는 함수 (옵티미스틱 업데이트에 필요)</p>
<p><strong>onMutate</strong>: Mutation이 실행되기 직전에 호출되는 함수 (옵티미스틱 업데이트에 필요)</p>
<p><strong>onSettled</strong>: Mutation이 성공/실패하든, 요청이 끝나면 실행되는 함수</p>
<pre><code class="language-tsx">const mutation = useMutation({
  mutationFn: deletePost,
  onMutate: async (postId) =&gt; {
    // 기존 데이터 백업
    const prevPosts = queryClient.getQueryData([&quot;posts&quot;]);

    // UI에서 삭제된 것처럼 보이게 처리
    queryClient.setQueryData([&quot;posts&quot;], (oldPosts) =&gt;
      oldPosts.filter((post) =&gt; post.id !== postId)
    );

    return { prevPosts }; // 에러 발생 시 기존 상태로 롤백
  },
  onError: (error, postId, context) =&gt; {
      // 기존 상태로 복구
      queryClient.setQueryData([&quot;posts&quot;], context.prevPosts);
  },
  onSettled: () =&gt; {
    // 서버와 동기화 (쿼리 무효화)
    queryClient.invalidateQueries([&quot;posts&quot;]);
  },
});

const handleDelete = (postId) =&gt; {
  mutation.mutate(postId);
};</code></pre>
<h3 id="결과-예상-1">결과 예상</h3>
<ol>
<li>삭제 버튼 클릭 시 UI에서 먼저 삭제됨</li>
<li>실제 API 요청을 보냄</li>
<li>요청이 실패하면 기존 데이터로 복구</li>
<li>요청이 끝나면 쿼리 무효화</li>
</ol>
<h2 id="5-mutation-상태-관리-isloading-issuccess-iserror">5. Mutation 상태 관리 (isLoading, isSuccess, isError)</h2>
<p>Mutation 사용 시 상태도 함께 사용하면 더 좋은 UX를 제공할 수 있다.</p>
<pre><code class="language-tsx">&lt;script setup&gt;
const updatePost = async ({ id, title }) =&gt; {
  const { data } = await axios.put(`${api_url}/posts/${id}`, { title });
  return data;
};

const mutation = useMutation(updatePost);
const editPost = () =&gt; {
  mutation.mutate({ id: 1, title: &quot;업데이트된 제목&quot; });
};
&lt;/script&gt;

&lt;template&gt;
  &lt;div&gt;
    &lt;button @click=&quot;editPost&quot;&gt;게시글 수정&lt;/button&gt;
    &lt;p v-if=&quot;mutation.isLoading&quot;&gt;수정 중...&lt;/p&gt;
    &lt;p v-else-if=&quot;mutation.isError&quot;&gt;오류 발생: {{ mutation.error.message }}&lt;/p&gt;
    &lt;p v-else-if=&quot;mutation.isSuccess&quot;&gt;수정 완료&lt;/p&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre>
<hr>
<h2 id="정리">정리</h2>
<p><code>useMutation</code>을 사용하면 데이터를 추가/수정/삭제 가능</p>
<p><code>invalidateQueries</code>를 사용하면 데이터 변경 후 최신 상태 유지 가능</p>
<p><strong>옵티미스틱 업데이트</strong>를 적용하면 UI를 빠르게 반영 가능</p>
<p><code>isLoading</code>, <code>isError</code>, <code>isSuccess</code>를 활용하면 UX 개선 가능</p>
<hr>
<h2 id="궁금">궁금</h2>
<h3 id="contextpreviousposts-활용-vs-oldusers-그대로-반환">context.previousPosts 활용 VS oldUsers 그대로 반환</h3>
<p>context.previousPosts는 저장해뒀던 값을 다시 설정하는 거니까 이해가 간다. 하지만 oldUsers도 users쿼리의 현재 변경된 값의 이전 값이니까 반환하면 첫번째 방식이랑 같다고 생각했다.</p>
<p>그런데 oldUsers<strong>는 변경 전 데이터가 아니라, 이미 변경된 캐시 데이터</strong>라고 한다.</p>
<pre><code class="language-tsx">onError: (error, postId, context) =&gt; {
  queryClient.setQueryData([&quot;posts&quot;], context.previousPosts);
},</code></pre>
<pre><code class="language-tsx">onError: () =&gt; {
  queryClient.setQueryData([&quot;users&quot;], (oldUsers: User[]) =&gt; {
    return oldUsers;
  });
},</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tanstack Query] 2. 데이터 가져오기와 캐싱]]></title>
            <link>https://velog.io/@forest_xox/Tanstack-Query-2.-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0%EC%99%80-%EC%BA%90%EC%8B%B1</link>
            <guid>https://velog.io/@forest_xox/Tanstack-Query-2.-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0%EC%99%80-%EC%BA%90%EC%8B%B1</guid>
            <pubDate>Sun, 16 Feb 2025 06:50:50 GMT</pubDate>
            <description><![CDATA[<h2 id="1-query의-기본-동작"><strong>1. Query의 기본 동작</strong></h2>
<p>useQuery는 데이터를 가져올 때 아래 순서로 동작한다.</p>
<ol>
<li><strong>캐시 확인</strong> → 기존 데이터가 있으면 재사용</li>
<li><strong>stale 상태 확인</strong> → staleTime 이내면 캐시 사용, 넘어가면 서버 재요청</li>
<li><strong>서버 요청</strong> → 데이터가 없거나 staleTime이 지나면 API 호출</li>
<li><strong>자동 갱신</strong> → refetchInterval, enabled 옵션 등으로 새 데이터 가져오기</li>
</ol>
<h3 id="staletime과-gctime의-차이"><strong>staleTime과 gcTime의 차이</strong></h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>staleTime</td>
<td>데이터가 “stale” 상태로 간주되기 까지의 시간</td>
<td>0ms</td>
</tr>
<tr>
<td>gcTime</td>
<td>데이터가 캐시에 남아있는 시간</td>
<td></td>
</tr>
<tr>
<td>(v5, cacheTime → gcTime 변경)</td>
<td>5분</td>
<td></td>
</tr>
</tbody></table>
<hr>
<h2 id="2-staletime과-gctime-비교"><strong>2. staleTime과 gcTime 비교</strong></h2>
<pre><code class="language-tsx">&lt;script setup lang=&quot;ts&quot;&gt;
import { getUsersApi } from &#39;@/apis/users&#39;
import { useQuery } from &#39;@tanstack/vue-query&#39;

const {data, isLoading, isFetching, error,} = useQuery({
  queryKey: [&#39;users&#39;],
  queryFn: getUsersApi, 
  staleTime: 5000, // 5초 동안 캐시된 데이터 사용
  gcTime: 10000,   // 10초 후 캐시 삭제
})
&lt;/script&gt;</code></pre>
<p><strong>staleTime</strong></p>
<p>해당 시간 동안 캐시된 데이터를 사용한다.</p>
<p>상태: fresh → stale</p>
<p><strong>gcTime</strong></p>
<p>해당 시간 동안 캐시를 유지, 시간이 지나면 삭제</p>
<p>페이지를 벗어나면 <strong>inactive</strong> 상태가 되고, gcTime이 지나면 사라진다.</p>
<p>inactive 상태에 해당 페이지로 돌아갔는데 fresh상태라면 재요청 없이 캐시된 데이터를 사용, stale 상태라면 새 데이터를 가져오는 동안 캐시 데이터 표시</p>
<p>상태: inactive → (삭제)</p>
<p><strong>staleTime과 gcTime 조합 정리</strong></p>
<table>
<thead>
<tr>
<th>상황</th>
<th>설명</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>fresh data, 유효한 gcTime</td>
<td>캐시 데이터가 fresh 상태(staleTime 내)</td>
<td>리패치 없음, 캐시 데이터 바로 반환</td>
</tr>
<tr>
<td>stale data, 유효한 gcTime</td>
<td>staleTime이 지나서 데이터가 stale 상태</td>
<td>리패치 발생, 기존 캐시 데이터 먼저 표시</td>
</tr>
<tr>
<td>gcTime 초과 (캐시 삭제됨)</td>
<td>gcTime이 지나 캐시 데이터가 삭제됨</td>
<td>리패치 발생, 캐시가 없으므로 빈 상태부터 시작</td>
</tr>
</tbody></table>
<p><strong>변형 케이스</strong></p>
<p>일반적으로 일어나지 않는다. 특정 설정을 일부러 넣어야만 발생한다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>설명</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>staleTime 무한대</td>
<td>데이터가 항상 fresh 상태</td>
<td>Refetch 없음, 캐시 데이터만 계속 사용</td>
</tr>
<tr>
<td>gcTime 무한대</td>
<td>캐시가 절대 삭제되지 않음</td>
<td>언제든 캐시된 데이터 반환 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-자동-새로고침--refetchinterval">3. 자동 새로고침 : refetchInterval</h2>
<pre><code class="language-tsx">&lt;script setup lang=&quot;ts&quot;&gt;
import { getUsersApi } from &#39;@/apis/users&#39;
import { useQuery } from &#39;@tanstack/vue-query&#39;

const {data, isLoading, isFetching, error,} = useQuery({
  queryKey: [&#39;users&#39;],
  queryFn: getUsersApi, 
    refetchInterval: 5000, // 5초 후 자동 새로고침
})
&lt;/script&gt;</code></pre>
<p>데이터를 가져오는 즉시 fresh→stale이 되고 5초 마다 새로 데이터를 가져온다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tanstack Query] 1. 개요 및 설치 (vue3)]]></title>
            <link>https://velog.io/@forest_xox/Tanstack-Query-%EA%B0%9C%EC%9A%94-%EB%B0%8F-%EC%84%A4%EC%B9%98-vue</link>
            <guid>https://velog.io/@forest_xox/Tanstack-Query-%EA%B0%9C%EC%9A%94-%EB%B0%8F-%EC%84%A4%EC%B9%98-vue</guid>
            <pubDate>Sun, 16 Feb 2025 06:47:58 GMT</pubDate>
            <description><![CDATA[<h1 id="1-개요">1. 개요</h1>
<p>대부분의 상태 관리 라이브러리는 클라이언트 상태 작업에는 좋다. 하지만 클라이언트와 서버의 상태가 다르기 때문에 비동기 및 서버 상태 작업에는 좋지 않다.</p>
<p><strong>서버 상태</strong> </p>
<ul>
<li>내가 통제/소유할 수 없는 위치에 원격으로 유지된다.</li>
<li>가져오기,업데이트를 위해 비동기 API가 필요하다.</li>
<li>공유 소유권을 의미하며 나의 지식 없이 다른 사람이 변경할 수 있다.<ul>
<li>공유 소유권: 한 컴포넌트가 데이터를 요청했을 때, 그 데이터를 다른 컴포넌트 및 사용자가 수정할 수 있는 것</li>
</ul>
</li>
<li>조심하지 않으면 잠재적으로 APP에서 &quot;오래된&quot; 상태가 될 수 있다.<ul>
<li>서버와 클라이언트 간 상태를 동기화하는 것이 중요하다는 의미이다.</li>
<li>ex) 변경된 데이터를 APP에 반영하지 않아 오래된 데이터를 보여주는 경우</li>
</ul>
</li>
</ul>
<hr>
<p>APP에서 서버 상태의 특성을 파악하며 작업을 하면 아래의 과제들이 발생할 수 있다.</p>
<p>이 문제들을 해결하는 것은 쉽지 않지만 탄스택 쿼리를 쓰면 쉽게 해결 가능하다.</p>
<ul>
<li>캐싱</li>
<li>동일한 데이터에 대한 여러 요청을 단일 요청으로 중복 제거</li>
<li>백그라운드에서 &quot;오래된&quot; 데이터 업데이트</li>
<li>데이터가 &quot;오래된&quot; 시점을 아는 방법</li>
<li>가능한 빨리 데이터 업데이트를 반영</li>
<li>페이지 분할 및 지연 로딩 데이터와 같은 성능 최적화</li>
<li>서버 상태의 메모리 관리 및 가비지 수집</li>
<li>구조적 공유를 통한 쿼리 결과 메모</li>
</ul>
<hr>
<h3 id="기본-개념">기본 개념</h3>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Query</strong></td>
<td>데이터를 가져오는 요청 (useQuery)</td>
</tr>
<tr>
<td><strong>Mutation</strong></td>
<td>데이터를 변경하는 요청 (useMutation)</td>
</tr>
<tr>
<td><strong>Cache</strong></td>
<td>요청한 데이터를 저장하고 다시 활용</td>
</tr>
<tr>
<td><strong>staleTime</strong></td>
<td>데이터가 “오래됨” 상태로 간주되기까지의 시간</td>
</tr>
<tr>
<td><strong>cacheTime</strong></td>
<td>데이터가 메모리에 유지되는 시간</td>
</tr>
</tbody></table>
<hr>
<h1 id="2-설치">2. 설치</h1>
<h2 id="tanstack-query">Tanstack Query</h2>
<ol>
<li>라이브러리 설치</li>
</ol>
<pre><code>$ yarn add @tanstack/vue-query axios</code></pre><ol start="2">
<li>등록</li>
</ol>
<pre><code class="language-tsx">// main.ts
import { createApp } from &#39;vue&#39;
import { VueQueryPlugin } from &#39;@tanstack/vue-query&#39;

import App from &#39;./App.vue&#39;

const app = createApp(App)

app.use(VueQueryPlugin)
app.mount(&#39;#app&#39;)</code></pre>
<ol start="3">
<li>기본 동작 설정</li>
</ol>
<p>defaultOptions 옵션으로 queryClient를 생성할 때 기본 옵션을 지정하면 모든 useQuery와 useMutation에 적용된다. 하지만 기본 옵션을 지정했어도 개별 useQuery에서 옵션을 변경할 수 있다.</p>
<pre><code class="language-tsx">// App.vue, VueQuery 설정
const vueQueryPluginOptions: VueQueryPluginOptions = {
  queryClientConfig: {
    defaultOptions: {
      queries: {
        queryFn: defaultQueryFn, // 기본 queryFn 적용
        staleTime: 1000 * 60, // 1분 동안 fresh 상태 유지
        cacheTime: 1000 * 60 * 5, // 5분 후 캐시 삭제
        refetchOnWindowFocus: false, // 창 포커스 시 자동 리패치 비활성화
      },
    },
  },
};

app.use(VueQueryPlugin, vueQueryPluginOptions); // VueQueryPlugin 등록</code></pre>
<h2 id="devtools">DevTools</h2>
<p>탄스택 쿼리 DevTools로 쿼리 상태를 쉽게 확인하고 디버깅할 수 있다.</p>
<ol>
<li>설치</li>
</ol>
<pre><code class="language-html">$ yarn add @tanstack/vue-query-devtools</code></pre>
<ol start="2">
<li>등록</li>
</ol>
<p> VueQueryDevtools는 컴포넌트라서 app.use()로 등록하지 않는다.</p>
<pre><code class="language-html">&lt;!-- App.vue --&gt;
&lt;script setup lang=&quot;ts&quot;&gt;
import { VueQueryDevtools } from &#39;@tanstack/vue-query-devtools&#39;
&lt;/script&gt;
&lt;template&gt;
  &lt;RouterView /&gt;
  &lt;VueQueryDevtools /&gt;
&lt;/template&gt;</code></pre>
<h3 id="주요-기능">주요 기능</h3>
<p><strong>캐시된 쿼리 확인:</strong> 현재 캐시에 저장된 쿼리 목록과 상태를 확인 가능</p>
<p><strong>쿼리 상태 변경:</strong> 강제 리페치, 무효화 등 실시간으로 상태 변경 가능</p>
<p><strong>네트워크 요청 확인:</strong> 요청과 응답 데이터를 확인하고 성능 분석 가능</p>
<p><strong>캐시 타이머 확인:</strong> staleTime, gcTime 등 타이머 정보 확인 가능</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[플러터] table_calendar 마이그레이션 (2.3.3 → 3.0.8)]]></title>
            <link>https://velog.io/@forest_xox/%ED%94%8C%EB%9F%AC%ED%84%B0-tablecalendar-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@forest_xox/%ED%94%8C%EB%9F%AC%ED%84%B0-tablecalendar-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</guid>
            <pubDate>Sat, 16 Sep 2023 15:01:57 GMT</pubDate>
            <description><![CDATA[<p><a href="https://pub.dev/documentation/table_calendar/latest/table_calendar/table_calendar-library.html">api 문서</a>,  <a href="https://pub.dev/packages/table_calendar/versions/2.3.3">2.3.3</a> → <a href="https://pub.dev/packages/table_calendar/versions/3.0.8">3.0.8</a>       </p>
<p>table_calendar 라이브러리가 3.0.0 버전에서 큰 업데이트를 하면서 기존의 속성들이 대부분 삭제되고 새로운 속성들이 등장했다. 문제는 이전 속성들의 이름이 뭘로 바뀌었다는 내용이 없다는 것이다. </p>
<p>그래서 3.0.0이전 버전에서 쓰던 속성들의 역할과 3.0.0이후 버전의 속성들의 역할을 비교해 최대한 유사한 것들로 대체를 해주었고 없는 것들은 코드를 새로 작성하였다.</p>
<h2 id="기본설정">기본설정</h2>
<hr>
<h3 id="firstday-lastday">firstDay&amp; lastDay</h3>
<p>3.0.0부터 firstDay, lastDay가 추가 되었고 필수이기 때문에 꼭 넣어줘야 한다. 이 속성값 이외 범위의 날짜는 확인할 수 없다. focusedDay는 첫 화면에서 보여줄 날짜이며 이 3가지는 필수다. </p>
<pre><code class="language-dart">Widget build(BuildContext context) {
    return TableCalendar(
        firstDay: DateTime.utc(2010, 01, 01),
        lastDay: DateTime.utc(2030, 12, 31),
        focusedDay: focusDate,
    )
};</code></pre>
<h3 id="daysofweekheight">daysOfWeekHeight</h3>
<p>이전에는 요일의 높이를 변경하려면 dowWeekdayBuilder나 dowWeekendBuilder의 높이를 변경해야 했는데 이제는 변경해도 적용이 안되고 daysOfWeekHeight의 값을 변경해야 한다.
<img src="https://velog.velcdn.com/images/forest_xox/post/45067f31-d36b-45c9-831f-afd2a077b4fc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/forest_xox/post/1ba5301c-b8e3-4fe2-87b7-65cfdc071a8e/image.png" alt=""></p>
<pre><code class="language-dart">Widget build(BuildContext context) {
    return TableCalendar(
        daysOfWeekHeight: 40;
    )
}</code></pre>
<h2 id="ui-커스텀-calendarbuilders">UI 커스텀 (<strong>CalendarBuilders)</strong></h2>
<hr>
<p>CalendarBuilders를 사용해 UI를 변경할 수 있다. 각 빌더를 사용해 선택적으로 UI를 변경할 수 있다.</p>
  <img src="https://velog.velcdn.com/images/forest_xox/post/c0dbc820-8ac6-4cfb-a897-1ca6f28e6b0e/image.png" width=400px style=" margin: auto;"/>

<h3 id="daybuilder-→-defaultbuilder"><strong>dayBuilder → defaultBuilder</strong></h3>
<p>dayBuilder에서 defaultBuilder로 이름이 변경됐고 모든 날짜의 빌더라 변경하면 모두 적용된다.</p>
<h3 id="outsidedaybuilder-→-outsidebuilder"><strong>outsideDayBuilder → outsideBuilder</strong></h3>
<p>outsideDayBuilder에서 outsideBuilder로 이름이 변경됐다. 하얀 날짜 박스 바깥의 뒤부분이다.</p>
<h3 id="todaydaybuilder-→-todaybuilder"><strong>todayDayBuilder → todayBuilder</strong></h3>
<p>todayDayBuilder는 todayBuilder로 이름이 변경됐다. 오늘 날짜 빌더이다.</p>
<h3 id="dowweekdaybuilder-dowweekendbuilder-→-dowbuilder"><strong>dowWeekdayBuilder, dowWeekendBuilder → dowBuilder</strong></h3>
<p>위의 3개는 단순히 이름만 바뀌었지만 요일관련 빌더는 펼일과 주말로 나뉘었던 것이 하나로 통합됐다. 만약 일요일의 색상을 바꾸고 싶다면 아래와 같이 요일을 다 정해줘야 한다. 바꾸지 않는다면 하나하나 정의하지 않아도 요일이 잘 나온다. </p>
<pre><code class="language-dart">calendarBuilders: CalendarBuilders(
    dowBuilder: (context, day) {
      late String dayStr;
      switch(day.weekday){
        case 1: dayStr = &#39;월&#39;; break;
        case 2: dayStr = &#39;화&#39;; break;
        case 3: dayStr = &#39;수&#39;; break;
        case 4: dayStr = &#39;목&#39;; break;
        case 5: dayStr = &#39;금&#39;; break;
        case 6: dayStr = &#39;토&#39;; break;
        case 7: dayStr = &#39;일&#39;; break;
      }
      return Container(
        child: Text(
          &#39;$dayStr&#39;, style: TextStyle().copyWith(
              color: dayStr == &#39;일&#39; ? Color(0xffff0000) : Color(0xff000000),
          ),
        ),
      );
    },
);</code></pre>
<p>방금 공식문서를 보다 알게된 것인데 저렇게 하나를 바꾸기 위해 전체를 정의할 필요가 없었다! 아래 코드처럼 day.weekday가 일요일일 때만 검증하고 색상을 바꿔주면 된다. </p>
<pre><code class="language-dart">calendarBuilders: CalendarBuilders(
  dowBuilder: (context, day) {
    if (day.weekday == DateTime.sunday) {
      final text = DateFormat.E().format(day);
      return Center(
        child: Text(
          text,
          style: TextStyle(color: Colors.red),
        ),
      );
    }
  },
),</code></pre>
<h2 id="references">References</h2>
<hr>
<ul>
<li><a href="https://velog.io/@adbr/flutter-tablecalendar-builders-example">https://velog.io/@adbr/flutter-tablecalendar-builders-example</a></li>
<li><a href="https://velog.io/@jun7332568/%ED%94%8C%EB%9F%AC%ED%84%B0flutter-%EB%8B%AC%EB%A0%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-TableCalendar-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%ED%99%9C%EC%9A%A9">https://velog.io/@jun7332568/플러터flutter-달력-구현하기-TableCalendar-라이브러리-활용</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[git] Git 커밋 메시지 컨벤션]]></title>
            <link>https://velog.io/@forest_xox/git-Git-%EC%BB%A4%EB%B0%8B-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%BB%A8%EB%B2%A4%EC%85%98</link>
            <guid>https://velog.io/@forest_xox/git-Git-%EC%BB%A4%EB%B0%8B-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%BB%A8%EB%B2%A4%EC%85%98</guid>
            <pubDate>Sat, 09 Sep 2023 03:56:16 GMT</pubDate>
            <description><![CDATA[<h2 id="git-커밋-메시지-컨벤션이란">Git 커밋 메시지 컨벤션이란?</h2>
<hr>
<p>Git 커밋 메시지 컨벤션(이하 커밋 컨벤션)은 일관된 형식의 커밋 메시지를 작성하기 위한 규칙이다.</p>
<p>프로젝트 상황에 맞게 수정할 수 있다.</p>
<ul>
<li><strong>커밋 메시지</strong>: 코드 변경 사항을 요약하는 역할</li>
</ul>
<h2 id="도입-이유">도입 이유</h2>
<hr>
<p>한 프로젝트에 여러 개발자가 서로 다른 방식의 커밋 메시지를 작성한다면 여러 문제를 겪게 된다. 하지만 커밋 컨벤션을 도입함으로 아래의 이득을 얻을 수 있다.</p>
<ol>
<li><p><strong>가독성</strong> </p>
<p> 가독성 향상으로 소스 변경 이력을 쉽게 파악할 수 있다.</p>
</li>
<li><p><strong>커뮤니케이션</strong></p>
<p> 버그 수정 과정에서 타 개발자와의 불필요한 의사소통을 줄일 수 있다.</p>
</li>
<li><p><strong>변경 이력 추적</strong></p>
<p> 문제 발생 시 변경 이력을 효율적으로 추적하여 대처할 수 있고 이로인해 프로젝트 안정성을 향상시킬 수 있다.</p>
</li>
<li><p><strong>문서화 기능</strong></p>
<p> 소스 변경 내역을 제공하는 문서 역할을 하여 효율적인 소스 코드 분석이 가능하다.</p>
</li>
<li><p><strong>자동화</strong></p>
<p> 특정 태그 및 내용을 기반으로 버전 릴리즈, 머지, 통계, 분석 등 다양한 작업을 자동화할 수 있다.</p>
</li>
</ol>
<h1 id="conventional-commits"><strong>Conventional Commits</strong></h1>
<hr>
<p>다양한 커밋 컨벤션이 존재하지만 이 문서는 가장 대중적인 커밋 컨벤션인 <strong>Udacity Git Commit Message Style Guide</strong><a href="https://udacity.github.io/git-styleguide/">🔗</a> 를 기반으로 한다.</p>
<p>(이모티콘을 사용한 <a href="https://gitmoji.dev/">gitmoji</a> 컨벤션도 존재한다.)</p>
<h2 id="메시지-구조">메시지 구조</h2>
<hr>
<p>메시지 구조는 제목, 본문, 꼬리말로 나뉘고 한 줄을 띄워 서로 구분한다. </p>
<p>본문, 꼬리말은 선택사항이다.</p>
<pre><code>type: Subject  -&gt; 제 목  
(한칸 띄우기)
body(option)   -&gt; 본 문  
(한칸 띄우기)
footer(option) -&gt; 꼬리말</code></pre><p>아래는 각 부분을 어떻게 작성하는지 설명한다.</p>
<h3 id="type">Type</h3>
<p>태그를 통해 어떤 유형의 변경 사항이 도입되었는지 식별한다.</p>
<p>만약 사용할 태그가 2개 이상이라면 커밋을 더 작게 나눌 필요가 있다.</p>
<ol>
<li>타입은 태그와 제목으로 구성되고 콜론(<code>:</code> )을 통해 구분한다. (콜론 뒤 띄어쓰기)</li>
<li>첫글자는 대문자로 작성한다.</li>
</ol>
<table>
<thead>
<tr>
<th>태그 이름</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Feat</td>
<td>새로운 기능 추가</td>
</tr>
<tr>
<td>Fix</td>
<td>버그 수정</td>
</tr>
<tr>
<td>Docs</td>
<td>문서 수정</td>
</tr>
<tr>
<td>Style</td>
<td>코드 의미에 영향을 주지 않는 변경 (예: 코드 포맷팅, 세미콜론 누락 등)</td>
</tr>
<tr>
<td>Refactor</td>
<td>프로덕션 코드 리펙토링</td>
</tr>
<tr>
<td>Test</td>
<td>테스트 코드, 리펙토링 테스트 코드 추가</td>
</tr>
<tr>
<td>Chore</td>
<td>빌드 업무 수정, 패키지 매니저 수정</td>
</tr>
<tr>
<td>Design</td>
<td>UI 디자인 변경</td>
</tr>
<tr>
<td>Rename</td>
<td>파일 수정 및 이동</td>
</tr>
<tr>
<td>Remove</td>
<td>파일 삭제</td>
</tr>
</tbody></table>
<h3 id="subject">Subject</h3>
<ol>
<li>개조식 구문으로 작성한다. </li>
<li>명령형으로 작성한다.</li>
<li>글자 수는 50자를 넘기지 않는다.</li>
<li>마지막에 특수문자는 삽입하지 않는다. </li>
</ol>
<p><strong>영어의 경우</strong></p>
<ul>
<li>첫글자는 대문자로 작성한다.</li>
<li>동사 원형을 작성한다.</li>
</ul>
<h3 id="body">Body</h3>
<ol>
<li>한 줄 당 72자를 넘기지 않는다.</li>
<li>길이 상관없이 최대한 상세히 작성한다.</li>
<li>어떻게 변경했는지 보다 무엇을 또는 왜 변경했는지 설명한다.</li>
</ol>
<h3 id="footer">Footer</h3>
<ol>
<li>이슈 트래커 ID를 작성한다.</li>
<li><code>유형: #이슈 번호</code> 형식이다. </li>
<li>이슈가 여러개면 쉼표로 구분한다.</li>
<li>이슈 트래커 유형은 아래와 같다.</li>
</ol>
<pre><code>| 유형 | 내용 |
| --- | --- |
| Fixes | 이슈 수정중 |
| Resolves | 이슈 해결 |
| Ref | 참고할 이슈  |
| Related to | 해결되지 않은 관련된 이슈 |

예) `Fixes: #5 Related to: #2, #3`</code></pre><h2 id="예제">예제</h2>
<hr>
<pre><code>Feat: 로그인 함수 추가

로그인 API 개발

Resolves: #123
Ref: #456
Related to: #48, #45</code></pre><h2 id="마치며">마치며</h2>
<hr>
<p>우선 커밋 컨벤션 관련 글을 쓰게된 이유는 현회사에서 커밋 컨벤션이 존재하지 않아 대표님과 이야기하여 도입하기로 하였다. 지금까지 제목만 사용했는데 조사하면서 본문, 꼬리말이 있는 것도 처음 알았다. 또한 단순 가독성 뿐 아니라 여러 장점이 많아서 신기하기도 했다.</p>
<p>아직은 완벽히 활용하지는 못하지만 차츰 완벽에 가깝게 사용할 수 있으면 좋겠다.</p>
<h2 id="참고-자료">참고 자료</h2>
<hr>
<p><a href="https://github.com/slashsbin/styleguide-git-commit-message#tools">https://github.com/slashsbin/styleguide-git-commit-message#tools</a></p>
<p><a href="https://overcome-the-limits.tistory.com/entry/%ED%98%91%EC%97%85-%ED%98%91%EC%97%85%EC%9D%84-%EC%9C%84%ED%95%9C-%EA%B8%B0%EB%B3%B8%EC%A0%81%EC%9D%B8-git-%EC%BB%A4%EB%B0%8B%EC%BB%A8%EB%B2%A4%EC%85%98-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0#%ED%83%80%EC%9E%85">https://overcome-the-limits.tistory.com/entry/협업-협업을-위한-기본적인-git-커밋컨벤션-설정하기#타입</a></p>
<p><a href="https://www.conventionalcommits.org/ko/v1.0.0/">https://www.conventionalcommits.org/ko/v1.0.0/</a></p>
<p><a href="https://insight.infograb.net/blog/2023/04/21/why-commit-convention-is-important/">https://insight.infograb.net/blog/2023/04/21/why-commit-convention-is-important/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Dart] 기본 함수]]></title>
            <link>https://velog.io/@forest_xox/Dart-%EA%B8%B0%EB%B3%B8-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@forest_xox/Dart-%EA%B8%B0%EB%B3%B8-%ED%95%A8%EC%88%98</guid>
            <pubDate>Tue, 27 Jun 2023 01:43:29 GMT</pubDate>
            <description><![CDATA[<h2 id="함수">함수</h2>
<p>타입과 함께 매개변수를 선언한다. 해당 매개변수는 함수내에서만 사용된다.</p>
<pre><code class="language-dart">void main() {
    introduce(&#39;Tom&#39;, 30); //hello my name is Tom i am 30 years old
}

introduce(String name, int age){
    print(&#39;hello my name is $name i&#39;m $age years old&#39;);
}</code></pre>
<h3 id="함수-타입">함수 타입</h3>
<p>함수가 반환하는 값이 없을 때 함수 앞에 void 키워드를 붙인다. 함수가 값을 반환한다면 값의 타입을 함수 앞에 작성하면 된다.</p>
<pre><code class="language-dart">void printStr() {
    print(&#39;string&#39;);
}
int getThree() {
    return 3;
}</code></pre>
<h3 id="화살표-함수">화살표 함수</h3>
<p>화살표 함수를 사용하면 화살표 다음이 반환되는 값이된다.</p>
<pre><code class="language-dart">int getThree() {
    return 3;
}
// ⬆️ 똑같다.
int getThree() =&gt; 3; //화살표 함수</code></pre>
<h3 id="선택적-매개변수">선택적 매개변수</h3>
<p>함수 호출 시 인수를 다 넘기지 않으면 에러가 난다. 매개변수를 함수내에서 선택적으로 사용한다면 선택적 매개변수를 사용해야 한다.</p>
<p>대괄호안에 매개변수를 선언하면 된다. 이때 변수값이 없으면 함수내에서 에러가 발생하기에 기본값을 설정해줘야 한다. 함수 호출시 넘어온 인수가 없다면 기본값을 사용한다.</p>
<pre><code class="language-dart">void main() {
    addNum(20); //30
    addNum(20,20); //40
}

addNum(int num, [int num2 = 10]){
    print(num + num2);
}</code></pre>
<h3 id="네임드-매개변수">네임드 매개변수</h3>
<p>기본적으로 매개변수는 순서가 중요하다. 하지만 네임드 매개변수는 순서가 중요하지 않다.</p>
<p>매개변수 자리에 중괄호를 넣고 타입 앞에 required 키워드를 작성한다. 만약 선택적 매개변수를 사용하고 싶다면 required를 안쓰면 된다. 함수를 호출할 때 <code>매개변수 : 값</code> 이러한 형태로 인자를 넘기면 된다. 순서가 바뀌어도 아무 상관 없다.</p>
<pre><code class="language-dart">void main() {
    addNum(num:20); //30
    addNum(num2:30, num:10); //40
}

addNum({
    required int num,
    int num2 = 10,
}){
    print(num + num2);
}</code></pre>
<hr>
<h2 id="typedef">typedef</h2>
<p>typedef로 body가 없는 함수를 선언하면 해당 함수와 형태가 같은 함수들을 하나의 typedef로 선언할  수 있다</p>
<p>아래처럼 변수 앞에 typedef를 선언하고 Operation타입에 해당하는 함수를 할당하면 앞으로 oper란 변수는 add함수의 역할을 수행한다. </p>
<pre><code class="language-dart">void main() {
    Operation oper = add;
    print(oper(20,10);) //30
    operation = subtract;
    print(oper(20,10);) //10
}

typedef Operation = int Function(int num, int num2);
int add(int num, int num2) =&gt; num + num2;
int subtract(int num, int num2) =&gt; num - num2;</code></pre>
<h3 id="응용">응용</h3>
<p>typede를 사용한다면 보통 이런식으로 자주 사용한다고 한다.</p>
<pre><code class="language-dart">void main() {
    print(calculate(30, 20, add)); //50
}

typedef Operation = int Function(int num, int num2);
int add(int num, int num2) =&gt; num + num2;
int subtract(int num, int num2) =&gt; num - num2;
int calculate(int num, int num2, Operation oper) =&gt; oper(num, num2);</code></pre>
<hr>
<h2 id="if-else-if-else">if, else if, else</h2>
<p>if문의 조건식에 부합하면 if문 안의 결과를 실행, 아니라면 else if문으로 넘어가고 else if문의 조건식에도 부합하지 않다면 else문의 결과를 실행한다. </p>
<pre><code class="language-dart">void main() {
    int num = 5;
    if(num &gt;= 20) {
        print(&#39;20 이상&#39;);    
    }else if(num &gt;= 10) {
        print(&#39;10이상&#39;);    
    }else {
        print(&#39;10이하&#39;);
    }
}</code></pre>
<hr>
<h2 id="switch">switch</h2>
<p>switch 문은 하나 이상의 case문으로 구성되며 default문은 필수가 아니다. </p>
<pre><code class="language-dart">void main(){
    String browser = &#39;safari&#39;;

    switch(browser){
        case &#39;chrome&#39;:
            print(&#39;크롬 사용중&#39;);
            break;
        case &#39;safari&#39;:
            print(&#39;사파리 사용중&#39;);
            break;
        default:
            print(&#39;타 브라우저 사용중&#39;);
            break;
    }
}</code></pre>
<hr>
<h2 id="반복문-loop">반복문 (loop)</h2>
<h3 id="for-반복문">for 반복문</h3>
<p>for 문의 초기식은 사용할 변수를 선언하고 초기값을 할당한다. 조건식은 언제까지 실행할지를 결정한다. 증감식은 반복문이 한번 종료될 때  어떤 코드를 실행할지 결정한다. </p>
<p>조건식이 참일 경우 중괄호 속 코드가 실행된다. 아래 코드의 경우 0부터 9까지 출력될 것이다.</p>
<pre><code class="language-dart">void main(){
    for(int i = 0; i &lt; 10; i++){ //(초기식; 조건식; 증감식)
        print(i)        //조건식이 참일 때 실행할 코드
    }
}</code></pre>
<h3 id="for-in-반복문">for in 반복문</h3>
<p>for in 문은 List의 요소를 순회할 수 있는 반복문이다. 아래의 경우 nums List를 하나씩 출력한다. (Map X)</p>
<pre><code class="language-dart">void main(){
    List&lt;int&gt; nums = [1,2,3,4,5];
    for(int num in nums){
        print(num);
    }
}</code></pre>
<h3 id="while-반복문">while 반복문</h3>
<p>while 문은 조건식이 거짓일 때 까지 코드를 실행한다. 만약 계속 참인 조건식을 넣으면 계속 실행되어 컴퓨터에 문제가 발생한다.</p>
<pre><code class="language-dart">void main(){
    int total = 0;
    while(total &lt;= 10){
        total += 2;
    }
}</code></pre>
<h3 id="do-while-반복문">do while 반복문</h3>
<p>do while문은 while문과 비슷하지만 do while의 경우 조건에 상관없이 우선적으로 코드를 실행하고 그 다음에 조건이 맞는지 확인한다. (잘 사용하지 않음)</p>
<pre><code class="language-dart">void main(){
    int total = 0;
    do {
        total += 2;
    }    while(total &lt;= 10);
}</code></pre>
<h3 id="break-문">break 문</h3>
<p>break문을 사용하면 현재 반복문에서 나간다. 따라서 if문을 통해 특정 조건에 반복문을 중단할 수 있다.</p>
<p>while문 뿐 아니라 for문에서도 가능하다.</p>
<pre><code class="language-dart">void main(){
    int total = 0;
    while(total &lt;= 10){
        total += 2;
        if(total == 6){
            break;
        }
    }
}</code></pre>
<h3 id="continue-문">continue 문</h3>
<p>continue문은 break와 달리 조건에 해당하면 현재 식을 종료하고 다음 식으로 넘어간다. 아래의 경우 1,2,4,5가 출력되고 3은 출력되지 않는다.</p>
<pre><code class="language-dart">void main(){
    for(int i = 1; i &lt; 5; i++){ //(초기식; 조건식; 증감식)
        if(i == 3) continue;
        print(i);
    }
}</code></pre>
<hr>
<h2 id="enum">enum</h2>
<p>enum은 상수값을 나열한 형태로 타입 사용을 강제하고 싶을 경우 사용한다. enum의 이름은 첫글자를 대문자로 작성한다.  (TS의 enum과 비슷하다.)</p>
<pre><code class="language-dart">enum Fruits{
    apple,
    banana,
    kiwi,
}

void main() {
    Fruits fruits = Fruits.kiwi;
    if(fruits == Fruits.apple) print(&quot;사과&quot;);
    else if(fruits == Fruits.banana) print(&quot;바나나&quot;);
    else print(&quot;키위&quot;);
}</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Dart] 기본 문법]]></title>
            <link>https://velog.io/@forest_xox/Dart-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95</link>
            <guid>https://velog.io/@forest_xox/Dart-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95</guid>
            <pubDate>Tue, 27 Jun 2023 01:41:10 GMT</pubDate>
            <description><![CDATA[<h1 id="콘솔">콘솔</h1>
<h2 id="print">print()</h2>
<p>print 함수를 통해 콘솔을 찍을 수 있다.(JS의 console.log) </p>
<p>dart는 뒤에 꼭 세미콜론을 붙여야 한다.</p>
<pre><code class="language-dart">void main(){
    print(&#39;hello world!&#39;); //hellog world
}</code></pre>
<hr>
<h1 id="타입-추론-변수-동적-타입-변수">타입 추론 변수, 동적 타입 변수</h1>
<h2 id="var-타입-추론-변수">var (타입 추론 변수)</h2>
<p>var는 모든 타입을 할당할 수 있고 재할당 또한 가능하다. 하지만 첫 할당한 타입과 다른 타입으로 재할당이 불가능하다. </p>
<pre><code class="language-dart">void main(){
    var name = &#39;simon&#39;;
    print(name); //simon
    name = 1; //error🚨
}</code></pre>
<h2 id="dynamic-동적-타입-변수">dynamic (동적 타입 변수)</h2>
<p>dynamic은 어떤 타입이건 할당할 수 있다. 첫 할당한 타입과 다른 타입으로 재할당이 가능하다.</p>
<pre><code class="language-dart">void main(){
    dynamic text = &#39;hello&#39;;
    text = 1;
    text = true;
} </code></pre>
<h3 id="둘의-차이">둘의 차이</h3>
<p>var와 dynamic은 모든 타입을 할당할 수 있는 공통점이 있다. </p>
<p>하지만 var는 초기화 시 타입을 추론하여 재할당 시 다른 타입으로 변경이 <strong>불가능</strong>하다.</p>
<p>dynamic은 동적으로 타입이 변경되어 재할당 시 다른 타입으로 변경이 <strong>가능</strong>하다.</p>
<h2 id="-runtimetype">+ runtimeType</h2>
<p>runtimeType을 통해 변수의 타입을 확인할 수 있다.</p>
<pre><code class="language-dart">void main(){    
    var idk = true;
    print(idk.runtimeType); //bool
}</code></pre>
<hr>
<h1 id="기본-타입">기본 <strong>타입</strong></h1>
<h2 id="string">String</h2>
<p>String 타입은 문자열만 할당할 수 있다. 문자를 더하면 문자가 합쳐진다. </p>
<p><code>${}</code>안 혹은 <code>$</code>뒤에 변수를 넣어서 사용할 수도 있다. JS의 백틱(````)과 유사한것 같다.</p>
<pre><code class="language-dart">void main(){
    String text = &#39;hello&#39;;
    String text2 = &#39;world&#39;
    print(text + &#39; &#39; + text2); //hello world
    print(&#39;${text} ${text2}&#39;); //hello world
    print(&#39;$text $text2&#39;); //hello world
}</code></pre>
<h2 id="int-integer">Int (integer)</h2>
<p>int 타입은 정수만 할당할 수 있다.</p>
<pre><code class="language-dart">void main(){
    int num = 1;
    print(num + num); //2
}</code></pre>
<h2 id="double">double</h2>
<p>double 타입은 실수만 할당할 수 있다.</p>
<pre><code class="language-dart">void main(){
    double num = 0.5;
    print(num + num); //1
}</code></pre>
<h2 id="bool">bool</h2>
<p>bool 타입은 true, false만 할당할 수 있다.</p>
<pre><code class="language-dart">void main(){    
    bool t = true;
    bool f = false;
}</code></pre>
<h2 id="nullable">nullable</h2>
<p>변수에 <code>null</code>을 할당하면 에러가 발생한다. 하지만 타입뒤에 <code>?</code>를 붙이면 <code>null</code>도 할당할 수 있게된다.</p>
<pre><code class="language-dart">void main(){
    String text = &#39;hello&#39;;
    text = null; //error🚨
    String? text2 = &#39;hello&#39;;
    text2 = null; 
}</code></pre>
<h3 id="non-nullable"><strong>non-nullable</strong></h3>
<p>타입 앞에 <code>?</code>를 사용하는 경우, <code>변수와 !</code>를 호출하면 현재 값이 <code>null</code>이 아니라는 뜻이다.</p>
<pre><code class="language-dart">void main(){
    String? text = &#39;hello&#39;;
    print(text!); //hello
}</code></pre>
<hr>
<h1 id="컬렉션-타입">컬렉션 <strong>타입</strong></h1>
<h2 id="list">List</h2>
<p>List 타입은 <code>&lt;&gt;</code>안에 나열할 값의 타입을 적어주면 된다. 만약 List에 설정한 타입과 다른 값이 있으면 에러가 발생한다. JS의 배열과 비슷하다.</p>
<pre><code class="language-dart">void main() {
    List&lt;String&gt; alphabet = [&#39;a&#39;,&#39;b&#39;,&#39;c&#39;];
    pring(alphabet[1]); //b
}</code></pre>
<h3 id="list-add-메소드">List) <strong>add 메소드</strong></h3>
<p>add 메소드를 이용해 List에 값을 추가할 수 있다.</p>
<pre><code class="language-dart">void main() {
    List&lt;String&gt; alphabet = [&#39;a&#39;,&#39;b&#39;,&#39;c&#39;];
    alphabet.add(&#39;d&#39;);
    pring(alphabet); // [a, b, c, d]
}</code></pre>
<h3 id="list-remove-메소드">List) <strong>remove 메소드</strong></h3>
<p>remove 메소드를 이용해 특정 값을 제거할 수 있다.</p>
<pre><code class="language-dart">void main() {
    List&lt;String&gt; alphabet = [&#39;a&#39;,&#39;b&#39;,&#39;c&#39;];
    alphabet.remove(&#39;a&#39;)
    pring(alphabet); // [b, c, d]
}</code></pre>
<h3 id="list-indexof-메소드">List) <strong>indexOf 메소드</strong></h3>
<p>indexOf 메소드를 통해 원하는 값의 인덱스를 구할 수 있다.</p>
<pre><code class="language-dart">void main() {
    List&lt;String&gt; alphabet = [&#39;a&#39;,&#39;b&#39;,&#39;c&#39;];
    pring(alphabet.indexOf(&#39;c&#39;)); // 2
}</code></pre>
<h2 id="map">Map</h2>
<p>Map의 꺽쇠안에는 &lt;key, value&gt;의 타입을 넣어주면 된다. 대괄호를 통해 특정 값에 접근할 수 있다. (JS의 객체)</p>
<pre><code class="language-dart">void main() {
    Map&lt;String, int&gt; price = {
        &#39;staek&#39;: 120,
        &#39;pasta&#39;: 30,
    };
    print(price); //{staek: 120, pasta:30}  
    print(price[&#39;staek&#39;]); // 120
}</code></pre>
<h3 id="map-addall-메소드">Map) <strong>addAll 메소드</strong></h3>
<p>addAll 메소드로 Map안에 프로퍼티를 추가할 수 있다. 또는 그냥 할당해서 추가할 수도 있다.</p>
<pre><code class="language-dart">void main() {
    Map&lt;String, int&gt; price = {
        &#39;staek&#39;: 120,
        &#39;pasta&#39;: 30,
    };

    price.addAll({
        &#39;pizza&#39;: 20,    
    })
    print(price); //{staek: 120, pasta: 30, pizza: 20}  

    price[&#39;soup&#39;] = 5;
    print(price); //{staek: 120, pasta: 30, pizza: 20, soup: 5}  
}</code></pre>
<h3 id="map-remove-메소드">Map) <strong>remove 메소드</strong></h3>
<p>remove 메소드로 특정 프로퍼티를 삭제할 수 있다.</p>
<pre><code class="language-dart">void main() {
    Map&lt;String, int&gt; price = {
        &#39;staek&#39;: 120,
        &#39;pasta&#39;: 30,
    };

    price.remove(&#39;pizza&#39;)
    print(price); //{staek: 120}  
}</code></pre>
<h3 id="map-keys-values-메소드">Map) <strong>keys, values 메소드</strong></h3>
<p>keys 메소드로 Map의 keys만 얻을 수 있고, value 메소드로는 values를 얻을 수 있다. </p>
<pre><code class="language-dart">void main() {
    Map&lt;String, int&gt; price = {
        &#39;staek&#39;: 120,
        &#39;pasta&#39;: 30,
    };
    print(price.keys); // (staek, pasta)
    print(price.values); // (120, 30)
}</code></pre>
<h2 id="set">Set</h2>
<p>List 처럼 값들을 나열할 수 있지만 다른점은 중복값이 포함 될 수 없다. 포함시켜도 자동으로 제거해준다. 대부분의 메소드는 List와 비슷하다.</p>
<pre><code class="language-dart">void main() {
    Set&lt;int&gt; price = {1,2,3};
    print(price); // {1,2,3}

    price.add(1);
    print(price); // {1,2,3}

    price.remove(3);
    print(price); // {1,2}
}</code></pre>
<h3 id="set-contains-메소드">Set) <strong>contains 메소드</strong></h3>
<p>contains 메소드로 Set내부에 특정값이 있는지 확인할 수 있다.</p>
<pre><code class="language-dart">void main() {
    Set&lt;int&gt; price = {1,2,3};
    print(price.contains(1)); // true
}</code></pre>
<hr>
<h1 id="상수">상수</h1>
<h2 id="const-final">const, final</h2>
<p>타입 앞에 const 혹은 final을 붙이면 상수가 되어 재할당이 불가능해진다. 또한 식별자 앞에 타입을 쓰지 않아도 된다.</p>
<pre><code class="language-dart">void main(){
    const String text2 = &#39;text2&#39;;
    text2 = &#39;string&#39;; //error🚨

    final String text = &#39;text&#39;;
    text = &#39;string&#39;; //error🚨

    const text3 = &#39;ABC&#39;;
}</code></pre>
<h3 id="const-final-차이점"><strong>const, final 차이점</strong></h3>
<p><strong>const</strong>: 컴파일 타임에 값 결정</p>
<p><strong>final</strong>: 런타임에 값 결정</p>
<ul>
<li>컴파일 타임: 소스코드에서 기계어로 변환되는 과정</li>
<li>런타임: 컴파일 종료 후 프로그램이 실행되는 때</li>
</ul>
<pre><code class="language-dart">void main(){
    const DateTime time1 = DateTime.now(); //error🚨
    final DateTime time2 = DateTime.now();
}</code></pre>
<p><code>DateTime.now()</code>는 함수를 실행한 순간의 시간을 반환한다. 그래서 컴파일 타임에 값이 결졍되는 const는 에러가 발생한다.</p>
<hr>
<h1 id="연산자">연산자</h1>
<p>산술/ 할당/ 비교 연산자는 다른 언어들과 동일하다.</p>
<h2 id="null-병합-연산자">null 병합 연산자</h2>
<p>null 병합 연산자는 왼쪽 값이 null이 아니면 왼쪽 값을 반환, null이면 오른쪽 값을 반환한다.</p>
<pre><code class="language-dart">void main(){    
    int? x;
    int y = 10;
    int z = x ?? y;
  print(z); //10
}</code></pre>
<h2 id="null-병합-할당-연산자">null 병합 할당 연산자</h2>
<p>null 병합 할당 연산자는 변수의 값이 <code>null</code>이면 오른쪽 값을 할당한다.</p>
<pre><code class="language-dart">// null일 경우
void main(){
  double? number = 2;
  print(number); //2

  number = null;
  print(number); // null

  number ??= 100;
  print(number); //100
}
// null이 아닐 경우
void main(){
  double? number = 2;
  print(number); //2

  number ??= 100;
  print(number); //2
}</code></pre>
<h2 id="타입-검사-연산자">타입 검사 연산자</h2>
<p><code>is</code>앞에 변수가 <code>is</code>뒤의 타입이면 <code>true</code>를 아니면 <code>false</code>를 반환한다. <code>is</code>뒤에 <code>!</code>를 붙이면 반대값이 나온다.</p>
<pre><code class="language-dart">void main(){
    String text = &#39;a&#39;;
    print(text is String); //true
    print(text is int); //false
    print(text is! int); //true
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Dart] Dart란? (flutter가 dart를 선택한 이유, 주요 기능)]]></title>
            <link>https://velog.io/@forest_xox/Dart-Dart%EB%9E%80-flutter%EA%B0%80-dart%EB%A5%BC-%EC%84%A0%ED%83%9D%ED%95%9C-%EC%9D%B4%EC%9C%A0-%EC%A3%BC%EC%9A%94-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@forest_xox/Dart-Dart%EB%9E%80-flutter%EA%B0%80-dart%EB%A5%BC-%EC%84%A0%ED%83%9D%ED%95%9C-%EC%9D%B4%EC%9C%A0-%EC%A3%BC%EC%9A%94-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Tue, 27 Jun 2023 01:14:55 GMT</pubDate>
            <description><![CDATA[<p>Dart는 구글이 개발한 프로그래밍 언어이며 주로 플러터 앱 개발에 사용되는 언어이다.</p>
<h2 id="왜-dart인가">왜 Dart인가</h2>
<p>왜 많고 많은 언어중에 flutter는 Dart를 선택했을까? </p>
<p>초기 플러터 팀은 12개 이상의 언어중 UI 구축 방식이 일치하는 dart를 선택했다. </p>
<p>dart는 아래의 기준에서 높은 점수를 받았다.</p>
<ol>
<li>개발자 생산성<ol>
<li>플러터는 동일한 코드 베이스로 개발해 개발 속도를 높인다.</li>
<li>플러터는 dart로 구축되어 가독성, 접근 가능성을 높여 코드 생산성을 유지한다.</li>
</ol>
</li>
<li>객체 지향<ol>
<li>객체 지향 언어는 UI 프레임워크를 구축한 오랜 경험이 있다.</li>
<li>객체 지향 개발 경험이 있다면 플러터를 쉽게 배울 수 있다.</li>
</ol>
</li>
<li>예측 가능한 고성능<ol>
<li>화면 끊김 현상 없이 고성능과 예측 가능한 성능을 제공하는 언어가 필요하다.</li>
</ol>
</li>
<li>빠른 할당<ol>
<li>플러터는 <strong>기본 메모리 할당자</strong>에 의존하여 작은 객체의 할당, 해제를 효율적으로 하여 메모리 사용량을 최소화한다.</li>
<li>위의 기능을 제공하는 언어에서 효율적으로 작동한다.</li>
</ol>
</li>
</ol>
<ul>
<li><strong>기본 메모리 할당자</strong>: 프로그램에서 메모리를 동적으로 할당/ 해제하는 작업을 관리하는 시스템의 일부</li>
</ul>
<hr>
<h2 id="dart의-주요-기능">dart의 주요 기능</h2>
<p>이제 dart의 주요 기능들에 알아보자!</p>
<ul>
<li>두 가지 컴파일 지원(JIT, AOT)</li>
<li>Hot Reload</li>
<li>초당 60 프레임의 애니메이션</li>
<li>통합 레이아웃</li>
</ul>
<h2 id="두-가지-컴파일-지원">두 가지 컴파일 지원</h2>
<p>Dart는 JIT과 AOT 두가지 컴파일을 지원한다.</p>
<p><strong>JIT</strong>(Just In Time): 프로그램을 실행하는 동안 기계어를 생성한다.</p>
<p><strong>AOT</strong>(Ahead Of Time): 프로그램을 실행하기 전에 기계어를 생성한다.</p>
<p>두가지를 사용해 얻을 수 있는 이점은 아래와 같다.</p>
<ul>
<li>개발과 디버깅 동안 JIT 컴파일러를 사용해 실시간으로 코드 수정사항을 확인할 수 있다.</li>
<li>AOT 컴파일러를 사용해 미리 실행파일을 컴파일하여 변환 과정 없어 빠른 실행 시간, 성능 향상을 얻는다.</li>
</ul>
<p>따라서 Dart는 JIT을 통한 빠른 개발 주기, AOT를 통한 빠른 실행 속도 두 가지 이점을 제공한다.</p>
<h2 id="핫-리로드">핫 리로드</h2>
<p>핫 리로드는 앱 개발 중 코드 변경을 바로 적용해 앱을 빠르게 리로드하는 기능이다.</p>
<p>일반적으로 수정한 코드를 적용하기 위해 앱을 다시 빌드하여 실행하는 단점이 있지만 핫 리로드를 사용하면 코드를 수정해도 다시 빌드할 필요 없이 실시간으로 수정 사항 확인이 가능하다.</p>
<p>이처럼 수정 코드를 실시간 반영하여 개발 생산성 향상으로 개발과 디버깅 시간을 단축할 수 있다.</p>
<h2 id="초당-60프레임의-애니메이션">초당 60프레임의 애니메이션</h2>
<p>플러터의 Skia 그래픽 엔진은 하드웨어 속도를 높여 애니메이션 그리기와 처리를 최적화해서 초당 60프레임의 애니메이션을 구현한다. 이를 통해 <strong>jank</strong>는 최소화되고 사용자들에게 부드러운 애니메이션을 제공한다.</p>
<p>따라서 플러터의 그래픽 엔진, dart의 JIT, AOT를 통한 성능 최적화로 초당 60프레임의 애니메이션 구현이 가능하다.</p>
<ul>
<li><strong>Jank</strong>: 사이트 혹은 앱이 주사율에 맞추지 못해 버벅거리거나 잠시 정지한 상태를 사용자가 보는 것.</li>
</ul>
<h2 id="통합-레이아웃">통합 레이아웃</h2>
<p>dart로 플러터가 별도의 프로그램, 추가 템플릿, 레이아웃 언어 없이 레이아웃을 구성할 수 있다.</p>
<hr>
<p>이외에도 “선제적 스케줄링, 타임 슬라이싱 및 공유 리소스”, “할당 및 가비지 수집” 등 몇가지 주요 기능들이 존재하지만 이는 추후에 다루도록 하겠다.</p>
<h2 id="references">References</h2>
<p><a href="https://docs.flutter.dev/resources/faq#why-did-flutter-choose-to-use-dart">https://docs.flutter.dev/resources/faq#why-did-flutter-choose-to-use-dart</a></p>
<p><a href="https://hackernoon.com/why-flutter-uses-dart-dd635a054ebf">https://hackernoon.com/why-flutter-uses-dart-dd635a054ebf</a></p>
<p><a href="https://beomseok95.tistory.com/315">https://beomseok95.tistory.com/315</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[플러터] Flutter 초기 세팅(Mac)]]></title>
            <link>https://velog.io/@forest_xox/%ED%94%8C%EB%9F%AC%ED%84%B0-Flutter-%EC%B4%88%EA%B8%B0-%EC%84%B8%ED%8C%85Mac</link>
            <guid>https://velog.io/@forest_xox/%ED%94%8C%EB%9F%AC%ED%84%B0-Flutter-%EC%B4%88%EA%B8%B0-%EC%84%B8%ED%8C%85Mac</guid>
            <pubDate>Wed, 21 Jun 2023 05:18:20 GMT</pubDate>
            <description><![CDATA[<h2 id="1-flutter-sdk-다운로드">1. Flutter SDK 다운로드</h2>
<p><a href="https://docs.flutter.dev/get-started/install">링크</a>에 접속해 자신의 OS에 맞게 다운로드를 진행하면 된다. </p>
<hr>
<h2 id="2-android-studio-설치">2. Android Studio 설치</h2>
<p><a href="https://developer.android.com/studio">링크</a>로 접속해 밑으로 내리다 보면 OS 버전에 맞게 다운로드 할 수 있는 화면이 나온다.</p>
<ul>
<li>apple silicon인 경우 <code>Mac(64-bit, ARM)</code>을 선택하면 된다.</li>
</ul>
<p>설치가 됐다면 flutter 확장 프로그램을 설치한다.</p>
<hr>
<h2 id="3-환경변수-등록">3. 환경변수 등록</h2>
<pre><code>// zsh
$ touch ~/.zshrc
$ open ~/.zshrc
// bash
$ touch ~/.bash_profile
$ open ~/.bash_profile</code></pre><p>터미널을 열고 자신의 터미널 이름에 맞는 명령어를 입력하면 된다. 그럼 텍스트 편집기가 열린다. </p>
<pre><code>export PATH=&quot;$PATH: (플러터 파일 경로) /bin&quot;</code></pre><p>위의 텍스트가 소괄호를 제외하고 적혀있을텐데 소괄호 부분에 다운받은 플러터 파일 경로를 작성하고 저장하면 된다. 이로써 환경변수 등록이 끝났다.</p>
<ul>
<li>PATH 환경 변수는 실행 파일의 경로를 지정하는 데 사용되고, 운영 체제는 실행 파일을 어디에서 찾아야 하는지 알 수 있다.</li>
</ul>
<hr>
<h2 id="4-기타">4. 기타</h2>
<p>마지막으로 터미널에 <code>flutter doctor</code>를 입력해 flutter 개발에 필요한 것들이 잘 있는지 확인한다.</p>
<p><img src="https://velog.velcdn.com/images/forest_xox/post/ff50442c-cfeb-4ddb-bcac-c688029510fc/image.png" alt=""></p>
<p>필자의 경우 위와 같은 결과가 나왔고 <code>!</code>가 있는 부분들은 해당 글의 마지막의 명령어를 입력하면 된다.</p>
<p><code>Xcode</code>는 필요에 따라 설치하면 된다. 그 다음 <code>Accept?(y/N)</code>가 나오는데 다 <code>yes</code> 해주면된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] SSH (Secure Shell), SSH key 생성 방법]]></title>
            <link>https://velog.io/@forest_xox/SSH-Secure-Shell-SSH-key-%EC%83%9D%EC%84%B1-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@forest_xox/SSH-Secure-Shell-SSH-key-%EC%83%9D%EC%84%B1-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sat, 10 Jun 2023 11:35:56 GMT</pubDate>
            <description><![CDATA[<h2 id="sshsecure-shell-란">SSH(Secure Shell) 란?</h2>
<p>SSH는 “원격 접속 프로토콜”로 원격지 컴퓨터에 접속하기 위해 사용되는 인터넷 프로토콜이다.</p>
<p>따라서 이를 이용해 다른 지역의 컴퓨터 또는 서버를 관리할 수 있고 파일 공유도 가능하다.</p>
<p>SSH는 대표적으로 데이터 전송(ex: github), 원격 제어(ex: AWS)에 사용된다. </p>
<p>SSH 말고도 Telent, Rlogin, VNC 등과 같은 다양한 원격 접속 프로토콜들이 존재하지만 그 중 SSH가 가장 보편적이고 보안적으로 안전하다.  </p>
<ul>
<li>Telent와 Rlogin은 SSH와 달리 데이터를 평문으로 전송해 보안 측면에서 취약하다.</li>
</ul>
<h2 id="동작-방식">동작 방식</h2>
<p>SSH key는 연결 상대를 인증하고 안전하게 데이터를 교환할 수 있게 해준다.</p>
<p>SSH key는 공개키와 개인키로 구성되어있다. 공개키는 암호화에 사용되고, 개인키는 복호화에 사용된다. 그리고 이름에서도 알 수 있듯 공개키는 외부에 공개되어도 안전하지만 개인키는 절대 노출되어서 안된다.</p>
<p>SSH에서는 대칭키 방식과 공개키(비대칭키) 방식을 사용하여 인증과 암호화를 하게 되는데 비대칭키를 통해서 서버 인증과 클라이언트 인증을 하고, 대칭키(세션키)를 통해 데이터를 암호화 한다.</p>
<p><strong>SSH key 인증 과정</strong></p>
<ul>
<li>최초 접속 시 비대칭키 방식으로 클라이언트와 서버간의 인증을 진행한다.</li>
</ul>
<ol>
<li>클라이언트 혹은 서버는 키 쌍을 생성한다.</li>
<li>클라이언트는 서버에 자신의 공개키를 전송한다.</li>
<li>서버는 공개키를 받아 랜덤한 값을 클라이언트의 공개키로 암호화해 전송한다.</li>
<li>클라이언트는 개인키를 이용해 복호화해 서버에 다시 전송한다.</li>
<li>서버는 자신이 보낸 값과 받은 값을 비교해 같다면 접속을 허용한다.</li>
</ol>
<ul>
<li>서로의 신원을 확인하고, 이제 대칭키를 만들어 정보를 교환한다.</li>
</ul>
<ol>
<li>클라이언트 혹은 서버는 대칭키를 만들어 공유한다. </li>
<li>공유된 대칭키를 통해 암호화, 복호화하여 정보를 교환한다.</li>
<li>접속 종료시 대칭키는 폐기되어 재 접속시 새로 생성한다.</li>
</ol>
<h2 id="ssh-key-생성">SSH key 생성</h2>
<p>위와 같은 과정을 통해 원격지의 서버를 관리하고 파일을 공유할 수 있다. 이제 key를 생성하는 방법에 대해 알아보자.</p>
<ul>
<li>위 과정을 통해 알 수 있듯 우리의 공개키를 서버에 보내줘야 원격 접속이 가능하다.</li>
</ul>
<h3 id="1-key-확인">1. key 확인</h3>
<p>우선 이전에 생성한 공개키가 있는지 확인할 필요가 있다. 생성한 공개키가 없다면 다음 단계로 넘어간다.</p>
<pre><code class="language-bash">cat ~/.ssh/id_rsa.pub</code></pre>
<h3 id="2-key-생성">2. key 생성</h3>
<p>아래의 명령어를 통해 key를 생성한다.</p>
<pre><code class="language-bash">ssh-keygen</code></pre>
<h3 id="3-비밀번호-설정">3. 비밀번호 설정</h3>
<p>key 생성을 하면 비밀번호를 설정하라는 문구가 나올텐데 이때 비밀번호 없이 사용하고 싶다면 <code>enter</code>키를 누르면 된다.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://library.gabia.com/contents/infrahosting/9002/">https://library.gabia.com/contents/infrahosting/9002/</a></li>
<li><a href="https://velog.io/@skyepodium/Github-SSH-Key-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0">https://velog.io/@skyepodium/Github-SSH-Key-등록하기</a></li>
<li><a href="https://thisblogfor.me/web/ssh/">https://thisblogfor.me/web/ssh/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] 자주 사용되는 HTTP 상태 코드 (Status Code)]]></title>
            <link>https://velog.io/@forest_xox/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-HTTP-%EC%83%81%ED%83%9C-%EC%BD%94%EB%93%9C-Status-Code</link>
            <guid>https://velog.io/@forest_xox/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-HTTP-%EC%83%81%ED%83%9C-%EC%BD%94%EB%93%9C-Status-Code</guid>
            <pubDate>Tue, 02 May 2023 17:49:12 GMT</pubDate>
            <description><![CDATA[<p>HTTP 상태 코드는 클라이언트 요청의 상태를 나타내기 위해 서버에서 반환하는 3자리 숫자이다.</p>
<p>응답은 5개의 그룹으로 나뉜다.</p>
<h2 id="1xx-정보-응답">1XX <strong>정보 응답</strong></h2>
<p>요청을 받았고 처리 중이다.
100번대 코드는 HTTP 1.0에서 지원되지 않는다.</p>
<h2 id="2xx-성공">2XX 성공</h2>
<h3 id="200-ok">200 OK</h3>
<p>서버가 요청을 성공적으로 처리했다.</p>
<h3 id="201-created">201 Created</h3>
<p>요청을 성공했고 결과로 새 리소스가 생성되었다.
이 응답은 일반적으로 POST 요청 또는 일부 PUT 요청 이후에 온다.</p>
<h3 id="204-no-content">204 No Content</h3>
<p>요청은 성공했으나 제공할 콘텐츠가 없다.</p>
<p>ex) PUT 요청을 했지만 수정할게 없는 경우</p>
<hr>
<h2 id="3xx-리다이렉션">3XX 리다이렉션</h2>
<h3 id="301-moved-permanently">301 <strong>Moved Permanently</strong></h3>
<p>요청한 리소스의 URI가 <strong>영구적으로</strong> 변경되었다. 응답에 새 URI가 주어질 수 있다.</p>
<h3 id="302-found">302 Found</h3>
<p>요청한 리소스의 URI가 <strong>일시적으로</strong> 변경되었다.</p>
<h3 id="304-not-modified">304 Not Modified</h3>
<p>캐시를 목적으로 사용된다. 클라이언트에게 응답이 수정되지 않았음을 알려줘서 클라이언트는 응답의 캐시된 버전을 사용할 수 있다.</p>
<hr>
<h2 id="4xx-클라이언트-에러">4XX 클라이언트 에러</h2>
<h3 id="400-bad-request">400 Bad Request</h3>
<p>잘못된 문법으로 요청해 서버가 이해할 수 없다.</p>
<p>ex) 누락된 데이터, 도메인 유효성 검사 및 잘못된 형식</p>
<h3 id="401-unauthorized">401 Unauthorized</h3>
<p>인증이 필요한 리소스에 인증 없이 접근하다. (비인증)</p>
<h3 id="403-forbidden">403 Forbidden</h3>
<p>콘텐츠 접근 권한이 없다. 401과 다른점은 서버가 클라이언트가 누군지 알고있다.</p>
<h3 id="404-not-found">404 Not Found</h3>
<p>요청한 리소스를 서버에서 찾을 수 없다.</p>
<h3 id="409-conflict">409 Conflict</h3>
<p>요청이 현재 서버의 상태와 충돌하다.</p>
<p>ex) 서버에 이미 있는 파일보다 오래된 파일을 업로드하면 버전 제어 충돌이 발생한다.</p>
<hr>
<h2 id="5xx-서버-에러">5XX 서버 에러</h2>
<h3 id="500-internal-server-error">500 Internal Server Error</h3>
<p>서버가 처리 방법을 모르는 상황이다.</p>
<h3 id="503-service-unavailable">503 Service Unavailable</h3>
<p>서버가 요청을 처리할 준비가 되지 않았다.
흔히 서버가 점검을 위해 다운되거나 과부하로 발생한다.
Retry-After HTTP 헤더는 가능한 서비스 복구 예상 시간을 포함해야 한다.</p>
<hr>
<h2 id="references">References</h2>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Status">https://developer.mozilla.org/ko/docs/Web/HTTP/Status</a></p>
<p><a href="https://en.wikipedia.org/wiki/List_of_HTTP_status_codes">https://en.wikipedia.org/wiki/List_of_HTTP_status_codes</a></p>
<p><a href="https://www.dotcom-monitor.com/blog/the-10-most-common-http-status-codes/">https://www.dotcom-monitor.com/blog/the-10-most-common-http-status-codes/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] CORS (SOP, error해결)]]></title>
            <link>https://velog.io/@forest_xox/CORS</link>
            <guid>https://velog.io/@forest_xox/CORS</guid>
            <pubDate>Sat, 29 Apr 2023 18:07:03 GMT</pubDate>
            <description><![CDATA[<h1 id="cors란">CORS란</h1>
<p>CORS는 Cross Origin Reasource Sharing의 약자로 직역하면 <strong>교차 출처 리소스 공유</strong>라는 뜻이다.</p>
<p>CORS는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.</p>
<p>쉽게 말해 <strong>다른 출처의 리소스 공유에 대한 허용/비허용 정책</strong>이다.</p>
<p>여기서 <strong>교차 출처</strong>는 <strong>다른 출처</strong>를 말한다. 그럼 출처(Origin)는 무엇일까?</p>
<p>URL은 하나의 문자열이 아닌 여러 구성요소로 이루어져있다. 출처(Origin)는 <code>Protolcol</code>, <code>Host</code>, <code>Port</code>를 합친 URL을 의미한다. </p>
<p><img src="https://velog.velcdn.com/images/forest_xox/post/21c98e08-75f6-43b9-bd6c-81608ad4f692/image.png" alt=""></p>
<h2 id="sopsame-origin-policy">SOP(Same-Origin Policy)</h2>
<p>SOP는 <strong>동일 출처 정책</strong>이란 뜻으로 <em>‘동일한 출처에서만 리소스를 공유할 수 있다’</em> 라는 규칙을 가지고있다. 따라서 브라우저는 다른 출처의 자원에 접근 하는 것을 차단한다.</p>
<p>만약 SOP 정책이 없다면 해커가 CSRF나 XSS 등의 방법을 이용해 개인 정보를 탈취할 수 있다. 
하지만 그렇다고 다른 출처로 리소스를 요청할 수 없는건 아니다. CORS 정책만 지킨다면 가능하다.</p>
<ul>
<li>출처 비교 로직은 서버가 아닌 브라우저에 구현된 스펙이다. 따라서 브라우저 없이 서버 간 통신하면 CORS 정책이 적용되지 않는다.</li>
</ul>
<blockquote>
<p>➕ <strong>CSRF</strong> (cross site request forgery attack)
사용자의 의지와 무관하게 공격자가 의도한 행동을 특정 웹페이지에 요청하게 하는 공격 방법이다.
➕ <strong>XSS</strong> (Cross-site Scripting)
공격자가 상대방의 웹 사이트에 악의적 스크립트를 삽입하는 공격 방법이다.</p>
</blockquote>
<h2 id="cors-기본-동작">CORS 기본 동작</h2>
<ol>
<li>클라이언트에서 HTTP요청 헤더에 Origin필드에 출처를 담아 전달한다.</li>
<li>서버는 응답헤더에 Access-Control-Allow-Origin 필드를 추가하고 값으로 &#39;이 리소스를 접근하는 것이 허용된 출처 url&#39;을 전달한다.</li>
<li>응답 받은 브라우저는 요청 Origin과 응답 받은 Access-Control-Allow-Origin을 비교해 차단 여부를 결정한다.
 a. 유효하면 다른 출처의 리소스를 가져온다.
 b. 유효하지 않다면 응답을 사용하지 않고 버린다. (CORS 에러)</li>
</ol>
<p>결론은 서버에서 Access-Control-Allow-Origin 헤더에 허용할 출처를 기재해서 클라이언트에 응답하면 된다.</p>
<h1 id="cors-작동-방식-3가지">CORS 작동 방식 3가지</h1>
<p>CORS 동작 방식은 한가지가 아니라 3가지 시나리오 따라 변경된다.</p>
<h2 id="예비-요청-preflight-request">예비 요청 (Preflight Request)</h2>
<p>브라우저는 본 요청 전 예비 요청을 통해 이 요청을 보내는 것이 안전한지 확인한다. </p>
<ul>
<li>예비 요청의 메소드는 OPTIONS 요청이 사용된다.</li>
</ul>
<ol>
<li>JS fetch 메서드를 통해 리소스를 받아오려고 한다.</li>
<li>브라우저는 서버로 HTTP OPTIONS 메소드로 예비 요청을 먼저 보낸다.<ul>
<li>Origin 헤더: 자신의 출처</li>
<li>Access-Control-Request-Method 헤더: 실제 요청에 사용할 메소드</li>
<li>Access-Control-Request-Headers 헤더: 실제 요청에 사용할 헤더들</li>
</ul>
</li>
<li>서버는 예비 요청 응답으로 어떤 것을 허용/ 금지하고 있는지에 대한 헤더 정보를 담아서 브라우저로 보낸다.<ul>
<li>Access-Control-Allow-Origin 헤더: 허용되는 Origin들의 목록</li>
<li>Access-Control-Allow-Methods 헤더: 허용되는 메소드들의 목록</li>
<li>Access-Control-Allow-Headers 헤더: 허용되는 헤더들의 목록</li>
<li>Access-Control-Max-Age 헤더: 해당 예비 요청이 브라우저에 캐시 가능한 시간(초 단위)</li>
</ul>
</li>
<li>브라우저는 요청과 응답 정책을 비교해 요청이 안전한지 확인하고 본 요청을 보낸다.</li>
<li>서버가 본 요청에 응답하면 최종적으로 이 응답 데이터를 JS로 넘겨준다.</li>
</ol>
<h3 id="문제점">문제점</h3>
<p>예비 요청으로 인해 실제 요청 시간 증가와 서버 요청이 배로 발생해 비용, 성능 문제가 생긴다. </p>
<p>하지만 이는 서버로 부터 Access-Control-Max-Age 응답 헤더를 받아 해당 시간동안 브라우저 캐시에 결과를 저장해 해결할 수 있다.</p>
<h2 id="단순-요청-simple-request">단순 요청 (Simple Request)</h2>
<p>단순 요청은 예비 요청 없이 본 요청을 보낸 후, 서버가 이에 대한 응답의 헤더에 Access-Control-Allow-Origin 헤더를 보내주면 브라우저가 CORS정책 위반 여부를 검사하는 방식이다.</p>
<p>하지만 단순 요청은 3가지 경우를 만족해야 한다.</p>
<ol>
<li>요청 메소드가 GET, HEAD, POST 중 하나</li>
<li><code>Accept</code>, <code>Accept-Language</code>, <code>Content-Language</code>, <code>Content-Type</code>, <code>DPR</code>, <code>Downlink</code>, <code>Save-Data</code>, <code>Viewport-Width</code>, <code>Width</code> 헤더일 경우</li>
<li>Content-Type 헤더가 <code>application/x-www-form-urlencoded</code>, <code>multipart/form-data</code>, <code>text/plain</code> 중 하나</li>
</ol>
<p>위 조건은 까다롭고 대부분 HTTP API 요청은 <code>text/xml</code> 이나 <code>application/json</code> 으로 통신해서 Content-Type에 위반된다. 따라서 대부분 예비 요청이 일어난다.</p>
<h2 id="인증된-요청-credentialed-request">인증된 요청 (Credentialed Request)</h2>
<p>인증된 요청은 클라이언트가 서버로 <strong>자격 인증 정보</strong>(쿠키, 토큰 등)를 실어 요청할때 사용되는 요청이다.</p>
<p>또한 예비 요청 처럼 preflight가 먼저 일어난다.</p>
<p>기본적으로 브라우저가 제공하는 요청 API 들은 인증 관련된 데이터를 요청 데이터에 담지 않도록 되어있지만 credentials 옵션으로 요청에 인증과 관련된 정보를 담을 수 있다. </p>
<p>이 옵션은 3가지의 값을 사용할 수 있다. 별도 설정을 하지 않으면 인증 정보는 서버에 자동 전송되지 않는다.</p>
<ul>
<li><strong>same-origin(기본값)</strong> : 같은 출처 간 요청에만 인증 정보를 담을 수 있다.</li>
<li><strong>include</strong> : 모든 요청에 인증 정보를 담을 수 있다.</li>
<li><strong>omit</strong> : 모든 요청에 인증 정보를 담지 않는다.</li>
</ul>
<p>인증된 요청을 보내는 방법은 fetch 메서드나 axios, jQuery 라이브리리 등이 있다.</p>
<p>서버도 인증된 요청에 대해 일반적인 CORS 요청과는 다르게 대응해야 한다.</p>
<ol>
<li>응답 헤더의 Access-Control-Allow-Credentials 항목을 true로 설정해야 한다.</li>
<li>응답 헤더의 <code>Access-Control-Allow-Origin</code> 값에 와일드카드 문자(&quot;*&quot;)는 사용할 수 없고 분명한 Origin으로 설정해야 한다.</li>
<li>응답 헤더의 <code>Access-Control-Allow-Methods</code>, <code>Access-Control-Allow-Headers</code> 값에 와일드카드 문자(&quot;*&quot;)는 사용할 수 없다.</li>
</ol>
<br/>

<h1 id="cors-해결-방법">CORS 해결 방법</h1>
<h2 id="best👍🏻-access-control-allow-origin-응답-헤더-세팅">BEST👍🏻) Access-Control-Allow-Origin 응답 헤더 세팅</h2>
<p>서버측에서 Access-Control-Allow-Origin 헤더에 유효한 값을 포함해 브라우저에 응답하면 된다.</p>
<p>또한 Access-Control-Allow-Origin 값에 와일드카드 문자(&quot;*&quot;)를 사용하면 모든 출처의 요청을 받아 보안적 이슈가 발생하기에 분명한 Origin을 설정해야 한다. </p>
<ul>
<li>서버측 응답에서 접근 권한을 주는 헤더를 추가하여 해결한다.</li>
</ul>
<pre><code class="language-jsx">// CORS관련 HTTP 헤더 값
Access-Control-Allow-Origin
Access-Control-Request-Methods
Access-Control-Allow-Headers
Access-Control-Max-Age
Access-Control-Allow-Credentials
Access-Control-Expose-Headers</code></pre>
<h2 id="프록시-사이트-이용하기">프록시 사이트 이용하기</h2>
<p>요청해야 하는 URL 앞에 프록시 서버 URL을 붙여서 요청하게 된다. 프록시 서버를 사용하면 중간에 요청을 가로채서 HTTP 응답헤더에 Access-Control-Allow-Origin : * 를 설정해준다.</p>
<pre><code class="language-jsx">// heroku 프록시 서버 URL
https://cors-anywhere/herokuapp.com
// 요청
axois({
  method: &quot;GET&quot;,
  url: `https://cors-anywhere/herokuapp.com/{주소}`,
  header:{
    &#39;APIKey&#39;: ${API_key}
  }
})</code></pre>
<p>현재 무료 프록시 서비스들은 모두 악용 사례로 api 요청 횟수 제한을 두어 실전이 아닌 테스트용으로 사용해야 한다. 실전에서는 프록시 서버를 구축하여 사용해야 한다.</p>
<h2 id="webpack-dev-server로-리버스-프록싱하기">Webpack Dev Server로 리버스 프록싱하기</h2>
<p>webpack-dev-server가 제공하는 프록시 기능을 사용한다. 이는 로컬에서만 가능하다.</p>
<p>아래의 설정을 하면 <code>/api</code>로 시작하는 URL로 보내는 요청을 브라우저는 <code>localhost:8000/api</code>로 요청한줄 알지만 실은 웹팩이 target값인 URL로 요청을 프록싱한다. </p>
<pre><code class="language-jsx">module.exports = {
  devServer: {
    proxy: {
      &#39;/api&#39;: {
        target: &#39;https://api.evan.com&#39;,
        changeOrigin: true,
        pathRewrite: { &#39;^/api&#39;: &#39;&#39; },
      },
    }
  }
}</code></pre>
<h2 id="http-proxy-middleware-사용">http-proxy-middleware 사용</h2>
<p>로컬 환경에서 사용하여 클라이언트단에서 쉽게 해결할 수 있다.</p>
<p><code>http-proxy-middleware</code>를 설치하고 <code>src 폴더</code>안에<code>setupProxy.js</code> 파일을 만들고 아래 코드를 작성하면 된다. 그럼 로컬 환경에서 <code>http://localhost:3000/api</code>로 시작하는 요청을 라이브러리가 <code>http://localhost:5000/api</code>로 프록싱 해준다.</p>
<pre><code class="language-jsx">const { createProxyMiddleware } = require(&quot;http-proxy-middleware&quot;)

module.exports = function (app) {
    app.use(
        &quot;/api&quot;,
        createProxyMiddleware({
            target: &quot;http://localhost:5000&quot;,
            changeOrigin: true,
        })
    )
}</code></pre>
<h2 id="packagejson에-proxy값-설정">package.json에 proxy값 설정</h2>
<p>CRA로 생성한 프로젝트에서는 package.json에 proxy 값을 설정해 proxy기능을 활성화할 수 있다.</p>
<pre><code class="language-jsx">{
  //...
  &quot;proxy&quot;: &quot;http://localhost:4000&quot;
}</code></pre>
<h2 id="jsonp">JSONP</h2>
<p>브라우저에서 css나 js 같은 리소스 파일들은 SOP 영향을 받지않아 외부 서버에서 읽어온 js 파일을 json으로 바꿔주는 일종의 편법이다. 단점은 GET 방식의 API만 요청이 가능하다.</p>
<hr>
<h2 id="references">References</h2>
<p><a href="https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-CORS-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%F0%9F%91%8F">https://inpa.tistory.com/entry/WEB-📚-CORS-💯-정리-해결-방법-👏</a></p>
<p><a href="https://evan-moon.github.io/2020/05/21/about-cors/">https://evan-moon.github.io/2020/05/21/about-cors</a></p>
<p><a href="https://ingg.dev/cors/#whatiscors">https://ingg.dev/cors</a></p>
<p><a href="https://xiubindev.tistory.com/115">https://xiubindev.tistory.com/115</a></p>
<p><a href="https://simsimjae.medium.com/cors%EC%99%80-jsonp%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-aa3ec0456e97">https://simsimjae.medium.com/cors와-jsonp에-대해서-aa3ec0456e97</a></p>
<p><a href="http://yoonbumtae.com/?p=2452">http://yoonbumtae.com/?p=2452</a></p>
]]></description>
        </item>
    </channel>
</rss>