<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>하기나 해</title>
    <link>https://indeeah.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 15 May 2026 07:36:27 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>indeeah</managingEditor>
    <image>
      <title>하기나 해</title>
      <url>https://tistory1.daumcdn.net/tistory/4438338/attach/fa6c4242a69f42758f804279230a70bd</url>
      <link>https://indeeah.tistory.com</link>
    </image>
    <item>
      <title>SvelteKit이냐 svelte-spa-router냐? 라우터로 뭐쓰지?</title>
      <link>https://indeeah.tistory.com/75</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;React와 Next.js만 써보다가 이번 프로젝트에서 Svelte를 쓰게 되어 머리가 리셋이 되어버렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여긴 어디? 나는 누구?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;i2178981990.jpg&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/msnZd/btsL1BGgd8H/KsqBWP7GlICN4mEebPhM5K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/msnZd/btsL1BGgd8H/KsqBWP7GlICN4mEebPhM5K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/msnZd/btsL1BGgd8H/KsqBWP7GlICN4mEebPhM5K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmsnZd%2FbtsL1BGgd8H%2FKsqBWP7GlICN4mEebPhM5K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;387&quot; data-filename=&quot;i2178981990.jpg&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;꽤나 익숙해졌던 Next.js를 쓰지 않게 되니, 나는 다시 프린이가 되어버렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;라우터조차 어떻게 쓰는지 모르겠어서 검색해보니 &lt;b&gt;svelte-spa-router&lt;/b&gt;를 많이 쓰는 것 같아 일단 사용해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;svelte-spa-router&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클라이언트 사이드 라우팅을 구현하기 위한 경량 라우터 라이브러리(클라이언트 사이드 라우팅만 지원)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;라우팅 설정은 &lt;b&gt;routes.js&lt;/b&gt;나 기타 설정 파일에서 경로를 정의해야 함.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737730225081&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Home from './pages/Home.svelte';
import About from './pages/About.svelte';
import User from './pages/User.svelte';

const routes = {
  '/': Home,
  '/about': About,
  '/:id': User
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1737730675663&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  import { Router } from 'svelte-spa-router';
  import routes from './routes';
&amp;lt;/script&amp;gt;

&amp;lt;Router {routes} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;:id&lt;/b&gt;와 같은 동적 경로를 지원&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;beforeNavigate&lt;/b&gt;나 &lt;b&gt;afterNavigate&lt;/b&gt; 훅을 통해 라우팅 동작을 제어 할 수 있음.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;라이브러리가 매우 가볍고 간단하다는 장점이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737730536081&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  import { goto } from 'svelte-spa-router';

  function navigateToAbout() {
    goto('/about'); // '/about' 페이지로 이동
  }
&amp;lt;/script&amp;gt;

&amp;lt;button on:click={navigateToAbout}&amp;gt;About 페이지로 이동&amp;lt;/button&amp;gt;
&amp;lt;a href=&quot;/about&quot;&amp;gt;About 페이지로 이동&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;button과 goto 함수를 사용하거나, a 태그를 사용하여 페이지를 이동 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;SvelteKit&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;풀스택 애플리케이션 프레임워크로, 라우터 기능뿐 아니라 전체 애플리케이션 구조를 제공한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;src/routes 디렉토리 구조를 기반, 자동으로 라우팅을 설정&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예) &lt;b&gt;src/routes/about/+page.svelte -&amp;gt; /about&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;서버 사이드 렌더링, 정적 사이트 생성, 클라이언트 사이드 렌더링을 모두 지원한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;백엔드 API 엔드포인트를 &lt;b&gt;+server.js&lt;/b&gt; 파일로 쉽게 설정할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;[id].svelte&lt;/b&gt;처럼 대괄호를 사용하여 동적 라우팅을 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;load 함수로 데이터를 비동기적으로 가져오고 로딩 상태를 관리 할 . 수있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SEO와 퍼포먼스 최적화에 강점이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;선택은?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SvelteKit을 선택했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결정적인 계기는 내가 못찾았을 가능성이 훠얼~씬 높지만, svelte-spa-router에서는 &lt;u&gt;&lt;b&gt;페이지를 새로고침 하지 않고 + 함수 내에서 페이지를 이동하는 방법&lt;/b&gt;&lt;/u&gt;이 없었기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예를 들어 Next.js에서는&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1737731294378&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default function Example() {
    const handleSomething = async () =&amp;gt; {
    	await addSomething();
        
        router.push('/about');
    };

	return (
    	&amp;lt;div&amp;gt;
        	&amp;lt;button onClick={handleSomething}&amp;gt;
            	누르면 어떤 일을 처리하고 페이지를 이동하는 버튼
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 식으로 이동 할 수 있으나, svelte-spa-router를 사용하면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737731537157&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	const handleSomething = async () =&amp;gt; {
    	if (window === undefined) return;
        
    	await addSomething();
        
        window.location.href = '/about';
    }
&amp;lt;/script&amp;gt;

&amp;lt;div&amp;gt;
	&amp;lt;button on:click={handleSomething}&amp;gt;
    	누르면 어떤 일을 하고 새로고침 되는 것 처럼 보이는 버튼
    &amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 식으로 브라우저의 주소를 변경하여 페이지를 로드하도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그럼 전체 페이지 새로고침이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;반면 SvelteKit을 이용하면 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1737731680612&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	const handleSomething = async () =&amp;gt; {        
    	await addSomething();
        
        goto('/about');
    }
&amp;lt;/script&amp;gt;

&amp;lt;div&amp;gt;
	&amp;lt;button on:click={handleSomething}&amp;gt;
    	누르면 어떤 일을 하고 새로고침 하지 않는 버튼
    &amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 이동이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한, 페이지를 생성할때마다 routes.js에 어떻게 귀찮아서 하나 하나 적냐!!!!! 라는 크나큰 귀차니즘 때문에 SvelteKit 사용을 적극적으로 어필했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SvelteKit은 라우터만의 역할을 하는 것이 아니므로, 다른 프레임워크나 프레임워크를 사용하지 않는다면 라우터만의 기능을 위해 사용하는 것은 불필요한 선택이라고 생각된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 프로젝트 초기 단계에서 이것 저것 규칙 정하기 귀찮고, 풀스택 개발도 어짜피 할 예정이다~ 라면 그냥 SvelteKit을 사용하는 것을 추천한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;나에게는 러닝 커브는 svelte-spa-router가 더 높게 느껴졌기 때문이다.. (생각보다 문서나 커뮤니티가 부실.. 아마도 내가 완전히 잘못 쓰고 있었을 가능성이 높지만..)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>svelte</category>
      <category>svelte-spa-router</category>
      <category>SvelteKit</category>
      <category>라우터</category>
      <author>indeeah</author>
      <guid isPermaLink="true">https://indeeah.tistory.com/75</guid>
      <comments>https://indeeah.tistory.com/75#entry75comment</comments>
      <pubDate>Sat, 25 Jan 2025 00:24:38 +0900</pubDate>
    </item>
    <item>
      <title>Next.js 의 Dynamic server usage 빌드 에러</title>
      <link>https://indeeah.tistory.com/61</link>
      <description>&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6cMCd/btsI80PwsjB/iaR8MLiPYn4sX5cgUXS5Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6cMCd/btsI80PwsjB/iaR8MLiPYn4sX5cgUXS5Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6cMCd/btsI80PwsjB/iaR8MLiPYn4sX5cgUXS5Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6cMCd%2FbtsI80PwsjB%2FiaR8MLiPYn4sX5cgUXS5Qk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;client-side에서 데이터를 패칭 할 때 쿼리 스트링 등 동적으로 데이터 패칭이 필요 할 때 빌드 시 해당 API는 server-side 데이터를 가져 올 수 없으므로 빌드 에러가 나게 된다.&lt;/p&gt;
&lt;h3&gt;문제 코드&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// index.tsx
const fetchOrders = useCallback(async () =&amp;gt; {
    try {
      if (
        !user ||
        searchConditions.merchantId === undefined ||
        searchConditions.sellerId === undefined
      )
        return;

      const { totalCount, orders: fetchedOrders } = await getOrders({
        merchantId: searchConditions.merchantId,
        sellerId: searchConditions.sellerId,
        status:
          searchConditions.status !== &amp;#39;전체&amp;#39;
            ? [getStatusEng(searchConditions.status!) as OrderItemStatus]
            : null,
        startDate: searchConditions.startDate,
        endDate: searchConditions.endDate,
        page: pageInfo.page,
        perPage: pageInfo.perPage,
      });

      setOrders(fetchedOrders);
      setTotalCount(Number(totalCount));
    } catch (error) {
      handleErrorWithModal(
        error,
        &amp;#39;주문 내역을 불러오는 중 오류가 발생했습니다.&amp;#39;,
        openModal,
        fetchOrders,
      );
    }
  }, [
    openModal,
    setOrders,
    searchConditions.merchantId,
    searchConditions.sellerId,
    searchConditions.status,
    searchConditions.startDate,
    searchConditions.endDate,
    pageInfo.page,
    pageInfo.perPage,
    user,
  ]);

  useEffect(() =&amp;gt; {
    if (!user) return;

    fetchOrders();
  }, [user, fetchOrders]);

const exportDataToExcel = async () =&amp;gt; {
    try {
      if (isDownloading) return;
      setIsDownloading(true);

      if (
        !user ||
        searchConditions.merchantId === undefined ||
        searchConditions.sellerId === undefined
      ) {
        showToastMessage(&amp;#39;잠시 후 다시 시도해주세요.&amp;#39;, &amp;#39;error&amp;#39;, true);
        return;
      }

      const { headers, data } = await getOrdersExcelData({
        merchantId: searchConditions.merchantId,
        sellerId: searchConditions.sellerId,
        status:
          searchConditions.status !== &amp;#39;전체&amp;#39;
            ? [getStatusEng(searchConditions.status!) as OrderItemStatus]
            : null,
        startDate: dayjs(searchConditions.startDate).format(&amp;#39;YYYYMMDD&amp;#39;),
        endDate: dayjs(searchConditions.endDate).format(&amp;#39;YYYYMMDD&amp;#39;),
      });

      const fileName = `${dayjs(searchConditions.startDate).format(&amp;#39;YYYYMMDD&amp;#39;)}_${dayjs(searchConditions.endDate).format(&amp;#39;YYYYMMDD&amp;#39;)}_주문내역`;
      const sheetName = &amp;#39;주문내역&amp;#39;;

      exportToExcel({ headers, data, fileName, sheetName });
    } catch (error) {
      handleErrorWithToast(
        error,
        &amp;#39;주문 내역을 엑셀로 다운로드하는 중 오류가 발생했습니다.&amp;#39;,
        showToastMessage,
      );
    } finally {
      // NOTE: 2초 후에 다운로드 가능한 상태로 변경
      setTimeout(() =&amp;gt; setIsDownloading(false), 2000);
    }
  };

  // route.ts
  export async function GET(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;req.url을 가져오는 데 문제가 생김&lt;/p&gt;
&lt;h2&gt;설명&lt;/h2&gt;
&lt;h3&gt;렌더링 방식&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;정적 생성 (Static Generation, SSG)&lt;ul&gt;
&lt;li&gt;정적 HTML 생성: 빌드 타임에 HTML 파일을 생성하고, 이 파일을 정적 서버에 배포&lt;/li&gt;
&lt;li&gt;빠른 응답 시간: 서버는 미리 생성된 HTML 파일 즉시 제공&lt;/li&gt;
&lt;li&gt;SEO 친화적: 완전히 렌더링된 HTML을 제공하므로 검색 엔진 크롤러가 쉽게 인덱싱&lt;/li&gt;
&lt;li&gt;제한점: 빌드 타임에 모든 데이터를 필요로 하며, 이후에 데이터 변경 사항은 반영되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;동적 렌더링 (Dynamic Rendering, SSR)&lt;ul&gt;
&lt;li&gt;서버 사이드 렌더링: 각 요청 시마다 서버가 최신 데이터를 기반으로 HTML을 생성하여 반환&lt;/li&gt;
&lt;li&gt;실시간 데이터: 항상 최신 데이터를 사용하여 페이지를 렌더링&lt;/li&gt;
&lt;li&gt;SEO 친화적: 서버가 완전히 렌더링된 HTML을 제공하므로 SEO에 유리&lt;/li&gt;
&lt;li&gt;서버 부하: 각 요청 시마다 서버가 렌더링 작업을 수행하므로 서버 부하가 증가 할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;클라이언트 사이드 데이터 패칭:&lt;ul&gt;
&lt;li&gt;React 컴포넌트가 브라우저에서 실행&lt;/li&gt;
&lt;li&gt;API는 동적으로 처리: 서버는 클라이언트로부터의 요청에 따라 실시간으로 데이터를 처리하고 응답 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;빌드 타임과 런타임 차이&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;빌드 타임: 정적 사이트 생성 시점&lt;ul&gt;
&lt;li&gt;이때 Next.j는 서버 사이드 함수나 API 라우트를 실행 할 수 없음&lt;/li&gt;
&lt;li&gt;모든 데이터는 정적으로 존재해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;런타임: 사용자가 페이지를 요청하는 시점. 동적 렌더링의 경우, 이 시점에서 서버가 요청을 받아 데이터를 가져와 페이지를 렌더링&lt;ul&gt;
&lt;li&gt;이때 Next.js는 서버 사이드 함수나 API 라우트를 실행할 수 있음&lt;/li&gt;
&lt;li&gt;최신 데이터를 가져와 페이지를 동적 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;API 요청&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;정적 요청&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getStaticProps&lt;/code&gt; 를 이용해 요청 시 API 요청은 정적 요청이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;동적 요청&lt;ul&gt;
&lt;li&gt;server-side 및 client-side에서 요청 시 API 요청은 동적 요청이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;해결 방법&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// route.ts
export const dynamic = &amp;#39;force-dynamic&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;빌드 타임에 해당 API 라우트를 정적으로 생성하지 않고, 런타임에 처리하도록 하는 설정&lt;/li&gt;
&lt;li&gt;빌드 시점에 접근할 수 없는 리소스나 환경 변수를 필요로 할 때 사용&lt;/li&gt;
&lt;li&gt;API가 데이터베이스 쿼리, 외부 API 호출 등 빌드 타임에 실행될 수 없는 작업을 포함하는 경우 유용&lt;/li&gt;
&lt;li&gt;모든 동적 요청 API에 처리할 필요는 없고, 동적 데이터가 필요한 API에만 처리&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>dynamic server usage</category>
      <category>next.js</category>
      <author>indeeah</author>
      <guid isPermaLink="true">https://indeeah.tistory.com/61</guid>
      <comments>https://indeeah.tistory.com/61#entry61comment</comments>
      <pubDate>Mon, 19 Aug 2024 17:39:14 +0900</pubDate>
    </item>
    <item>
      <title>too many clients에서 벗어나보자</title>
      <link>https://indeeah.tistory.com/60</link>
      <description>&lt;aside&gt;
  백오피스 개발 중 가장 화나게 만드는 too many clients 오류를 수정해보려 합니다.

&lt;/aside&gt;

&lt;p&gt;스토어&amp;amp;센터 백오피스 모두 Next.js와 knex를 사용하고 있는데, 개발 과정에서 커넥션 수가 넘치는 상황이 너무나도 많이 발생했다.&lt;/p&gt;
&lt;p&gt;(프로덕션 환경에서는 확인 해보지 않았으나) 내가 확인한 커넥션이 발생하는 케이스는&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;페이지 이동&lt;/li&gt;
&lt;li&gt;내가 띄어논 페이지 화면에서의 코드 수정 후 저장 시 리로딩&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;두 가지 케이스에서 발생 하는 것을 확인 할 수 있었다.&lt;/p&gt;
&lt;p&gt;2번 케이스의 경우 &lt;code&gt;npm run start:dev&lt;/code&gt; 로 실행시켜놓고 계속해서 작업을 하니 문제가 되지 않나 생각이 들었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://prod-files-secure.s3.us-west-2.amazonaws.com/fd4600bc-b925-494d-9270-fe7775f5c23e/763d9ccd-aa0b-4a3d-a2a9-3e69f9e4e2bf/Untitled.png&quot; alt=&quot;Untitled&quot;&gt;&lt;/p&gt;
&lt;h3&gt;커넥션 수 확인 방법&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;SELECT
  datname,
  count(*) as connection_count
FROM
  pg_stat_activity
GROUP BY
  datname;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;해결 과정&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;max_connections&lt;/code&gt; 수정&lt;/p&gt;
&lt;p&gt; 전혀 도움이 되지 않았다.&lt;/p&gt;
&lt;p&gt; 그저 연결되는 양만 많아지게 할 뿐.. 근본적인 해결책은 되지 않는다.&lt;/p&gt;
&lt;p&gt; 무한으로 증가하는 커넥션을 막을 방법이 아니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;connection pool 설정&lt;/p&gt;
&lt;p&gt; &lt;code&gt;knexConfig&lt;/code&gt; 에 커넥션 풀을 설정해두었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;   pool: { min: 2, max: 10 }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 적용한 즉시 잘 적용되나 확인은 할 수 없었으나, 추후 로그를 확인 해 봤을 때 커넥션 수에는 도움이 되지 않는 것을 확인 했다.&lt;/p&gt;
&lt;p&gt; 커넥션 수가 초과하는 원인은 아래와 같을 수 있다고 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; 1.    커넥션 누수(Connection Leak):
     •    트랜잭션이나 쿼리를 실행한 후 커넥션을 제대로 반환하거나 종료하지 않는 경우, 커넥션 풀에서 커넥션이 누수되어 계속 증가할 수 있습니다.
 2.    커넥션 풀 설정이 제대로 적용되지 않음:
     •    설정 파일이나 코드에서 커넥션 풀 설정이 제대로 적용되지 않거나, 잘못된 설정이 있을 수 있습니다.
 3.    다중 인스턴스 문제:
     •    애플리케이션이 여러 인스턴스로 실행되는 경우, 각 인스턴스가 자신의 커넥션 풀을 관리하게 되어 총 커넥션 수가 증가할 수 있습니다.
 4.    커넥션 풀의 임시 증가:
     •    특정 상황에서 커넥션 풀이 일시적으로 최대 값을 초과할 수 있는 버스트(Burst) 상황이 발생할 수 있습니다. 예를 들어, 순간적으로 많은 요청이 몰릴 때 일시적으로 더 많은 커넥션이 필요할 수 있습니다.&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;애플리케이션 종료 할 때만이라도 커넥션을 끊어버리자&lt;/p&gt;
&lt;p&gt; 자주 종료해주면 커넥션 문제는 없을 것 처럼 보였다.&lt;/p&gt;
&lt;p&gt; 커넥션이 끊기는 엄청난 로그들도 확인 할 수 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; async function handleExit(signal: string) {
   logger.info(`Received ${signal}. Closing database connection pool...`);
   await connection.destroy();
   process.exit(0);
 }

 process.on(&amp;#39;SIGINT&amp;#39;, () =&amp;gt; handleExit(&amp;#39;SIGINT&amp;#39;));
 process.on(&amp;#39;SIGTERM&amp;#39;, () =&amp;gt; handleExit(&amp;#39;SIGTERM&amp;#39;));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; &lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfzHXD/btsI7Jg14LF/W3y0WJLLPekSKQf9d6rKj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfzHXD/btsI7Jg14LF/W3y0WJLLPekSKQf9d6rKj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfzHXD/btsI7Jg14LF/W3y0WJLLPekSKQf9d6rKj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfzHXD%2FbtsI7Jg14LF%2FW3y0WJLLPekSKQf9d6rKj1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;커넥션을 전역으로 쓰자&lt;/p&gt;
&lt;p&gt; &lt;code&gt;knexInstance&lt;/code&gt;가 있으면 있는 걸로 쓴다.&lt;/p&gt;
&lt;p&gt; 없으면 새로 만들어서 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; declare global {
   var knexInstance: Knex | undefined;
 }

 if (!global.knexInstance) {
   global.knexInstance = knex(knexConfig);
 }

 const connection = global.knexInstance; &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 테스트 해보니 1,2번 케이스 모두 커넥션 수가 늘어나지 않는 걸 확인 했다.&lt;/p&gt;
&lt;p&gt; 하지만 아래와 같은 단점이 있다고 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; 1.    모듈 간 결합: 전역 변수를 사용하면 모듈 간 결합이 강해져 코드의 유연성과 모듈화가 떨어질 수 있습니다. 이는 코드 유지보수에 어려움을 초래할 수 있습니다.
 2.    테스트 어려움: 전역 상태를 사용하는 코드는 단위 테스트나 통합 테스트 시 독립적으로 테스트하기 어려울 수 있습니다. 테스트 환경에서 전역 상태를 초기화하거나 변경하는 데 추가 작업이 필요합니다.
 3.    다중 인스턴스 환경: 클라우드 환경이나 컨테이너화된 애플리케이션에서는 여러 인스턴스가 동시에 실행될 수 있습니다. 이 경우, 각 인스턴스가 자체 전역 커넥션 풀을 관리하게 되어 데이터베이스 커넥션 수가 예상보다 많아질 수 있습니다.
 4.    메모리 누수 가능성: 전역 변수를 사용하면 애플리케이션이 종료될 때까지 메모리에서 해제되지 않습니다. 올바르게 관리되지 않으면 메모리 누수의 원인이 될 수 있습니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 3번과 4번의 단점이 크리티컬하다고 느껴 조언을 구해보니 Next.js는 전역 DB Instance를 사용하는 것이 맞다고 해서 위와 같이 수정했다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL</category>
      <category>knex</category>
      <category>next.js</category>
      <author>indeeah</author>
      <guid isPermaLink="true">https://indeeah.tistory.com/60</guid>
      <comments>https://indeeah.tistory.com/60#entry60comment</comments>
      <pubDate>Mon, 19 Aug 2024 17:38:07 +0900</pubDate>
    </item>
    <item>
      <title>노션 API를 공지사항에 적용해보면 어떨까?</title>
      <link>https://indeeah.tistory.com/59</link>
      <description>&lt;aside&gt;
  우연히 보게 된 화해 블로그의 Notion API 연동기.

&lt;p&gt;잘 적용해보면 이리저리 쓰일 곳이 많을 것 같아서 백오피스 공지사항에 R&amp;amp;D 해보았다.&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;&lt;a href=&quot;https://blog.hwahae.co.kr/all/tech/10960&quot;&gt;Notion API와 함께 정적 페이지로의 여정 – 화해 블로그 | 기술 블로그&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;관리자가 쓰기 쉬운 툴&lt;/h1&gt;
&lt;p&gt;우리 회사는 노션을 이미 사용 하고 있다. 스케쥴부터 공유 문서 등에서 모든 부서가 노션을 사용하고 있었으며, 그렇기에 노션 사용이 익숙할 것이라고 생각했다. 그렇지만 이미 백오피스에서 공지사항 입력 폼은 만들어 놓았었다. &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lDPmQ/btsI8Zb0IJq/E3FGKA5x0SlJnQ5ELTNdA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lDPmQ/btsI8Zb0IJq/E3FGKA5x0SlJnQ5ELTNdA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lDPmQ/btsI8Zb0IJq/E3FGKA5x0SlJnQ5ELTNdA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlDPmQ%2FbtsI8Zb0IJq%2FE3FGKA5x0SlJnQ5ELTNdA0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;그러나 아직 데이터베이스 공지사항에 셋팅 해놓지 않았고, 내용 입력에서는 아직 따로 텍스트 입력기 등의 라이브러리를 사용하지 않으므로 글자에 스타일 적용이 불가하다. 텍스트 입력기 라이브러리를 적용하면 되잖아? 싶지만 테이블 셋팅이 아직 안됐기에 노션 API를 적용해보기로 했다.&lt;/p&gt;
&lt;h1&gt;계획&lt;/h1&gt;
&lt;p&gt;저 위의 화해 블로그에 너무도 자세히 써져 있으므로 그대로 따라가기로 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;노션 &lt;a href=&quot;https://developers.notion.com/docs/create-a-notion-integration&quot;&gt;integration&lt;/a&gt; 추가&lt;/li&gt;
&lt;li&gt;원하는 페이지에 connection 추가&lt;/li&gt;
&lt;li&gt;database 페이지 추가&lt;/li&gt;
&lt;li&gt;properties 세팅&lt;/li&gt;
&lt;li&gt;데이터 추가&lt;/li&gt;
&lt;li&gt;API를 통해 데이터 가져오기&lt;/li&gt;
&lt;li&gt;마크다운으로 변경&lt;/li&gt;
&lt;li&gt;HTML 코드로 변경&lt;/li&gt;
&lt;li&gt;예쁘게 보여주기- &lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h1&gt;노션 API를 통해 데이터를 가져오기&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/makenotion/notion-sdk-js&quot;&gt;&lt;code&gt;@notionhq/client&lt;/code&gt;&lt;/a&gt; 를 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const notion = new Client({ auth: token });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 token은 integration 추가 시 받는 secret이다.&lt;/p&gt;
&lt;p&gt;주의할 점&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;노션 API 호출은 서버 사이드에서만 가능하다.&lt;br&gt;클라이언트 사이드에서의 호출은 CORS ERROR를 일으킨다!&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;공지사항 리스트 가져오기&lt;/h2&gt;
&lt;p&gt;공지사항 리스트를 가져올 때는 필터해서 가져오는 기능이 필요했다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;내가 보여줄 사이트에 관한 데이터&lt;/li&gt;
&lt;li&gt;공지사항 데이터&lt;/li&gt;
&lt;li&gt;노출 가능한 상태의 데이터&lt;/li&gt;
&lt;li&gt;노출일자에서 내림차순으로도 데이터를 가져올 수 있으면 좋겠다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;노션 리스트에서는 properties를 이용해서 쿼리를 할 수 있는데, 그 타입에 따라서 쿼리 하는 방법이 다르다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// 단일 필터
filter: {
    property: &amp;#39;이름&amp;#39;,
    [property type]: {
        [비교문]: &amp;#39;내용&amp;#39;,
    },
}
// 다중 필터
filter: {
    and: [
        {
            property: &amp;#39;이름&amp;#39;,
            [property type]: {
                [비교문]: &amp;#39;내용&amp;#39;,
            }
        },
    ],
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;비교문은 property의 타입에 따라서 사용 방법이 다른 듯 하다.&lt;/p&gt;
&lt;p&gt;노션 가이드에서는 찾질 못하였고, 코파일럿이나 ChatGPT에서 도움을 받는다면 쉽게 해결 할 수 있다.&lt;/p&gt;
&lt;p&gt;sort하는 방법은 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;sorts: [
    {
        property: &amp;#39;property 이름&amp;#39;,
        direction: &amp;#39;ascending or descending&amp;#39;,
    },
],&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;limit를 지정 할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;page_size: number | undefined&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;필요한 것들을 적용한 코드는 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const response = await notion.databases.query({
  database_id: databaseId,
  page_size: !isNaN(Number(limit)) ? Number(limit) : undefined,
  filter: {
    and: [
      {
        property: &amp;#39;사이트&amp;#39;,
        select: {
          equals: &amp;#39;백오피스&amp;#39;,
        },
      },
      {
        property: &amp;#39;태그&amp;#39;,
        select: {
          equals: &amp;#39;공지사항&amp;#39;,
        },
      },
      {
        property: &amp;#39;상태&amp;#39;,
        status: {
          equals: &amp;#39;노출&amp;#39;,
        },
      },
      {
        property: &amp;#39;노출일자&amp;#39;,
        date: {
          on_or_before: dayjs().format(&amp;#39;YYYY-MM-DD&amp;#39;),
        },
      },
    ],
  },
  sorts: [
    {
      property: &amp;#39;노출일자&amp;#39;,
      direction: &amp;#39;descending&amp;#39;,
    },
  ],
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;databaseId&lt;/code&gt; 는 해당 페이지의&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.notion.so/name/%5BdatabaseId%5D&quot;&gt;https://www.notion.so/name/[databaseId]&lt;/a&gt;?.. 에서 확인 할 수 있다.&lt;/p&gt;
&lt;p&gt;주의할 점&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;property의 이름이 달라지거나 타입이 달라지게 되면 쿼리에서 에러가 난다.&lt;br&gt;뭐 이렇게 쉽게 에러가 나지? 싶었지만, SQL에서 컬럼 이름이 변경되거나 타입이 변경되면 에러가 나는 것과 동일하다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;response를 콘솔에 찍어보면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// response
{
  object: &amp;#39;list&amp;#39;,
  results: [
    {
      object: &amp;#39;page&amp;#39;,
      id: &amp;#39;page-id&amp;#39;,
      created_time: &amp;#39;2024-05-23T02:30:00.000Z&amp;#39;,
      last_edited_time: &amp;#39;2024-05-23T09:23:00.000Z&amp;#39;,
      created_by: [Object],
      last_edited_by: [Object],
      cover: null,
      icon: null,
      parent: [Object],
      archived: false,
      in_trash: false,
      properties: [Object],
      url: &amp;#39;https://www.notion.so/&amp;#39;,
      public_url: null
    },
  ],
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;response만 찍어 봐서는 공지사항 리스트를 작성하기 위해 쓸만한 데이터가 하나도 없다.&lt;/p&gt;
&lt;p&gt;여기서는 공지사항 세부 내용을 보기 위해 필요한 id만 건질 수 있다.&lt;/p&gt;
&lt;p&gt;이외에 지정한 properties(컬럼들)를 보려면 properties를 보아야 한다.&lt;/p&gt;
&lt;p&gt;타입 별로 구성되어 있는 데이터가 모두 다르니 꼭 찍어보기를 권한다.&lt;/p&gt;
&lt;p&gt;공지사항 리스트를 구성하기 위한 데이터들을 얻었다!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const notices = response.results.map((result: any) =&amp;gt; {
    return {
        id: result.id,
        title: result.properties[&amp;#39;제목&amp;#39;].title[0].plain_text,
        category: result.properties[&amp;#39;카테고리&amp;#39;].multi_select.map(
            (category: { name: string }) =&amp;gt; category.name,
        ),
        displayedAt: result.properties[&amp;#39;노출일자&amp;#39;].date.start,
    };
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;공지사항 세부 내용 가져오기&lt;/h2&gt;
&lt;p&gt;페이지 세부 내용을 가져와보면 끔찍한 JSON 데이터를 받아볼 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const blocks = await notion.blocks.children.list({ block_id: pageId });

{
  object: &amp;#39;list&amp;#39;,
  results: [
    {
      object: &amp;#39;block&amp;#39;,
      id: &amp;#39;&amp;#39;,
      parent: [Object],
      created_time: &amp;#39;2024-05-23T02:48:00.000Z&amp;#39;,
      last_edited_time: &amp;#39;2024-05-23T10:51:00.000Z&amp;#39;,
      created_by: [Object],
      last_edited_by: [Object],
      has_children: false,
      archived: false,
      in_trash: false,
      type: &amp;#39;heading_2&amp;#39;,
      heading_2: [Object]
    },
    {
      object: &amp;#39;block&amp;#39;,
      id: &amp;#39;&amp;#39;,
      parent: [Object],
      created_time: &amp;#39;2024-05-23T02:48:00.000Z&amp;#39;,
      last_edited_time: &amp;#39;2024-05-23T02:48:00.000Z&amp;#39;,
      created_by: [Object],
      last_edited_by: [Object],
      has_children: false,
      archived: false,
      in_trash: false,
      type: &amp;#39;heading_1&amp;#39;,
      heading_1: [Object]
    },
    {
      object: &amp;#39;block&amp;#39;,
      id: &amp;#39;&amp;#39;,
      parent: [Object],
      created_time: &amp;#39;2024-05-23T02:49:00.000Z&amp;#39;,
      last_edited_time: &amp;#39;2024-05-23T02:49:00.000Z&amp;#39;,
      created_by: [Object],
      last_edited_by: [Object],
      has_children: true,
      archived: false,
      in_trash: false,
      type: &amp;#39;table&amp;#39;,
      table: [Object]
    },
    {
      object: &amp;#39;block&amp;#39;,
      id: &amp;#39;&amp;#39;,
      parent: [Object],
      created_time: &amp;#39;2024-05-23T02:48:00.000Z&amp;#39;,
      last_edited_time: &amp;#39;2024-05-23T10:26:00.000Z&amp;#39;,
      created_by: [Object],
      last_edited_by: [Object],
      has_children: false,
      archived: false,
      in_trash: false,
      type: &amp;#39;code&amp;#39;,
      code: [Object]
    },
    {
      object: &amp;#39;block&amp;#39;,
      id: &amp;#39;&amp;#39;,
      parent: [Object],
      created_time: &amp;#39;2024-05-23T10:27:00.000Z&amp;#39;,
      last_edited_time: &amp;#39;2024-05-23T10:27:00.000Z&amp;#39;,
      created_by: [Object],
      last_edited_by: [Object],
      has_children: false,
      archived: false,
      in_trash: false,
      type: &amp;#39;paragraph&amp;#39;,
      paragraph: [Object]
    }
  ],
  next_cursor: null,
  has_more: false,
  type: &amp;#39;block&amp;#39;,
  block: {},
  request_id: &amp;#39;&amp;#39;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;더 이상 알고싶지 않아졌다.&lt;/p&gt;
&lt;p&gt;그 페이지에 어떤 내용이 쓰여질지 모르는데 paragraph, code, heading 등을 직접 정리해주고 싶지 않았다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;notion-to-md&lt;/code&gt; 를 사용한다.&lt;/p&gt;
&lt;p&gt;우리는 저 데이터를 볼 필요가 없이 이게 마크다운으로 만들어 줄 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const mdBlocks = await n2m.pageToMarkdown(pageId);
const mdContent = n2m.toMarkdownString(mdBlocks).parent;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;안타깝게도 제목 등 properties들은 리턴해주지 않는다.&lt;/p&gt;
&lt;p&gt;그래서 다시 검색해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const page = (await notion.pages.retrieve({ page_id: pageId })) as any;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;예쁘게(?) 보여주기&lt;/h2&gt;
&lt;p&gt;마크다운을 그대로 보여줄 수 없으므로 HTML 코드로 만들어준다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;marked&lt;/code&gt; 를 사용하기로 했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;const [content, setContent] = useState&amp;lt;string&amp;gt;(&amp;#39;&amp;#39;);

useEffect(() =&amp;gt; {
    const convertMarkdownToHtml = async () =&amp;gt; {
        const htmlString = await marked(notice.mdContent);

        setContent(htmlString);
    };

    convertMarkdownToHtml();
}, []);

return (
    &amp;lt;div dangerouslySetInnerHTML={{ __html: content }} /&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tailwind를 사용하고 있어서 그런지 스타일이 적용되지 않아서 css 파일도 추가해주었다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;.markdown-body {
  font-family: Arial, sans-serif;
  line-height: 1.6;
  padding: 20px;
}

.markdown-body h1 {
  font-size: 2em;
  margin-bottom: 0.5em;
}

.markdown-body h2 {
  font-size: 1.75em;
  margin-bottom: 0.5em;
}

.markdown-body h3 {
  font-size: 1.5em;
  margin-bottom: 0.5em;
}

.markdown-body h4 {
  font-size: 1.25em;
  margin-bottom: 0.5em;
}

.markdown-body p {
  margin-bottom: 0.8em;
}

.markdown-body ul,
.markdown-body ol {
  margin: 0 0 1em 1.5em;
}

.markdown-body li {
  margin-bottom: 0.5em;
}

.markdown-body a {
  color: #0366d6;
  text-decoration: none;
}

.markdown-body a:hover {
  text-decoration: underline;
}

.markdown-body code {
  background-color: #f6f8fa;
  border-radius: 3px;
  font-size: 85%;
  padding: 0.2em 0.4em;
}

.markdown-body pre {
  background-color: #f6f8fa;
  border-radius: 3px;
  padding: 1em;
  overflow: auto;
}

.markdown-body blockquote {
  border-left: 0.25em solid #dfe2e5;
  padding: 0.5em 1em;
  color: #6a737d;
  background-color: #f6f8fa;
}

.markdown-body table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 1em;
}

.markdown-body th,
.markdown-body td {
  border: 1px solid #dfe2e5;
  padding: 0.5em 1em;
}

.markdown-body th {
  background-color: #f6f8fa;
  font-weight: bold;
}

.markdown-body img {
  max-width: 100%;
  height: auto;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;완성&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buklod/btsI9etaNap/I9Kc4haslOIZqY9SPHENnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buklod/btsI9etaNap/I9Kc4haslOIZqY9SPHENnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buklod/btsI9etaNap/I9Kc4haslOIZqY9SPHENnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbuklod%2FbtsI9etaNap%2FI9Kc4haslOIZqY9SPHENnk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z891a/btsI82Gw3b3/GixmzgfTpNXkUe2fS8ggAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z891a/btsI82Gw3b3/GixmzgfTpNXkUe2fS8ggAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z891a/btsI82Gw3b3/GixmzgfTpNXkUe2fS8ggAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ891a%2FbtsI82Gw3b3%2FGixmzgfTpNXkUe2fS8ggAk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blN5r4/btsI7kIDCVD/iQZb2q2iMPkgNDSReYuF40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blN5r4/btsI7kIDCVD/iQZb2q2iMPkgNDSReYuF40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blN5r4/btsI7kIDCVD/iQZb2q2iMPkgNDSReYuF40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblN5r4%2FbtsI7kIDCVD%2FiQZb2q2iMPkgNDSReYuF40%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;사용 가능성&lt;/h1&gt;
&lt;p&gt;백오피스에 별다른 구현 없이 노션 페이지를 그대로 읽어 오는 것은 참 매력적인 방식인 것 같다. 속도 면에서도 아쉬운 부분은 없었고, 사용자가 익숙한 툴로 쉽게 입력 가능하다는게 제일 큰 장점으로 생각된다. 또한 쉬운 스타일 변경도 장점이다.&lt;/p&gt;
&lt;p&gt;그러나 단점도 느껴졌다.&lt;/p&gt;
&lt;h2&gt;단점&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;사용자에게 조심성을 바라야 한다.&lt;ul&gt;
&lt;li&gt;property가 바뀌면 바로 에러가 난다.&lt;ul&gt;
&lt;li&gt;데이터베이스 테이블이라고 생각하면 에러가 나는 것이 당연하지만, SQL처럼 어떠한 명령문을 쳐서 바뀌는게 아니기 때문에 너무 손쉽게 바뀔 수 있는 위험성이 있다.&lt;/li&gt;
&lt;li&gt;property 변경은 꼭 개발부서에 확인 후 변경이 필요하다는 인지가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;수정 중인 글인지 확인 할 수 없다.&lt;ul&gt;
&lt;li&gt;찾아본 바로는 노션의 자동 저장 기능이 가장 대표적인 기능이기 때문에 그걸 끄는 방법은 제공하지 않는다고 한다.&lt;/li&gt;
&lt;li&gt;Formula 함수를 이용 할 수는 있으나 완벽하게 원하는 함수를 구현 할 수 없다.&lt;ul&gt;
&lt;li&gt;예) &lt;code&gt;if(now() &amp;gt; prop(&amp;quot;노출일자&amp;quot;) and prop(&amp;quot;상태&amp;quot;) == &amp;quot;노출&amp;quot;, prop(&amp;quot;Last edited time&amp;quot;).dateAdd(1, &amp;quot;hours&amp;quot;) &amp;gt; now(), false)&lt;/code&gt; : 수정 후 1시간 뒤 노출 등..&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;한 쿼리에 MAX LIMIT가 있다.&lt;ul&gt;
&lt;li&gt;page_size에 undefined를 넣으면 100개까지의 리스트를 가져온다고 한다.&lt;/li&gt;
&lt;li&gt;next_cursor등으로 페이지네이션을 구현 할 수 있으나, API 상에서 이전 페이지 데이터를 불러오기는 지원하지 않아 따로 구현이 필요하다.&lt;ul&gt;
&lt;li&gt;그러나 3페이지에서 5페이지로 이동이나 7페이지에서 4페이지의 이동 등은 구현하기 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;마크다운이나 HTML을 알아야 완벽히 원하는 페이지를 만들 수 있다.&lt;ul&gt;
&lt;li&gt;글자 색상 변경 등은 마크다운에서 지원하지 않으므로 HTML으로 작성해야 한다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;span style=&amp;quot;color:red;&amp;quot;&amp;gt;글자&amp;lt;/span&amp;gt;&lt;/code&gt; 등..&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다중 엔터 등..&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 단점들을 이겨낼 수 있다면 사용 할 수 있을 것 같다.&lt;/p&gt;
&lt;p&gt;많은 사람들이 사용할 예정인 공지사항보다는 일부 사람들만 사용하고 더 조심스럽게 다룰 내용같은 약관 등에 적합하다고 느꼈다.&lt;/p&gt;</description>
      <category>TIL</category>
      <category>notionapi</category>
      <category>노션</category>
      <category>노션api</category>
      <author>indeeah</author>
      <guid isPermaLink="true">https://indeeah.tistory.com/59</guid>
      <comments>https://indeeah.tistory.com/59#entry59comment</comments>
      <pubDate>Mon, 19 Aug 2024 17:36:51 +0900</pubDate>
    </item>
    <item>
      <title>token을 관리해보자!</title>
      <link>https://indeeah.tistory.com/58</link>
      <description>&lt;aside&gt;
  우리는 백단을 Nest.js로, 프론트단을 Next.js로 사용하고 있다.
그런데 Access Token을 Nest.js에서 cookie로 만들려고 했더니, Nest.js → Next.js(api) → Next.js(front)로 가기 때문에 생성을 할 수 없었다.
그래서 다른 방법을 생각해 보는데…

&lt;/aside&gt;

&lt;p&gt;위에서 말한대로 Next.js에도 SSR이 있기 때문에 그걸 통해서 Nest.js와 통신을 하려고 작업 중이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;로그인 시 순서는&lt;ul&gt;
&lt;li&gt;Next.js Front → Next.js Back → Nest.js → Next.js Back → Next.js Front의 순서로 가고 있다.&lt;/li&gt;
&lt;li&gt;Token도 이와 동일한 순서로 가서 Nest.js에서 만들어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;그래서!&lt;/h3&gt;
&lt;p&gt;Nest.js에서 Access Token과 Refresh Token을 모두 만들고, 보안이 중요한 Refresh Token은 Next.js Back에서 쿠키로 만들어주고, Access Token은 Local Storage에 저장하기로 했다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Next.js Front에서 소셜 로그인 요청 &lt;code&gt;page.ts&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next.js Back에서 Nest.js로 요청 &lt;code&gt;route.ts&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nest.js에서 소셜 로그인&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;토큰 생성&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; this.jwtService.sign({ id }, { secret });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Access Token과 Refresh Token 모두 생성해준다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;아직 Refresh Token을 저장할 DB는 만들어주지 않았다. (추후 구현 예정)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next.js에서 Token 받아오기&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; const response = NextResponse.json({ token });
 response.cookies.set(&amp;#39;refreshToken&amp;#39;, refreshToken, {
   httpOnly: true,
   path: &amp;#39;/&amp;#39;,
   sameSite: &amp;#39;strict&amp;#39;,
   secure: true,
 });

 return response&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 여기서 참 많이 헤맸다..&lt;/p&gt;
&lt;p&gt; &lt;code&gt;NextResponse.json({ token });&lt;/code&gt; 은 Front까지 갈 녀석이고,&lt;/p&gt;
&lt;p&gt; 아래는 쿠키를 굽는 방법이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;받아온 Access Token을 Local Storage에 저장&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; const { token } = data;

 LocalStorage.setItem(&amp;#39;AccessToken&amp;#39;, token);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; LocalStorage에 저장하려면 useEffect를 쓰는 방법과, Class를 만들어서 사용하는 방법이 있는데 나는 Class를 사용하는 방법으로 구현하였다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; class LocalStorage {
   static setItem(key: string, value: string) {
     if (typeof window === &amp;#39;undefined&amp;#39;) return;
     localStorage.setItem(key, value);
   }

   static getItem(key: string) {
     if (typeof window === &amp;#39;undefined&amp;#39;) return null;
     return localStorage.getItem(key);
   }

   static removeItem(key: string) {
     if (typeof window === &amp;#39;undefined&amp;#39;) return;
     localStorage.removeItem(key);
   }
 }

 export default LocalStorage;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; &lt;em&gt;이렇게 하지 않으면 rendering되기 전에 Local Storage에 저장하려고 하기 때문에 에러가 발생한다!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt; 나중에 꺼내서 쓸 때는 &lt;code&gt;const accessToken = LocalStorage.getItem(&amp;#39;AccessToken&amp;#39;);&lt;/code&gt; 이렇게 쓸 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL</category>
      <category>nest.js</category>
      <category>next.js</category>
      <category>Token</category>
      <author>indeeah</author>
      <guid isPermaLink="true">https://indeeah.tistory.com/58</guid>
      <comments>https://indeeah.tistory.com/58#entry58comment</comments>
      <pubDate>Mon, 19 Aug 2024 17:31:52 +0900</pubDate>
    </item>
    <item>
      <title>x-api-key authorization</title>
      <link>https://indeeah.tistory.com/57</link>
      <description>&lt;ul&gt;
&lt;li&gt;추가 패키지&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;@nestjs/passport
@types/passport
passport
passport-headerapikey&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;참고 문서&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@alpercitak/nest-js-authenticate-with-both-api-key-and-jwt-4a22bf7b3049&quot;&gt;https://medium.com/@alpercitak/nest-js-authenticate-with-both-api-key-and-jwt-4a22bf7b3049&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@Dee_Mayoor/apikey-authentication-for-nestjs-using-passport-js-6db467fc31f7&quot;&gt;https://medium.com/@Dee_Mayoor/apikey-authentication-for-nestjs-using-passport-js-6db467fc31f7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.stewright.me/2021/03/add-header-api-key-to-nestjs-rest-api/&quot;&gt;https://www.stewright.me/2021/03/add-header-api-key-to-nestjs-rest-api/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;authorization을 각각 &lt;code&gt;UseGuard&lt;/code&gt; 를 이용해 컨트롤러에 붙이고 싶지 않고, 미들웨어를 이용해서 전역으로 붙이고 싶었다.&lt;/p&gt;
&lt;p&gt;auth.middleware.ts 파일을 만들어 준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import {
   Injectable,
   NestMiddleware,
   UnauthorizedException,
 } from &amp;#39;@nestjs/common&amp;#39;;

 import passport from &amp;#39;passport&amp;#39;;

 @Injectable()
 export class AuthMiddleware implements NestMiddleware {
   use(req: Request, res: Response, next: () =&amp;gt; void) {
     passport.authenticate(&amp;#39;headerapikey&amp;#39;, { session: false }, value =&amp;gt; {
       if (value) next();
       else throw new UnauthorizedException();
     })(req, res, next);
   }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;app.module.ts에 미들웨어 세팅을 해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;export class AppModule {
   configure(consumer) {
     consumer.apply(AuthMiddleware).forRoutes(&amp;#39;*&amp;#39;);
   }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;api-key.strategy.ts 파일을 생성해준다. (힘들어..)&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { Injectable, UnauthorizedException } from &amp;#39;@nestjs/common&amp;#39;;
import { PassportStrategy } from &amp;#39;@nestjs/passport&amp;#39;;

import { HeaderAPIKeyStrategy } from &amp;#39;passport-headerapikey&amp;#39;;

@Injectable()
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) {
 constructor() {
   super({ header: &amp;#39;x-api-key&amp;#39;, prefix: &amp;#39;&amp;#39; }, true, (apikey, done) =&amp;gt; {
     const API_KEYS = [&amp;#39;liche_lomad_api_key&amp;#39;];
     const isValid = API_KEYS.find(key =&amp;gt; key === apikey);
     if (!isValid) {
       return done(false);
     }
     return done(true);
   });
 }
}  &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;많이 헤맸던 부분은 &lt;code&gt;header: &amp;#39;x-api-key&amp;#39;&lt;/code&gt; 부분이었다.&lt;/p&gt;
&lt;p&gt;나는 키 이름을 &lt;code&gt;x-api-key&lt;/code&gt; 로 하고싶은데, 예제에 있는 것 똑같이 &lt;code&gt;apiKey&lt;/code&gt; 로 되어있었다.&lt;/p&gt;
&lt;p&gt;또한 위 참고 예제들에서 super→validate사용을 하라고 되어있는데, 똑같이 따라하면 서버가 죽어버렸다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// 401에러를 반환하지만 서버가 죽는 코드
async validate(apiKey: string, done: (error: Error | boolean, data) =&amp;gt; void) {
  const API_KEYS = [&amp;#39;liche_lomad_api_key&amp;#39;];

  const isValid = API_KEYS.find(key =&amp;gt; key === apiKey);
  if (!isValid) {
    return done(new UnauthorizedException(), null);
  }

  return done(null, true);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;done에도 첫번째 인자에 null을 넣으면 서버가 죽었다.&lt;/p&gt;
&lt;p&gt;그래서 위에 super에 넣어서 작업했더니 서버도 죽지 않고, 원하는 x-api-key를 받았을 때만 데이터를 주게 되었다!&lt;/p&gt;</description>
      <category>TIL</category>
      <category>x-api-key authorization</category>
      <author>indeeah</author>
      <guid isPermaLink="true">https://indeeah.tistory.com/57</guid>
      <comments>https://indeeah.tistory.com/57#entry57comment</comments>
      <pubDate>Mon, 19 Aug 2024 17:20:18 +0900</pubDate>
    </item>
    <item>
      <title>supertest시 :id가 500으로 떨어진다면?</title>
      <link>https://indeeah.tistory.com/56</link>
      <description>&lt;p&gt;먼저 module별로 테스트 하는 방법은 찾아내지 못했다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;app.module.ts&lt;/code&gt; 를 테스트 하기로 했고, 신기하게도 테스트 돌리니 전에 get에서 find로 함수명을 바꿨는데, resolver에는 적용되지 않았던 부분을 잡아줬다!&lt;/p&gt;
&lt;p&gt;블로그에는 app을 &lt;code&gt;INestApplication&lt;/code&gt; 를 이용해서 테스트 하라고 하지만, 그럴 필요 없다.&lt;/p&gt;
&lt;p&gt;supertest도 &lt;code&gt;import * as request from &amp;#39;supertest&amp;#39;&lt;/code&gt; 로 하라고 하지만 에러가 난다. (아마 버전 차이일 수도..)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;import request from &amp;#39;supertest&amp;#39;&lt;/code&gt; 로 변경해주니 에러가 나지 않았다.&lt;/p&gt;
&lt;p&gt;그래서 import 해주어야 될 것은 아래와 같다&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { Test } from &amp;#39;@nestjs/testing&amp;#39;;

import request from &amp;#39;supertest&amp;#39;;

import { AppModule } from &amp;#39;./app.module&amp;#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그리고 테스트 전과 테스트 후 처리를 위해 다음과 같은 코드를 추가해 준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;beforeAll(async () =&amp;gt; {
  const module = await Test.createTestingModule({
    imports: [AppModule],
  }).compile();

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

afterAll(async () =&amp;gt; {
  await app.close();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 세팅 하고 테스트 코드를 짜면 예쁘게 200으로 떨어지는 테스트 코드를 볼 수 있을 것이다.&lt;/p&gt;
&lt;p&gt;그런데 :id값을 받아야 할 때는 어떨까?&lt;/p&gt;
&lt;p&gt;아무리 &lt;code&gt;return request(app.getHttpServer()).get(&amp;#39;/brands/1&amp;#39;).expect(200)&lt;/code&gt; 을 해줘도 500 에러가 예상된다는 결과를 받았다.&lt;/p&gt;
&lt;p&gt;그래서 id값을 숫자로 인식하지 못하는 것 아닐까? 라는 생각에 도달하게 되었고 찾아보니 controller에서 추가해줘야 하는 부분이 있었다.&lt;/p&gt;
&lt;p&gt;nest.js가 예쁘게 숫자로 파싱 해주지만, 테스트코드는 그렇지 못하나 보다.&lt;/p&gt;
&lt;p&gt;아래와 같이 controller를 변경해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;async findOne(@Param(&amp;#39;id&amp;#39;, ParseIntPipe) id: number) {&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ParseIntPipe&lt;/code&gt; 는 받은 id값을 int로 파싱해주는 역할을 한다고 한다.&lt;/p&gt;
&lt;p&gt;이로써 우리는 500에러 받는 법과, 200 성공 받는 법을 둘 다 알게 되었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2023.08.04&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;POST, PUT, DELETE를 하면 실제 DB가 변경된다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 테스트 한 후에 롤백을 해주어야 되는데 그 부분은 찾지 못해서 더 공부가 필요할 것 같다.&lt;/p&gt;
&lt;p&gt;(일단 &lt;code&gt;xit&lt;/code&gt; 으로 테스트 skip 해두었다.)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2023.08.05&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rollback하는 방법을 못찾아서 결국에는&lt;/p&gt;
&lt;p&gt;GET → POST → PUT → DELETE 순으로 테스트 하고, POST된 마지막 값을 PUT과 DELETE에서 삭제하는 방향으로 테스트를 진행했다.&lt;/p&gt;</description>
      <category>TIL</category>
      <category>supertest</category>
      <author>indeeah</author>
      <guid isPermaLink="true">https://indeeah.tistory.com/56</guid>
      <comments>https://indeeah.tistory.com/56#entry56comment</comments>
      <pubDate>Mon, 19 Aug 2024 17:18:52 +0900</pubDate>
    </item>
    <item>
      <title>mysql typeorm으로 한방에 generate하기</title>
      <link>https://indeeah.tistory.com/55</link>
      <description>&lt;aside&gt;
  lomad2.5-api로 toy project를 하는 중 typeorm을 nest.js에서 많이 쓰는 것을 알게되었고, typeorm을 쓰려고 했다.

&lt;p&gt;블로그에 적혀있는 typeorm사용법은 entity를 만드는 것 부터 적혀있는데, 테이블이 db에 자동으로 생성된다.&lt;/p&gt;
&lt;p&gt;생성하는 기능은 좋으나, 현재는 이미 있는 모든 테이블들을 generate하고 싶었다.&lt;br&gt;(나는 생성할 테이블이 현재 없기에..)&lt;/p&gt;
&lt;/aside&gt;

&lt;p&gt;그래서 구글링해본 결과&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;entity에 &lt;code&gt;synchronize: false&lt;/code&gt; 를 해준다.&lt;/p&gt;
&lt;p&gt; 이는 이미 완성된 테이블에 연결 하는 경우, 모든 컬럼을 다 정의해주어야 한다고 한다.&lt;/p&gt;
&lt;p&gt; 하지만 users 테이블만 봐도 컬럼이 어마어마 하기 때문에 할 자신이 들지 않았다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; import { Entity, Column, PrimaryGeneratedColumn, PrimayColumn } from &amp;#39;typeorm&amp;#39;;

 @Entity({ name: &amp;#39;users&amp;#39;, synchronize: false })
 export class Users {
     @PrimaryGeneratedColumn()
     id: number;

     @PrimaryColumn()
     name: string;

     @Column()

 ...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 그래서 아예 배제하고 다른 방법을 찾아보았다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;typeorm-model-generator 사용&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; npm i -g typeorm-model-generator&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 로 설치 할 수 있다.&lt;/p&gt;
 &lt;aside&gt;
   -h : host, 연결할 서버 ip
 -d : database, 연결할 db 이름
 -p : port, 연결할 서버 port
 -u : user, db 사용자 id
 -x : db 사용자 패스워드
 -e : engine, db 종류 (mssql, postgres, mysql, mariadb, oracle, sqlite)
 -o : out, entity 파일 생성할 폴더 경로

 &lt;/aside&gt;

&lt;p&gt; 위와 같은 명령어가 필요하고,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; typeorm-model-generator -h server_ip -d database_name -p server_port -u server_id -x server_pw -e db_종류 -o entity_생성할_folder_경로&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 예제는 위와 같다.&lt;/p&gt;
&lt;p&gt; 하지만 우리의 DB패스워드에는 특수문자가 들어간다는 점!&lt;/p&gt;
&lt;p&gt; 그냥 저대로 치면&lt;/p&gt;
&lt;p&gt; &lt;code&gt;@# event not found&lt;/code&gt; 라는 에러를 맞이할 수 있을 것이다.&lt;/p&gt;
&lt;p&gt; 큰 따옴표로 묶어도 마찬가지고, &lt;code&gt;\@\#&lt;/code&gt; 로 해도 마찬가지고, &lt;code&gt;set -H&lt;/code&gt; 명령어 후에 쳐도 마찬가지인 결과를 얻을 수 있다.&lt;/p&gt;
&lt;h3&gt;결론&lt;/h3&gt;
&lt;p&gt; 작은 따옴표로 묶자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt; typeorm-model-generator \
   -h dev.lomad.kr \
   -p 43306 \
   -d dev \
   -u dev \
   -x &amp;#39;Qwe123!@#&amp;#39; \
   -e mysql \
   -o ./src/dtos \
   --noConfig \
   --generate-dto&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; 작은따옴표로 묶은 위 명령어로 순식간에 typeorm으로 generate 할 수 있었다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL</category>
      <category>mysql</category>
      <category>TypeOrm</category>
      <author>indeeah</author>
      <guid isPermaLink="true">https://indeeah.tistory.com/55</guid>
      <comments>https://indeeah.tistory.com/55#entry55comment</comments>
      <pubDate>Mon, 19 Aug 2024 17:18:17 +0900</pubDate>
    </item>
    <item>
      <title>구글 오늘 뭐먹지 챗봇 만들기</title>
      <link>https://indeeah.tistory.com/54</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://developers.google.com/chat/api/guides/message-formats/cards?hl=ko&quot;&gt;https://developers.google.com/chat/api/guides/message-formats/cards?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;{
  &amp;quot;cardsV2&amp;quot;: [
    {
      &amp;quot;cardId&amp;quot;: &amp;quot;unique-card-id&amp;quot;,
      &amp;quot;card&amp;quot;: {
        &amp;quot;header&amp;quot;: {
          &amp;quot;title&amp;quot;: &amp;quot;Sasha&amp;quot;,
          &amp;quot;subtitle&amp;quot;: &amp;quot;Software Engineer&amp;quot;,
          &amp;quot;imageUrl&amp;quot;:
          &amp;quot;https://developers.google.com/chat/images/quickstart-app-avatar.png&amp;quot;,
          &amp;quot;imageType&amp;quot;: &amp;quot;CIRCLE&amp;quot;,
          &amp;quot;imageAltText&amp;quot;: &amp;quot;Avatar for Sasha&amp;quot;,
        },
        &amp;quot;sections&amp;quot;: [
          {
            &amp;quot;header&amp;quot;: &amp;quot;Contact Info&amp;quot;,
            &amp;quot;collapsible&amp;quot;: true,
            &amp;quot;uncollapsibleWidgetsCount&amp;quot;: 1,
            &amp;quot;widgets&amp;quot;: [
              {
                &amp;quot;decoratedText&amp;quot;: {
                  &amp;quot;startIcon&amp;quot;: {
                    &amp;quot;knownIcon&amp;quot;: &amp;quot;EMAIL&amp;quot;,
                  },
                  &amp;quot;text&amp;quot;: &amp;quot;sasha@example.com&amp;quot;,
                }
              },
              {
                &amp;quot;decoratedText&amp;quot;: {
                  &amp;quot;startIcon&amp;quot;: {
                    &amp;quot;knownIcon&amp;quot;: &amp;quot;PERSON&amp;quot;,
                  },
                  &amp;quot;text&amp;quot;: &amp;quot;&amp;lt;font color=\&amp;quot;#80e27e\&amp;quot;&amp;gt;Online&amp;lt;/font&amp;gt;&amp;quot;,
                },
              },
              {
                &amp;quot;decoratedText&amp;quot;: {
                  &amp;quot;startIcon&amp;quot;: {
                    &amp;quot;knownIcon&amp;quot;: &amp;quot;PHONE&amp;quot;,
                  },
                  &amp;quot;text&amp;quot;: &amp;quot;+1 (555) 555-1234&amp;quot;,
                }
              },
              {
                &amp;quot;buttonList&amp;quot;: {
                  &amp;quot;buttons&amp;quot;: [
                    {
                      &amp;quot;text&amp;quot;: &amp;quot;Share&amp;quot;,
                      &amp;quot;onClick&amp;quot;: {
                        &amp;quot;openLink&amp;quot;: {
                          &amp;quot;url&amp;quot;: &amp;quot;https://example.com/share&amp;quot;,
                        }
                      }
                    },
                    {
                      &amp;quot;text&amp;quot;: &amp;quot;Edit&amp;quot;,
                      &amp;quot;onClick&amp;quot;: {
                        &amp;quot;action&amp;quot;: {
                          &amp;quot;function&amp;quot;: &amp;quot;goToView&amp;quot;,
                          &amp;quot;parameters&amp;quot;: [
                            {
                              &amp;quot;key&amp;quot;: &amp;quot;viewType&amp;quot;,
                              &amp;quot;value&amp;quot;: &amp;quot;EDIT&amp;quot;,
                            }
                          ],
                        }
                      }
                    },
                  ],
                }
              },
            ],
          },
        ],
      },
    }
  ],
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런식으로 카드를 만들 수 있다.&lt;/p&gt;
&lt;p&gt;버튼은 현재 필요 없어 보이고, playground에서 response가 200이 오면 메뉴 추천을 해주는 방식으로 구현&lt;/p&gt;
&lt;p&gt;(사실 onClick.action을 어떻게 추가해야될 지 잘 모르겠음)&lt;/p&gt;
&lt;p&gt;슬래쉬(’/’)도 추가하려면 google cloud console에서 api를 추가해서 해야 하는 것으로 보여&lt;/p&gt;
&lt;p&gt;cron을 이용해서 스케쥴링 하려고 한다.&lt;/p&gt;
&lt;p&gt;cron이 한국 시간인지 확인하기 위해 테스트 해본 결과 한국 시간으로 잘 나오는 것을 확인.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import axios from &amp;quot;axios&amp;quot;;
import { CronJob } from &amp;quot;cron&amp;quot;;

async function sendTestCard() {
  const url =
    &amp;quot;https://chat.googleapis.com/v1/spaces/AAAA7b2njxQ/messages?key=AIzaSyDdI0hCZtE6vySjMm-WEfRq3CPzqKqqsHI&amp;amp;token=MswzAG2Rz24qXe0N_NDFhahCZkJBdvZQkoWo-j4Cxyk&amp;quot;;

  const header = {
    &amp;quot;Content-Type&amp;quot;: &amp;quot;application/json&amp;quot;,
  };

  const message = {
    cardsV2: [
      [
        {
          cardId: &amp;quot;test&amp;quot;,
          card: {
            header: {
              title: &amp;quot;오늘 뭐먹지?&amp;quot;,
              subtitle: &amp;quot;먹을 것을 골라줘라 BOT&amp;quot;,
            },
          },
        },
      ],
    ],
  };

    // 카드형 메세지는 text가 아니라 message로 보낸다.
  const response = await axios.post(url, message, header);

  if (response.status === 200) {
    const menu = [
      &amp;quot;다옴&amp;quot;,
      &amp;quot;공복식당&amp;quot;,
      &amp;quot;두부두루치기&amp;quot;,
      &amp;quot;한양돈까스&amp;quot;,
      &amp;quot;명동칼국수&amp;quot;,
      &amp;quot;오제볶음&amp;quot;,
      &amp;quot;맥도날드&amp;quot;,
      &amp;quot;쌀국수&amp;quot;,
      &amp;quot;중식&amp;quot;,
      &amp;quot;콩나물국밥&amp;quot;,
    ];
    const message = menu[Math.floor(Math.random() * menu.length)];
    await axios.post(url, { text: message }, header);
  }
}

const job = new CronJob(&amp;quot;30 11 * * 1-5&amp;quot;, sendTestCard); // 평일 오전 11시 30분
job.start();
// sendTestCard();
console.log(&amp;quot;스케쥴러가 시작됐습니다.&amp;quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;response에 따라서 다시 메세지를 구현하는 것으로 작업했으나 굳이 오늘 뭐먹지? 카드와 메뉴 추천 카드를 분리 시킬 필요는 없어 보여서 수정이 필요함.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;menu[Math.floor(Math.random() * menu.length)];&lt;/code&gt; 랜덤하게 메뉴를 가져오게 하는 방법은 이렇게 사용&lt;/p&gt;</description>
      <category>TIL</category>
      <category>google chat</category>
      <category>구글챗</category>
      <category>구글챗봇</category>
      <author>indeeah</author>
      <guid isPermaLink="true">https://indeeah.tistory.com/54</guid>
      <comments>https://indeeah.tistory.com/54#entry54comment</comments>
      <pubDate>Mon, 19 Aug 2024 17:17:43 +0900</pubDate>
    </item>
    <item>
      <title>E2E cypress 테스트 코드 짜기</title>
      <link>https://indeeah.tistory.com/53</link>
      <description>&lt;p&gt;cypress를 이용하여 login test코드를 짜보았다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cypress install&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;npm i cypress&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;문서나 예제에는 dev로 install하라고 써져 있지만 dev로 하면 import시 에러가 난다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;run&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// package.json
&amp;quot;test:cypress&amp;quot;: &amp;quot;cypress open&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;cypress open을 하면 멋지게 열린다!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cypress는 Mocha 기반이고, admin3는 Jest를 쓰고 있어 config파일을 설정해주지 않으면 jest가 동작하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;// route의 tsconfig.json
&amp;quot;types&amp;quot;: [&amp;quot;jest&amp;quot;, &amp;quot;@types/testing-library__jest-dom&amp;quot;],  // compilerOptions에 추가
&amp;quot;include&amp;quot;: [&amp;quot;next-env.d.ts&amp;quot;, &amp;quot;**/*.ts&amp;quot;, &amp;quot;**/*.tsx&amp;quot;, &amp;quot;next-additional.d.ts&amp;quot;],  // next-additional.d.ts는 왜 추가하는지 확인 필요;;
&amp;quot;exclude&amp;quot;: [&amp;quot;node_modules&amp;quot;, &amp;quot;dist&amp;quot;, &amp;quot;cypress&amp;quot;, &amp;quot;cypress.config.ts&amp;quot;],  // route에 cypress 불포함
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;cypress.config.ts파일 세팅&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { defineConfig } from &amp;#39;cypress&amp;#39;;

export default defineConfig({
  e2e: {
    setupNodeEvents() {},
    baseUrl: &amp;#39;http://localhost:4002/&amp;#39;,
    supportFile: false,
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;e2e에 대한 설정을 한다.&lt;/p&gt;
&lt;p&gt;supportFile을 쓰지 않았기 때문에 &lt;code&gt;supportFile: false&lt;/code&gt; 를 설정해준다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;baseUrl&lt;/code&gt; 의 마지막에 &lt;code&gt;/&lt;/code&gt; 를 붙이지 않으면 &lt;code&gt;Cypress.config().baseUrl&lt;/code&gt; 시 timeout이 발생한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cypress/e2e/login.cy.ts&lt;ul&gt;
&lt;li&gt;이전에는 integration을 썼지만 e2e로 변경되었다.&lt;/li&gt;
&lt;li&gt;이전에는 spec.ts를 썼지만 cy.ts로 변경되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;기본 문법&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cy.get(&amp;#39;&amp;#39;)&lt;/code&gt; 으로 attribute를 가져온다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cy.visit(url)&lt;/code&gt; 으로 첫 화면으로 이동한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;value가 없는지 확인&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cy.get(&amp;#39;&amp;#39;).should(&amp;#39;to.be.empty&amp;#39;)&lt;/code&gt; 처럼 쓸 수 있으나&lt;br&gt;&lt;code&gt;cy.get(’’).should((input) ⇒ expect(input.val()).to.be.empty);&lt;/code&gt; 로 작성하였다.&lt;/li&gt;
&lt;li&gt;이전 코드처럼 작성하면 value를 잘 검사하지 못하는 듯 하여 후자로 작성하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;value가 있는지 확인&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cy.get(&amp;#39;&amp;#39;).type(&amp;#39;&amp;#39;)&lt;/code&gt; 또는 &lt;code&gt;cy.get(&amp;#39;&amp;#39;).invoke(&amp;#39;val&amp;#39;, &amp;#39;&amp;#39;)&lt;/code&gt; 로 작성할 수 있으나 후자는 이전 설정한 value들이 사라지고 invoke로 마지막에 넣은 value만 받는 것처럼 보여 전자로 작성하였다.&lt;/li&gt;
&lt;li&gt;value가 있는지 확인 할 때도 문서에는 &lt;code&gt;cy.get(&amp;#39;&amp;#39;).should(&amp;#39;to.be.not.empty&amp;#39;)&lt;/code&gt; 로 작성하라고 나와있지만 계속해서 undefined로 나오는 문제가 생겼다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cy.get(&amp;#39;&amp;#39;).should((input) =&amp;gt; expect(input.val()).to.not.be.empty)&lt;/code&gt; 로 작성하여 해결하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;url로 이동하는지 확인&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cy.url().should(&amp;#39;equal&amp;#39;, url)&lt;/code&gt; 로 작성 할 수 있으며, baseUrl로 가는지 확인 하기 위해서는 &lt;code&gt;cy.url().should(&amp;#39;equal&amp;#39;, Cypress.config().baseUrl)&lt;/code&gt; 로 따로 url을 다시 작성 할 필요 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;log 확인&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cy.log()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>CYPRESS</category>
      <category>e2e</category>
      <author>indeeah</author>
      <guid isPermaLink="true">https://indeeah.tistory.com/53</guid>
      <comments>https://indeeah.tistory.com/53#entry53comment</comments>
      <pubDate>Mon, 19 Aug 2024 17:16:55 +0900</pubDate>
    </item>
  </channel>
</rss>