# 2024. 04.

# 04. 01.

# File vs Blob

File 객체는 Blob객체를 상속받은 객체이며 Blob의 모든 속성을 다 갖고 있음
Blob이 파일 그 자체의 데이터라면 File은 메타데이터 등 부가 데이터가 존재함.

# FileReader.readDataAsUrl vs URL.createObjectURL

다른건 없다. readDataAsUrl는 비동기적으로 처리하고 createObjectURL는 동기적으로 처리한다.

# 04. 02.

# s3-presigned-post vs s3-request-presigner

presigned url을 사용해야하는데 두가지 방식이 보였다.
s3-request-presigner는 PUT 메소드를 사용하고 만료 시간밖에 정하지 못하는 반면 s3-presigned-post는 POST 메소드를 사용하여 더 많은 제약 조건을 설정할 수 있다. s3-request-presigner의 상위 호환 느낌이다.

s3-request-presigner AWS 공식문서 (opens new window)
s3-presigned-post AWS 공식문서 (opens new window)

POST 메소드를 활용한 제약조건 예시 (opens new window)

S3-Presigned-Url-도입하기 (opens new window)

# 04. 03.

# 이미지 압축 처리에 대한 고민

사용자가 이미지와 게시물을 업로드할때 이미지를 압축하고 싶다.
클라이언트에서 하기엔 사용자 경험이 안좋고, 서버에서 하기엔 효율이 떨어지는 느낌이다.

AWS lambda를 사용하여 s3에 origin 버킷으로 이미지가 업로드 되었을 때 동작하여 이미지를 압축하여 resize 버킷으로 재 업로드하는 전략이다.
resize 버킷으로의 업로드 성공 후 게시물을 업로드 하고싶다.

# lambda 에서 sharp로 이미지 압축

최신 버전의 sharp(0.33)는 Lambda와 호환되지 않는다. 0.32.6 버전을 사용해야한다.


npm install --arch=x64 --platform=linux --target=16x sharp@0.32.6

더 나은 사용자 경험을 위한 이미지 리사이징을 해보자 (opens new window)
자습서: Amazon S3 트리거를 사용하여 썸네일 이미지 생성 (opens new window)

# NextJS Image Optimizing

알아보니 NextJS에서 remote 이미지도 최적화 해준다. 이러한 이유로 next.config.js에 Next Image에서 사용하는 src의 도메인을 작성하라고 하는것 같다.
동작 방식은 클라이언트에서 이미지를 요청하면 Next Server에서 이미지를 요청하여 자체적으로 최적화해서 브라우저에 제공해주는걸로 보인다.

Next Server와 Next Client간의 로딩은 빠르겠지만 Next Server와 S3(이미지 저장소)간의 통신은 S3에 이미지를 압축해서 저장하지 않으면 여전히 느리고 비용도 많이 나올것으로 보인다.
일단 Next에서 제공해주는 최적화 방식을 사용해보고 문제가 있다 싶으면 바꾸자.

Next Server에서 자체적으로 압축해줄때 기본적으로 squoosh를 쓰는데 sharp가 더 성능이 좋다고 매우 추천하니 써보자.

# 04. 04.

# presigned URL에 업로드할 때 Condition Content-Type issue

presigned url 생성 시
Condition (제약조건)은 다음과 같다.

const Conditions: any = [
    ["eq", "$acl", "public-read"], 
    ["eq", "$bucket", process.env.AWS_S3_BUCKET as string], // 버킷
    ["starts-with", "$Content-Type", "image/"], // 이미지 타입만
    ["starts-with", "$key", `user/${session.user.id}/`], // 사용자 폴더에만
];

업로드 요청 후 s3에서 에러가 날아왔다.

<Error>
    <Code>AccessDenied</Code>
    <Message>Invalid according to Policy: Policy Condition failed: ["starts-with", "$Content-Type", "image/"]</Message>
    <RequestId>0CPB85WE4SJQ18GW</RequestId>
    <HostId>o2mMEAUT0jrGx+eOnaPkPPZRNJKncnjDHZwkjsM3bMCzxa/d0cWNkQy9BfkVThuzvu0iuNx7P3o=</HostId>
</Error>

axios에 요청 때 Content-Type를 바꿔서 보냈는데 안되었고

form.append('file', file);
form.append('Content-Type', type)

Form 타입에 이와 같이 기입해줘었는데 에러가 났다.

form.append('Content-Type', type)
form.append('file', file);

순서 바꿔주니까 되더라
어이없음.

# 04. 05.

# 클라이언트에서 이미지를 압축하는 짓은 하지 말자.

프론트에 이미지 업로드하고 다른 작업을 할 동안 백그라운드에서 압축하는 로직을 넣어봤다.

화면 밑에 보면 압축 큐가 비어있으면 true, 이미지 압축이 진행되고 있으면 false이다.

테스트 결과, 데스크탑 환경에서는 아무 렉 없이 잘되는데 모바일 환경에서는 백그라운드에서 압축할 동안 다른 작업이 안된다.
멀티쓰레드로 동작한다지만 안그래도 없는 메모리 web worker가 다 잡아먹어서 그런것 같다.

내 3시간.

# 04. 06.

# upload image to s3 via cloudfront

s3와 cloudfront를 연결한 상태에서 presigned url을 사용해서 업로드를 할려니 에러가 난다.

<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>InvalidArgument</Code>
        <Message>x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-UNSIGNED-PAYLOAD-TRAILER, STREAMING-AWS4-HMAC-SHA256-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER or a valid sha256 value.
        </Message>
    <ArgumentName>x-amz-content-sha256<ArgumentName>
    <ArgumentValue>null</ArgumentValue>
    <RequestId>C3B2DY8CB4FZ4N40</RequestId>
    <HostId>SgOlN+Wp+2kbRHOaPfywiIMg2f8mwabKW+Ex3AUtrUuMXbdxnpq7zM5xYIM9DydChOjr77MlTlw=</HostId>
</Error>

몇몇 글을 보면서 삽질을 해보았다.

Uploading into S3 directly vs through CloudFront (opens new window)
Patterns for building an API to upload files to Amazon S3 (opens new window)
Using CloudFront to upload files to S3 bucket using presign URL and custom domain (opens new window)

첫번째 글에서 cloudfront를 통해 업로드하는 방법은 바람직하지 못한 방법이라고 언급하였고 (업로드 시 캐싱 이득을 못보기 때문)
두번째 글에서는 s3에 업로드하는 다양한 패턴들을 소개하는데 cloutfront를 통해 업로드하는 경우는 없었다.
세번째 글은 그냥 긴가민가하다.

생각해본 결과, 이미지 get은 cloudfront로 하되, 업로드는 s3의 presigned url로 하도록하는게 해답인것 같다.

# CloudFront를 통해서만 S3 객체 접근

CloudFront로 s3를 연결했고 get은 CloudFront의 url로만, upload는 s3의 presigned url로만 하고 싶었다.

버킷 권한 설정을 다음과 같이하고

객체를 get하는 요청은 CloudFront로부터만 받겠다.


{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "AllowCloudFrontServicePrincipal",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::a-spot-thur/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "arn:aws:cloudfront::533267392768:distribution/E2C03KKK1Y4NIL"
                }
            }
        }
    ]
}

버킷 권한을 다음과 같이 해주었다.

모든 객체에 대해 퍼블릭 액세스를 무시한다.

이러면 모든 객체에 대한 퍼블릭 액세스는 무시되고 버킷 정책이 적용되어 CloudFront를 통해서면 객체에 접근(get)할 수 있다.

야매로 읽고 하긴헀는데, 조만간 버킷 퍼블릭 엑세스에 대해서 더 공부해야겠다.

# 04. 07.

# Object.keys는 O(n)이다.

객체의 길이를 가져오고 싶어서 Object.keys(obj)를 사용해서 길이를 구했다.

const obj = {
    '1': 4,
    '2': 6
}
console.log(Object.keys(obj).length)
// 2

이는 O(n)의 시간복잡도를 갖고있으므로 반복문 내에서 사용하면 비효율적이다.

# 04. 08.

# prisma migrate dev vs prisma migrate deploy

prisma 스키마를 작성하고 실제 db에 반영시키는 두가지 방법이다.

npx prisma migrate dev
  • shadoww db에서 기존 마이그레이션 기록을 전부 재실행
  • shadow db에 보류중인 마이그레이션 모두 적용
  • 스키마에 변경이 생겼을 경우 새 마이그레이션 생성
  • 모든 마이그레이션 적용 후 _prisma_migrations 에 기록
  • prisma client 재 생성
  • 마이그레이션 적용 시 모든 데이터 리셋
npx prisma migrate dev --create-only

--create-only 명령어를 사용하면 원격으로 적용하지 않고 마이그레이션을 생성한다.

npx prisma migrate deploy
  • 보류중인 마이그레이션을 적용한다
  • 데이터베이스를 리셋하지 않는다
  • shadow db에 의존하지 않는다

# 04. 09.

# NextJS multi root layout 간의 라우팅

NextJS에서 Multi root layout 구조에서 root 간의 이동은 전체 페이지가 다시 로드된다 (캐싱이 안됨). Link 컴포넌트나 route prefetch 메소드로도 안통함.

Multi root layout 구조에서만 해당됨.

# 04. 10.

# NextJS 14에서 커스텀 metadata (google) 추가

크롬으로 접속할때 항상 번역 팝업이 떠서 짜증나서 비활성화하려고 메타데이터에 google: notranslate를 추가해야되는데 NextJS에 기본 제공되는 필드가 아니었다.

NextJS 14에서 메타데이터를 추가하려면 기본 제공되는 메타 데이터들을 활용해야한다.
그 이 외의 데이터들은 other (opens new window) 필드를 사용해서 추가하면 된다.

export const metadata: Metadata = {
  title: "AT - A Spot Thur",
  description: "나만의 지도를 만들어보세요!",
  other: {
    google: 'notranslate'
  }
};

# 04. 13

# Tanstack Query useQuery 캐싱

useQuery가 자동으로 refetch 하는 경우는 다음과 같다. 우선 데이터가 Stale 상태여야하고,

  • 네트워크 재연결
  • 윈도우 재포커스
  • 컴포넌트 마운트
  • refetchInterval

수동으로 refetch하는 과정은 useQuery에서 제공하는 refetch 함수를 실행하면 된다.
refetch 함수를 실행하면 일단 기존 캐시값을 반환하고 백그라운드에서 최신 데이터를 가져와서 캐시를 업데이트한다.

(확실하지 않음) useQuery 함수에 enabled를 false로 주면 useQuery가 위와 같이 자동으로 refetch 하지 않으므로 수동(refetch 함수)으로 호출해야 된다.

[TIL-35] react-query와 검색 기능에서의 캐싱 처리 (opens new window)
[React Query] 리액트 쿼리 '잘' 사용해보자 - 네트워크 비용 감소 / UX 개선 (opens new window)

# Tanstack Query useQuery 상태

상태는 총 4가지

  • Fresh
  • Fetching
  • Stale
  • Pause
  • Inactive

Stale은 Fresh의 반대이며, Stale 상태에 있으면 데이터가 신선하지 않다는 뜻이다 (오래된 데이터)

staleTime이 지나면 데이터는 더이상 Fresh하지 않다.

query가 언마운트되면 쿼리는 Inactive 상태가 되며 gcTime이 지나면 아예 삭제된다.

StaleFresh 상태는 Inactive 상태여도 기억하고 있다가, 다시 마운트되면 반영된다.
즉, staleTime이 Infinity면 Inactive된 상태에서 다시 마운트가 되어도 재요청하지 않는다.
반대로 stale 상태의 쿼리가 언마운트되어 Inactive가 되었다면 이 후에 마운트 되면 다시 refetch된다.

staleTime과 gcTime을 Infinity로 두면 처음 요청 이외에는 자동으로 요청하지 않는다.(데이터가 계속 Fresh하기 때문) -> refetch 함수로 캐시를 업데이트할 수 있다. (refetch 함수는 캐시가 있더라도 최신 데이터로 다시 업데이트 하기 때문)

Tanstack Query Caching Examples (opens new window)

# 04. 16.

# 모듈

모듈은 관련 변수나 함수로 구성되어있으며 공개 및 비공개 데이터로 구분되어있다.

모듈은 stateful하며 상태를 관리할 수 있다.

상태를 저장하는 변수 없이 함수만 있다면 이건 모듈이 아니라 네임스페이스이다.

데이터와 상태 저장 함수가 포함된 구조를 가진 함수여도 데이터에 대한 캡슐화가 없으면 모듈이라고 보기 어렵다

모듈이라고 함은 변수와 함수로 구성되어있되, 변수에 접근성을 부여할 수 있어야한다. (공개, 비공개)

IIFE를 사용한다는것은 싱글톤으로 사용하겠다는 의미이다.

  • There must be an outer scope, typically from a module factory function running at least once.

  • The module's inner scope must have at least one piece of hidden information that represents state for the module.

  • The module must return on its public API a reference to at least one function that has closure over the hidden module state (so that this state is actually preserved).

  • 모듈 팩토리 함수가 외부 스코프에서 한번 이상 실행되어야함.

  • 모듈의 내부 스코프가 하나 이상의 비공개 정보를 참조하고 있어야함.

  • 모듈은 내부 데이터에 접근할 수 있는 public API를 제공해야 하며, 내부 데이터에 대한 클로저를 갖는 최소 하나 이상의 함수를 반환해야한다.

# 04. 17.

# React Hook 동작 방식

# 의문

실제 코드를 짤 때 hook은 컴포넌트 내부에 있다. 정확히 말하면 함수 내부에 있음. hook을 호출할 때 컴포넌트에 관련된 정보가 없음에도 해당 상태는 컴포넌트와 어떻게 매핑이 되는가?? 내부 자료구조가 궁금함.

실제 React 코드를 들여다보기 시작..

mountState에서 dispatch변수는 setter 함수이고 mountState 함수에서 마지막에 반환된다. dispatch는 dispatchAction 함수이며 currentlyRenderingFiber와 queue를 포함하고 있다. queue.dispatch는 dispatch function이랑 같은 참조를 하고 있다.

아오 삽질하면서도 뭔말인지 모르겠다 다음에 다시 하자.

# 삽질 Reference

전체 틀

React Codebase Overview (opens new window)

Fiber

Naver D2 React 파이버 아키텍처 분석 (opens new window)
React Fiber Architecture (opens new window)

Hook 탄생 배경

[10분 테코톡] 룩소의 React Hooks (opens new window)

클로저 기반 설명

JSCont - Can Swyx recreate React Hooks and useState in under 30 min? (opens new window)

React Code 기반 분석

Under the hood of React’s hooks system (opens new window)
How does React associate Hook calls with components? (opens new window)
React Hooks - What's happening under the hood? (opens new window)
How Does setState Know What to Do? (opens new window)
How React Hooks Work - in depth + React Render Cycle Explained (opens new window)
How do react hooks determine the component that they are for? (opens new window)

# 04. 21.

# 라이브러리는 목적성을 갖고 사용하자.

이번 AT 프로젝트를 진행하면서 요청 라이브러리는 아무 생각없이 axios로 채택을 했다.
axios가 내놓는 장점들을 알지도 못한 채 말이다..

그러다가 Param을 전달받아서 빈 값이 아닌 경우에만 쿼리 스트링에 포함하고 싶어서 아래와 같이 더러운 로직을 짰다.

type Param = {
  query: string;
  name: string;
  at_id: string;
}

// TODO axios 테스트환경 & 배포환경 baseurl 지정해주기
export const URL = "https://www.a-spot-thur.app/api/at/count"
export const GETALLMAP_QUERY_KEY = `${URL}[GET]`;

export const fetcher = ({query, name, at_id}: Param) =>{
  const querie_str = query ? `query=${query}` : ''
  const name_str = name ? `name=${name}` : ''
  const at_id_str = at_id && at_id != "index" ? `at_id=${at_id}` : ''
  const merge = [querie_str, name_str, at_id_str].filter(str=>str != '')
  const query_string = merge.length ? `?${merge.join("&")}` : ''

  return atAxios.get(`${URL}${query_string}`).then(({ data }) => data);
}

오늘 axios 공식 문서를 보는데

{
  // `params`은 요청과 함께 전송되는 URL 파라미터입니다.
  // 반드시 일반 객체나 URLSearchParams 객체여야 합니다.
  // 참고: null이나 undefined는 URL에 렌더링되지 않습니다.
  params: {
    ID: 12345
  },
}

이런 편리한 기능이 있더라.


type Param = {
  query: string;
  name: string;
  at_id: string;
}

// TODO axios 테스트환경 & 배포환경 baseurl 지정해주기
export const URL = "https://www.a-spot-thur.app/api/at/count"
export const GETALLMAP_QUERY_KEY = `${URL}[GET]`;

export const fetcher = ({query, name, at_id}: Param) =>{
  return atAxios.get(`${URL}`, {
    params: {
      query,
      name,
      at_id
    }
  }).then(({ data }) => data);
}

바로 수정.
라이브러리는 단순 트렌디하고 핫하다는 이유보단 목적성을 갖고 사용하자.

# 04. 22.

# Next.js에서 리렌더링 없이 url 바꾸기

버튼 클릭시 동일한 페이지에서 렌더링해주고 싶은데 뒤로가기가 동작했으면 했다.
(1번 케이스 사용)

1. history stack에 포함시키고 변경

window.history.pushState({ ...window.history.state }, "", '/add')

2. history stack 포함시키지 않고 변경

window.history.replaceState({ ...window.history.state, as: '/', url: '/' }, '', '/');

Reference

# 04. 24.

# React ContextAPI 문제점

Context가 객체라고 할 때, 객체의 일부 프로퍼티만 업데이트 된다하더라도 해당 Context를 가져다가 사용하는 모든 Consumer가 리렌더링된다.

우리 효자 Recoil은 안그럼

# 04. 25.

# tanstack Query refetchOnMount

refetchOnMount 옵션은 stale 상태일때만 발동함.

# 04. 26.

# 하나의 페이지에서 뒤로가기 구현

하나의 페이지 내에서 컴포넌트간에 이동을 구현하고 싶은데 뒤로가기가 적용되었으면 했다.

이 화면에서 상단 바만 남기고

이렇게 변했으면 했다.

layout으로 감싸고 page만 이동하면 되는거 아니냐 할 수 있겠지만 다음과 같은 조건을 부합하지 못했다.

  • URL이 바뀌면 안된다. (page.tsx는 하나의 url을 대표하는 컴포넌트이기 때문에 하나에 두개의 page 컴포넌트로는 부합할 수 없다.)
  • 애니메이션이 적용되야 함 (url이 바뀌면 왜인진 모르겠는데 Next에서 exit 애니메이션이 동작하지 않았다. Framer 기준)

해결 방법

  1. 지도를 보여주는 Map 컴포넌트와 목록을 보여주는 List 컴포넌트는 selectedArea라는 상태로 인해 분기된다. (값이 없으면 Map 있으면 List)
  2. Map에서 List로 페이지가 이동할 때 window.history.pushState()를 사용하여 history에 스택을 쌓는다. (동일한 url)
  3. List에 popState 이벤트를 리스너에 연결해주어 뒤로갈 때 selectedArea 상태를 null로 변경한다. 이 때, 2번에서 쌓아준 스택은 없어진다.

코드

/* useGhostHistory.ts */

import { useEffect } from "react";

type Props = {
    onPopState?: any
}

export const useGhostHistory = () => {

    const push = () => {
        // ghost stack 하나 쌓기
        window.history.pushState(null, "", '/')
    }

    function use({onPopState}: Props){

        const handlePopState = (e:any) => {
            // 뒤로가기 시 호출
            if(onPopState) onPopState()
        }

        useEffect(() => {
            // 브라우저 history에 stack 하나 쌓기 (뒤로가기 눌렀을때 이 스택이 사라짐)
            window.addEventListener("popstate", handlePopState);
            return (() => {
              window.removeEventListener("popstate", handlePopState);
            });
        }, []);
    }
    return { push, use }
}

Hook으로 만들어봤다.
Map 컴포넌트에 push를 하고, List 컴포넌트에서 use를 사용하면된다. 위와 같은 예에서는 onPopState에는 selectArea의 상태를 바꿔주는 함수를 작성하면된다.

# 04. 27.

# RecoilRoot를 여러개 사용할 때

RecoilRoot가 중첩되었을 떄 부모 RecoilRoot에서 관리되고 있는 atom과는 별개로 새로운 영역을 만들어서 독립적으로 관리함.
같은 영역 내에서 key가 겹치면 나중에 override 된다.

진짜 필요할때 아니면 사용안하는게 좋을듯.

# 04. 30.

# Nextjs Static Asset 캐싱

Nextjs는 기본적으로 public 폴더에 있는 asset들에 대해선 캐싱을 하지 않는다. (바뀔 수 있기 때문에)

기본적으로 적용된 캐시는 다음과 같다.

Cache-Control: public, max-age=0

# 여러 Layer에서 캐싱 전략

cache 유효 시간이 지나면 revalidation(재검증)을 수행한다.

재검증은 요청 헤더에 다음과 같은 헤더를 추가함으로서 수행할 수 있다.

  • If-None-Match : 캐시된 리소스의 ETag 값으로 비고
  • If-Modified-Since : 캐시된 리소스의 Last-Modified 값 이후 서버 리소스와 비교

결과가 유효하면 304 유효하지 않으면 200을 내려준다.

GPT 피셜로는 304 응답을 받은 경우에도 max-age가 다시 초기화되는게 아니라 서버 구현에 따라 다르다고 한다.

  • no-cache 캐시의 max-age가 항상 0인 값 사용하려고 할 때마다 재검증 요청을 보냄-

  • no-store 캐시를 아예 저장안함. 가장 강력한 Cache-Control 값

  • public 중간 서버(cdn)도 캐시를 저장할 수 있음

  • private 가장 끝에 사용자만 캐시를 저장할 수 있음

  • s-maxage 중간 서버에만 적용되는 캐시 max-age

s-maxage=31536000, max-age=0 // 중간서버에는 1년, 브라우저에는 매번 재검증 요청

Reference
Toss 웹 서비스 캐시 똑똑하게 다루기 (opens new window)

# Tanstack Query Inactive 상태에서 invalidate 시키기

Tanstack Query에서는 다음과 같이 캐싱된 query를 강제로 stale상태로 만들 수 있다.

queryClient.invalidateQueries({ queryKey: ['/at/count'] })

하지만 다음과 같이 Inactive 상태에서는 stale 상태가 되지 않는다.

그림 1. Tanstack Query Developer Tool.

뒤에 refetchType을 지정해주어 어느 상태일때 동작할지 명시해주면 된다.

queryClient.invalidateQueries({ queryKey: ['/at/count'], refetchType: 'all'  })
  • all Inactive, Active 상태일때
  • inactive Inactive 상태일때
  • active Active 상태일때

Reference
Tanstack Query 공식문서 - refetchActive / refetchInactive (opens new window)