1. Google Console 설정

서비스 계정 생성 및 키 등록:

  • Google Cloud Console에서 프로젝트 생성
  • "API 및 서비스" > "사용자 인증 정보" > "+ 자격 증명 만들기" > "서비스 계정"
  • 서비스 계정 생성 후 "키 추가" > "JSON" 선택하여 키 다운로드
  • 이 JSON 파일은 서버에서 사용하므로 안전하게 보관

2. Google Play 콘솔 설정

앱 사용자 및 권한 등록:

  • Google Play 콘솔에 접속
  • "설정" > "사용자 및 권한" > "사용자 초대"
  • 서비스 계정 이메일 추가
  • 권한 설정:
    • 재무 데이터 관련 항목 전체 체크
    • 앱 액세스 권한에서 관리자 제외 나머지 항목 체크

3. 인앱 상품 및 구독 검증 API

Google Play의 결제 검증은 두 가지 API를 통해 이루어집니다.

구독 검증 API:

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{package_name}/purchases/subscriptions/{product_id}/tokens/{token}

인앱 상품 검증 API:

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{package_name}/purchases/products/{product_id}/tokens/{token}
  • package_name: 앱의 패키지명 (예: com.example.app)
  • product_id: 상품 또는 구독 ID
  • token: 결제 완료 후 Flutter 앱에서 전달받은 구매 토큰

4. FastAPI 서버에서 검증 로직

토큰 발행:

  • 서버에서는 Google API에 요청을 보낼 때 액세스 토큰이 필요합니다.
  • 아래는 FastAPI 예제 코드입니다.
import requests
from fastapi import FastAPI

app = FastAPI()

@app.post("/verify_purchase/")
def verify_purchase(package_name: str, product_id: str, token: str, is_subscription: bool):
    # Google API 토큰 발급
    with open('path/to/your/json/key.json') as f:
        key_data = json.load(f)

    auth_url = "https://oauth2.googleapis.com/token"
    payload = {
        "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "assertion": generate_jwt(key_data)
    }
    response = requests.post(auth_url, data=payload)
    access_token = response.json().get("access_token")

    # 검증 API 호출
    api_url = (f"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{package_name}/purchases/"
               f"{'subscriptions' if is_subscription else 'products'}/{product_id}/tokens/{token}")

    headers = {"Authorization": f"Bearer {access_token}"}
    result = requests.get(api_url, headers=headers)

    return result.json()

설명:

  • generate_jwt(): Google 서비스 계정 JSON 파일을 사용하여 JWT 생성 (구현 필요)
  • is_subscription: 구독 여부에 따라 API URL 변경
  • Google API 요청에는 반드시 Bearer 토큰 포함
  •  

5. 문제 발생 시 체크 리스트

  • 서비스 계정 JSON 파일 정확히 등록했는가?
  • Google Play 콘솔에서 권한 설정이 올바른가?
  • API 요청 시 올바른 패키지명, 상품/구독 ID, 토큰을 사용했는가?
  • 액세스 토큰이 유효한가?
  • 오류가 발생하면 Google API 응답의 에러 메시지를 참고
  • 401 오류가 나오면 다음 두 가지 원인을 우선 체크:
    1. 토큰 오류: Google Play, Google Console에 권한 및 등록이 제대로 안 되었을 가능성
    2. API URL 오류: 잘못된 URL 사용
  • 물론 위 두 가지 외에도 다양한 원인이 있을 수 있으므로, 에러 메시지를 주의 깊게 살펴보자.

올 한해 별로 많이 커밋을 안한것 같지만 그래도 기부가 되니 기분이 좋네!

Actions

React 19에서는 비동기 데이터 변형과 상태 업데이트를 더 쉽게 관리할 수 있는 새로운 기능인 Actions를 도입했습니다. Actions는 보류 중인 상태, 오류 처리, 낙관적 업데이트를 자동으로 처리합니다.

function UpdateName({}) {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleSubmit = () => {
    startTransition(async () => {
      const error = await updateName(name);
      if (error) {
        setError(error);
        return;
      }
      redirect("/path");
    })
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

새로운 훅: useActionState

Actions의 일반적인 경우를 쉽게 만들기 위해 새로운 훅 useActionState를 추가했습니다.

const [error, submitAction, isPending] = useActionState(
  async (previousState, newName) => {
    const error = await updateName(newName);
    if (error) {
      return error;
    }
    return null;
  },
  null,
);

<form> Actions

React 19에서는 <form> 요소에 action 및 formAction props를 사용하여 자동으로 양식을 관리할 수 있습니다.

function ChangeName({ name, setName }) {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) {
        return error;
      }
      redirect("/path");
      return null;
    },
    null,
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>Update</button>
      {error && <p>{error}</p>}
    </form>
  );
}

새로운 훅: useOptimistic

데이터 변형 중에 낙관적 업데이트를 쉽게 만들기 위해 새로운 훅 useOptimistic을 추가했습니다.

function ChangeName({currentName, onUpdateName}) {
  const [optimisticName, setOptimisticName] = useOptimistic(currentName);

  const submitAction = async formData => {
    const newName = formData.get("name");
    setOptimisticName(newName);
    const updatedName = await updateName(newName);
    onUpdateName(updatedName);
  };

  return (
    <form action={submitAction}>
      <p>Your name is: {optimisticName}</p>
      <p>
        <label>Change Name:</label>
        <input
          type="text"
          name="name"
          disabled={currentName !== optimisticName}
        />
      </p>
    </form>
  );
}

새로운 API: use

렌더링 시 리소스를 읽을 수 있는 새로운 API인 use를 도입했습니다.

const profile = use(fetchProfile());
const theme = use(serverContext ? ServerTheme : ClientTheme);

React DOM

  • <form> Actions: <form> 요소에 action 및 formAction props를 추가했습니다.
  • Ref를 prop으로 사용: forwardRef 없이 함수 컴포넌트에서 Ref를 사용할 수 있습니다.
  • 향상된 수화 오류 보고: 수화 오류를 단일 메시지로 보고합니다.
<form action={actionFunction}>

스타일시트 및 리소스 관리

스타일시트 및 리소스를 더 잘 관리하기 위한 새로운 API를 추가했습니다.

import {useStyleSheet} from 'react-dom';
useStyleSheet("/styles.css", {precedence: 1000});

import {usePreload} from 'react-dom';
usePreload("/styles.css", {as: "style"});

기타 개선 사항

  • Context 제공자 단순화: Context 자체를 제공자로 사용할 수 있습니다.
  • Ref의 정리 함수: Ref 콜백에서 정리 함수를 반환할 수 있습니다.
  • useDeferredValue의 초기 값: 초기 값을 허용하여 초기 렌더링을 더 잘 처리합니다.
  • 문서 메타데이터: <title>, <link> 및 <meta> 태그에 대한 네이티브 지원을 추가했습니다.
const inputRef = useCallback(node => {
  if (node) {
    // 마운트 시점에 작업
  }
  return () => {
    // 언마운트 시점에 정리 작업
  };
}, []);

 

참조 : https://react.dev/blog/2024/04/25/react-19

1. useState

  • 함수형 컴포넌트에서 상태를 관리합니다.
  • 사용법: const [state, setState] = useState(initialState);
  • 예시:
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

2. useEffect

  • 부수 효과를 수행합니다 (데이터 페칭, 구독 설정 등).
  • 컴포넌트가 렌더링된 이후에 실행됩니다.
  • 사용법: useEffect(() => { /* 효과 */ }, [의존성 배열]);
  • 예시:
function DataFetcher({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`https://api.example.com/user/${userId}`);
      const result = await response.json();
      setData(result);
    };
    fetchData();
  }, [userId]); // userId가 변경될 때만 실행

  if (!data) return <div>Loading...</div>;
  return <div>{data.name}</div>;
}

3. useContext

  • 컴포넌트 트리 안에서 전역적으로 데이터를 공유할 수 있게 합니다.
  • 사용법: const value = useContext(MyContext);
  • 예시:
const ThemeContext = React.createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme }}>I am styled by theme context!</button>;
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton />
    </ThemeContext.Provider>
  );
}

4. useReducer

  • 복잡한 상태 로직을 관리할 때 useState의 대안으로 사용됩니다.
  • 사용법: const [state, dispatch] = useReducer(reducer, initialArg, init);
  • 예시:
const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

5. useCallback

  • 콜백의 메모이제이션 된 버전을 반환합니다.
  • 불필요한 렌더링을 방지하는 데 유용합니다.
  • 사용법: const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
  • 예시:
function ParentComponent() {
  const [count, setCount] = useState(0);

  const incrementCount = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return <ChildComponent onIncrement={incrementCount} />;
}

6. useMemo

  • 계산 비용이 높은 함수의 결과값을 메모이제이션합니다.
  • 사용법: const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 예시:
function ExpensiveComponent({ a, b }) {
  const expensiveResult = useMemo(() => {
    // 복잡한 계산
    return a * b * Math.random() * 1000000;
  }, [a, b]);

  return <div>{expensiveResult}</div>;
}

7. useRef

  • 변경 가능한 ref 객체를 생성합니다. 주로 DOM 요소에 접근할 때 사용합니다.
  • 렌더링에 영향을 주지 않고 값을 저장할 때도 유용합니다.
  • 사용법: const refContainer = useRef(initialValue);
  • 예시:
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

8. useImperativeHandle

  • 부모 컴포넌트에 노출되는 인스턴스 값을 사용자화합니다.
  • forwardRef와 함께 사용됩니다.
  • 사용법: useImperativeHandle(ref, () => ({ /* 노출할 메서드들 */ }), []);
  • 예시:
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} />;
});

9. useLayoutEffect

  • DOM 변경 후 동기적으로 실행되는 효과를 위해 사용됩니다.
  • 레이아웃 측정 등에 사용됩니다.
  • 사용법: useLayoutEffect(() => { /* 효과 */ }, [의존성 배열]);
  • 예시:
function Tooltip() {
  const [tooltipHeight, setTooltipHeight] = useState(0);
  const tooltipRef = useRef();

  useLayoutEffect(() => {
    const height = tooltipRef.current.clientHeight;
    setTooltipHeight(height);
  }, []);

  return <div ref={tooltipRef}>Tooltip content</div>;
}

10. useDebugValue

  • React DevTools에서 사용자 정의 Hook에 대한 표시 라벨을 추가합니다.
  • 사용법: useDebugValue(value);
  • 예시:
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  // ... 친구의 온라인 상태를 추적하는 로직 ...
  useDebugValue(isOnline ? 'Online' : 'Offline');
  return isOnline;
}

11. useDeferredValue

  • 긴급하지 않은 부분의 재렌더링을 지연시킵니다.
  • 사용법: const deferredValue = useDeferredValue(value);
  • 예시:
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  // deferredQuery를 사용하여 결과를 렌더링
  // ...
}

12. useTransition

  • UI를 차단하지 않고 상태를 업데이트할 수 있게 합니다.
  • 사용법: const [isPending, startTransition] = useTransition();
  • 예시:
function App() {
  const [isPending, startTransition] = useTransition();
  const [count, setCount] = useState(0);

  function handleClick() {
    startTransition(() => {
      setCount(c => c + 1);
    });
  }

  return (
    <>
      {isPending && <Spinner />}
      <button onClick={handleClick}>{count}</button>
    </>
  );
}

13. useId

  • 클라이언트와 서버에서 안정적인 고유 ID를 생성합니다.
  • 접근성 속성에 유용합니다.
  • 사용법: const id = useId();
  • 예시:
function NameFields() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id + '-firstName'}>First Name</label>
      <input id={id + '-firstName'} type="text" />
      <label htmlFor={id + '-lastName'}>Last Name</label>
      <input id={id + '-lastName'} type="text" />
    </div>
  );
}

14. useSyncExternalStore

  • 외부 저장소를 구독할 때 사용합니다.
  • 동시성 모드에서 안전한 데이터 구독을 보장합니다.
  • 사용법: const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);
  • 예시:
const store = {
  state: { count: 0 },
  subscribers: new Set(),
  subscribe(callback) {
    this.subscribers.add(callback);
    return () => this.subscribers.delete(callback);
  },
  getSnapshot() {
    return this.state;
  },
  increment() {
    this.state = { count: this.state.count + 1 };
    this.subscribers.forEach(callback => callback());
  }
};

function Counter() {
  const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
  return <div>Count: {state.count}</div>;
}

15. useInsertionEffect

  • CSS-in-JS 라이브러리를 위해 설계된 Hook입니다. DOM 변경 전에 실행됩니다.
  • 일반적으로 라이브러리 작성자들이 사용합니다.
  • 사용법: useInsertionEffect(() => { /* 스타일 삽입 */ });
  • 예시:
// 이 예제는 실제 CSS-in-JS 라이브러리의 내부 구현을 간단히 모방한 것입니다.
function useCSS(rule) {
  useInsertionEffect(() => {
    const style = document.createElement('style');
    style.textContent = rule;
    document.head.appendChild(style);
    return () => document.head.removeChild(style);
  }, [rule]);
}

function MyComponent() {
  useCSS(`
    .my-component {
      color: red;
      font-size: 16px;
    }
  `);
  return <div className="my-component">Styled content</div>;
}

 

참조 : https://legacy.reactjs.org/docs/hooks-reference.html#usestate

 

가상 DOM과 key

  1. 가상 DOM의 역할:
    • React의 가상 DOM은 실제 DOM의 가벼운 복사본입니다.
    • 변경사항을 먼저 가상 DOM에 적용한 후, 실제 DOM과 비교하여 필요한 부분만 업데이트합니다.
  2. key의 목적:
    • key는 React가 어떤 항목이 변경, 추가 또는 제거되었는지 식별하는 데 도움을 줍니다.
    • key는 가상 DOM의 요소들을 구분하는 데 사용됩니다.
  3. index를 key로 사용할 때의 문제:
    • index를 key로 사용하는 것 자체가 가상 DOM의 재연산을 유발하지는 않습니다.
    • 문제는 리스트 항목의 순서가 변경되거나 항목이 추가/제거될 때 발생합니다.

index를 key로 사용할 때의 실제 문제

  1. 불필요한 리렌더링:
    • 리스트 항목의 순서가 변경될 때, index를 key로 사용하면 React는 많은 컴포넌트를 불필요하게 리렌더링할 수 있습니다.
    • 이는 성능 저하로 이어질 수 있습니다.
  2. 컴포넌트 상태 문제:
    • 각 리스트 항목이 자체 상태를 가지고 있을 때, index를 key로 사용하면 항목의 순서 변경 시 상태가 엉뚱한 컴포넌트와 연결될 수 있습니다.
  3. 재조정(Reconciliation) 문제:
    • React의 재조정 과정에서, index를 key로 사용하면 항목 추가/제거 시 React가 컴포넌트를 올바르게 식별하지 못할 수 있습니다.

예시

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>
          <input type="checkbox" />
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

이 예시에서, 만약 리스트의 첫 번째 항목이 제거되면:

  • 모든 나머지 항목의 index가 변경됩니다.
  • React는 모든 항목이 변경되었다고 판단할 수 있습니다.
  • 결과적으로, 모든 <li> 요소를 다시 렌더링할 수 있습니다.
  • 체크박스의 상태같은 내부 상태도 잘못 유지될 수 있습니다.

해결책

  1. 고유한 ID 사용:
    • 가장 좋은 방법은 각 항목에 고유한 ID를 부여하는 것입니다.
    • 예:
      function TodoList({ todos }) 
        return (
          <ul>
            {todos.map(todo => (
              <li key={todo.id}>{todo.text}</li>
            ))}
          </ul>
        );
      }
       

2. 안정적인 고유 값 생성:

  • 데이터에 고유 ID가 없는 경우, 안정적인 고유 값을 생성할 수 있습니다.
  • 예를 들어, 항목의 내용과 인덱스를 조합하여 사용할 수 있습니다
    :
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={`${todo.text}-${index}`}>{todo.text}</li>
      ))}
    </ul>
  );
}

3. 라이브러리 사용:

  • uuid나 nanoid 같은 라이브러리를 사용하여 고유 ID를 생성할 수 있습니다.
  • 예:

import { v4 as uuidv4 } from 'uuid';

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={uuidv4()}>{todo.text}</li>
      ))}
    </ul>
  )

주의사항

  • 인덱스를 key로 사용하는 것이 항상 잘못된 것은 아닙니다. 리스트가 정적이고 재정렬되지 않는 경우에는 인덱스를 사용해도 괜찮습니다.
  • 그러나 대부분의 경우, 고유한 ID나 안정적인 고유 값을 사용하는 것이 더 안전하고 효율적입니다.

왜 이 문제를 고민하게 되었는가?

평소에는 프론트엔드에서 API 요청을 보낼 때 useEffect, useState, 그리고 axios를 주로 사용했습니다. 사용해보니 대회나 사이트 운영 경험을 통해 동일한 요청이 반복적으로 발생하는 상황을 자주 접하게 되었습니다. 이러한 문제를 처리하기 위한 방법에 대해 고민을 하게 되었습니다.

 

 

어떻게 동일한 요청을 처리해야하는가?

일단 결론부터 말하자면 저같은 경우는 next, react, express를 주로 다루고 있습니다. next를 기준으로 설명을 하겠습니다.

next에는 경로 재검증이라는 기능이 따로 존재합니다. 경로 재검증이란 특정 경로에 대해 캐시된 데이터를 필요에 따라 제거할 수 있는 기능을 말합니다. 

 

 

이 이야기만 듣는 내심정...

 

데이터 캐싱 과정

1. Client에서 수많은 요청이 갑니다.

2. Server에서 api요청을 온걸 경로 재검증을 합니다.

3. 재검증 되었을때 HIT라고 판단되면 이제 응답 결과를 보내줍니다.

 

이렇게 밖에 설명을 못해 죄송해서 아래의 자세한 설명을 첨부했습니다.

 

 

이렇게 들으면 별로 처리하는 것 같지 않지만 아래의 Next사이트에서 가져온 그림을 보면 많은 일이 있는 걸 볼 수 있습니다.

 

데이터 캐싱과정

 

 

간단한 예시

client 코드

"use client";

const Page = () => {
  const handleTest = async () => {
    try {
      const res = await fetch("/api/data", {
        next: {
          revalidate: 1000,
        },
      }).then((res) => {
        console.log(res);
      });
    } catch (error) {
      console.log("Click", error);
    }
  };

  return <button onClick={() => handleTest()}>Page</button>;
};

export default Page;

 

server 코드 

import { revalidatePath } from "next/cache";

export const GET = async (request) => {
  try {
    const path = request.nextUrl.searchParams.get("path");

    if (path) {
      revalidatePath(path);
      return Response.json(
        { revalidated: true, now: Date.now() },
        { status: 200 }
      );
    }

    return Response.json(
      {
        revalidated: false,
        now: Date.now(),
        message: "Missing path to revalidate",
      },
      { status: 202 }
    );
  } catch (error) {
    return Response.json({ error: `Error: ${error.message}` }, { status: 500 });
  }
};

 

이렇게 간단한 예시 코드를 만들어봤고 코드에 대해 설명 드리겠습니다.

client측 코드에 보시면 

 const res = await fetch("/api/data", {
        next: {
          revalidate: 1000,
        },
      }).then((res) => {
        console.log(res);
      });

이 부분을 주로 보시면 되는데 fetch의 첫 번째 인자는 당연히 api주고입니다. 두 번쨰 인자는 next에서 시간 재검증이라는 캐싱처리 방법입니다. 설명을 드리자면  일정 시간 간격으로 데이터를 다시 검증하려면 리소스의 캐시 수명(초)을 설정입니다.

 

시간 기반 재검증 작동 방식은 아래의 사진과 같습니다.

 

  • 처음으로 가져오기 요청이 revalidate호출되면 외부 데이터 소스에서 데이터를 가져와 데이터 캐시에 저장합니다.
  • 지정된 시간 프레임(예: 60초) 내에 호출되는 모든 요청은 캐시된 데이터를 반환합니다.
  • 해당 기간이 지난 후, 다음 요청은 캐시된(이제는 오래된) 데이터를 반환합니다.
    • Next.js는 백그라운드에서 데이터의 재검증을 트리거합니다.
    • 데이터를 성공적으로 가져오면 Next.js가 데이터 캐시를 최신 데이터로 업데이트합니다.
    • 백그라운드 재검증이 실패하면 이전 데이터는 변경되지 않고 보관됩니다.

 

서버 측 코드에 대해 설명드리겠습니다.

서버 측 코드는 

 const path = request.nextUrl.searchParams.get("path");

    if (path) {
      revalidatePath(path);
      return Response.json(
        { revalidated: true, now: Date.now() },
        { status: 200 }
      );
    }


이 부분만 잘 보시면 됩니다. 서버에서 사용한 데이터 캐싱방법은 주문형 재검증입니다. 주문형 재검증이란 revalidatePath데이터는 경로( ) 또는 캐시 태그( revalidateTag) 를 통해 필요에 따라 다시 검증될 수 있습니다 .

 

주문형 재검증 작동 방식

  • 요청 이 처음 fetch호출되면 외부 데이터 소스에서 데이터를 가져와 데이터 캐시에 저장합니다.
  • 주문형 재검증이 트리거되면 해당 캐시 항목이 캐시에서 제거됩니다.
    • 이는 최신 데이터를 가져올 때까지 오래된 데이터를 캐시에 보관하는 시간 기반 재검증과 다릅니다.
  • 다음에 요청이 이루어지면 다시 캐시가 되고 MISS, 데이터는 외부 데이터 소스에서 가져와서 데이터 캐시에 저장됩니다.

 

먼가.... 업..그레이드 된 것 같은 나!!!

 

이렇게 같은 요청을 여러번 왔을 떄 데이터 캐싱 처리로하여 1번 api요청으로 서버의 비용을 대폭 줄이는 방식이 있어서 오늘 공부해 봤습니다. 다음에는 kafka와 redis를 공부해볼려고 합니다.

 

후... 작업이나 하러가자!!!

 

참고 사이트들

https://fe-developers.kakaoent.com/2024/240418-optimizing-nextjs-cache/

 

Next.js 캐싱으로 웹 서버 성능 최적화 | 카카오엔터테인먼트 FE 기술블로그

남윤복(kiwi) 초등학생 때부터 장래희망 칸에 프로그래머라고 적었는데, 그 이유를 개발자로 일하면서 찾아가고 있습니다.

fe-developers.kakaoent.com

https://nextjs.org/docs

 

Docs | Next.js

Welcome to the Next.js Documentation.

nextjs.org

 

 

create-cloudflare CLI(C3)을 사용하여 새 프로젝트 생성

create-cloudflare CLI(C3)은 Cloudflare Pages에 적합한 Next.js 사이트를 구성합니다. 터미널에서 다음 명령어를 실행하여 새 Next.js 사이트를 생성하세요:

npm create cloudflare@latest my-next-app -- --framework=next

C3은 일련의 설정 질문을 하고 필요한 종속성(예: Wrangler CLI 및 @cloudflare/next-on-pages 어댑터)을 설치합니다. 프로젝트 생성 후, C3은 기본 Next.js 템플릿을 사용하여 my-next-app 디렉토리를 생성하고 이를 Cloudflare Pages와 완벽히 호환되도록 업데이트합니다.

프로젝트 생성 시, C3은 초기 버전을 Direct Upload를 통해 배포할지 선택할 수 있습니다. 프로젝트 디렉토리에서 다음 명령어를 실행하여 언제든지 애플리케이션을 다시 배포할 수 있습니다:

npm run deploy

Git 통합

기존 프로젝트 또는 수동 프로젝트 설정 및 배포

이미 Next.js 프로젝트가 있거나 C3를 사용하지 않고 수동으로 프로젝트를 생성하고 배포하려는 경우, Cloudflare는 @cloudflare/next-on-pages를 사용하고 README 파일을 참조하여 프로젝트를 개발하고 배포하는 것을 권장합니다.

Git 통합 설정

Direct Upload 배포 외에도 Git 통합을 통해 프로젝트를 배포할 수 있습니다. Git 통합을 통해 GitHub 또는 GitLab 저장소를 Pages 애플리케이션에 연결하고 새로운 커밋이 푸시될 때마다 애플리케이션이 자동으로 빌드되고 배포됩니다.

기존 Pages 애플리케이션에는 Git 통합을 추가할 수 없으므로, 새로운 Pages 애플리케이션을 생성해야 합니다.

새 GitHub 저장소 생성

다음 명령어를 터미널에 입력하여 새 GitHub 저장소를 생성하고 로컬 애플리케이션을 GitHub에 푸시합니다:

git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin <https://github.com/>/
git push -u origin main

Cloudflare 대시보드를 통해 애플리케이션을 GitHub 저장소에 연결

  1. Cloudflare 대시보드에 로그인하고 계정을 선택합니다.
  2. 계정 홈에서 Workers & Pages > Create application > Pages > Connect to Git을 선택합니다.
  3. GitHub 계정에 대한 접근 권한을 승인합니다.
  4. 새 GitHub 저장소를 선택하고 다음 정보를 입력합니다:

설정 옵션 값

Production branch main
Build command npx @cloudflare/next-on-pages@1
Build directory .vercel/output/static

필요에 따라 프로젝트 이름을 사용자 정의할 수 있습니다. 기본값은 GitHub 저장소 이름입니다.

설정을 완료한 후 Save and Deploy를 선택합니다. Pages는 모든 종속성을 설치하고 프로젝트를 지정된 대로 빌드합니다. 새로운 커밋이 푸시될 때마다 프로젝트를 자동으로 재빌드하고 배포합니다.

Next.js 애플리케이션에서 바인딩 사용

바인딩을 통해 애플리케이션이 Cloudflare 개발자 제품(KV, Durable Objects, R2, D1 등)과 상호작용할 수 있습니다. 프로젝트에서 바인딩을 사용하려면 로컬 및 원격 개발을 위한 바인딩을 먼저 설정해야 합니다.

로컬 개발을 위한 바인딩 설정

C3로 생성된 프로젝트는 로컬 개발을 위한 바인딩이 기본적으로 설정되어 있습니다.

로컬 개발에서 바인딩을 사용하려면, 프로젝트의 wrangler.toml 파일을 기반으로 플랫폼 에뮬레이션을 설정하는 setupDevPlatform 함수를 사용합니다.

예를 들어, KV 바인딩을 로컬에서 사용하려면 Next.js 구성 파일에 다음을 추가합니다:

import { setupDevPlatform } from '@cloudflare/next-on-pages/next-dev'

if (process.env.NODE_ENV === 'development') {
  await setupDevPlatform()
}

const nextConfig = {}

export default nextConfig

wrangler.toml 파일의 루트에 KV 바인딩을 선언합니다:

name = "my-next-app"

compatibility_flags = ["nodejs_compat"]

[[kv_namespaces]]
binding = "MY_KV"
id = "<YOUR_KV_NAMESPACE_ID>"

배포된 애플리케이션에 바인딩 설정

배포된 애플리케이션에서 바인딩에 접근하려면 프로젝트 설정 페이지에서 필요한 바인딩을 구성하고 연결해야 합니다.

Typescript 프로젝트에 바인딩 추가

TypeScript 프로젝트에서 바인딩에 대한 타입 지원을 설정하려면 새 env.d.ts 파일을 생성하고 CloudflareEnv 인터페이스를 확장합니다.

interface CloudflareEnv {
  MY_KV: KVNamespace
}

바인딩을 애플리케이션에서 사용

로컬 및 원격 바인딩은 @cloudflare/next-on-pages에서 제공하는 getRequestContext 함수를 사용하여 접근할 수 있습니다.

import { getRequestContext } from '@cloudflare/next-on-pages'

export const runtime = 'edge'

export async function GET(request) {
  const myKv = getRequestContext().env.MY_KV
  const kvValue = await myKv.get('kvTest') || false
  return new Response(`The value of kvTest in MY_KV is: ${kvValue}`)
}

Image 컴포넌트

Cloudflare 네트워크는 Vercel 네트워크와 동일한 이미지 최적화 지원을 제공하지 않습니다. <Image /> 컴포넌트를 사용하려면 적절한 로더를 설정해야 합니다.

추천 개발 워크플로우

Cloudflare는 next-on-pages 애플리케이션을 개발할 때 다음 워크플로우를 권장합니다:

  1. 표준 Next.js 개발 서버 사용
  2. 로컬에서 애플리케이션 빌드 및 미리보기
  3. 애플리케이션 배포 및 반복

문제 해결

다음은 next-on-pages를 사용하여 Next.js 애플리케이션을 개발할 때 발생할 수 있는 일반적인 실수와 문제를 해결하는 방법입니다.

Edge 런타임

모든 서버 사이드 라우트는 Edge 런타임 라우트로 구성해야 합니다. 각 서버 사이드 라우트에 export const runtime = 'edge'를 추가해야 합니다.

Not Found 페이지

Next.js는 빌드 과정에서 자동으로 not-found 라우트를 생성합니다. 이러한 라우트가 서버 사이드 로직을 필요로 하면 Node.js 서버리스 함수를 생성할 수 있습니다. 이를 방지하려면 custom not-found 라우트를 제공하고 edge 런타임을 명시적으로 선택해야 합니다.

export const runtime = 'edge'

export default async function NotFound() {
  return (
    // ...
  )
}

generateStaticParams

정적 사이트 생성(SSG) 시 Next.js는 기본적으로 비정적 생성 라우트를 Node.js 서버리스 함수를 통해 처리합니다. 이를 방지하려면 edge 런타임을 선택하거나 dynamicParams를 false로 지정해야 합니다.

상위 수준의 getRequestContext

getRequestContext 함수는 라우트 파일의 최상위 수준에서 호출할 수 없습니다. 요청 처리 과정 내에서 호출해야 합니다.

Learn More

이 가이드를 완료하면 Next.js 사이트를 Cloudflare Pages에 성공적으로 배포한 것입니다. 다른 프레임워크 시작을 위해서는 프레임워크 가이드를 참조하세요.

 

https://developers.cloudflare.com/pages/framework-guides/nextjs/deploy-a-nextjs-site/

 

Full-stack deployment · Cloudflare Pages docs

Deploy a full-stack Next.js site (recommended).

developers.cloudflare.com

 

소개

useState & useEffect 와 react-query의 장단점을 비교하고, Next.js 환경에서도 왜 react-query를 사용하는 것이 좋은지 쉽게 설명하겠습니다. 기본적으로 코드를 먼저 보겠습니다.

 

1. 기존 useState + useEffect로 가져오는 방식

"use client";
import React, { useEffect, useState } from "react";
import { todo } from "./type";
import axios from "axios";

const Before = () => {
  const [todo, setTodo] = useState<Array<todo>>([]);

  useEffect(() => {
    const getData = async () => {
      const res = await axios.get("http://localhost:3001/posts");

      setTodo(res.data);
    };
    getData();
  }, []);

  if (todo.length === 0) {
    return <div>Loading...</div>;
  }

  return (
    <section>
      <h2>Before : useState + axios</h2>
      {todo &&
        todo.map((item, index) => {
          return (
            <ul key={index} className=" flex gap-[20px]">
              <li>{item.id}</li>
              <li>{item.title}</li>
              <li>{item.body}</li>
            </ul>
          );
        })}
    </section>
  );
};

export default Before;

 

2. useQuery를 사용해서 가져오는 방식

"use client";
import React from "react";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { todo } from "./type";

const fetchTodos = async (): Promise<todo[]> => {
  const res = await axios.get("http://localhost:3001/posts");
  return res.data;
};

const Before = () => {
  const {
    data: todo,
    isLoading,
    isError,
    error,
  } = useQuery({ queryKey: ["todos"], queryFn: fetchTodos });

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (isError) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <section>
      <h2>Before : useState + axios</h2>
      {todo &&
        todo.map((item, index) => (
          <ul key={index} className="flex gap-[20px]">
            <li>{item.id}</li>
            <li>{item.title}</li>
            <li>{item.body}</li>
          </ul>
        ))}
    </section>
  );
};

export default Before;

useState와 useEffect로 데이터 가져오기

장점:

  1. 간단함: 사용하기 쉽고 이해하기 쉬워서 초보자도 금방 배울 수 있습니다.
  2. 직접 제어 가능: 데이터와 상태를 직접 제어할 수 있어서 원하는 대로 조정할 수 있습니다.

단점:

  1. 반복적인 코드: 로딩 중이거나 에러가 발생했을 때 처리를 매번 직접 써야 합니다.
  2. 복잡해질 수 있음: 웹사이트가 커지면 여러 데이터와 상태를 관리하는 것이 점점 어려워집니다.
  3. 캐싱 없음: 데이터를 저장해두는 기능이 없어서, 똑같은 데이터를 여러 번 가져올 수 있습니다.
  4. 백그라운드 업데이트 없음: 화면을 다시 열 때 자동으로 데이터를 새로 가져오려면 추가적인 코드를 작성해야 합니다.

react-query로 데이터 가져오기

장점:

  1. 간편한 데이터 가져오기: 로딩 중이거나 에러가 발생했을 때 처리를 자동으로 해줍니다.
  2. 자동 캐싱: 데이터를 자동으로 저장해두어서, 똑같은 데이터를 다시 가져오지 않습니다.
  3. 백그라운드 업데이트: 화면을 다시 열 때 자동으로 데이터를 새로 가져와서 항상 최신 상태로 유지합니다.
  4. 자동 재요청: 인터넷이 끊겼다가 다시 연결되면 자동으로 데이터를 새로 가져옵니다.
  5. 개발자 도구: 데이터를 어떻게 가져오고 있는지 쉽게 확인할 수 있는 도구를 제공합니다.

단점:

  1. 배우기 조금 어려움: 새로운 개념을 배우는 것이 처음에는 어려울 수 있습니다.
  2. 파일 크기 증가: 웹사이트의 크기가 조금 커질 수 있습니다.

Next.js 애플리케이션에서 react-query를 사용해야 하는 이유

Next.js는 서버에서 데이터를 가져오는 기능이 있지만, react-query가 제공하는 모든 기능을 포함하지는 않습니다. 여기 react-query가 Next.js를 도와주는 이유가 있습니다:

  1. 수분 공급 (Hydration): react-query는 서버에서 가져온 데이터를 클라이언트에서도 잘 사용할 수 있게 해줍니다.
  2. 클라이언트 상태 관리: Next.js는 서버에서 데이터를 잘 가져오지만, react-query는 화면을 다시 열 때 자동으로 데이터를 새로 가져오는 기능을 제공합니다.
  3. 최적화된 성능: react-query는 데이터를 저장해두고 자동으로 업데이트해서, 웹사이트가 빠르고 반응이 좋습니다.
  4. 사용 편리성: react-query는 데이터를 가져오고 관리하는 일을 더 쉽게 만들어줍니다.

결론

useState와 useEffect는 기본적인 데이터 가져오기를 제공하지만, react-query는 더 강력하고 확장 가능한 도구입니다. 반복적인 일을 자동으로 해주고 성능을 향상시키며 개발 과정을 더 쉽고 빠르게 만듭니다. Next.js 애플리케이션에서도 react-query는 서버와 클라이언트에서 데이터를 잘 관리할 수 있게 도와줍니다. 복잡한 웹사이트를 만들 때, react-query를 사용하면 더 쉽고 빠르게 만들 수 있습니다.

 

참조 사이트

https://qiita.com/75ks/items/d5d5bfe21a3e8bb964ae

 

Next.js 13 & TanStack Query(React Query) の注意点 - Qiita

はじめにNext.js 13とTanStack Query(React Query)を使ってハマってしまった部分があったので、備忘録として投稿します。現在(2023/08)は、日本語の記事がまだ…

qiita.com

https://zenn.dev/noko_noko/articles/fd8a10c14de9c3

 

[Next.js] Tanstack Query の SSR での利用法

概要 今回は実務の中で、Tanstack Query を用いた SSR の実装を行ったので、その使用方法を記事としてまとめていこうと思います。 基本的には公式のドキュメントを参照しながらまとめたため、

zenn.dev

https://tanstack.com/query/v5/docs/framework/react/guides/query-functions

 

Query Functions | TanStack Query React Docs

Does this replace [Redux, MobX, etc]? react

tanstack.com

https://zenn.dev/akineko/articles/8c047c3473aed0

 

React Query-基礎知識編

青い炎のC++er / オンラインゲームのサーバーエンジニアを経て現在 Web エンジニア / TypeScript / C++ / Go / Rust / Neovim / GCP

zenn.dev

https://qiita.com/taisei-13046/items/05cac3a2b4daeced64aa

 

React Queryはデータフェッチライブラリではない。非同期の状態管理ライブラリだ。 - Qiita

はじめにこの記事はDominikさんが執筆された「Thinking in React Query」を参考にReact Queryの考え方をまとめたものになります。DominikさんはTanStac…

qiita.com

 

+ Recent posts