[컴][웹] CORS 설정 / 서버, client

Cross-Origin Resource Sharing /

CORS 설정 / 서버, client


   +-----------------------+                                                            +-----------------------+
   |     [client]          |       withCredential : true                                |                       |
   |                       | +------------------------------------------------------->  |     [server]          |
   |                       | <--------------------------------------------------------  |                       |
   | http://localhost:8090 |    Access-Control-Allow-Origin: http://localhost:8090      |                       |
   |                       |    Access-Control-Allow-Credentials: true                  |                       |
   |                       |                                                            | http://localhost:8091 |
   +-----------------------+                                                            +-----------------------+                         

서버쪽 설정

spring 의 경우:

// ref: https://www.baeldung.com/spring-cors#1-javaconfig
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
            .addMapping("/**")
            .allowedOrigins("http://localhost:8090")
            .allowCredentials(true)
            .allowedMethods("*");
    }
    ...
}

management.endpoints.web.cors.allowed-origins :

client code

withCredentials 가 set 돼야 credential 을 설정하는 cookie 나 credential 등이 request 시점에 같이 넘어가게 된다.

withCredential를 true로 set 하면, 다른 domain 으로 요청하는 request 에 cookie 가 설정되고, 그로 인해 응답을 받을때 3rd party cookie 들을 얻게 된다. 이렇게 얻은 cookie 들은 여전히 same-origin policy 를 유지한다. 그런 이유로 document.cookie 또는 ’response headers’로부터 접근할 수 없다.[see also. 2]

XMLHttpRequest

// ref: [XMLHttpRequest.withCredentials - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/api', true);
xhr.withCredentials = true;
xhr.setRequestHeader('Content-Type', 'application/json');

xhr.addEventListener('load', function() {
  if (xhr.status === 200) {
    console.log(xhr.responseText);
  } else {
    console.error('Error:', xhr.statusText);
  }
});

xhr.send();

fetch api

from, ref. 2

fetch("https://example.com", {
  credentials: "include",
  method: "POST", // or 'PUT'
  headers: {
    "Content-Type": "application/json",
  }
})
  .then((response) => response.json())
  .then((data) => {
    console.log("Success:", data);
  })
  .catch((error) => {
    console.error("Error:", error);
  });
;

preflight

cors 의 경우 preflight request 를 보내는 경우가 있다. 이것은 실제 request를 보내기전에 이제 보내려는 request 의 몇개 특징을 서버에게 보내서 서버에서 이런 request 를 처리해주느냐? 라고 묻는용으로 보내는 request 로 보면 될 듯 하다.

preflight request 가 발생되는 조건:

  • Cross-Origin Resource Sharing (CORS) - HTTP | MDN : 자세한 조건은 이 글을 확인하자.
  • request에 credential 이 있는 경우 preflight 이 날라간다.
  • Content-Type 이 ‘application/x-www-form-urlencoded’, ‘multipart/form-data’, ’text/plain’외의 값이면 preflight request 가 날라간다.
  • custom header 가 있다면 preflight request 가 날라간다.

preflight request가 발생하지 않는 경우:

See Also

  1. Cross-Origin Resource Sharing (CORS) - HTTP | MDN
  2. XMLHttpRequest.withCredentials - Web APIs | MDN
  3. 쿠...sal: [컴] 제 3자 쿠키란?

[컴][웹] nextjs commerce 에서 SWR handler.useHook 이 호출되는 과정

nextjs commerce 에서 SWR handler.useHook 이 호출되는 과정

  1. packages/commerce/src/cart/use-cart.tsx, useCart 에서 useSWRHook 호출
  2. packages/commerce/src/utils/use-hook.ts, useSWRHook 에서 hook.useHook 호출
  3. packages/vendure/src/cart/use-cart.tsx, handler.useHook

자세한 code

packages/commerce/src/cart/use-cart.tsxuseCart를 보면, useSWRHook 를 호출하고 있다. 이 때 hook을 넘기는데, 이 hook 은 useHook을 이용해서 얻는다. 아래 packages/commerce/src/utils/use-hook.ts 를 보면 된다.

useHook은 parameter 로 fn 을 넘기는데, 이 fn 에서 어떤 hook 을 사용할 것인지 고른다고 보면 된다. 참고로, 이 useHookhook.useHook 은 다른 함수다.

// packages/commerce/src/cart/use-cart.tsx
//
import Cookies from 'js-cookie'
import { useHook, useSWRHook } from '../utils/use-hook'
import type { SWRHook, HookFetcherFn } from '../utils/types'
import type { GetCartHook } from '../types/cart'
import { Provider, useCommerce } from '..'

export type UseCart<H extends SWRHook<GetCartHook> = SWRHook<GetCartHook>> =
  ReturnType<H['useHook']>

export const fetcher: HookFetcherFn<GetCartHook> = async ({
  options,
  input: { cartId },
  fetch,
}) => {
  return cartId ? await fetch(options) : null
}

const fn = (provider: Provider) => provider.cart?.useCart!

const useCart: UseCart = (input) => {
  // hook 을 가져오는, 여기선 useCart 에 해당하는 hook 을 가져오기 위한 함수  
  const hook = useHook(fn) 
  const { cartCookie } = useCommerce()
  const fetcherFn = hook.fetcher ?? fetcher
  const wrapper: typeof fetcher = (context) => {
    context.input.cartId = Cookies.get(cartCookie)
    return fetcherFn(context)
  }
  return useSWRHook({ ...hook, fetcher: wrapper })(input)
}

export default useCart

packages/commerce/src/utils/use-hook.ts 를 보면, useSWRHook 이 있다. 여기서 hook.useHook 을 호출한다.

// packages/commerce/src/utils/use-hook.ts
//
import { useCallback } from 'react'
import { Provider, useCommerce } from '..'
import type { MutationHook, PickRequired, SWRHook } from './types'
import useData from './use-data'

export function useFetcher() {
  const { providerRef, fetcherRef } = useCommerce()
  return providerRef.current.fetcher ?? fetcherRef.current
}

export function useHook<
  P extends Provider,
  H extends MutationHook<any> | SWRHook<any>
>(fn: (provider: P) => H) {
  const { providerRef } = useCommerce<P>()
  const provider = providerRef.current
  return fn(provider)  // 여기서 provider는 provider.tsx에 있는 provider이다.
}

export function useSWRHook<H extends SWRHook<any>>(
  hook: PickRequired<H, 'fetcher'>
) {
  const fetcher = useFetcher()

  return hook.useHook({
    useData(ctx) {
      const response = useData(hook, ctx?.input ?? [], fetcher, ctx?.swrOptions)
      return response
    },
  })
}
...

use-cart.tsx 를 예로 보면, 위에서 호출한 hook.useHookuse-cart.tsx 에 정의된 handler.useHook 이 된다.

packages/vendure/src/cart/use-cart.tsx 를 보면, useHook 에서는 useData를 호출한다.

// packages/vendure/src/cart/use-cart.tsx
import { SWRHook } from '@vercel/commerce/utils/types'
import useCart, { type UseCart } from '@vercel/commerce/cart/use-cart'
import { ActiveOrderQuery, CartFragment } from '../../schema'
import { normalizeCart } from '../utils/normalize'
import { useMemo } from 'react'
import { getCartQuery } from '../utils/queries/get-cart-query'
import type { GetCartHook } from '@vercel/commerce/types/cart'

...

export default useCart as UseCart<typeof handler>

export const handler: SWRHook<GetCartHook> = {
  fetchOptions: {
    query: getCartQuery,
  },
  async fetcher({ input: { cartId }, options, fetch }) {
    const { activeOrder } = await fetch<ActiveOrderQuery>(options)
    return activeOrder ? normalizeCart(activeOrder) : null
  },
  useHook:
    ({ useData }) =>
    (input) => {
      const response = useData({
        swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
      })

      return useMemo(
        () =>
          Object.create(response, {
            isEmpty: {
              get() {
                return (response.data?.lineItems.length ?? 0) <= 0
              },
              enumerable: true,
            },
          }),
        [response]
      )
    },
}

<nextjs-commerce>\packages\commerce\src\utils\use-data.tsx 를 보면, useSWR 이 보인다.

import useSWR, { SWRResponse } from 'swr'
...


const useData: UseData = (options, input, fetcherFn, swrOptions) => {
  ...
  const fetcher = async (
    url: string,
    query?: string,
    method?: string,
    ...args: any[]
  ) => {
    try {
      return await options.fetcher({
        options: { url, query, method },
        // Transform the input array into an object
        input: args.reduce((obj, val, i) => {
          obj[hookInput[i][0]!] = val
          return obj
        }, {}),
        fetch: fetcherFn,
      })
    } catch (error) {
      // SWR will not log errors, but any error that's not an instance
      // of CommerceError is not welcomed by this hook
      if (!(error instanceof CommerceError)) {
        console.error(error)
      }
      throw error
    }
  }

  const response = useSWR(
    () => {
      const opts = options.fetchOptions
      return opts
        ? [opts.url, opts.query, opts.method, ...hookInput.map((e) => e[1])]
        : null
    },
    fetcher,
    swrOptions
  )
  ...
  return response as typeof response & { isLoading: boolean }
}

export default useData

swr package

See Also

  1. 쿠…sal: [컴][웹] next.js commerce + vendure 서버 실행

[컴][웹] reactjs 의 swr package

cache / react next / nextjs / reactjs

reactjs 의 swr package

swr package 라는 vercel 에서 만든 package 를 사용한다. 이 녀석은 stale-while-revalidate 이라는 전략을 쉽게 구현할 수 있도록 해준다. 즉 일단 cache 된 값을 가져오고 보여주고(stale), 그러는 동안에 정보를 요청해서 최신 data 를 받은 후에 뿌려준다.

좀 더 쉽게 설명한다면, data 를 가져오는 동안에 cache 된 정보를 그냥 보여주면서, 시간을 버는 것이라 보면 된다.

사용도 어렵지 않다. 다음은 예제이다.

import useSWR from 'swr'

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

코드도 간단한다. fetcher 만 구현해서 넣으면 된다. 그러면, 값을 가져오는 동안 이전에 cache 했던 값을 보여주는 것은 알아서 해주는 것이다.

[컴][웹] nextjs commerce 에 debugger 붙이기

nextjs debug / debugger/ commerce

nextjs commerce 에 debugger 붙이기

chrome dev tools

코드에 debugger keyword 를 집어넣고, chrome dev tools 를 열면, debugger 가 있는 곳에서 break 한다.

vscode debugger

이방식으로 debugger 를 attach 해도 break point 가 제대로 동작하진 않는다. 이것도 debugger keyword 를 넣어야 만 break 가 동작한다.[ref. 1]

<commerce_root>\package.jsonNODE_OPTIONS=--inspect 를 넣으면 9229 port 로 debugger 를 attach 할 수 있다.

{
  "scripts": {
    "dev": "turbo run dev",
    "dev-inspect": "set NODE_OPTIONS=--inspect&& turbo run dev",

vscode 에서는 다음과 같은 launch.json 을 사용하면 된다.[ref. 2]

{
      "type": "node",
      "request": "attach",
      "name": "Next: Node",
      "skipFiles": ["<node_internals>/**"],
      "cwd": "${workspaceFolder}",
      "port": 9229
}

주의할점

서버에서 동작하는 코드에 log를 찍으면, browser의 console 에서는 보이지 않는다. 이건 띄워놓은 서버의 log console에서 보인다.

See Also

  1. 쿠…sal: [컴][웹] next.js commerce + vendure 서버 실행

Reference

  1. Debugging with VS Code · Discussion #788 · vercel/commerce · GitHub

[컴] database query 관련 python unittest

 

파이썬 / db test / mysql test / test 방법 / db 쉽게

database query 관련 python unittest

import unittest
from datetime import datetime
import pymysql

class TestInsertPurchaseData(unittest.TestCase):
    def shortDescription(self):
        # 이것을 하지 않으면 첫줄만 print 된다. [ref. 1]
        return self._testMethodDoc

    @classmethod
    def setUpClass(cls):
        cls.conn = pymysql.connect(
            host="localhost",
            user="root",
            password="namhun",
            cursorclass=pymysql.cursors.DictCursor,
        )
        
        cls.db = 'testdb'
        cls._setUpCreateDB(cls.db)
        cursor = cls.conn.cursor()
        cursor.execute(f"use {cls.db}")


        # query from .sql file
        with open('schema.sql', encoding='utf8') as f:
            commands = f.read().split(';')

        for command in commands:
            if command.strip() != "":
                cursor.execute(command)

    @classmethod
    def tearDownClass(cls):
        # drop test database
        try:
            cls.cursor.execute(f"DROP DATABASE {cls.db}")
            cls.conn.commit()
            cls.cursor.close()
        except pymysql.Error as err:
            print(f"Database {cls.db} does not exists. Dropping db failed")
        cls.conn.close()

    @classmethod
    def _setUpCreateDB(cls, db):
        cursor = cls.conn.cursor()
        try:
            cursor.execute(f"DROP DATABASE {db}")
            cursor.close()
            print("DB dropped")
        except pymysql.Error as err:
            print(f"{db}{err}")
            cursor.close()

        # create database
        cls.cursor = cls.conn.cursor()
        try:
            cls.cursor.execute(f"CREATE DATABASE {db} DEFAULT CHARACTER SET 'utf8'")
        except pymysql.Error as err:
            print(f"Failed creating database: {err}")
            exit(1)

    def test_insert_record(self):
        """
        Test whether the inserted row count is correct
        """
        pass

if __name__ == "__main__":
    # 이것은 python -m unittest 로 하면 호출이 되지 않는다. `python -m unittest test.py` 
    unittest(verbosity=2)
CREATE TABLE `product_price` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
    `purchase_date` date NOT NULL COMMENT '',
    `total_amount` int(11) NOT NULL COMMENT '',
    `total_price` bigint(20) NOT NULL COMMENT '',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `price2` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
    `sales_date` date NOT NULL COMMENT '',
    `price2` int(11) NOT NULL COMMENT '',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
INSERT INTO mapping (id,`sales_date`,price2) VALUES
    (1,'2022-01-01',3558),
    (2,'2022-01-02',40000);

unittest verbose option

unittest(verbosity=2)를 하면, test case 를 실행할때 첫줄을 print 해준다.[ref. 1]

다음 부분은 python -m unittest test.py 를 실행하면 실행되지 않는다. __name__ 값이 '__main__' 이 아니기 때문이다.

python -m unittest -v test.py 를 하면 verbosity=2 와 같은 결과가 나온다.

if __name__ == "__main__":
    unittest(verbosity=2)

Reference

  1. Print a different long description in nose tests along with test name python - Stack Overflow

[컴] next.js 의 getStaticPaths

 넥스트js / nextjs /넥스트

next.js 의 getStaticPaths

getStaticPaths은 production 에서 build 될 때 실행된다. runtime 에 호출되진 않는다.

getStaticPaths 사용(참고):

  • getStaticPathsgetStaticProps와 함께 사용해야만 한다.
  • getStaticPathsgetServerSideProps와 함께 사용할 수 없다.
  • Dynamic Routes 에서 getStaticPaths를 export 할 수 있다. Dynamic Route도 또한 getStaticProps 을 사용한다.
    • Dynamic Routes 은 pages/post/[pid].js 같은 것들이라고 보면 될 듯([참고]](https://nextjs.org/docs/routing/dynamic-routes))
  • page file 아닌 파일(예를 들면, 컴포넌트 폴더)에서 getStaticPaths를 export 할 수 없다.
  • getStaticPaths를 page component 의 property 로 export 할 수 없다. standalone함수로 export 해야만 한다.

next dev에서는 매 request 마다 getStaticPaths 가 호출된다.

paths 변수

참고: paths | getStaticPaths | Next.js

  • page 이름이 pages/posts/[postId]/[commentId]인 경우 params에는 postId와 commentId가 있어야 한다.
  • page 이름이 pages/[...slug]와 같은 catch-all routes를 사용하는 경우 parameter에는 array type 의 slug가 포함되어야 한다. 이 배열이 ['hello', 'world']인 경우 Next.js는 /hello/world에 page를 정적으로 생성합니다.
  • page가 선택적인 catch-all routes를 사용하는 경우 null, [], undefined 또는 false를 사용하여 root-most path를 렌더링합니다. 예를 들어, pages/[[...slug]]slug: false를 제공하면 Next.js는 / 페이지를 정적으로 생성합니다.
  • pages/product/[slug].tsx 인 경우, paths: [ 'product/notebook', 'product/electro'] 등으로 설정하면 /product/notebook, /product/electro 페이지가 생성된다.
...
import { useRouter } from 'next/router'
import { Layout } from '@components/common'

export async function getStaticProps({
  params,
  locale,
  locales,
  preview,
}: GetStaticPropsContext<{ slug: string }>) {
  ...

  return {
    props: {
      pages,
      product,
      relatedProducts,
      categories,
    },
    revalidate: 200,
  }
}
export async function getStaticPaths({ locales }: GetStaticPathsContext) {
  ...

  console.log('namhnamhnamhnamhnamh')
  console.log(products)
  return {
    paths: ['/products/notebook', '/products/tails'],
    fallback: 'blocking',
  }
}

next.js 의 getStaticProps 실행

  • next build 할 때 실행된다.
  • fallback: true 를 사용하면, background 로 실행된다.
  • fallback: blocking 을 사용하면, initial render 이전에 실행된다.
  • revalidate 를 사용할때 background 로 실행된다.
  • revalidate() 를 사용할 때 getStaticProps가 background 에서 실행된다.(on-demand)

getStaticPropsgetStaticPaths와 관련한 동작(참고)

  • build 될때 return 되는 모든경로(any paths) 에 대한 next build 를 하는 동안 getStaticProps는 실행된다.
  • fallback: true 를 사용할 때 getStaticProps는 background 로 실행된다.
  • fallback: blocking 을 사용할 때, initial render 이전에 호출된다.

Reference

  1. Data Fetching: getStaticPaths | Basic Features | Next.js
  2. Data Fetching: getStaticPaths | Next.js

[컴] nextjs commerce 의 Layout 설정

 

nextjs commerce 의 Layout 설정

_app.tsx 는 매번 page 가 rendering 될 때 가장 먼저 호출되는 file 이다.

여기서 (Component as any).Layout 이렇게 Layout 을 호출하기 때문에, 현재 각 pages/*.tsx 에 Layout property 를 붙인다.

// pages/_app.tsx

...
import type { AppProps } from 'next/app'
...
export default function MyApp({ Component, pageProps }: AppProps) {
  const Layout = (Component as any).Layout || Noop

  useEffect(() => {
    document.body.classList?.remove('loading')
  }, [])

  return (
    <>
      <Head />
      <ManagedUIContext>
        <Layout pageProps={pageProps}>
          <Component {...pageProps} />
        </Layout>
      </ManagedUIContext>
    </>
  )
}
// pages/index.tsx
...
import { Layout } from '@components/common'
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'

export async function getStaticProps({
  preview,
  locale,
  locales,
}: GetStaticPropsContext) {
  ...
  return {
    props: {
      products,
      categories,
      brands,
      pages,
    },
    revalidate: 60,
  }
}

export default function Home({
  products,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  debugger
  return (
    <>
      ...
    </>
  )
}

Home.Layout = Layout

See Also

  1. 쿠…sal: [컴][웹] Nextjs 의 pages/_app.tsx