리액트 미래

리액트 미래 v19

새로운 Hook, API, 리엑트 컴파일러, 리엑트 서버 컴포넌트

  • useMemo, useCallback, memoReact Compiler

  • forwardRefref is a prop

  • React.lazyRSC, promise-as-child

  • useContextuse(Context)

  • throw promiseuse(promise)

  • <Context.Provider><Context>

새로운 훅과 API

  • 시간이 걸릴 수 있는 상태 업데이트에 대기 표시

  • 해당 훅을 활용해서 비동기로 데이터를 가져오는 동안 로딩 인디케이터나 플레이스홀더의 렌더링을 관리할 수 있음

    • 직접 대기/로딩 상태 속성을 만들고 관리하지 않아도 된다.

  • 전환 진행 여부를 나타내는 '대기' 플래그를 제공함

const Component = () => {
  const [formState, setFormState] = useState(...)
  const [isPending, startTransition] = useTransition();
  
  const formAction = (e) => {
    e.preventDefault();
    
    startTransition(async () => {
      try {
        const result = await submitForm();
        setFormState(result);
      } catch (err) {
        setFormState({
          message: "Failed to complete action"
        })
      }
    })
  }
  
  return (
    <form onSubmit={formAction}>
      {isPending ? <h4>pending...</h4> : null}
      {formState?.message && (
        <h4>{formState.message}</h4>
      )
    </form>
  )
}

비동기 전환을 사용하는 함수는 이제 액션이라 부른다.

액션을 관리하는 훅들이 다수 존재함 (useActionState, ...)

폼 액션의 결과에 기반해 상태를 업데이트 가능

  • 매개변수

    • 하나의 액션 함수: 이 함수는 폼 액션이 트리거되면 실행됨

    • 하나의 초기 상태 객체: 사용자 상호작용이 일어나기 이전의 시작 상태를 설정

    • (옵션) 하나의 영구 링크: 이 링크는 이 폼이 수정할 고유한 페이지 URL을 가리킴

  • 반환값 (튜플)

    • 폼의 현재 상태

    • 폼 액션을 트리거하는 함수

    • 액션의 대기 여부를 나타내는 하나의 불리언값

import { useActionState } from 'react';

export const Component = () => {
  const [state, dispatch, isPending] = useActionState(action, initialState, permalink)
  //...
}

<form> 액션

<form>태그는 이제 action prop을 가짐

폼이 제출되었을 때 트리거되는 액션 함수

import { useActionState } from 'react';

const submitForm = async () => {/.../}

const action = async (currentState, formData) => {
  try {
    const result = await submitForm(formData);
    
    return { message: result };
  } catch {
    return { message: "Failed to complete action" }
  }
}

export const Component = () => {
  const [state, dispatch, isPending] = useActionState(action, null);
  
  return (
    <form action={dispatch}>
      <input {...} disabled={isPending} />
      <button type="submit" disabled={isPending}>
        Add todo
      </button>
      
      {state.message && <h4>{state.message}</h4>}
    </form>
  )
}

중첩된 컴포넌트 안에서의 폼 제출로부터 상태 정보에 접근할 수 있도록 디자인됨 (useContext 같은 너낌)

  • Context API를 사용하여도 폼 상태나 다른 데이터를 자손으로 전달할 수 있긴하지만 "action은 이런식으로 사용하라"라고 리엑트답지 않게 만들어둔것 같다.

  • 상위에서 <form action={action}>, 하위에서 상위 액션의 상태를 가져오기 위해 사용되는 훅

  • useFormStatus를 사용하면 직접 컨텍스트를 설정하지 않고 폼과 관련된 상태 관리를 직접적으로 간소화 가능

import { useFormStatus } from 'react';

export const NestedComponent = () => {
  const { pending, data, method, action } = useFormStatus();
  
  return (...)
}


export const App = () => {
  return (
    <form action={action}>
      <NestedComponent />
    </form>
  );
}

useOptimistic with 낙관적 업데이트

낙관적 업데이트란 비동기 조작과 UI 업데이트가 해당 조작의 실제 성공을 확인하기 전 이미 성공한것으로 가정

  • 사용자 경험 향상 기법 중 하나

  • 사용자 인터렉션 후 서버 응답을 기다리지 않고 결과를 사용자에게 즉각적으로 반영하는 것

  • 업데이트가 종료되거나, 에러가 발생하면 리엑트는 자동으로 템플릿에서 사용된 optimisticMessagemessage prop의 값으로 바꾼다.

export const Component = ({
  message,
  updateMessage
}) => {
  const [inputMessage, setInputMessage] = useState("");
  
  const [optimisticMessage, setOptimisticMessage] = useOptimistic(message);
  
  const submitForm = async (e) => {
    e.preventDefault();
    const newMessage = inputMessage;
    
    // API 변경 트리거 전 낙관적으로 값을 설정
    setOptimisticMessage(newMessage);
    
    // API 제출이 해결되면 상태를 업데이트
    const updatedMessage = await submitToAPI(newMessage);
    updateMessage(updatedMessage);
  }
  
  return (
    <form onSubmit={submitForm}>
      {/*낙관적 값 보여주기*/}
      <p>{optimisticMessage}</p>
      {...}
    </form>
  )
}

use API

컨텍스트, 프로미스 같은 리소스로부터 값을 읽을 수 있는 다양한 방법 제공

  • <Context.Provider> 를 사용하지 않고 <Context> 를 직접 렌더링할 수 있게 변경됨

  • 더이상 컨텍스트 값을 읽기위해 useContext 훅을 사용하지 않고 use() 사용

  • use()는 서스펜스 및 에러바운더리와 매끄럽게 통합되어 프로미스를 읽을 수 있음

const MessageContext = createContext();

const App = () => {
  const [message, setMessage] = useState("world");
  
  return (
    <MessageContext value={{ message, setMessage }}>
      {...}
    </MessageContext>
  )
}


// use
const NestedChild = () => {
  const { message, setMessage } = use(MessageContext);
}

리엑트 컴파일러

리액트 팀이 만든 실험적 컴파일러

최적화 프로세스를 자동화함으로써 앱 성능을 크게 개선하는것을 목적으로 함

  • 성능 튜닝의 책임을 오롯이 개발자가 지는게 아닌 프레임워크도 어느정도 지는 식 (패러다임 이동을 의미)

  • 리액트 컴파일러는 리액트 규칙 을 이해하며, 고급 정적 분석을 활용하여 컴포넌트와 리액트 앱의 훅 사이에 메모제이션을 지능적으로 적용함

    • 컴포너트가 상태 및 props를 사용하는 것을 분석하여 효과적으로 리렌더링을 최적화함

    • 실제로 변경되는 부분을 의존하는 컴포넌트들만 리렌더링하도록 지능적으로 결정

    • 리엑트 문서에는 이를 세분화된 반응이라 부름

  • 무엇을 메모화해야하는지에 대한 의사 결정을 자동화!

  • 최소한의 노력으로 최소한의 성능 보장

  • 가장 큰 영향을 미치는 부분에 메모이제이션이 정밀하고 효율적으로 적용되는 것을 보장

  • 리엑트 컴파일러는 useEffect 안에서의 자동 메모이제이션을 처리하지 않음 (연구 및 개발은 현재 진행형이라고 함)

등장 배경

  • 리액트를 사용하는 개발자들은 memo, useMemo, useCallback 을 사용해서 불필요한 리렌더링, 계산을 방지하였는데 이 기법들은 효과적이지만 개발자가 직접 무엇을 언제 메모화할것인지 결정해야만 했음

  • 이 수동 프로세스는 시간도 들고 휴먼에러(실수)가 발생될 수 있는 여지가 있고, 잘못 사용하면 성능을 저하시킬수 있었음

메모이제이션

값비싼 함수 호출의 결과를 저장한 뒤 동일한 입력이 발생했을 때 캐시해 둔 결과를 반환함으로써 컴퓨터 프로그램의 속도를 높임

  • 선언적 모델

  • 컴포넌트 기반 아키텍처를 위한 DOM의 직접적인 조작을 추상화함으로써 개발자들은 UI를 일련의 명령적 업데이트가 아닌 상태의 반영으로 생각할 수 있게 됨

  • 멘탈 모델, 특히 앱 복잡성이 증가할 때의 멘탈 모델을 크게 단순화시켰음

    • 사고방식

  • useEffect의 경우 의존성 배열에 의해 본질적으로 메모화되고 컴포넌트가 마운트 된 뒤 한 번만 실행되는것이 보장됨

  • memo의 경우 컴포넌트를 메모화하여 부모가 리렌더링되었을 때가 아닌 각 컴포넌트의 속성이 변경되었을 때만 리렌더링되는것을 보장

  • useMemo는 컴포넌트 안의 모든 계산된 값을 메모화할 수 있음

  • useCallback은 컴포넌트 안의 함수 참조를 메모화

외부 함수 메모이제이션

컴포넌트 내부에서 사용되는 값비싼 외부 함수의 메모이제이션도 해결함 (컴포넌트 외부에서 선언된 함수ㅇㅇ)

  • 함수 자체를 메모이제이션 하는것은 아님

    • 리엑트는 컴포넌트와 훅만 메모화함

  • 함수 호출을 메모화함

리엑트 컴파일러 핵심 가정

리액트 컴파일러는 앱을 자동으로 최적화하는 한편, 처리 대상 코드에 대한 몇가지 핵심적인 가정에 의존함

컴파일러의 기능을 최대한 활용하고 최적의 성능을 보장하기 위해 이 가정들을 이해하고 준수해야함

유효한 시멘틱 자바스크립트

  • 컴파일러가 처리하는 코드가 유효하며, 시맨틱 자바스크립트 원칙을 따른다고 가정

  • 즉, 유효한 자바스크립트 문법을 사용하는지

널러블 값의 안전한 처리

  • 안정성을 보장하기 위해, 컴파일러는 코드를 처리하기 전에 해당 코드가 널러블값과 옵셔널 값에 대한 안전 확인을 포함한다고 가정함

  • TS 설정에서 strictNullChecks 컴파일러 옵션을 활성화해 널러블 및 옵셔널 값을 안전하게 처리함을 보장할 수 있음

/*
  nullableProperty가 null 혹은 undefined가 아니면 'foo' 속성에 접근한다.
*/
if (object.nullableProperty) {
  return object.nullableProperty.foo
}
  • 혹은 옵셔널 체이닝을 사용하여 조건 확인 수행

return object.nullableProperty?.foo

리액트 규칙을 따름

  • 컴포넌트와 훅이 리액트 안에서 의도된 디자인 패턴과 일관되게 사용되었음을 보장하는것들이 포함됨

    • 컴포넌트와 훅은 순수해야함 → 같은 입력이 들어오면 같은 결과를 반환해야함

    • 컴포넌트는 멱등성을 가져야함 → 같은 입력으로 몇번을 실행해도 결과가 변하지 않아야함

    • props와 상태는 불변하게 다뤄져야함

    • 훅은 '최상위 레벨', 및 리엑트 함수형 컴포넌트에서만 호출되어야함

    • ...등등

  • 컴포넌트와 훅들이 예측대로, 유지보수 가능하게 작동하는 것을 보장하는데 매우 중요

  • 그리고 컴파일러가 효과적으로 최적화를 수행하는데 필수적

  • 컴파일러는 이 규칙을 위반한 코드를 만나면 코드가 컴파일되도록 강제하는 시도를 하지 않음

    • 안전하게 건너뛰고 나머지 코드를 계속 컴파일함

사용해보기

리액트 서버 컴포넌트

서버 컴포넌트를 통해 서버 사이드 렌더링을 리액트 아키텍처에 매끄럽게 통합 가능

  • 서버 사이드 렌더링 흐름은 간단히 페이지 진입 후 서버에서 HTML를 반환(렌더링) 후 클라이언트가 API 요청을 다시 서버로 보내 데이터를 가져와야 한다 (데이터 가져오기)

    • 즉, 렌더링과 데이터 가져오기를 별도로 처리하는게 불-편

    • 서버 사이드 데이터 가져오기를 직접적으로 컴포넌트 렌더링과 통합한 리액트 표준이 필요해짐 → 서버 컴포넌트

  • 서버의 첫 응답에 데이터를 가져온다면 보다 효율적이고 프로세스를 간소화시켜 요청을 여러번 보낼 필요가 줄어들게됨

    • 이전 next.js는 getServerSideProps라는 함수를 사용해 해결했음

Reference

서버 컴포넌트란

  • 리액트의 새로운 기능

  • 상태를 갖지 않는 컴포넌트

  • 서버에서 실행됨

  • 특정 계산과 데이터 가져오기 부담을 클라이언트에서 서버로 옮길 수 있음 (서버의 컴퓨팅 파워가 더 높으니 개이득)

  • 클라이언트로 전달되는 코드양을 줄이고, 로딩 시간을 줄임 → 앱 사용성 개선

  • 서버에서 배타적으로 실행되기에 전통적인 클라이언트 컴포넌트와 동작에 차이가 있음

클라이언트 사이드 리액트 API에 접근 불가

  • 서버에서 실행되므로 전통적인 리엑트 훅 사용 못함

  • 서버 컴포넌트에서 상호작용을 도입하려면 상호작용을 다루는 클라이언트 컴포넌트를 활용해야함

    • 즉, 서버 컴포넌트에서 상호작용 가능한 클라이언트 컴포넌트를 자식으로 사용

비동기 서버 컴포넌트

  • 서버 컴포넌트는 비동기로 작동할 수 있음

  • await 키워드를 사용해 데이터를 꺼낼 때까지 대기함으로써 데이터를 완전히 로딩한 뒤 컴포넌트를 렌더링 가능

  • await을 사용하지 않는 건은 데이터를 기다리지 않고 렌더링 프로세스를 진행시킴

    • 대신 Suspense 컴포넌트에 의해 비동기로 가져오고 관리됨

    • 서버에서 처리가 시작되지만 기다려지지 않으므로, 클라이언트에 비동기로 전달될 수 있음

    • use() 함수를 사용하면 컴포넌트는 서버 컴포넌트에서 전달된 프로미스를 구독하게됨

    • 결과적으로 클라이언트 컴포넌트는 서버에서 요청된 비동기 데이터를 받게 되는 순간 즉시 렌더링

    • 페이지 나머지 부분을 블로킹하지 않음

      • 점진적 향상 원칙을 내포함, 기본 컨텐츠 기능은 한 번에 제공하고, 추가적인 기능들은 점진적으로 제공

"use client";

import { use } from 'react';
import { Comment } from './Comment';

export const Comments({ commentsPromise }) {
  const comments = use(commentsPromise);
  
  return (
    comments.map((comment) => (
      <li key={comment.id}>
        <Comment {...comment} />
      </li>
    ))
  )
}

서버 액션 (서버 함수)

클라이언트 컴포넌트가 서버 사이드 함수를 직접 호출하게 할 수 있음

  • 이를 활용해서 때때로 서버 사이드 처리와 클라이언트 사이드의 동적 응답성의 장점을 조합할 수 있음

    • 이 프로세스는 전체 페이지 재로딩을 요구하지 않음

    • 클라이언트 사이드 상호작용의 실시간 응답성과 데이터 무결성 및 서버 사이드 동작의 처리 능력을 결합한 매끄러운 사용자 경험을 제공하게됨

  • 서버 액션은 서버 컴포넌트 안에서 "use server" 지시자를 사용해 정의함

  • 서버 컴포넌트에서 정의한 서버 액션을 props로 전달되어 클라이언트 컴포넌트에서 사용할 수 있음

import { Comments } from './Comments';
import db from './database';

const BlogPost = async ({ postId }) => {
  //...
  
  // 서버 액션
  const async upvoteAction = (commentId) => {
    "use server";
    await db.comments.incrementVotes(commentId, 1);
  }
  
  return (
    <div>
     {/*서버 액션은 props로서 아래로 전달됨*/}
     <Comments
       commentsPromise={comments}
       upvoteAction={upvoteAction}
     />
    </div>
  )
}
  • 클라이언트 컴포넌트 또한 "use server" 지시자를 선언한 파일에서 직접 서버 액션을 임포트 가능

// 별도 파일에 정의된 서버 액션
"use server";

import db from './database';

export const upvoteAction = async (commentId) => {
  await db.comments.incrementVotes(commentId, 1);
}

export const downvoteAction = async (commentId) => {
  await db.comments.incrementVotes(commentId, -1);
}

장점

서버 컴포넌트를 사용하면 서버 사이드 렌더링과 클라이언트 사이드 렌더링을 조합해 성능과 사용자 경험을 최적화 가능

성능 개선

  • RSC는 복잡한 계산과 데이터 가져오기를 서버로 넘겨서 클라이언트에 전달할 코드 양을 줄임

  • 빠른 초기 로딩 시간과 성능 개선

SEO 향상

  • 서버 사이드 렌더링은 컨텐츠가 이미 검색 엔진에서 읽을 수 있는 상태임을 보장하므로 SEO를 향상시킴

데이터 가져오기 단순화

  • RSC는 데이터 가져오기를 컴포넌트 렌더링 프로세스에 직접 통합하므로, 클라이언트는 별도의 API 호출을 할 필요가 없음

  • 이 간소화된 접근법은 클라이언트 사이드의 상태와 데이터 동기화를 관리하는 복잡성을 줄임

Last updated