반응형

이 포스트는 React 공식문서의 Suspense 관련 부분을 보던중 아래의 내용을 실행해보기 위한 포스트 입니다.

Effect를 이용하여 해결할 수 있다는 언급

Suspense를 사용하여 효율적으로 제어할 수 있는 race condition을 useEffect내에서도 처리하기 위한 방법입니다.

클로저, 스코프 체인와 cleanUp function을 이용하였습니다.

 

여기서 정의하는 경쟁상태란 코드가 실행되는 순서에 대한 부정확한 가정에서 비롯되는 버그 입니다.

위의 이미지는 useEffect 내에서 promise를 통해 데이터를 불러온후에 

컴포넌트의 상태를 업데이트하는 동작 화면입니다.

promise의 응답 시간은 무작위로 설정되어 있습니다.

next 버튼을 클릭하면 id를 변경해주고 변경된 id로 각 컴포넌트에서 promise로 요청을 보내줍니다.

 

 


 

useEffect내에서 클로저, 스코프체인과 cleanUp Function을 이용한 버전입니다.

 

function ProfileTimeline({ id }) {
  const [posts, setPosts] = useState(null);

  useEffect(() => {
    let status = true;
    fetchPosts(id).then((p) => {
      if (status) setPosts(p);
    });
    return () => {
      status = false;
      setPosts(null);
    };
  }, [id]);
  
 // 생략 
 }

코드를 살펴보면

1. useEffect 내에서 status변수를 선언합니다.

2. fetchPost 즉 Promise를 실행시키고

3. 컴포넌트가 unMount될때 cleanUp function을 통하여 스코프내의 status를 false, posts를 null로 변경해줍니다.

 

만약 next를 연속으로 실행하여 id가 바뀌었고 promise가 실행된 컴포넌트가 unMount 되었다면 

cleanUp function을 통해 status는 false로 재할당됩니다.

이후 useEffect내의 메소드는 실행컨텍스트가 소멸됩니다.

promise가 resolve를 통해 then을 실행하게 되면 then내의 함수는 status를 가지고 있지 않습니다.

이 함수가 선언된 위치를 기준으로 실행컨텍스트가 생성되고 실행컨텍스트의 스코프 체인을 통해 상위 스코프에 선언된 status에 접근을 할 수 있게 됩니다.

이때의 status는 false이므로 Posts의 업데이트는 실행되지 않습니다.

 

이러한 과정을 가진 코드가 많아진다면 코드는 지저분해질 수 있습니다.

깔끔하게 Suspense를 사용하면 해결할 수 있습니다.

 


Suspense를 사용하는 모습

 

 

Suspense가 안정된 버전으로 그리고 모든 라이브러리에서도 안정되게 제공해주고 타입스크립트도 적용이 완벽히 될수 있었으면 좋겠습니다.

 

 

출처: https://ko.reactjs.org/docs/concurrent-mode-suspense.html

 

데이터를 가져오기 위한 Suspense (실험 단계) – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

 

 

반응형

'Frontend > React' 카테고리의 다른 글

React 재조정 🌱  (1) 2022.04.15
React hook 🌱  (1) 2022.04.14
React suspense  (1) 2022.04.10
React 상태관리의 과거, 현재 그리고 미래 (번역글)  (0) 2022.04.09
Recoil 🌱  (0) 2022.04.08
반응형

React에 Suspense 컴포넌트가 추가되었습니다. 

기존의 명령형으로 loading 상태를 지정해 주어야 했던 것을 

Suspense 컴포넌트를 이용하면 선언형으로 코드를 작성할 수 있습니다.

 

React의 experimental에서 사용이 가능했었지만 3월 24일 정식 출시된 18버전에서 사용이 가능합니다.

Concurrent 모드에서 사용이  가능합니다.

기존에 ReactDom.render => ReactDom.createRoot().render 형식으로 사용하여야 Suspense 컴포넌트를 사용할 수 있습니다.

 

// 기존
import ReactDOM from 'react-dom'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>, 
  document.getElementById("root")
)

// 변경된 모습
import ReactDOM from 'react-dom/client'

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

 

Concurrent Mode는 어떤것일까요??

간단하게 살펴보자면 렌더링에 인터럽트 개념이 들어온 것입니다.

Concurrent는 동시에 발생하는 이라는 뜻입니다.

 

잠시 os의 이야기를 하자면

cpu의 자원을 소유하지 않은 프로세스는 cpu의 자원을 소유하고자 cpu에게 인터럽트를 통해 알립니다.

인터럽트를 받은 cpu는 인터럽트 테이블에 명시된 규칙에 따라 현재 점유중인 프로세스에서 인터럽트를 발생시킨 프로세스를 수행합니다.

예를 들자면 네트워크에서의 TCP/IP 요청은 OS의 커널영역에 위치한 프로토콜 스택에서 요청을 보냅니다.

cpu는 프로토콜 스택을 보내 요청을 보내고 다른 프로세스를 수행합니다.

만약 응답이 오면 프로토콜 스택에서는 cpu에게 인터럽트를 보내고 cpu는 응답을 처리합니다.

인터럽트를 발생시킬수 있음에 따라 cpu는 응답을 계속 기다리지 않고 다른 작업을 수행할 수 있습니다.

따라서 사용자는 여러 프로그램이 동시에 실행되는 것처럼 느끼게 됩니다.

 

이러한 인터럽트 개념이 렌더링에도 들어온 것입니다.

기존에는 상태가 변화되면 렌더링이 일어나게 되고 이러한 렌더링은 중간에 멈출수가 없었습니다.

그래서 입력과 동시에 렌더링이 발생하는 상황이였다면 렌더링이 일어나는 중에는 입력을 할 수 없으므로 버벅거림을 느낄수 있었을 겁니다.

Concurrent모드는 렌더링을 인터럽트가 가능하도록 만들어 줍니다.

React의 Concurrent 모드에서는 입력이 발생하면 입력에 대한 업데이트를 Paint 하고 메모리 내에 있는 업데이트 목록을 계속 렌더링 할 수 있도록 합니다. 렌더링이 끝나면 React는 DOM을 업데이트하고 변경사항들을 화면에 반영합니다.

 

Concurrent모드에서는 두가지 시나리오가 가능하게 해줍니다.

1. 현재 렌더링 중이라면 중단하고 우선순위가 높은 렌더링이 먼저 보여줄 수 있습니다.

2. 모든 데이터가 도달하기 전에 메모리에서 렌더링을 시작할 수 있고 로딩 상태 표시를 보여주지 않을 수도 있습니다.

 

자세한 실행흐름은 아래에서 코드의 실행 순서를 보며 알아보겠습니다.

아래에서 살펴볼 예시는 I/O 상황에 대한 Suspense의 적용입니다.

 

사용은 간단합니다.

Suspense 컴포넌트로 감싸주고 fallback시에 보여줄 컴포넌트를 지정해주면 됩니다.

import { Suspense } from "react";
import { fetchProfileData } from "./apis";

const resource = fetchProfileData();

const ProfilePage = () => {
  return (
    <Suspense fallback={<h1>Loading profile....</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts....</h1>}>
        <ProfilePosts />
      </Suspense>
    </Suspense>
  );
}

const ProfileDetails = () => {
  const user = resource.user.read()
  return <h1>{user.name}</h1>
}

const ProfilePosts = () => {
  const posts = resource.posts.read()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  )
}

export default ProfilePage;

비교를 위해 같은 코드를 useEffect로 처리한 코드를 첨부하겠습니다.

 

const ProfileDetails = () => {
  const user = resource.user.read();

  if (!user) {
    return <h1>Loading profile...</h1>
  }

  return <h1>{user.name}</h1>;
};

const ProfilePosts = () => {
  const posts = resource.posts.read();

  if (!posts) {
    return <h1>Loading posts...</h1>
  }

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
};

예외처리를 따로 지정해줘야 함을 확인할 수 있습니다.

네트워크를 통해 조회되는 데이터는 크게 3가지 상태를 가질 수 있습니다. fetching, succes, failed

기존에는 fetching, failed일때 다른 컴포넌트를 렌더링 해주어 사용자에게 네트워크의 진행상태를 보여줄 수 있도록 하였습니다.

아래에 예시가 있습니다.

import { useQuery } from 'react-query';

const TodoPage = () => {
  const { isLoading, isError, data } = useQuery(
    ["todo", 1], 
    async () => await fetch("https://jsonplaceholder.typicode.com/todos/1").then(r => r.json()),
   )

  if (isLoading) return <h1>Loading</h1>

  if (isError) return <h1>Something Wrong</h1>

  return (
    <div>
      {data.title}
    </div>
  )
}

export default TodoPage

가져와야 하는 데이터가 많아진다면 각 데이터 별로 상태를 가지게 되고 예외처리를 해주어야 할 상황은 기하급수적으로 많아질 수 있습니다.

데이터 2종류면 4가지 상황, 데이터 3종류면 8가지 상황 .....

그리고 명령형으로 코드가 작성되므로 가독성도 떨어집니다.

 

이 모든 단점을 Suspense를 이용해 해결할 수 있습니다.

Suspense를 사용하면 데이터의 로딩 상태, 여기서는 설명하지 않지만 ErrorBoundary를 사용하면 에러상태를 catch 할 수 있으므로 

컴포넌트 내에서는 데이터의 무결성을 보장받을수 있게 됩니다.

아래와 같이 작성이 가능해집니다.

import { useQuery } from 'react-query';

const TodoPage = () => {
  const { data } = useQuery(
    ["todo", 1], 
    async () => await fetch("https://jsonplaceholder.typicode.com/todos/1").then(r => r.json()),
    {
      suspense: true,
    })
 
  // 이 아래부터는 데이터가 존재함을 보장받을 수 있게 된다
  return (
    <div>
      {data.title}
    </div>
  )
}

export default TodoPage



// 생략

<ErrorBoundary fallback={<Error />}>
  <Suspense fallback={<Spinner />}>
    <TodoPage />
  </Suspense>
</ErrorBoundary>

// 생략

 

 

Concurrent Mode에서 발생하는 코드 동작을 단계별로 살펴보겠습니다.

import { Suspense } from "react";
import { fetchProfileData } from "./apis";

const resource = fetchProfileData();

const ProfilePage = () => {
  return (
    <Suspense fallback={<h1>Loading profile....</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts....</h1>}>
        <ProfilePosts />
      </Suspense>
    </Suspense>
  );
}

const ProfileDetails = () => {
  const user = resource.user.read()
  return <h1>{user.name}</h1>
}

const ProfilePosts = () => {
  const posts = resource.posts.read()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  )
}

export default ProfilePage;

1. React는 ProfilePage 렌더링을 시도합니다. 자식 컴포넌트인 ProfileDetails, ProfilePosts가 반환됩니다.

2. React는 ProfileDetails 렌더링을 시도합니다. resource.user.read()를 호출합니다. 아직 불러온 데이터가 없으므로 이 컴포넌트는 "정지" 합니다. React에서는 이 컴포넌트를 넘기고, 트리 상의 다른 컴포넌트의 렌더링을 시도합니다.

3. React는 ProfilePosts 렌더링을 시도합니다. resoure.posts.read()를 호출합니다. 아직 불러온 데이터가 없으므로 이 컴포넌트는 "정지" 합니다. React는 이 컴포넌트를 넘기고, 트리 상의 다른 컴포넌트의 렌더링을 시도합니다.

4. 렌더링을 시도할 컴포넌트가 없음을 확인 하였습니다. <ProfileDetails>가 정지된 상태이므로 React는 ProfileDetails에서 가장 가까운 Suspense의 Fallback을 찾고 화면에 보여줍니다.

5. resource.user.read()를 불러오고 나면 정지되어 있던 <ProfileDetails> 렌더링을 시도합니다. 렌더링이 완료되고 Fallback은 사라집니다.

 

위의 흐름대로 React에서는 렌더링을 시도합니다.

 

Suspense를 사용하기 위해서는 swr, react-query에서 제공하는 suspense 옵션이 있는지 확인해야 합니다.

아래에 첨부한 예제는 위에서 처리한 api이고 promise를 이용하여 suspense를

pending 상태일때는 promise를 throw 해주는 것을 확인할 수 있습니다.

export function fetchProfileData() {
  let userPromise = fetchUser();
  let postsPromise = fetchPosts();
  return {
    user: wrapPromise(userPromise),
    posts: wrapPromise(postsPromise)
  }
}

const wrapPromise = (promise) => {
  let status = "pending"
  let result
  let suspender = promise.then(
    (r) => {
      status = "success";
      result = r;
    }, 
    (e) => {
      status = "error";
      result = e;
    })

  return {
    read() {
      console.log(status)
      if (status === 'pending') {
        console.log(suspender)
        throw suspender;
      }
      if (status === 'success') {
        return result;
      }
      if (status === 'error') {
        throw result;
      }
    }
  }
}

const fetchUser = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: "WooBottle"
      })
    }, 3000)
  })
}

const fetchPosts = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 0, text: "this is first" },
        { id: 1, text: "this is second" },
        { id: 2, text: "this is third" },
      ]);
    }, 5000);
  });
}

제공할 수 있도록 한 코드입니다.

 

 

 

출처 : https://ko.reactjs.org/docs/concurrent-mode-suspense.html

 

데이터를 가져오기 위한 Suspense (실험 단계) – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

https://ko.reactjs.org/docs/concurrent-mode-intro.html

 

Concurrent모드 소개 (실험 단계) – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

https://ko.reactjs.org/docs/concurrent-mode-adoption.html

 

Concurrent 모드 도입하기 (실험 단계) – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

반응형
반응형

이 포스트는 아래 출처의 블로그 글을 번역한 내용입니다.

 


React는 2013년 5월에 소개되었습니다. 이것은 패러다임은 UI가 상태에 따라 동작하는것으로 전환되었습니다. 컴포넌트의 상태가 주어지면 React는 어떻게 컴포넌트가 보여야 하는지 결정할 수 있습니다. React는 상태라는 아이디어 위에서 조직되었습니다. 그러나 상태는 React 어플리케이션을 만드는데 있어 가장 어려운 부분으로 여겨지고 있습니다.

 

React의 상태 관리를 견고한 벨트로 상상해봅시다. 수년동안 이 벨트를 사용해왔고 느리게 새로운 도구들이 추가됩니다. 각각의 도구들은 각각의 목적을 수행합니다. 당신은 볼트를 조일때 망치를 사용하지 않습니다. 장인으로서 각각의 도구를 적절한 상황에 사용해야 합니다.

 

리액트의 상태관리 도구는 견고한 belt이지만 모든 사람들은 어떤 도구에 언제 접근해야 하는지 이전의 경험을 가지고 있지 않습니다.

이 글은 상태관리의 과거, 현재, 미래를 설명하여 팀, 프로젝트 조직에 정확한 결정을 내릴수 있게 도와줄 것입니다.

 

 

Glossary

시작하기 전에, 자주 쓰이는 용어들을 이해하는 것은 매우 중요합니다. 이것들은 표준이름이 아닙니다. 사소한 부분은 다를 수 있지만 아래의 개념은 같습니다.

  • UI 상태 - 상태는 어플리케이션의 interactive 부분들을 제어하기 위해 사용됩니다(dark mode toggle, modals)
  • Server Cache State – State from the server, which we cache on the client-side for quick access (e.g. call an API, store the result, use it in multiple places).
  • 서버 캐시 상태 - 클라이언트에서 빠른 접근을 위해 캐시에 위치한 서버의 상태(API 요청, 결과를 저장, 복수의 위치에서 사용)
  • Form 상태  - form의 다양한 상태(로딩, 제출중, 불가능, 검증, 재요청) 제어, 비제어 폼의 상태 또한 있다
  • URL 상태 - 브라우저에 의해 관리되는 상태(상품 필터, 쿼리 파라미터 저장, 동일한 필터링된 상품을 보기 위하여 새로고침 할때)
  • 상태 Machine - 시간에 따른 상태 명시적 모델( 초록 -> 노랑 -> 빨강으로 변하는 신호등, 그러나 절대 초록 -> 빨강은 되지 않음)

 

Past

리액트 컴포넌트는 어플리케이션이 구조적이고 재사용이 가능하도록 도와줍니다. 각각의 컴포넌트는 각각의 상태를 가지고 있습니다.

웹앱이 점점 복잡해지면서 컴포넌트간에 로직을 공유하는 것을 더 쉽게 해주는 해결책들이 등장하였습니다.

Timeline

상태관리 도구가 시간을 거치면서 어떻게 변화하였는지 보여주기 위해 리액트에서 유명한 상태관리 도구들의 대략적인 타임라인을 준비하였습니다. 이 리스트는 UI 상태에 초점이 맞춰져 있습니다. 이 목록은 문맥을 제공하기에는 충분합니다.

  • 2013 – 소개
  • 2014 – Flux (many libraries)
  • 2015 – Redux
  • 2016 – MobX
  • 2018 – Context
  • 2019 – Hooks Introduced (+ React Query, SWR)
  • 2019 – Zustand
  • 2019 – xState
  • 2020 – Jotai, Recoil, Valtio
  • 2021 – useSelectedContext

타임라인에 나와있는 것들은 배울필요가 있다는 것을 의미하지는 않습니다. 글의 이후에서 리액트의 상태관리 도구 역사를 깊게 살펴보겠습니다.

 

 

Redux

Redux는 Flux 아키텍처에 기초하여 탄생하였습니다.  Flux 패턴은 2014년 페이스북에 의해 제안되었습니다.

Redux는 2015년에 등장하였고 빠르게 Flux 영감을 받은 라이브러리중 가장 인기가 높아졌습니다. 이것의 도구와 라이브러리들은 UI 상태와 서버 캐싱 상태를 캡슐화 하였습니다. Redux는 여전히 가장 인기있고 널리 쓰입니다.

Server Caching State

React의 초기에는 많은 상태관리 도구가 API로 데이터를 요청하고 어플리케이션 간에 사용을 위하여 캐싱을 하는것으로 요약되었습니다.

사용자들은 Redux는 쉽지 않았고 단지 서버 캐시 상태를 관리하기 위해 널리 쓰였므로 Redux와 같은 라이브러리를 배우는데 많은 노력을 기울였습니다.

 

React Hook의 발표와 함께, 공통된 훅에 로직을 캡슐화 하는것이 쉬워졌고 접근하기 쉬웠습니다. SWR과 React 쿼리와 같은 라이브러리들이 문제를 해결하기 위해 등장하였습니다.

 

왜 서버 캐싱 상태를 다루기 위해 별도의 라이브러리를 써야 하는지 의문이 들수 있습니다. 우선 캐싱은 어렵습니다. 서버 상태를 캐싱하는 것은 UI상태와 다른 문제를 해결합니다. 아래의 리스트들은 위에서 언급한 라이브러리들이 다루어 주는것에 대한 리스트 입니다. 

  • Polling on interval (간격을 두고 계속 요청하는 것)
  • Revalidation on focus (focus되었을때 재검증 하는것)
  • Revalidation on network recovery (네트워크가 회복되었을때 재검증 하는것)
  • Local mutation (Optimistic UI)
  • Smart error retrying (에러 재요청)
  • Pagination and scroll position recovery (페이지 네이션, 스크롤 위치 기억)

위의 것들을 직접 해결하고 싶다면 아마 불가능 할것이다.

 

React Context

16.3 버전에서 React Context는 컴포넌트간에 로직을 공유하기위한 first-party 솔루션으로 등장하였다. Context는 연관된 컴포넌트간에 value를 props로서 계속 깊게 넘겨주어야 하는 것을 예방해 주었다.

 

React Context는 상태관리도구가 아니다. 그러나 useReducer와 같은 hook과 함께 사용하여 상태관리도구가 될 수 있다. 이 조합은 많은 상황에서 UI 상태를 해결해 주었다.

 

 

 

Present

2021년에 React에서 상태를 관리하기위한 다양한 방법이 존재합니다. 다양한 상태를 이해하는 커뮤니티가 성장함에 따라 구체적인 사용사례를 해결하는 보다 세분화된 라이브러리가 생성되었습니다.

State Machines

Switch 상태를 살펴보겠습니다. 만약 state가 어느 경우라도 일치한다면 그에 상응하는 코드가 실행됩니다. 유한한 경우가 존재하게 됩니다. 아래는 단순한 state machine 입니다.

 

switch (state) {
  case state === 'loading':
    // show loading spinner
    break;
  case state === 'success':
    // show success message
    break;
  default:
  // show error message
}

 

유한한 State Machine과 State chart는 컴퓨터과학 개념의 근간입니다. React에 특화된 것이 아닙니다. 서드파티 라이브러리 없이 useReducer를 이용하여 state machine을 사용할 수 있습니다.

 

State Machine들은 모든 곳에 잘 동작합니다. 데이터베이스, 전자, 자동차 등등 말이죠. 상태관리가 리액트 생태계 안에서 진화함에 따라 우리는 이러한 오래된 개념들이 현대의 상태관리 이슈를 해결할수 있을것으로 인식하였습니다. State Machine들은 form 상태를 해결하기에 가장 일반적입니다.

 

유한한 State Machine과 함께 우리는 어플리케이션 혹은 컴포넌트의 유한한 상태를 제어할 수 있습니다. 연습에서 State Machine은 정의된 엣지 케이스와 요구사항으로 부터 버그를 밝혀내는데 도움을 줄 수 있습니다. 정보를 더 제공하자면 xState 문서나 이 과정을 시청하길 추천합니다. state machine을 온라인에서 시각화 할 수도 있습니다.

 

 

Zustand, Recoil, Jotai, Valtio, Oh My!

React 상태관리에 왜 많은 라이브러리들이 존재할까요?

 

Figma를 살펴보겠습니다. 컴포넌트가 렌더링 되거나 컴포넌트의 지역 상태의 외부에서 상태를 제어하는 툴바를 확인할 수 있습니다.

 

 

상상할 수 있듯이 이러한 규모의 어플리케이션은 복잡한 상태관리 도구를 요구합니다. 성능과 frame 비율은 좋은 사용자 경험을 위해 필수적입니다. 그래서 언제 어떻게 리렌더 해야하는지 제어하길 원할겁니다. 이러한 활용 사례는 상태관리 영역으로의 탐험을 초래하였습니다.

 

이러한 라이브러리들간에 차이점을 요약해 보겠습니다.

  • Valtio uses proxies to provide a mutation-style API
  • Jotai is optimized for "computed values" and async actions
  • Zustand is a very thin library specifically focused on module state
  • Recoil is an experimental library using a data-flow graph

Having complex state doesn't necessarily mean you have to pull for a third-party library. You can start with React and JavaScript and see how far it takes you. If optimizing requires a state management library, you can track that metric (e.g. frame rate), measure it, and verify it solves a real problem.

 

복잡한 상태는 서드파티 라이브러리를 꼭 써야함을 무조건 의미하지는 않습니다. React와 javascript로도 꽤 많은 것을 할 수 있습니다. 최적화에 상태 관리 라이브러리가 필요한 경우 해당 메트릭(예: 프레임 속도)을 추적하고 측정하여 실제 문제를 해결하는지 확인할 수 있습니다

 

명백히 필요로 할때까지 이러한 라이브러리들중 선택을 하지 마세요

Immutable State

다른 논쟁거리는 mutable vs immutable 상태 입니다. 정답은 없습니다. 단지 의견일 뿐입니다. 만약에 vanilla javascript로 상태관리를 수행하고 있다면 mutable 상태를 다루고 있을것입니다. 변수를 선언하고 새로운 값과 일치하는지 설정을 할것입니다. let vs const는 이곳에서 논쟁을 확인할 수 있습니다.

 

Immutable 상태는 리액트와 함께 많은 인기를 얻었습니다. immutable 군중은 선택한 상태 관리 솔루션이 상태를 직접 변형하도록 허용하면 더 많은 버그가 발생한다고 주장합니다. mutable 군중은 복잡성과 trade-off할 가치가 없다고 주장합니다. 직접적인 조작은 비직접적인 조작보다 항상 불안정합니다. 이것은 편의성과 위험성간에 trade-off 입니다.

 

Immer와 같은 솔루션은 mutable 코드를 작성하고 immutable하게 실행되도록 해줍니다. 기본 idea는 현재 상태의 대리자인 draft 상태를 변화 시키는 것입니다. mutation이 완료되었다면 immer는 draft state의 변화에 근거하여 다음 state를 생성할 것입니다.

 

 

URL State

아마존과 같은 e-commerce 사이트를 만들고 있다고 합시다. React 책을 검색하고 별 4개 이상으로 filter 하였습니다. 이 상태는 쿼리 파라미터로 존재하게 되고 브라우저에서 관리됩니다. 만약 페이지를 새로고침하면 동일한 상품 들을 볼수 있습니다. 이 URL을 다른 이들과 공유할 수 있고 그들또한 같은 결과를 볼수 있습니다.

 

Nomad List와 같은 예시도 있습니다. 브라우저 URL 상태를 데이터 동작에 따라 변경할 수 있습니다.

추가로 사람 친화적인 URL을 만들 수 있습니다.

 

 

 

Future

대규모의 어플리케이션 들에서는 Context를 기초로한 상태관리 도구들(useReducer와 같은)이 과도한 리렌더링을 유발해 문제를 발생시킬수 있습니다. 만약에 상태가 변한다면 useContext를 사용하는 모든 컴포넌트들이 리렌더 됩니다. 이것은 UI의 동작을 느리게 만들수 있습니다. 만약 시각적으로 눈치챌 수 없다면 리렌더를 조사하기 위해 React Dev Tools를 사용할 수 있습니다.

 

React 팀에서는 useSelectedContext hook을 Context에서의 성능 문제를 해결하고자 제안하였습니다.

2019년 7월에 RFC가 소개되었고 2021년 1월에 작업이 시작되었다고 합니다. 이 hook은 Context의 일부만 선택할 수 있게 하고 일부가 변했을때 일부만 렌더링 되게 합니다.

 

이미 리렌더 퍼포먼스를 다루기 위한 방법들이 있지만 (useMemo와 같은) Context가 선호되어집니다.

useContextSelector 라이브러리와 같은것들이 있고 Jotai, Formik3는 이러한 맥락아래에서 사용됩니다. useSelectedContext를 React 표준 라이브러리로 가져가는것은 더 나은 성능을 제공하고 외부 라이브러리의 복잡성과 거대한 코드 사이즈를 줄여줄 것입니다.

 

먼 미래에 React는 자동으로 어떤 컴포넌트가 리렌더 되야하는지 이해할수 있을 것입니다(auto-memoization)

State Management Options

이것은 포괄적인 리스트 입니다. 오픈 소스 이고 만약에 동의하지 않거나 잘못되었다면 PR을 해주세요. 일반적으로 당신의 개발자들이나 팀에 의존되어 있습니다. 리덕스와 함께 행복하다면 유지하여도 됩니다.

Form State

Experience Learning Appetite Project/Team Size Solution
Beginner Low Small useState
Beginner Medium Medium, Small Form Library (Formik, Final Form)
Beginner High, Medium Large Ask your tech lead
Intermediate Low Medium, Small Form Library (Formik, Final Form)
Advanced Medium Medium State Machines
Advanced High Medium State Machines
Advanced High Large State Machines

 

UI State

Experience Learning Appetite Project/Team Size Solution
Beginner Low Small useState
Beginner Medium Medium, Small useContext + useReducer
Beginner High, Medium Large Ask your tech lead
Intermediate Low Medium, Small Redux Toolkit
Advanced Medium Medium useContext + useReducer
Advanced High Medium Jotai, Valtio
Advanced High Large Recoil (or Relay if you use GraphQL)

 

Server Cache State

팀 규모나 경험에 관계 없이 SWR과 React Query는 훌륭한 솔루션 들입니다. 두개중 하나에 만족할 것입니다. 만약 GraphQL을 사용한다면 이미 Apollo에 대해 알고있을 것입니다.

 

That's all, folks!

리액트에서 상태관리는 지난 8년간 진화해 왔습니다. 거대한 웹 어플리케이션들을 구축하는데 있어 가장 어렵고 미묘한 부분 중 하나입니다. 다양한 유형의 상태들과 그 절충안을 이해하는 것은 정보에 입각한 결정을 하는데 매우 중요합니다. 이 포스트가 도움이 되길 바라고 읽어주셔서 감사합니다.

 

 

 

출처: https://leerob.io/blog/react-state-management?fbclid=IwAR2jI5klYjGLoJXuOLcMIctncQuniOgMQIwmwEIb65T-C3x0-erc86Jvu00 

 

Past, Present, and Future of React State Management – Lee Robinson

Learn about the history of state management in React and what the preferred solutions are today.

leerob.io

 

반응형

'Frontend > React' 카테고리의 다른 글

React 연속된 요청 useEffect 내에서 처리하기 (useEffect, suspense)  (1) 2022.04.10
React suspense  (1) 2022.04.10
Recoil 🌱  (0) 2022.04.08
React 상태관리🌱외 minor concept  (0) 2022.04.07
React css-in-js  (0) 2022.04.06
반응형

리액트에서 전역 상태관리를 위해 Redux는 널리쓰입니다.

비동기 action을 지원하기 위해 redux-thunk, redux-saga등이 추가되어 미들웨어로서의 동작으로 비동기 동작을 이용할 수 있게 해줍니다.

Redux에서는 전역 컴포넌트 상태와 서버 상태의 분리를 위해 Redux-thunk query또한 추가 되었습니다.

 

Redux는 리액트를 비롯하여 다른 라이브러리, 프레임워크에서 사용이 가능합니다.

React-Redux는 Redux를 React만을 위해 사용하려고 등장한 라이브러리 입니다.

불필요한 리렌더링을 방지하고 동작을 최적화 하기위해 등장한 것으로 알고 있습니다.

이러한 서드파티 라이브러리 들이 사용되던 시기에 Recoil이 등장합니다.

 

Recoil은 리액트 만을 위해 생겨난 first party 라이브러리 입니다.

메타에서 관리되고 리액트의 라이프 사이클에 직접 관여하여 상태를 직접 다룹니다.

Redux와 다르게 boiler plate 코드가 적고 hook과 같이 사용할 수 있어 기존의 hook의 사용에 익숙한 개발자라면

쉽게 접근할 수 있습니다.

메타에서 관리되기 때문에 리액트의 동시성 모드나 Suspense등 experimental 에서 관리되던 부분도 빠르게 대응이 되지 않을까 기대를 해봅니다.

(Suspense는 React 18이 정식으로 나온 3월 24일 부터 추가되어있습니다. react-dom/client를 import하여 사용할 수 있습니다)

 

저는 전역상태 관리를 위해 Recoil을 사용하고 있고

Recoil의 기본 쓰임새에 대해 알아보고자 합니다.

 


 

Recoil Root

 

Recoil을 사용하기 위해서는 RecoilRoot로 Recoil을 사용하기 위한 컴포넌트를 감싸주어야 합니다.

function App() {
  return (
    <div className="App">
      <SelectorWithCustomHook />
      <hr />
      <SelectorWithContext />
      <hr />
      <RecoilRoot>
        <SelectorWithRecoil />
      </RecoilRoot>
    </div>
  );
}

이렇게 감싸면 RecoilRoot내의 컴포넌트에서 Recoil을 사용할 수 있습니다.

여러개의 RecoilRoot를 사용할 수도 있습니다.

이렇게 될경우 Root내의 컴포넌트 들은 현재 자신에서 가장 가까운 Root를 찾게 됩니다.

또한 각 Root는 독립적인 atom들을 가지게 됩니다.

atom은 각 Root에 따라 다른 값을 가지게 됩니다.

 


atom

atom은 Recoil의 상태를 표현합니다. 

key값으로 구별할 수 있고 default값을 정의할 수 있습니다.

 

atom을 제어할 때 많이 사용하는 훅들 입니다.

useRecoilState : [value, updater]의 형태로 값을 반환해 줍니다.

useRecoilValue : value만 이용할때 사용합니다.

useSetRecoilState : updater만 이용하고자 할때 사용합니다

useResetRecoilState : atom을 초깃값으로 초기화 하고자 할때 사용합니다.

 

atom을 설정할때 promise는 지정할 수 없습니다!!

비동기 값을 설정하고자 할때는 selector를 이용해야 합니다.

 

아래는 atom의 사용예시 입니다.

 

atom

// atom
export const colorState = atom({
  key: "Color",
  default: COLORS.RED,
});

useRecoilState

// useRecoilState
const Header = () => {
  const [selectedColor, setSelectedColor] = useRecoilState(colorState);

  const handleColor = (e: SyntheticEvent) => {
    if (!(e.target instanceof HTMLButtonElement)) {
      return;
    }

    const { color: nextColor } = e.target.dataset;

    if (!nextColor) {
      return;
    }

    setSelectedColor(nextColor);
  };
// 생략
}

 

useRecoilValue

// useRecoilValue

import { colorCounterState } from "../../../atoms";

const ScoreBoard = () => {
  const scores = useRecoilValue(colorCounterState);
  return (
    <>
      {Object.entries(scores).map(([key, value]) => (
        <div>
          {key}: {value}
        </div>
      ))}
    </>
  );
};

 

useSetRecoilState

// useSetRecoilState
const Header = () => {
  const setSelectedColor = useSetRecoilState(colorState);

  const handleColor = (e: SyntheticEvent) => {
    if (!(e.target instanceof HTMLButtonElement)) {
      return;
    }

    const { color: nextColor } = e.target.dataset;

    if (!nextColor) {
      return;
    }

    setSelectedColor(nextColor);
  };
// 생략
}

 

useResetRecoilState

// useResetRecoilState

import { colorCounterState } from "../../../atoms";

const ResetButton = () => {
  const resetScore = useResetRecoilState(colorCounterState);

  const handleReset = () => {
    resetScore();
  };
  return <button onClick={handleReset}> Reset</button>;
};

 

 


atomFamily

atomFamily는 atom을 Map형태로 사용하기 위해 사용하는 util 입니다.

키 값을 넘겨주고 없으면 생성, 있으면 updater를 이용해 상태를 변경할 수 있습니다.

export const boxState = atomFamily({
  key: "BoxState",
  default: COLORS.WHITE,
});

저는 25개의 박스를 관리해야했고 이를 일일히 atom으로 지정할 수도 있었습니다.

하지만 atomFamily util을 사용하여 각각의 개별 상태에 가져올 수 있도록 하였습니다.

id를 인자로 넘겨주어 boxState에서 개별요소만 가져와 상태를 변경해 줄 수 있도록 하였습니다.

const Box = ({ id }: BoxProps) => {
  const [boxColor, setBoxColor] = useRecoilState(boxState(id));
  const selectedColor = useRecoilValue(colorState);

  const clickHandler = () => {
    setBoxColor(selectedColor);
  };
  
}

selector

selector는 Recoil에서 함수나 파생된 상태를 나타냅니다.

atom에서는 비동기 동작을 수행할 수 없어서 selector에서 비동기 동작을 이용하고 비동기 동작안에서 atom을 이용하면 커스터마이징된 데이터를 반환할 수 있습니다.

 

get 함수를 정의할 수 있고 

set 함수를 정의할 수 있습니다.

get 내부에서 atomFamily로 선언한 atom에 키값으로 접근하여 counter 객체에 값을 변경해주고 있는 모습입니다.

export const colorCounterState = selector({
  key: "ColorCounterState",
  get: ({ get }) => {
    const counter: CounterProps = {};
    Object.keys(COLORS).forEach((el) => (counter[el] = 0));

    for (let i = 0; i < BOX_NUM; i++) {
      counter[get(boxState(i))] += 1;
    }

    return counter;
  },
  set: ({ set }) => {
    for (let i = 0; i < BOX_NUM; i++) {
      set(boxState(i), COLORS.WHITE);
    }
  },
});

 

 

각각 get과 set을 이용한 코드 입니다.

// get 이용
import { useRecoilValue } from "recoil";
import { colorCounterState } from "../../../atoms";

const ScoreBoard = () => {
  const scores = useRecoilValue(colorCounterState);
  return (
    <>
      {Object.entries(scores).map(([key, value]) => (
        <div>
          {key}: {value}
        </div>
      ))}
    </>
  );
};

export default ScoreBoard;


// set 이용
import { useResetRecoilState } from "recoil";
import { colorCounterState } from "../../../atoms";

const ResetButton = () => {
  const resetScore = useResetRecoilState(colorCounterState);

  const handleReset = () => {
    resetScore();
  };
  return <button onClick={handleReset}> Reset</button>;
};

export default ResetButton;

selectorFamily

selectorFamily는 selector와 유사하게 사용이 가능합니다.

get, set, selector의 콜백을 매개변수로 전달할 수 있어 다양하게 쓰일 수 있을것 같습니다.

 

매개변수로 숫자를 넘겨주어 파생된 값을 받아오는 것을 볼 수 있습니다.

const myNumberState = atom({
  key: 'MyNumber',
  default: 2,
});

const myMultipliedState = selectorFamily({
  key: 'MyMultipliedNumber',
  get: (multiplier) => ({get}) => {
    return get(myNumberState) * multiplier;
  },

  // optional set
  set: (multiplier) => ({set}, newValue) => {
    set(myNumberState, newValue / multiplier);
  },
});

function MyComponent() {
  // defaults to 2
  const number = useRecoilValue(myNumberState);

  // defaults to 200
  const multipliedNumber = useRecoilValue(myMultipliedState(100));

  return <div>...</div>;
}

 

 

출처: https://recoiljs.org/ko/docs

 

selector(options) | Recoil

Selector는 Recoil에서 함수나 파생된 상태를 나타낸다. 주어진 종속성 값 집합에 대해 항상 동일한 값을 반환하는 부작용이 없는 "순수함수"라고 생각하면 된다. get 함수만 제공되면 Selector는 읽기

recoiljs.org

https://github.com/woobottle/recoil_practice

 

GitHub - woobottle/recoil_practice

Contribute to woobottle/recoil_practice development by creating an account on GitHub.

github.com

 

반응형

'Frontend > React' 카테고리의 다른 글

React suspense  (1) 2022.04.10
React 상태관리의 과거, 현재 그리고 미래 (번역글)  (0) 2022.04.09
React 상태관리🌱외 minor concept  (0) 2022.04.07
React css-in-js  (0) 2022.04.06
React form 블로그 따라 다루기  (0) 2022.04.06
반응형

상태 

 

React에서는 컴포넌트 기반의 코드를 작성하게 됩니다.

컴포넌트는 여러 곳에서 재사용을 할 수 있고 추상화를 잘 이용하면 가독성이 좋은 코드를 작성하게 해줍니다.

컴포넌트는 상태를 가집니다.

예를 들면 이 블로그의 라이트/다크 모드, 카카오톡 대화창의 안읽은 메시지수 와 같은것들은 상태입니다.

 

상태는 사용자에게 보여지는 것을 의미하고 상태의 변경은 렌더링을 새로해주어야 함을 의미합니다.

사용자가 블로그의 라이트 모드에서 다크모드를 눌렀으면 어두운 화면을 보여주어야 하고

카카오톡 대화창의 메시지를 전부 읽었다면 안읽은 메시지 수는 0, 하나씩 올때마다 카운트를 올려주어야 할 것입니다.

 

컴포넌트는 자신이 정의한 상태를 가질수 있고, 서버로부터 데이터를 받아 자신의 상태로 정의할 수도 있고, 부모 컴포넌트로 부터 전달받아 상태를 가질수도 있습니다.

 

컴포넌트가 지니는 고유한 성질이나 사용자에게 보여줘야 하는 데이터가 상태라고 정의할 수 있을것 같고

상태는 리렌더링을 유발한다 정도로 정리할 수 있을것 같습니다.

 

이러한 상태를 효율적으로 관리하여 불필요한 렌더링을 방지하고 유지보수에 좋은 코드를 짜야합니다.

상태에는 크게 

  • 지역상태
  • 전역상태
  • Form의 상태
  • 서버 상태

와 같은 것들이 있을수 있을것 같습니다.


좋은 상태 관리

 

상태는 제어되는 컴포넌트에 최대한 가깝게 위치하는 것이 좋다고 생각합니다. 

둘의 거리가 멀면 멀수록 중간에 위치한 관련없는 컴포넌트들이 리렌더될수 있기 때문입니다.

리액트에서는 부모 컴포넌트의 상태가 변경되면 자식컴포넌트 또한 리렌더링 됩니다.

중간에 위치한 컴포넌트들은 불필요한 리렌더링을 겪을수 있습니다.

(리액트의 reconcilation 알고리즘에 의해 리렌더링이 결정됩니다.)

따라서 상태는 컴포넌트에 최대한 가깝게 위치되어 다른 컴포넌트들과의 결합도는 낮추고 상태와 사용하는 컴포넌트의 응집도는 높여야 할것 같습니다.

 

위에서 정의한 상태를 제어할때 아래와 같은 방식을 사용했었습니다.

 

지역상태는 커스텀 훅이나 리액트 컨텍스트를 사용하여 관리할 것 같습니다.

전역상태는 최대한 추가되지 않아야 한다고 생각합니다. 전역상태는 앱의 전체 렌더링을 수반하므로 주의해야 합니다.

전역상태의 관리에서는 recoil을 사용했었습니다. redux를 배워보려 했었으나 boiler plate코드가 비대하였고 학습량이 생각외로 많아져서 recoil을 접목했었습니다. 

recoil을 사용할때는 hook의 사용과 비슷하였고 다른 서드파티 라이브러리와 다르게 recoil은 react를 만드는 facebook에서 만드는 first 파티 라이브러리 입니다. 직접 react의 생명주기에 접근하여 동작을 처리한다고 알고 있습니다. 

그래서 더 react를 섬세하고 다룰수 있게 해주고 유지보수에도 유리한 측면을 가지고 있지 않을까 생각합니다.

 

Form의 상태는 Form 컴포넌트에서 상태를 정의하거나 커스텀 훅으로 제작하여 관리합니다.

input, errorMessage 컴포넌트를 이용할때는 각각의 컴포넌트들이 상태를 제어할 수 있도록 리액트 컨텍스트의 사용을 고려합니다.

리액트 컨텍스트를 사용하면 props drilling 없이 컴포넌트들 간에 상태를 공유할 수 있어 코드가 깔끔해집니다. 

하지만 자칫 잘못하면 제어하는 상태와의 거리가 멀어질 수 있습니다.  코드 하나를 수정할때 여러 모듈을 탐험해야 할 수도 있습니다.

input, errrorMessage를 작성할때는 추상화를 이용할 수 있습니다. 추상화 단계를 올리고 의존성을 주입해 주어서 선언형으로 코드를 작성한다면 의미가 잘 드러나고 이해하기 좋은 코드를 작성할 수 있습니다.  

이때 의존성을 주입해주지 않거나 잘못 주입한다면 자칫 갑자기 동떨어진 코드가 왜 등장하는지와 같이 느낄수 있을것 같습니다. 그래서 의도적으로 의존성을 드러내는 것이 좋을것 같습니다. 

Form의 상태를 제어하기 위한 좋은 라이브러리 들이 이미 존재합니다.

formik, react-hook-form과 같은 라이브러리 들이 있습니다.

Formik은 제어컴포넌트를 기반으로 구현되어있고 react-hook-form은 비제어 컴포넌트를 기반으로 구현되어있습니다.

validation 라이브러리인 yup과 결합하여 사용한다면 더 강력한 코드를 작성할 수 있습니다.

 

서버의 상태를 관리하는 것은 매우 중요합니다.

서버의 상태 관리를 위해 react-query 라이브러리를 사용합니다.

swr또한 자주 사용되는 라이브러리로 알고있습니다.

 

사용하는 이유에 대해서 아래에 천천히 풀어보겠습니다.

react에서는 서버에 요청을 보내고 서버에서는 db를 조회한후 데이터를 입맛에 맞게 조합하여 front로 보내줍니다.

cpu의 연산에 비해 네트워크 요청은 시간이 오래걸립니다. 이러한 상황에서 부하가 걸리는 것을 I/o bound라 합니다.

그래서 캐시를 효율적으로 이용해야 합니다.

동일한 요청을 서버에 보내지 않으면 그만큼 시간을 아낄수 있습니다.

네트워크에서는 캐시를 이용하기 위하여 header에 cache-control 속성을 다뤄야 합니다.

cache-control에는 max-age, no-store, no-cache, must-revalidate, stale-while-revalidate와 같은 값들이 들어갈 수 있습니다.

위 헤더를 사용하면 메모리에서 요청에 대한 응답을 관리하고 동일한 요청시 서버로 요청을 보내지 않고 메모리에서 꺼내어 응답해줍니다.

 

이러한 서버 캐싱을 다양한 방법으로 이용할 수 있습니다. 

session storage, local stoarge와 같이 브라우저의 storage에 저장하여 사용할 수 도 있고 

전역상태로 관리를 할 수도 있습니다. 상태로 관리를 위해 redux를 도입할 수 있었습니다. 

redux는 기존의 mvc 패턴에서 점점 관리되지 않던 상태관리를 일관된 흐름을 이용한 flux 패턴을 기반으로 탄생한 상태관리 라이브러리 입니다. 상태가 존재하는 store가 존재하고 컴포넌트에서는 dispatch를 통해 action을 호출하면 store에서 상태가 변하고 변경된 상태를 사용하는 컴포넌트는 리렌더링 됩니다.

전역상태의 관리를 위하여는 좋은 툴이지만 서버에 요청을 보내는 비동기 동작을 하기 위해서는 redux-thunk, redux-saga와 같은 미들웨어 라이브러리를 추가해야 했습니다. 

저는 redux-saga를 사용해보면서 두개의 의문점이 생겼었습니다.

1. 서버의 상태를 컴포넌트들의 전역 상태와 같이 관리하는게 맞는건가? -> saga의 분리로 절충안을 내놓을 순 있을것 같습니다.

2. 서버의 상태가 프론트 코드에 위치하고 있는게 맞는걸까?

 

2번은 아직 좀 더 생각중입니다. 

제가 정확히 가지고 있는 의문이 저 wording으로 표현될 수 있는지는 좀 더 생각을 해봐야 할것 같습니다.

두서없이 생각을 적어보자면

상태의 변경은 리렌더링을 유발하고 전역 상태의 변경은 전체의 리렌더링을 수반한다고 하였습니다.

즉 전체에 영향을 끼치는 상태가 전역상태로 존재해야 함을 의미한다고 생각합니다.

서버의 상태가 전체에 영향을 끼치는 전역상태로 관리되는게 맞는걸까 생각이 들었습니다.

위에서 정의한 전역상태의 정의와 맞지 않다고 느껴졌습니다.

서버의 캐싱을 위해 전역상태를 이용한 것 같았습니다.

 

상품을 보여주는 페이지에서 비동기로 api를 호출하여 가져온 정보를 store에서 업데이트 하면 전역에서 관리되는 상태는 변경되지만 전체 앱은 리렌더링 될 필요가 없습니다. 

상품을 보여주는 페이지만 리렌더링이 되면 됩니다.

 

또한 redux-saga를 이용하여 비동기 통신을 이용할 수는 있지만 boiiler plate코드가 무수히 늘어갔습니다.

store가 너무 비대해졌고 캐싱과 같은 기능을 구현하기 위해서 필요한 Interface를 생각해보았지만 기능이 너무 복잡해질것 같아 실제로는 구현하지 못했습니다.

필요한 interface에는 key값, 요청한 시간, 유효 시간, 데이터의 상태 등과 같은것들이 있지 않을까 생각했었습니다.

 

그래서 react-query를 적용하였습니다.

react-query는 공식문서에 'Fetch, cache and Update data in your React and React Native applciations all without touching any "global state"' 라 명시되어있습니다.

전역상태에 영향을 끼치 않으면서 데이터를 fetch, cache, update해준다는 의미가 됩니다.

 

리액트 공식문서에는 서버 상태를 아래와 같이 정의하고 있습니다

  • 컴포넌트에서 제어하거나 소유하지 않는 원격의 위치에 존재하여야 한다
  • fetching과 updating을 위한 비동기 API가 필요하다
  • 공유되는 정보이고 다른사람들로 인하여 변경될 수 있습니다.
  • 어플리케이션에서 신경쓰지 않는다면 out of date 가 될수도 있습니다

react-query에서는 기본 cache 시간이 300초로 정해져 있고 stale time은 0으로 정해져 있습니다.

stale의 의미는 fresh와 비슷한 뜻입니다.

신선하지 않기 때문에 stale time이 지나면 데이터를 요청하겠죠.

이때 cache time내에서는 서버에 요청을 보내지 않고 캐시에 있는 데이터를 return 해줄겁니다.

또한 서버상태를 key값으로 관리를 할 수 있습니다.

key값에는 배열 혹은 string과 같은 값들이 들어갈 수 있습니다.

 

프론트 코드상에서는 캐시의 key값만 정의해두고 cache, stale 시간등을 조절해서 사용하면 컴포넌트의 전역상태와 별개로 관리할 수 있어 편리해졌었습니다.

 

http의 cache-control 특성에 stale-while-revalidate 옵션이 있습니다. stale time을 정의할 수 있는 속성입니다.

max-age는 30, stale-while-revalidate는 30이라고 가정해보겠습니다.

30초 이내에 요청은 cache의 결과를 보여주고

31 ~ 60초에 요청이 들어오면 우선 cache에 있는 결과를 보여주고 신선하지 않은 데이터 이기 때문에 서버에 요청을 보냅니다. 응답을 받은후에는 값을 교체해줍니다.

그러면 cache에 있는 결과는 신선한 상태가 됩니다.

이러한 로직을 react-query에서 관리해주니 많은 장점이 있다고 판단하였습니다.

 

 

 

(아직 배움이 많이 필요한 주니어의 생각입니다. 잘못된 부분들은 따끔하게 알려주시면 큰 도움이 됩니다!)

반응형

'Frontend > React' 카테고리의 다른 글

React 상태관리의 과거, 현재 그리고 미래 (번역글)  (0) 2022.04.09
Recoil 🌱  (0) 2022.04.08
React css-in-js  (0) 2022.04.06
React form 블로그 따라 다루기  (0) 2022.04.06
React keys  (0) 2022.04.02
반응형

https://betterprogramming.pub/all-you-need-to-know-about-css-in-js-984a72d48ebc를 번역한 글입니다.

 

All You Need To Know About CSS-in-JS

CSS-in-JS abstracts the CSS model to the component level, rather than the document level (modularity). You’ve probably heard terms like CSS-in-JS, Styled Components, Radium, and Aphrodite, and you’re…

betterprogramming.pub

 

CSS-in-JS는 CSS model을 document 레벨이 아닌 component 레벨에서 추상화 합니다.

 


Styled React Component Example

import React, { Component } from 'react';
import styled from 'styled-components';

const Wrapper = styled.div`
  background: black;
`
const Title = styled.div`
  color: white;
`

class App extends Components {
  render(){
    return {
      <Wrapper>
        <Title>Hello Styled Components!</Title>
      </Wrapper>
    }
  }
}

 

이미 CSS-in-JS, Styled Components, Radium, Aphrodite와 같은 것들을 들어봤을 겁니다, 그리고 아마 "이미 CSS-in-CSS(Css in .css)에 만족하는데 나와 무슨 연관이 있는거지" 하는 생각을 했을 겁니다.

 

이제 왜 이게 연관이 있는지, 어떤 개념인지 이해하고, 왜 이게 연관되어있고 희망적인지 밝히기 위해 왔습니다.

이전에 말한것과 같이 CSS-in-CSS를 사용하는 것은 자유입니다. CSS-in-JS를 사용할 의무는 없습니다.

어느 작업이든지 행복하게 하는 것을 사용하는 것이 최상의 해결책입니다.

 

CSS-in-js는 섬세하고 논쟁의 여지가 있는 주제입니다. 스스로에게 이것이 정말 나의 작업을 효율적으로 해줄것인가 물어보세요

결국 중요한 것은 생산적이고 더 기쁘게 해줄 도구를 사용하는 것입니다.

 

저는 항상 거대한 stylesheets 폴더를 유지해야 하는데 어색함을 느꼈습니다. 다른 접근들을 시도했었고. 새로운 styling 아이디어가 없을까 묻는 사람들을 많이 봐왔습니다. CSS-in-JS는 최선의 개념입니다.

 

소중 규모 프로젝트의 CSS


What is CSS-in-JS?

JSS는 CSS보다 강력한 추상화 도구입니다. 이것은 선언적이고 유지보수 가능한 방식으로 style을 나타내기 위해 Javascript를 언어로 사용합니다. 이것은 서버사이드와 런타임에서 동작하는 강력한 JS to CSS 컴파일러 입니다. 이것의 핵심 라이브러리는 낮은 레벨이며 프레임워크에 구애받지 않습니다. 6KB(minified하고 압축했을때)이고 plugins API를 통해 확장할 수 있습니다.
인라인 스타일과 CSS-in-JS는 같지 않다는 것을 명심하세요. 그들은 다르고 아래에서 증명해 보일것입니다.
 

Difference

모든 Css 특성들은 Javascript의 이벤트 핸들러와 함께 별칭으로 지정할 수는 없습니다. 많은 pseudo-selectors(:disabled, :before, :nth-child)에서는 불가능 합니다. html태그와 body태그를 스타일링 하는 것도 지원되지 않습니다. 

 

CSS-in-JS에서는 CSS를 간단하게 다룰수 있습니다. 실제 CSS가 생성되면 모든 media query와 pseudo-selector를 원하는대로 사용할 수 있습니다. 몇몇 라이브러리(jss, styled-components)에서는 nesting과 같은 css의 native 특성이 아닌것도 지원해줍니다. 

 

그냥 CSS-in-CSS로 하면 어떨까요?

 

CSS-in-CSS는 오랫동안 행해졌던 방식인 반면, 현대의 웹은 페이지가 아닌 컴포넌트 기반으로 작성되었다는 것이 장애요소 입니다.

CSS는 컴포넌트 기반의 접근을 시도해본적이 없습니다. CSS-in-JS는 이 문제를 해결하였습니다. CSS-in-JS solves exactly this problem. Shout-out to Vue for solving this problem beautifully, even though Vue’s styles have no access to component state.

 


What Are the Benefits of Using CSS-in-JS

  • 컴포넌트 기반이다. 더이상 stylesheet 다발을 유지할 필요는 없다. CSS-in-JS는 document level에서가 아니라 컴포넌트 단계에서 CSS를 다룬다
  • CSS-in-JS는 CSS를 강화하기 위하여 Javascript 생태계의 모든 힘을 이용한다.
  • 고립 원칙. 범위 선택자는 충분하지 않습니다. CSS는 명시적으로 정의하지 않았다면 부모 요소의 속성을 자동으로 상속한다는 특성이 있습니다. jss-isolate 플러그인으로 인해 JSS 는 property들을 상속받지 않습니다.
  • 범위 선택자. CSS는 selector의 충돌을 피할수가 없습니다. BEM과 같은 컨벤션은 하나의 프로젝트 내에서는 도움이 될수 있지만 서드파티 코드와 통합되면 도움이 되지 않을것입니다. JSS는 CSS를 나타내기 위해 JSON형식으로 컴파일 될때 기본으로 유일한 class이름을 생성합니다.
  • vendor prefixing. css 규칙은 자동으로 vendor-prefixed되므로 걱정하지 않아도 됩니다.
  • 코드 분할. JS와 CSS간에 변수와 함수를 쉽게 공유할 수 있습니다.
  • 화면에서 현재 사용하는 style만이 DOM에 존재합니다(react-jss)
  • 쓰이지 않는 코드를 삭제할 수 있다(tree shaking 같은 느낌)
  • CSS 유닛 테스트가 용이하다

vendor prefix


What Are the Drawbacks of Using CSS-in-JS?

  • 러닝커브가 존재합니다.
  • 새로운 의존성들이 존재합니다.
  • code에 바로 적용하기에 어려울 수 있습니다. frontend가 처음이면 많은것을 배워야 합니다.
단점보다 장점이 크므로 CSS-in-JS의 사용을 고려하세요!
 

사견 

 

css-in-js를 heavy하게 사용하다 보면 js전체 파일의 사이즈가 커집니다.

이는 load performance가 발생할 수도 있다는 것을 의미합니다.

파일크기가 커지면 네트워크가 온전치 않은 상황에서는 이슈가 발생할 수도 있습니다.

일반 css파일을 사용하면 js파일의 사이즈는 작아집니다.

찰나의 순간에 js전체가 다운로드 되지 않는 순간에는 좋지 않은 Ux를 제공해주지 않을까 싶습니다.

 

출처 :  

https://betterprogramming.pub/all-you-need-to-know-about-css-in-js-984a72d48ebc

 

All You Need To Know About CSS-in-JS

CSS-in-JS abstracts the CSS model to the component level, rather than the document level (modularity). You’ve probably heard terms like CSS-in-JS, Styled Components, Radium, and Aphrodite, and you’re…

betterprogramming.pub

 

반응형

'Frontend > React' 카테고리의 다른 글

Recoil 🌱  (0) 2022.04.08
React 상태관리🌱외 minor concept  (0) 2022.04.07
React form 블로그 따라 다루기  (0) 2022.04.06
React keys  (0) 2022.04.02
React input onFocus, onBlur(focus out) 😶‍🌫️  (0) 2022.04.02
반응형

https://jeonghwan-kim.github.io/dev/2022/03/29/react-form-and-formik.html

 

리액트로 폼(Form) 다루기

폼 다루는 방법에 대해 얘기해 보자. 브라우져는 폼을 다루기 위한 내장 API를 가지고 있다. 이걸 이용하면 빠르게 폼을 만들수 있는데 이전에 한 번 정리했다. HTML5 폼 검증에 대해 정리해 보자

jeonghwan-kim.github.io

의 포스팅 내용에서 많은 인사이트를 얻을 수 있을것 같아 따라해본 과정입니다!!

 

머릿속에 강하게 남은 내용을 정리하고자 한다.

  • 기존의 로직을 커스텀 훅으로 추출(연관된 로직이 전부 커스텀 훅에 위치하게 되는 과정, 세부구현은 밖에서 넣어준다)
  • 추상화를 사용함으로써 속성은 숨기고 가독성은 증가(중요 개념만 남김)
  • 리액트 컨텍스트를 사용한 상태의 공유 및 제어

 

연습해본 결과문

 


인사이트들에 대해 정리해보기 이전에 

Before/After 부터 살펴보면 좋다

아래의 iframe에서 직접 확인할 수 있다.

 


기존의 로직을 커스텀 훅으로 추출(연관된 로직이 전부 커스텀 훅에 위치하게 되는 과정, 세부구현은 밖에서 넣어준다)

기존 로직

import { useEffect, useState, useCallback } from "react";

const LoginForm3 = () => {
  const [values, setValues] = useState({
    email: "",
    password: "",
  });

  const [errors, setErrors] = useState({
    email: "",
    password: "",
  });

  const [touched, setTouched] = useState({
    email: false,
    password: false,
  })

  const handleBlur = e => {
    setTouched({
      ...touched,
      [e.target.name]: true,
    })
  }

  const handleChange = (e) => {
    setValues({
      ...values,
      [e.target.name]: e.target.value,
    });
  };

  const validate = useCallback(() => {
    const errors = {
      email: "",
      password: "",
    };

    if (!values.email) {
      errors.email = "이메일을 입력하세요";
    }

    if (!values.password) {
      errors.password = "비밀번호를 입력하세요";
    }

    setErrors({...errors});
  }, [values]);

  useEffect(() => {
    validate()
  }, [validate])

  const handleSubmit = (e) => {
    e.preventDefault();

    setTouched({
      email: true,
      password: true,
    })

    validate();

    if (Object.values(errors).some((v) => v)) {
      return;
    }

    alert(JSON.stringify(values, null, 2));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="email"
        value={values.email}
        onChange={handleChange}
        onBlur={handleBlur}
      />
      {touched.email && errors.email && <span>{errors.email}</span>}
      <input
        type="password"
        name="password"
        value={values.password}
        onChange={handleChange}
        onBlur={handleBlur}
      />
      {touched.password && errors.password && <span>{errors.password}</span>}
      <button type="submit">로그인</button>
    </form>
  );
};

export default LoginForm3;

Component가 선언된 부분부터 jsx를 return하는 부분까지가 form이 동작하기 위한 코드이다.

3가지 종류의 상태를 가지고 있다. 

각각의 상태를 handle하는 함수들을 일일히 지정해 주고 있다.

위의 방식은 재사용이 불가능 하다. 

만약에 다른 컴포넌트에서 form을 사용한다면 위의 코드 전부가 복붙이 될것이다.

유지보수가 어떻게 될 지는 불을 보듯 뻔하다.

 

한가지 반성할 점은 이전까지 코드를 위와 같이 작성했었던 것 같다. 

커스텀 훅을 작성하는 것에 잘못된 기준을 가지고 있었던 것 같다. 

* 다른 곳에서 무조건 재사용 되어야 할것 

(+ 파일의 수가 많아지는 것에 대해 반감을 가지고 있었다)

내가 추구했던 기준이었다.

 

지금은 생각이 다르다.  

아래의 조건중 하나라도 해당한다면 커스텀 훅을 고려한다. 

* 여러 상태를 보유해야 하는 경우 (커스텀 훅 내에서 useReducer의 사용을 고려한다)

* 컴포넌트와 다른 추상화 수준  ( jsx내의 추상화 수준과 위의 선언 로직 또한 같은 단계의 추상화 수준을 유지하는 것이 best이지 않을까? -> 이건 계속 생각 중이다. view단의 추상화와 위의 선언 로직의 추상화 또한 같은 레벨을 추구하는 것이 맞을까?)

* 덩치가 너무 커진 코드

 

기능을 구현해야 할때 딱 필요한 만큼의 코드를 짜는 것을 추구한다. 
(닭을 잡아야 할때는 닭을 잡을때 쓰는 칼을 써야하는 느낌이다. 소를 잡을때 쓰는 칼을 쓰지 않는것처럼)

필요한 만큼의 코드를 짜야할 때는 클린코드의 개념이 선행되어야 한다고 생각한다.
프로그래머의 고뇌로 탄생한 글 이기에모든 코드는 유지되어야 할 가치가 있다.
하지만 짧은 코드는 클린코드의 개념에 반만 걸쳐져 있는것 같다.
짧은 코드는 읽기는 좋지만 의미를 파악하기에 충분치 않을 수도 있다.
의미를 효율적으로 전달하기에 긴 글이 필요하다면 당연히 긴 글을 선택해야 한다.
파일이 많아지고 덩치가 커지는 것이 의미를 더 효율적으로 전달할 수 있다면 클린코드의 방향에 그릇된 방향은 아니지 않을까

 

커스텀 훅 결과물

import { useEffect, useState, useCallback } from "react";

const useForm = ({ initialValues, validate, onSubmit }) => {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const handleBlur = (e) => {
    setTouched({
      ...touched,
      [e.target.name]: true,
    });
  };

  const handleChange = (e) => {
    setValues({
      ...values,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    setTouched(
      Object.keys(values).reduce((touched, field) => {
        touched[field] = true
        return touched
      }, {})
    );

    const errors = validate(values);
    setErrors(errors)

    if (Object.values(errors).some(v => v)) {
      return;
    }

    onSubmit(values);
  };

  const runValidator = useCallback(() => validate(values), [values])

  useEffect(() => {
    const errors = runValidator();
    setErrors(errors)
  }, [runValidator]);
  
  const getFieldProps = name => {
    const value = values[name]
    const onBlur = handleBlur
    const onChange = handleChange
    
    return {
      name,
      value,
      onBlur,
      onChange,
    }
  }

  return {
    getFieldProps,
    handleSubmit,
    handleChange,
    handleBlur,
    values,
    touched,
    errors
  }
};

export default useForm;

 

이 커스텀 훅은 작성할때 나중에 보게된 것이 있다. 

세부구현은 밖에서 넣어준다

실 사용예시를 살펴보자

세부 구현은 밖에서 넣어주자

현재 form component 에서 핵심 기능은 어떤것일까? 

0. 초기에 노출시켜줄 값(만약 edit이라면 이전의 값을 넣어주어야 할 것이다.) 

1. 입력된 값의 검증

2.입력된 값의 제출

 

크게 3개로 정리해 보았다.(이외에 여러가지가 있을 수도 있고 저 위에 3가지가 해당하지 않을 수도 있다.)

3개에 대한 구현은 Form 컴포넌트를 이용하는 곳에서 사용한다.

 

Form 을 사용할때 저 구현들을 입력값으로 받지 않고 Form의 할당 부분에서 정의할 수도 있었을 것이다.(동작은 하겠지.. 그게 문제가 아니자나!!)

// 생략
  return (
    <Form>
    // 생략
    </Form>
   )
// 생략

또한 위와 같이 작성되었다면 form의 동작을 확인하기 위해서는 Form이 정의된 다른 모듈을 참고해야한다.

(시간은 자원이다. 의미를 파악하기 위해 모듈간을 탐험해야 한다면 자원은 많이 소모될 것이다)

 

커스텀 훅을 만드는 이유는 재사용성에 있다고 생각한다. 

여러 컴포넌트에서 사용해야 할 경우가 있다는 것이다. 

이러면 경우의 수가 엄청나게 많이 생길수 있지 않을까(a에서 불러와 사용하는 경우, b에서 불러와 사용하는 경우 등등)

이러면 case by case로 커스텀 훅 내에서 대응할 게 아니라 오히려 사용법만 주고 이런걸 입력해야 해 라고 알려주는 것이 효율적으로 대응하는 수단이 아닐까 싶다.

마치 와플메이커를 가지고 사람들이 와플도 만들고 빵도 만들고 누룽지도 만드는 것처럼?? 

 

여기서 선언형 프로그래밍도 엿볼수 있지 않을까? (여기 내용은 아닐수도 있습니다!! 공부하고 나중에 수정될 수도 있습니다! 아직 주니어니까!)

세부구현은 뒤로 숨겨놓는다. 이때 세부구현은 명령형 프로그래밍으로 작성한다(a를 하고, b를 하고, c를 수행한후에 d를 return해)

그리고 프레임워크 or 라이브러리에게 말하는 것이다. 

내가 a,b,c를 넣었으니 d를 가져와줘 

<Component a={a} b={b} c={c}></Component> => d 결과물


<div>
  {a && <p>a</p>}
  {b && <p>b</p>}
  {c && <p>c</p>}
</div>  => d결과물

 

 

여튼 말하고 싶었던 내용은 커스텀 훅에 대한 두려움이 있었던 지난날을 반성하고

꽁꽁 숨기는 것보다는 오히려 드러내는 것(세부구현)이 재사용성, 가독성 면에서는 오히려 도움이 되지 않을까 하는 의견을 피력하고 싶었다.


추상화를 사용함으로써 속성은 숨기고 가독성은 증가(중요 개념만 남김)

위에서 글의 boundary를 알맞게 정하지 못해서 위에서 언급한 내용과 중복될 것 같다.

<!-- 1번 -->

<Form>
  <Field type="email" name="email" />
  <ErrorMessage name="email" />
  <Field type="password" name="password" />
  <ErrorMessage name="password" />
  <button type="submit">로그인</button>
</Form>


<!-- ------------------------------------------  -->

<!-- 2번 -->

<Form
  initialValues={{ email: "", password: "" }}
  validate={validate}
  onSubmit={handleSubmit}
>
  <Field type="email" name="email" />
  <ErrorMessage name="email" />
  <Field type="password" name="password" />
  <ErrorMessage name="password" />
  <button type="submit">로그인</button>
</Form>

1번의 형태는 

가독성이 좋다.

추상화 수준이 일관되어 있다. 라는 특징을 가지고 있다고 생각한다. 

하지만 form내에 어떤값이 들어가는지 검증은 어떻게 하는지, submit 버튼을 누르면 어떻게 되는지 알수는 없다

이는 모듈간의 탐험을 시작해야 한다는 의미가 된다.

 

중요개념은 남겨주어야 한다. 

중요개념만 props로 건네주면 의미를 파악하기위해 모듈간의 탐험은 시작하지 않아도 될것만 같다. (어렵다...)

(컴포넌트의 중요 의미를 정의하고 이 의미를 직접 주입하여 주자)


리액트 컨텍스트를 사용한 상태의 공유 및 제어

리액트의 컴포넌트는 상태를 가진다. 

상태는 컴포넌트 내부에서 관리되며 다른 컴포넌트에게 props로 전달할 수 있다.

상태의 변화는 곧 데이터가 변화가 되었다는 것을 의미한다. 

이는 곧 화면을 새로 보여주어야 한다는 의미가 된다. 

상태의 변화는 리렌더링을 해야한다는 의미이다.

 

이런 상태는 크게 3가지 쓰임이 있을 수 있을것 같다.

1. 컴포넌트 내부에서의 쓰임

2. 여러 컴포넌트 간의 쓰임

3. 전역에서의 쓰임

 

1번은 하나의 컴포넌트만 리렌더링이 되므로 괜찮다. 

(React에서는 메모리에 가상돔을 가지고 있고 재조정 알고리즘을 가지고 새로 렌더링 해줘야 하는 컴포넌트만 계산하여 렌더링을 시켜준다.
재조정 알고리즘에는 크게 2가지를 이용하는데
부모 컴포넌트가 변경되었다면 변경된 부모 컴포넌트의 모든 자식을 리렌더링,
형제 컴포넌트 간에는 key를 이용하여 key값이 변경된 컴포넌트만 리렌더링해준다.
이때 비교 알고리즘에는 Object.is 알고리즘을 사용한다)

2번은 부모 컴포넌트의 상태가 변경되면 부모를 포함한 모든 자식 컴포넌트가 리렌더링 된다. 

3번은 전체 컴포넌트가 리렌더링 된다.

 

잦은 리렌더링은 클라이언트에게 부하를 일으킨다(cpu bound)

따라서 전역 상태의 추가는 지양되어야 한다. (전역 상태가 될 수 있는 대표적인 2가지(ight/dark 모드, 외국어 지원) 이외에는 프로젝트의 성격에 따라 다를것 같다)

3번은 최대한 지양한다해도 2번은 어쩔수가 없다.

이때 state를 props로 컴포넌트간에 넘겨준다하면 props driiling이라고 하는 상황을 마주하게 된다.(가독성을 크게 해치게 될 것이다.)

 

이 props driiling은 리액트의 컨텍스트를 이용하면 피할 수 있다. (물론 전역 상태로 정의해서 피할수도 있다)

컨텍스트에 대한 자세한 쓰임은 언급하지 않을것 같다.

 

Form 내부에서 사용되는 Field, ErrorMessage들은 useContext hook을 통해 form의 상태에 접근한다. 

그리고 이를 제어한다.(리렌더링 발생)

 

상태는 전역으로 관리되지 않아야 하고 제어되는 곳에서 최대한 가까운 곳에서 정의되어 있어야 한다고 생각한다.

아래의 그림은 내가 생각하는 가장 적절한 상태 관리 흐름이다.

위의 그림은 dom tree를 형상화 한것이다.

상태의 정의는 체크 표시가 들어간 컴포넌트에서 하게 된다.

같은 색깔의 노드들은 같은 상태들을 공유한다. 

 

3depth 이상에서는 Context를 사용할 것 같고 아니라면 props 로서 전달을 고려할 것 같다.

 

React에서는 createContext로 context를 생성후에 provider로 감싸주면 value로 전달된 값을 

children에서 가져와 사용할 수 있다.

이때 값을 가져올때는 useContext훅을 사용하면 된다. 

반응형

'Frontend > React' 카테고리의 다른 글

React 상태관리🌱외 minor concept  (0) 2022.04.07
React css-in-js  (0) 2022.04.06
React keys  (0) 2022.04.02
React input onFocus, onBlur(focus out) 😶‍🌫️  (0) 2022.04.02
React moment locale 적용하기 🌹  (1) 2022.04.02
반응형

React에서 key는 어떤 항목을 변경, 추가, 삭제할지 식별하는 것을 도와줍니다.

key는 꼭 지정하는 것이 좋고 data의 id와 같이 고유한 값을 지정해 주는 것이 좋습니다.

이때 전체 파일에서 고유할 필요는 없습니다.

인접한 리스트에서만 key 값은 고유하면 됩니다.

 

항목의 순서가 바뀔 수 있는 경우에 key값에 Index값을 사용하는 것은 적절치 않습니다.

불필요한 렌더링이 일어날 수 있습니다.

key값을 명시해주지 않으면 React는 기본적으로 index를 key로 사용합니다.

 

key값이 존재하지 않을때

 

React에서는 인접한 요소들에서 Key 값이 설정되어 있지 않다면 index 값을 default로 사용한다고 하였습니다.

위의 gif에서 input에 있는 값들의 동작은 분명 이상합니다.

list의 뒤에 추가될 때는 이상이 없지만 앞에 추가될때는 동작이 이상해지는 것을 확인하였습니다.

이는 React에서 key 값이 동일한 경우에 동일한 DOM 요소를 보여주기 때문입니다.

a를 입력한 input의 요소는 key값 0에 mapping이 되어있습니다.

a의 앞에 list가 추가가 되면 새로 추가된 list가 index 0을 가지게 됩니다.

그래서 value가 a인 input은 key 값 0인 요소에 나타나 버립니다.

 

따라서 아래와 같이 key 값을 list에 추가해주면 list의 순서가 변경되더라도 key값이 변경되지 않으므로 우리가 원하는 결과를 가져올 수 있습니다.

 

key 값이 존재할때

아래의 코드는 예시에 사용된 코드 입니다.

import { useState } from 'react';
import { faker } from "@faker-js/faker";

function ListWithoutKey() {
  const [people, setPeople] = useState(['a', 'b', 'c'])
  const unShiftPeople = () => {
    const randomName = faker.name.findName();
    setPeople([randomName, ...people]);
  };

  const pushPeople = () => {
    const randomName = faker.name.findName();
    setPeople([...people, randomName])
  }

  return (
    <>
      <ul>
        {people.map((person) => (
          <li>{person}<input type="text" /></li>
        ))}
      </ul>
      <button onClick={unShiftPeople}>unshift</button>
      <button onClick={pushPeople}>push</button>
    </>
  );
}

function ListWithKey() {
  const [people, setPeople] = useState([
    { name: "a", id: 12 },
    { name: "b", id: 123 },
    { name: "c", id: 1234 },
  ]);
  const unShiftPeople = () => {
    const name = faker.name.findName();
    const id = faker.datatype.uuid();
    const newPerson = {name, id}
    setPeople([newPerson, ...people]);
  };

  const pushPeople = () => {
    const name = faker.name.findName();
    const id = faker.datatype.uuid();
    const newPerson = { name, id };
    setPeople([...people, newPerson]);
  };

  return (
    <>
      <ul>
        {people.map((person) => (
          <li key={person.id}>
            {person.name}
            <input type="text" />
          </li>
        ))}
      </ul>
      <button onClick={unShiftPeople}>unshift</button>
      <button onClick={pushPeople}>push</button>
    </>
  );
}

function App() {
  return (
    <div className="App">
      <ListWithoutKey />
      <hr />
      <ListWithKey />
    </div>
  );
}

export default App;

 

 

 

출처: https://ko.reactjs.org/docs/lists-and-keys.html#keys

 

리스트와 Key – React

A JavaScript library for building user interfaces

ko.reactjs.org

https://ko.reactjs.org/docs/reconciliation.html#keys

 

재조정 (Reconciliation) – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

반응형

'Frontend > React' 카테고리의 다른 글

React css-in-js  (0) 2022.04.06
React form 블로그 따라 다루기  (0) 2022.04.06
React input onFocus, onBlur(focus out) 😶‍🌫️  (0) 2022.04.02
React moment locale 적용하기 🌹  (1) 2022.04.02
React Derived State 🥲  (0) 2022.04.02

+ Recent posts