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

 

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

평소에는 프론트엔드에서 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

 

1. Electron 프레임워크 기술에 대한 소개

Electron은 웹 기술(HTML, CSS 및 JavaScript)을 사용하여 데스크톱 애플리케이션을 개발할 수 있게 해주는 오픈 소스 프레임워크입니다. 주로 웹 개발자들이 데스크톱 애플리케이션을 만들 때 사용하며, 크로스 플랫폼(Windows, macOS, Linux)에서 동작하는 애플리케이션을 구축할 수 있도록 도와줍니다.

 

장점 : 크로스 플랫폼 개발,  웹 기술 활용,  네이티브 기능 접근,  개발자 도구와 생태계

1. 크로스 플랫폼 개발: Electron을 사용하면 한 번의 개발로 여러 플랫폼에서 동작하는 애플리케이션을 만들 수 있습니다. 이는 개발 비용과 시간을 절약해줍니다.

2. 웹 기술 활용: 웹 개발자들은 이미 익숙한 HTML, CSS 및 JavaScript를 사용하여 데스크톱 애플리케이션을 개발할 수 있습니다. 이로 인해 웹 개발자들이 데스크톱 애플리케이션 개발에 빠르게 적응할 수 있습니다.

3. 네이티브 기능 접근: Electron은 Node.js를 통해 네이티브 기능에 접근할 수 있습니다. 이를 통해 데스크톱 애플리케이션은 파일 시스템, 네트워크 등 다양한 기능을 활용할 수 있습니다.

4. 개발자 도구와 생태계: Electron은 활발한 개발자 커뮤니티와 풍부한 생태계를 가지고 있습니다. 다양한 확장 패키지와 도구들을 활용하여 개발 및 디버깅을 더 효율적으로 수행할 수 있습니다.


단점 : 성능과 메모리 사용,  앱 크기,  보안과 취약점,  랜더러 프로세스 분리

1. 성능과 메모리 사용: Electron 애플리케이션은 Chromium 엔진과 Node.js 런타임을 함께 사용하기 때문에 상대적으로 메모리를 많이 사용하고 시작 속도가 느릴 수 있습니다. 특히 경량화가 필요한 애플리케이션에는 적합하지 않을 수 있습니다.

2. 앱 크기: Electron 애플리케이션은 Chromium과 Node.js를 함께 포함하기 때문에 파일 크기가 상대적으로 큽니다. 이로 인해 다운로드 및 설치 시간이 길어질 수 있습니다.

3. 보안과 취약점: Electron은 Chromium 엔진의 버전을 사용하며, 이에 따라 웹 브라우징과 관련된 보안 취약점이 영향을 미칠 수 있습니다. 주기적으로 업데이트를 수행하여 보안 문제를 관리해야 합니다.

4. 렌더러 프로세스 분리: Electron의 렌더러 프로세스 분리는 안정성과 보안을 향상시키지만, 애플리케이션 전체 구조를 복잡하게 만들 수 있습니다.

결론적으로, Electron은 웹 개발자들이 데스크톱 애플리케이션을 쉽게 개발하고 배포할 수 있도록 도와주는 강력한 프레임워크이지만, 성능과 메모리 사용, 앱 크기 등의 측면에서 고려해야 할 점들도 존재합니다. 

 

2. 구현 코드

사용한 기술스텍 : React, Electron, Css 사용.

const {app,BrowserWindow} = require('electron');
 
const remote = require('@electron/remote/main')
remote.initialize()
 
function createWindow() {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            enableRemoteModule: true
        }
    })
 
    win.loadURL('http://localhost:3000')
 
    remote.enable(win.webContents);
}
 
app.on('ready', createWindow)
 
app.on('window-all-closed', function() {
    if(process.platform !== 'darwin') {
        app.quit()
    }
})
 
app.on('activate', function() {
    if(BrowserWindow.getAllWindows().length === 0) createWindow()
})


1. app은 애플리케이션의 생명 주기와 관련된 이벤트와 동작을 관리하며, BrowserWindow는 브라우저 창을 생성하고 제어하는 데 사용됩니다.

2. const remote = require('@electron/remote/main') : 렌더러 프로세스에서 메인 프로세스의 모듈과 상호 작용할 수 있게 해줍니다.

3. remote.initialize() : Electron Remote 모듈을 초기화합니다.

4. createWindow() : 함수는 브라우저 창을 생성하고 설정하는 역할을 합니다. BrowserWindow객체를 생성하여 창의 크기와 웹 페이지 렌더링 설정을 지정합니다.

6. win.loadURL('http://localhost:3000'): 생성한 브라우저 창에 지정된 URL(`http://localhost:3000`)을 로드합니다.

7. remote.enable(win.webContents) : 생성한 브라우저 창의 웹 컨텐츠를 원격으로 제어할 수 있도록 활성화합니다.

8. app.on('ready', createWindow) : 애플리케이션이 준비되었을 때 createWindow()를 호출하여 초기 창을 생성합니다.

9. app.on('window-all-closed', function() { ... }) : 모든 창이 닫혔을 때 애플리케이션을 종료합니다. 다만, macOS의 경우에는 마지막 창이 닫혀도 애플리케이션은 종료되지 않도록 합니다.

10. app.on('activate', function() { ... }) : 애플리케이션이 활성화되었을 때, 창이 없으면 새 창을 생성합니다. 

 

부가설명 => darwin : macOS,   win32 : Windows,   linux : Linux

 

3. 구현 영상

 

Electron은 서버냐? 클라이언트냐?

배열

배열이란?

  • 배열이란 쉽게 말하면 창고(저장장소)라고 생각하면 된다.

배열 선언하는 방법은 2가지로 나눠진다.

  1. 리터널 방식으로 만들기
  2. Array 객체의 인스턴스로 만들기

예로 한번 살펴보자!

1. 리터널 방식
var numbers = ['one', 'two' , 'three'];
numbers 

결과값 : ['one', 'two' , 'three']

2. Array 객체의 인스턴스
var numbers = new Array("one", "two", "three");
number

결과값 : ["one", "two" , "three"]

배열 호출하는 방법

위의 예제를 재활용하여 설명하겠습니다.

var numbers = ['one', 'two' , 'three'];

numbers의 첫 번째 저장장소에는 'one'이 있습니다. 사람들이라면 그러면 1번쨰 저장장소에는 'one'이
있네요라고 할 겁니다.

하지만 컴퓨터는 첫 번째 장소라고 하지않고 0번째 저장장소라고한다. 이유는 컴퓨터가 0부터
숫자를 시작하기 때문이다.

고로 numbers 0번째는 'one'이 된다.

그러면 'three'는 몇 번째일까?
답은 numbers의 2번째가 'three'가 된다.

호출방법 : 변수명 [ 인덱스(저장위치) ]

var numbers = ['one', 'two' , 'three'];로 호출해보자

'one'을 호출할려고 하면 변수명 number이고 인덱스(저장위치)가 0번째이였으니 number[0]을 입력하면
'one'이 호출된다.

concat

concat

  • 기존의 배열에 또 다른 배열이나 값을 합쳐서 새로운 배열을 만드는 함수
  • 원본(원래 배열)을 건들이지 않는다!
var nums = ["1","2","3"];
var chars = ["a","b","c","d"];
nums.concat(chars);  //nums 배열에 chars 배열 연결

결과값 : ["1", "2", "3", "a", "b", "c", "d"]

join

join

  • 배열  요소를 연결해서 나열할 때 각 요소 사이에 넣을 구분 기호가 필요한데  join() 은 이 기호를 직접  지정할 수 있음
  • 원본(원래 배열)을 건들이지 않는다!

예)

var num = [1,2,3,4]; 
num.join('-');

결과값 : "1-2-3-4";

결과값처럼 배열의 요소 사이사이에 넣을 수 있다.

num.join('');

결과값 : "1234";

또한 요소없이도 붙일 수 있다.

push와 pop

push

  • push는 배열의 맨 끝에 새로운 요소를 추가하는 함수
  • 새 요소가 추가된 후의 배열 요소의 개수가 반환됨
  • concat( ), join( ) 과 다르게  원본(원래 배열)이 바뀜
var nums = ["1","2","3"]
nums.push("4") //nums 배열 맨 끝에 "4"와 "5"요소 추가
nums //호출

결과값 : ["1", "2", "3", "4"]

원본이 바뀌어 호출이 추가된 상태로 나옴

pop

  • pop은 배열의 맨 끝에 요소를 제거하는 함수
  • 맨 끝 요소가 제거된 후의 배열 요소의 개수가 반환됨
  • push와 같은 원본(원래 배열)이 바뀐다.
var nums = ["1", "2", "3", "4"]
nums.pop("4") //nums 배열 맨 끝에 "4"와 "5"요소 추가
nums //호출

결과값 : ["1","2","3"]

원본이 바뀌어 호출이 됨

unshift와 shift

unshift

  • 배열의 맨 앞 에 새로운 요소를 추가하는 함수
  • 새 요소가 추가된 후의 배열 요소의 개수가 반환됨
  • 원본(원래 배열)이 바뀐다.
var nums = ["1", "2", "3"]
nums.unshift("4") //nums 배열 맨 끝에 "4"와 "5"요소 추가
nums //호출

결과값 : ["4", "1", "2", "3"]

shift

  • 배열의 맨 앞 요소를 제거하는 함수
  • 맨 앞 요소가 제거된 후의 배열 요소의 개수가 반환됨
  • 원본(원래 배열)이 바뀐다.
var nums = ["4", "1", "2", "3"]
nums.shift("4") //nums 배열 맨 끝에 "4"와 "5"요소 추가
nums //호출

결과값 : ["1", "2", "3"]

splice, slice, substr

splice

  • splice는 배열의 중간 부분에 요소를 추가하거나 삭제할 때 사용
  • 한꺼번에 2개 이상의 요소 를 추가하거나 삭제할 때도 사용
  • 원본(원래 배열)이 바뀐다.
  1. 인수가  **** 1 개일 때
    • 괄호 안의 인수는 그 배열의 인덱스 값을 가리킨다. 이 때  인수가 가리키는 인덱스의 요소 ~ 배열의 끝 요소까지 삭제 함
var numbers = [0,1,2,3,4,5];
numbers.splice(2); //인덱스 2(세 번째 요소)부터 끝까지 삭제

결과값 : [2, 3, 4, 5]

numbers;

결과값 : [0, 1] //원본이 바뀐다.
  1. 인수가 2 개 일 때
    • splice(첫번째 인수 : 인덱스 값, 두번째 인수 :  삭제할 개수)
var study = ["html", "css", "web", "jQuery"];
study.splice(2,1);

결과값 : ["web"];

study

결과값 : ["html", "css", "jQuery"];
  1. 인수가 3개 이상일 때
    • splice(첫번째 인수 : 인덱스 값, 두번째 인수 :  삭제할 개수, 세번째 인수 : 앞서 삭제한 위치에 새로 추가할 요소를 지정)
study.splice(2, 1, "js");

결과값 : ["jQuery"]

study

결과값 : ["html", "css", "js"]
  • 기존 배열의 요소를 삭제하지 않고 새로운 요소 추가할 때는 삭제할 개수를 지정하는 두 번째 인수에 0을 입력
study.splice(2, 0, "jQuery")

결과값 : []

study

결과값 : ["html", "css", "jQuery", "js"]

slice

  • slice는 여러 개의 요소를 꺼낸다 ( 삭제 X)
  • 배열에서 요소를 꺼낸다는 점은 pop(), shift() 와 같음
  • 첫번째 인수 : 시작 인덱스 값, 두번째 인수 : 끝 인덱스 값 -1 ( 끝 인덱스 값의 직전 까지만 추출 )
  • 원래 배열은 변경되지 않음 =>  진짜 ' 추출 ' 만 함
var colors = ["red", "green", "blue", "white", "black"];
colors.slice(2);

결과값 : ["blue", "white", "black"]

colors

결과값 : ["red", "green", "blue", "white", "black"]

연습문제

위에 연습했던 colors 배열에서 “blue”와 “white”만 추출하려면 slice() 함수를 어떻게 지정해야 할까요

정답 : colors.slice(2,4);

결과값 : ['blue', 'white']

데이터 타입

출처 : https://boycoding.tistory.com/4

객체 모델링

  • 객체란?
  • 객체(Object) : 복합 자료형
    ⇒ 객체 안에 숫자, 문자열 등 여러가지 자료형이 포함되기 때문
    ⇒ 자바스크립트에서 객체는 자료를 저장하고 처리하는 기본 단위
    ⇒ 하나의 변수에 다양한 정보를 담기 위해 사용하는 자료형
        ex) 책(객체) - 제목, 이 책의 분야, 저자, 쪽수, 가격 출간일 등의 정보들이 들어있음
  • 자바스크립트 객체출처 : 
    • 사용자 정의 객체 : 사용자가 필요할 때마다 자신의 객체를 정의해서 사용하는 객체 ex) Car(), House(), Hotel()
    • 내장 객체 : 자바스크립트 프로그램 자체에서 정의하여 사용자에게 제공하는 객체 ex) Object(), Array(), Date()
      • 브라우저 객체 모델(BOM, Browser Object Model)
        - 웹 브라우저의 주소 표시줄이나 창 크기 등 웹 브라우저 정보를 객체로 다루는 것
        예)
        Navigator : 사용 중인 브라우저 종류나 버전을 담은 객체
        History : 브라우저에서 방문한 기록을 남기는 객체
        Location : 주소 표시줄 정보를 담고 있는 객체
        Screen : 화면 크기 정보가 들어있는 객체

      • 문서 객체 모델(DOM, Document Object Model)
        - 웹 문서 안에 포함된 이미지, 링크, 텍스트 필드 등도 모두 각 별도의 객체로 미리 존재함 예) Document, Image 등
      • 전역 자바스크립트 객체(Global Javascript Object)
        - 자바스크립 프로그램 전체에서 사용하는 내장 객체
  • 객체의 속성과 메서드
    • 속성(Property) : 객체에서 값을 담고 있는 정보
      - 내장 객체에도 만들어져 있음
      - 객체의 속성 값을 가져올 때는 객체 이름 뒤에 마침표(.)를 찍고 그 뒤에 속성명을 적음
    navigator.vendor
    => "Google Inc"
    
    • 메서드(Method) : 객체가 어떻게 동작할지 선언해 놓은 함수
      - 객체의 메서드를 사용할 때도 마침표(.)를 사용 예) window.alert("안녕하세요")
    • 프로토타입(Prototype) : 객체를 다루기 위한 기본틀
      예) Image 객체는 모든 웹 이미지가 공통으로 가지는 속성과 기능을 모아놓은 것
    • 인스턴스(Instance), 개별 객체 : 프로토타입을 사용해 만들어낸 객체
      - 예) 웹 문서에 3개의 이미지를 포함시켜야 한다면 Image 객체를 사용해서 똑같은 모양의 객체 3개를 찍어낸 다음 객체마다 원하는 이미지를 담는 것
      - 인스턴스 만들기
         객체를 똑같이 찍어 새 객체를 만들 때는 new 예약어 사용
          * new 예약어 뒤에 프로토타입 객체 이름과 괄호()를 써주면 됨

                  여기서 time은 Date 객체의 인스턴스 => Date 객체에서 정의한 속성과 함수를 모두 사용할 수 있음

  • 사용자 정의 객체 만들기
    • 리터널 표기법을 사용해 객체를 만드는 방법
    • 리터널(Literal) : 자료를 표기하는 방식 프로그래밍 언어 전체에서 사용하는 방식
      • 정해진 값을 가진 객체를 한 번만 만들어 냄 ⇒ 객체 틀을 만들지 않고 개별적으로 객체를 선언하고 사용하는 방법
      • 중괄호{}안에 '속성 이름 : 값'을 하나의 쌍으로 지정
      • 속성이 많다면 '속성명 : 값'마다 쉼표(,)를 넣어 구분
      • 함수 또한 '함수명 : function(){…}'로 정의
        ⇒ 객체를 정의해 변수에 할당할 때는 닫는 중괄호{} 다음에 세미클론(;)을 붙여줌
        예)

  • 생성자 함수 : 객체를 만들어 내는 함수
    • function 예약어를 사용
    • 생성자 함수 안에 객체의 속성과 함수를 정의할 때는 this 예약어 다음에 마침표와 속성명 입력 ⇒ this가 가르키는 것은 선언하고 있는 객체 자체
    • 객체의 속성은 객체에서 사용하는 변수
    • 객체의 함수는 해당 객체에서 사용하는 함수

 

노션에 정리한걸 옮기는데 잘 안옮겨지네요 ㅠㅠ

+ Recent posts