04. 서버 사이드 렌더링
4.1 서버 사이드 렌더링이란?
4.1.1 싱글 페이지 어플리케이션의 세상
SPA(Single Page Application) 란?
- 렌더링과 라우티엥 필요한 대부분의 기능을 서버가 아닌 브라우저의 JS 에 의존하는 방식
- 서버에서 HTML 을 내려 받지 않고, 하나의 페이지의 JS 에 의해 모두 작어이 처리 되는 방식
- 초기에 큰 JS 파일을 다운 받아야 하지만, 한번 로딩 된 이후에는 사용자에게 훌륭한 UI/UX 를 제공할 수 있다
전통적인 방싱의 어플리케이션과 싱글 페이지 어플리케이션의 작동 비교
- 전통적 방식의 어플리케이션은 화면 전환시 서버에서 다시 HTML 을 받아서 그리기 때문에 부자연스러운 모습을 보인다
- 반면 SPA 는 최초 한번 리소스를 다운 받으면 페이지 전환 시 추가 다운로드 없이 페이지 전환이 일어나므로 깔끔한 모습을 보인다 (Ex, Gmail 페이지)
싱글 페이지 렌더링 방식의 유행과 JAM 스택의 등장
- 과거 PHP 나 JSP 기반의 웹 어플리케이션은 대부분 서버 사이드 렌더링으로 이루어졌으며, JS 는 사용자에게 추가적인 기능을 제공하는 보조적인 수단으로 사용이 되었다
- 2010년경 Backbone.js, Angular.js, Knockout.js 등이 등장하면서 JS 로도 MV + @ 프레임워크를 구현이 가능해 짐
- 결국 JS 의 기능이 커짐에 따라 다른 많은 것들을 신경 써야하는 서버 사이드 렌더링 방식이 아닌 JS 로만 구성 된 프레임워크로 작성 된 SPA 가 인기를 끌게 됨
- JAM(JS, API, MarkUp) 스택이 점차 확산되어 LAMP(Linux, Apache, MySQL, PHP) 를 대체하기 시작
- Node.js 의 고도화에 따라 백엔드 또는 API 도 JS 로 개발하는 MEAN, MERN 스택이 인기를 끔(MongoDB, Express, Angular, React, Node.js)
새로운 패어다임의 웹서비스를 향한 요구
- 웹페이지에서 요구되는 사항이 점점 더 커짐에 따라 JS 리소스의 크기가 커져 결국 웹페이지 로딩의 속도가 과거에 비해 차이가 없거나 느려지는 문제가 발생
- 이를 해결하기 위해 등장한 방식이 SSR(SSR, Sever Side Rendering) 이다
4.1.2 서버 사이드 렌더링이란?
서버 사이드 렌더링의 장점
- 사용자가 최초 페이지에 진입 했을 때 페이지에 유의미한 정보(FCP, First Contentful Paint)가 그려지는 시간이 빠르다.
- SPA 의 경우 JS 다운로드 후, HTTP 통신 작업이 완료 된 이후 페이지를 그리게 되므로 느리다
- SSR 의 경우 HTTP 통신을 백엔드에서 하는 것이 더 빠르기도 하며, HTML 을 그리는 작업도 서버에서 직접 미리 그려서 내려주기 때문에 속도에서 이점을 가진다
- 단, 서버가 충분한 리소스를 확보 하였을 때 이야기이다
- 검색 엔진(SEO, Serch Engine Ooptimization)과 SNS 공유 등 메타데이터 제공이 쉽다
- 검색 엔진은 HTML 의 정적인 데이터를 분석하므로 SPA 의 JS 의 데이터는 읽을 수 없어, SEO 및 메타 데이터 제공에 약점을 가진다
- 누적 레이아웃 이동이 적다
- 사용자에게 FCP 를 보여준 이후 뒤늦게 어떤 HTML 정보가 추가 되거나, 삭제되어 화면이 갑자기 변하는 부정적 사용자 경험이 줄어든다.
- 기사 페이지에서 기사를 읽고 있는데, 위 배너가 갑자기 로딩되어 글이 아래로 덜컥 이동하는 현상 같은 것
- 다만, SSR 을 이용한다고 해서 누적 레이아웃 이동으로 부터 완전히 자유로울 순 없다
- 사용자의 디바이스 성능에 비교적 자유롭다
- 보안에 좀 더 안전하다
- 주요 로직이 서버에서 전부 작동되어 전달 되므로, 보안에 안정적이다
서버 사이드 렌더링의 단점
- 소스코드 작성 시 항상 서버를 고려해야 한다
- 기존 CSR에서 사용하던 window 객체 또는 sessionStorage 과 같이 브라우저에만 있는 객체 사용이 제한된다
- 외부 의존 라이브러리 역시 서버에 대한 고려가 필요하다
- 적절한 서버가 구축되어 있어야 한다
- 사용자의 수, 요청에 따라 적절히 대응할 수 있는 서버를 구축 해야만 함
- 서버 장애에 대한 대응과, 분산 처리등에 대해서도 신경을 써서 개발이 필요하다
- 서비스 지연에 따른 문제
- CSR 은 통신 지연이 일어나도 어떤 화면이라도 뜬 상태가 되지만, SSR 은 아무런 화면이 안뜨는 현상이 생길 수 있으므로 각별한 주의가 필요하다
4.1.3 SPA 와 SSR 을 모두 알아야 하는 이유
SSR 역시 만능이 아니다
- 서버에 무거운 작업을 모두 미루는 것이 능사가 아니다, 잘못된 설계로 인해서 성능 저하는 물론 관리를 두 곳 모두 해야하는 문제 역시 발생 가능하다
SPA 와 SSR 어플리케이션
- 가장 뛰어난 SPA 는 SSR 로 생성되는 MPA(Multi Page Application) 보다 낫다
- 최초 렌더링 부분만 최적화하여 보여주고, 나머지는 게으른 로디응로 렌더링 하도록 처리 -> 뛰어난 성능과 매끄러운 사용자 경험 제공 가능
- 평균적인 SPA 는 MPA 보다 느리다
- 성능 최적화가 안된 SPA 는 서버에서 빠르게 렌더링이 되는 MPA 보다 느릴 가능성이 높다
- MPA 라우팅으로 인한 문제를 해결하기 위한 API 들
- 페인트 홀딩(Paint Holding) : 같은 출처의 라우팅은 새로운 화면을 그릴 때 빈 화면이 아닌 이전 페이지의 모습을 잠깐 보여주는 방법
- Back Forward Cache(BFCache) : 브라우저 앞, 뒤로 가기 실행 시 캐시에 저장된 페이지를 보여주는 기법
- Shared Element Transitions : 페이지 라우팅 발생 시, 동일 요소는 콘텍스트를 유지하여 부드럽게 보여주는 기법
** [p. 268] MPA 에서 라우팅으로 인해 발생하는 문제를 해결하기 위한 API 를 SPA 에서는 JS 와 CSS 의 도움을 받아서 상당한 노력을 통해 기울여야 한다고 하는데, 진짜 그런가요?
** 개인적으로 사용자 디바이스가 정말 안좋은게 아닌 이상에는 오히려 반대가 아닌가 싶습니다. 통신 상황이 안좋다면 오히려 SSR 이 더 안좋은 유저 경험을 준다고 이미 책에서 밝히고 있으며, 페인트 홀딩, BFCache, Shared Element Transitions 은 SPA 프레임 워크에서 신경을 오히려 안써도 되는 부분 아닌가 싶어서 의문이 남네요. 다들 어떻게 생각하시나요?
현재의 서버사이드 헨더링
- 기존 LAMP 방식은 모든 렌더링을 서버에 의존했지만, 요즘은 최초 진입시에는 SSR 로 렌더링된 적은 리소스의 HTML 을 받아서 빠르게 화면을 보여주고 SEO 도 만족을 시켜준다. 그리고 사용자가 최초 진입 된 페이지를 보는 동안 받아진 JS 를 로딩하여 나머지 동작은 SPA 처럼 동작한다
4.1.4 정리
- 최근에는 SPA 와 SSR 의 장점을 전 부 알고, 좋은 사용자 경험을 위해 두 가지 방법을 모두 이해하고 필요에 맞게 사용하는 것이 중요하다.
4.2 서버 사이드 렌더링을 위한 리액트 API 살펴보기
4.2.1 renderToString
- 리액트 컴포넌트를 랜더링해서 HTML 문자열로 반환하는 함수로 가장 기초적인 SSR API 이다
const result = ReactDOMServer.renderToString(
React.createElement('div', { id: 'root' }, <SampleComponent />);
)
- 결과물은 아래와 같이 반환된다
<div id="root" data-reactroot="">
<div>hello</div>
<ul>
<li>apple</li>
<li>banana</li>
<li>peach</li>
</ul>
</div>
- 빠르게 브라우저가 HTML 을 그릴 수 있도록 제공해 주는 것이 목적이기 때문에 해당 API 는 이벤트 핸들러와 같은 JS 는 포함이 안되는 것을 볼 수 있다
- data-reactroot="" 속성을 통해 컴포넌트의 루트 엘리먼트가 무엇인지를 식별하여, 이후 JS 를 실행하기 위한 기반이 된다
4.2.2 renderToStaticMarkup
- 리액트 컴포넌트를 HTML 로 만드는 renderToString 과 매우 유사한 함수
- 단, 리액트에서만 사용하는 추가적인 DOM 속성을(data-reactroot="" 같은 것) 만들지 않아 크기를 약간 줄일 수 있는 장점이 있다
- 따라서, useEffect 와 같은 브라우저 API 사용이 불가능하다
<div id="root">
<div>hello</div>
<ul>
<li>apple</li>
<li>banana</li>
<li>peach</li>
</ul>
</div>
4.2.3 renderToNodeStream
- renderToString 과 동일한 결과물을 만들어내지만 두가지 차이를 가진다
- 브라우저에서 사용이 불가능
- 결과물이 string 이 아닌 Node.js 에 의존하는 ReadableStream 로 만들어진다
- 결과물이 스트림으로 들어오기 때문에 데이터가 클 경우 작은 Chunk 로 분할하여 가져온게 된다. 따라서 HTML 의 크기가 클경우 작은 Chunk 로 분리되어 작성되므로 이점을 가진다
- 대부분 널리 알려진 리액트 SSR 프레임워크는 해당 API 를 채택
4.2.4 renderToStaticNodeStream
- renderToNodeStream 의 결과물에서 renderToStaticMarkup 과 마찬가지로 리액트 JS 에 필요한 속성만 빼는 API
4.2.5 hydrate
- renderToString, renderToNodeStream 로 생성된 HTML 컨텐츠에 JS 핸들러나 이벤트를 붙이는 역할
- 기본적으로 랜더링된 HTML 이 있다는 가정하에, 이벤트를 붙이는 작업을 실행한다
import * as ReactDOM from 'react-dom' import App from './App';
// containerId를 가리키는 element는 서버에서 렌더링된 HTML의 특정 위치를 의미한다.
// 해당 element를 기준으로 리액트 이벤트 핸들러를 붙인다.
const element = document.getElementById(containerId);
ReactDOM.hydrate(<App />, element);
- 리액트 관련 정보가 없는 순수한 HTML 정보가 전달 될 경우?
<!DOCTYPE html>
<head>
<title>React App</title>
</head>
<body>
<!-- root에 아무런 HTML도 없다. -->
<div id="root"></div>
</body>
</html>
function App() {
return <span>안녕하세요.</span>;
}
import * as ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
// Warning: Expected server HTML to contain a matching <span> in <div>.
// at span // at App ReactDOM.hydrate(<App />, rootElement)
- span 요소가 있는 것을 가정하고 작동되는 hydrate 이므로 주석과 같은 경거 문구가 출력
- 다만, 경고가 출력 될 뿐 실행은 되는데 위와 같은 불일치가 발생하면 hydrate 가 렌더링한 결과물을 기준으로 웹페이지를 그리기 때문이다 => 물론 잘못된 사용법이다
- hydrate 가 아무리 빨리 끝나도 시간이 걸리므로 시간을 기록하는 기능등에는 불일치가 발생할 수 밖에 없다
- 이러한 에러를 해결하기 위해서는 해당 요소에 suppressHydrationWarning 을 추가하여 경고를 제거 가능
<div suppressHydrationWarning>{new Date().getTime()}</div>
4.2.6 서버 사이드 렌더링 예제 프로젝트
4.2.7 정리
- SSR 구현을 위해서는 서버에서 다뤄야 할 것들이 많아 복잡하다
- 따라서, 리액트 팀에서도 적절한 프레임 워크 사용을 권한다
4.3 Next.js 톺아보기
** 리액트도 아직 볼게 많은데, 굳이 SSR 과 Next 가 여기 나오는 이유는 좀 이해가 안가네요. 오히려 머리 속만 더 복잡해지는 느낌입니다
** 아마도 책 완성 단계에서 워낙 Next 랑 SSR 이 핫해지다보니 급하게 넣은게 아닌가 하는 의심이...
4.3.1 Next.js 란?
- Vercel 에서 만든 리액트 기반 풀스택 프레임 워크
- PHP 대용으로 사용되기 위해 개발을 했다고 언급할 정도로 서버 사이드 렌더링을 염두에 둔 프레임워크
- 과거 리액트 내부에서 SSR 을 위해 개발하다 중지 되었던 react-page 의 방향성을 유지
4.3.2 Next.js 시작하기
- npx-create-next-app 명령어로 프로젝트 생서
next.config.js
- Next 의 환경설정을 담당
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinifi: true,
};
- /*_ @type _/ : 타입 스크립트의 타입 도움을 받기 위한 코드
- reactStrictMode : 리액트의 엄격 모드, 잠재적인 문제를 개발자에게 알리기 위한 도구
- swcMinify : 번들링과 컴파일을 빠르게 수행하기 위해 만든 SWC 를 사용하여, 코드 최소화 작업을 진행. Babel 의 대용이다. Rust 로 구성된 SWC 로 인하여 속도가 더 빠르다
pages/_app.tsx
- 전체 페이지의 시작점
** v14 에서는 page.tsx 가 해당 역할을 한다
pages/_document.tsx
- HTML 를 초기화 하는 부분
- SEO 에 필요한 정보나 Title 등을 담을 수 있다
- crate-next-app 으로는 생성이 되지 않는다
** v14 에서는 layout.tsx 가 해당 역할을 한다
pages/_error.tsx
- 클라이언트 또는 서버에서 발생하는 500 에러는 담담하기 위한 페이지
- crate-next-app 으로는 생성이 되지 않는다
pages/404.tsx
- 404 페이지를 정의하는 파일
- 따로 정의하지 않으면, next 에서 기본 제공하는 페이지로 작동한다
pages/500.tsx
- 서버에서 발생하는 에러를 핸들링하는 페이지
- _error.tsx 와 500.tsx 가 동시에 존재하면 500.tsx 가 우선하여 실행된다
pages/index.tsx
-
pages 폴더의 하위 폴더명을 주소 값으로 사용 가능, 해당 폴더의 index.tsx 파일이 해당 주소 값에 대응
-
폴더 내부의 다른 파일명은 해당 주소의 하위 주소로 참조 가능
-
/pages/hello.tsx => localhost:3000/hello 대응
-
/pages/test/hello.tsx => localhost:3000/test/hello 대응
-
v13 에서 App Router 의 적용으로 구조가 변경 되었다
-
이제는 pages 폴더가 app 폴더로 대체 되었으며, index.tsx 의 역할을 page.tsx 가 대신한다
-
app/test/page.tsx => localhost:3000/test 대응
-
또한 기존의 파일명이 바로 주소에 대응되는 구조는 지원하지 않으며, layout.tsx -> template.tsx 의 순서로 구조를 가진다
서버 라우팅과 클라이언트 라우팅의 차이
- Next 는 CSR 과 SSR 을 동시에 지원한다
- 사용자에게 최선의 경험을 제공하기 위해 최초 페이지는 SSR 을 사용하고, 페이지 이동이나 변화는 CSR 의 장점을 사용한다
페이지에서 getServerSideProps 를 제거하면 어떻게 될까?
export default function Hello() {
console.log(typeof window === "undefined" ? "서버" : "클라이언트");
return <>hello</>;
}
// 만약 아래 부분을 제거하면?
export const getServerSideProps = () => {
return {
props: {},
};
};
- getServerSideProps 가 제거되면 서버에서 실행이 필요 없는 페이지로 처리되어 빌드시에 별도로 페이지 빌드를 하지 않는다
** getServerSideProps 도 안알려주고 이런걸 알려주면... next 모르는 사람은 어쩌라는 것인지...
/pages/api/hello.ts
- Next 는 서버이므로 간단한 형태의 백엔드 api 구성이 가능
- BFF(Backend for Frontend) 형태로 활용하거나, 자체로 풀스택 어플리케이션 구축, CORS 이슈 우회등을 위해 사용이 가능하다
4.3.3 Data Fetching
getStaticPaths 와 getStaticProps
- 사용자, 통신에 관계 없이 정적으로 결정된 페이지를 보여주고자 할 때 사용되는 함수
import { GetStaticPaths, GetStaticProps } from "next";
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: [{ params: { id: "1" } }, { params: { id: "2" } }],
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const { id } = params;
const post = await fetchPost(id);
return {
props: { post },
};
};
export default function Post({ post }: { post: Post }) {
// post로 페이지를 렌더링한다.
}
- /pages/post/[id] 가 접근 가능한 정적 페이지를 구성하는 코드
- params 로 1, 2 만 정의되어 있으므로 /post/3 이 호출되면 404 페이지가 반환
- 미리 서버에서 예상되는 요청에 맞는 HTML 페이지를 만들어 놓기 때문에 빠른 응답이 가능하다
- 정적인 데이터만 제공하는 블로그 글, 약관 등을 빠르게 제공하는데 사용이 가능하다
getServerSideProps
- 서버에서 실행되는 함수로 무조건 페이지 진입 전에 함수를 실행한다
- props 로 전달이 가능한 값은 JSON 형태로 직렬화가 가능해야만 함. class 나 Date 는 전달 불가능
- 서버에서만 실행 되므로 window, document 등 사용 불가
- 브라우저와는 달리 자신의 호스트 유추가 불가능 하므로 완전한 주소로만 api 요청이 가능 (/api/test 와 같은 주소 사용 불가)
- 해당 함수가 실행이 완료 되기 전까지는 사용자에게 어떠한 HTML 도 보여줄 수 없다
- 조건에 따라 다른 페이지로 보내고 싶다면 redirect 를 사용 가능
export const getServerSideProps: GetServerSideProps = async (context) => {
const {
query: { id = '' },
} = context const post = await fetchPost(id.toString());
if (!post) {
redirect: {
destination: '/404'
}
}
return {
props: { post },
}
}
getInitialProps
- getStaticProps, getServerSideProps 가 나오지 전의 유일한 수단
- 대부분의 경우에는 getStaticProps, getServerSideProps 를 사용을 권장
- 해당 함수는 서버와 클라이언트 모두에서 사용이 가능하므로 이러한 특징을 반드시 감안하여 코드를 작성해야만 한다
- 레거시에서 사용을 대비하여 알아둘 것
4.3.4 스타일 적용하기
전역 스타일
- _app.tsx 에 적용
- v14 이후에는 app/layout.tsx 에 적용
컴포넌트 레벨 CSS
- [name].moudule.css 같은 명명 규칙만 준수하면 된다
SCSS 와 SASS
- 기존과 동일하게 사용 가능
CSS-in-JS
- JS 내부에 스타일시트를 삽입하는 방법
- styled-jsx, styled-components, Emotion, Linaria 등등
- styled-components 의 경우 HTML 에 스타일을 입히는 것이 아니라 CSSOM 트리에 직접 삽입하므로 속도면의 이점이 있다
- next 와 swc 사용을 원하면 styled-jsx, styled-components, emotion 중 하나 쓸 것
- 단, v14 에서는 지원이 잘 안된다는 이슈가 있음
4.3.5 _app.tsx 응용하기
4.3.6 next.config.js 살펴보기
- bathPath : 기본 주소에 원하는 주소를 추가하는 기능
- swcMinifiy : swc 를 이용해 코드 압축 여부 설정
- poweredByHeader : 응답 헤더에 next 관련 헤더를 넣을지 말지 결정하는 옵션, 보안 관련해서는 끄는 것을 추천
- redirects : 특정 주소를 다른 주소로 보내고 싶을 때 사용, 정규식 지원
- reactStrictMode : 리액트에서 제공하는 엄격 모드 적용 여부
- assetPrefix : 빌드 결과물을 호스트가 아닌 다른 CDN 에 업로드하고자 할 때 해당 부분에 CDN 주소 명시 필요
4.3.7 정리
05. 리액트와 상태 관리 라이브러리
5.1 상태 관리는 왜 필요한가?
- 상태는 어떠한 의미를 지닌 값이면 어플리케이션의 시나리오에 따라 지속적으로 변경될 수 있는 값을 의미한다
- UI : 상호 작용이 가능한 모든 요소의 현재 값
- URL : 브라우저에 의해 관리되고 있는 상태 값
- Form: 폼의 상태, 로딩 / 제출 / 접근 가능여부 / 값의 유효성
- 서버에서 가져온 값
5.1.1 리액트 상태 관리의 역사
Flux 패턴의 등장
- 리액트로 작성 된 어플리케이션의 크기가 방대해짐에 따라 상태를 관리 추적하는 것에 어려움이 발생
- 페이스북 팀은 상태 관리 어려움의 원인을 양방향 데이터 바인딩이라고 보고, 단방향으로 데이터 흐름을 변경하는 것을 제안하는데 이것이 바로 Flux 패턴이다
- Action -> Dispatcher -> Store -> View 의 방향
- 이러한 단방향 흐름은 상태의 관리 및 추적에는 유리했지만, 사용자의 입력에 따라 데이터를 갱신하고 화면을 업데이트 하는 코드도 추가가 되는 불편함이 존재
시장 지배자 리덕스의 등장
- 리덕스는 Flux 구조에 Elm 아키텍쳐를 도입하여 시장을 지배
- Elm 은 데이터를 Model, View, Update 라는 단방향 흐름으로 강제하여 어플리케이션의 상태를 안정적으로 관리
- 다만 해당 기능을 사용하기 위해 많은 보일러 플레이트 코드가 필요하다는 단점이 존재
Context API 와 useContext
- Props Drilling 등의 문제 해결을 위해 16.3 버전에서 Context API 를 출시
훅의 탄생, 그리고 React Query 와 SWR
- 16.8 버전에서 함수 컴포넌트에 사용 가능한 훅 API 를 추가
- 훅으로 인하여 state 관리가 단순화 되어 React Query 와 SWR 라는 통신 요청에 특화된 상태 관리 라이브러리가 탄생
- 리덕스에 비해 보일러 플레이트 코드를 줄일 수 있는 장점이 존재
Recoil, Zustand, Jotai, Valtio 에 이르기까지
- 리덕스에 이어 훅을 이용하여 작은 크기의 상태를 효율적으로 관리하는 상태관리 라이브러리가 탄생
5.1.2 정리
5.2 리액트 훅으로 시작하는 상태 관리
5.2.1 가장 기본적인 방법: useState 와 useReducer
- useState 와 useReducer 를 사용하면 간단한 상태 관리가 가능
- 하지만 훅을 사용할 때 마다 컴포넌트 별로 초기화 되므로 컴포넌트 별로 다른 상태를 가지게 되어, 해당 컴포넌트에서만 상태가 유효하다는 한계점이 존재
- 아래와 같이 상태를 한단계 끌어올리는 방법이 존재하지만, 상태를 자식에게 props 로 전달해야하는 불편함이 발생
function Counter1({ counter, inc }: { counter: number, inc: () => void }) {
return (
<>
<h3>Counter1: {counter}</h3>
<button onClick={inc}>+</button>
</>
);
}
function Counter2({ counter, inc }: { counter: number, inc: () => void }) {
return (
<>
<h3>Counter2: {counter}</h3>
<button onClick={inc}>+</button>
</>
);
}
function Parent() {
const { counter, inc } = useCounter();
return (
<>
<Counter1 counter={counter} inc={inc} />
<Counter2 counter={counter} inc={inc} />
</>
);
}
5.2.2 지역 상태의 한계를 벗어나보자: useState의 상태를 바깥으로 분리하기
- 상태를 클로저에 맡기는 것이 아니라, JS 실행 문맥 내에서 초기화가 가능하도록 분리해보기
// counter.ts
export type State = { counter: number };
// 상태를 아예 컴포넌트 밖에 선언했다. 각 컴포넌트가 이 상태를 바라보게 할 것이다.
let state: State = { counter: 0 };
// getter
export function get(): State {
return state;
}
// useState와 동일하게 구현하기 위해 게으른 초기화 함수나 값을 받을 수 있게 했다.
type Initializer<T> = T extends any ? T | ((prev: T) => T) : never;
// setter
export function set<T>(nextState: Initializer<T>) {
state = typeof nextState === "function" ? nextState(state) : nextState;
}
// Counter
function Counter() {
const state = get();
function handleClick() {
set((prev: State) => ({ counter: prev.counter + 1 }));
}
return (
<>
<h3>{state.counter}</h3>
<button onClick={handleClick}>+</button>
</>
);
}
- 하지만 해당 코드는 상태값의 변경은 정상적으로 작동되나 컴포넌트가 리렌더링되지 않아 문제가 발생한다
- 해당 문제를 해결하기 위해, 외부 state 값을 컴포넌트 내부의 useState 의 인수로 전달하는 방식으로 해결
function Counter1() {
const [count, setScount] = useState(state);
function handleClick() {
// 외부에서 선언한 set 함수 내부에서 다음 상태값을 연산한 다음, 그 값을 로컬 상태값에도 넣기
set((prev: State) => {
const newState = { counter: prev.counter + 1 };
// setCount 의 호출로 리렌더링 발생
setCount(newState);
// return 으로 업데이트 된 상태값을 외부 상태에 반영
return newState;
});
}
return (
<>
{/* 컴포넌트 내부의 state 인 count 사용 */}
<h3>{count.counter}</h3>
<button onClick={handleClick}>+</button>
</>
);
}
- 위와 같은 코드는 작동은 하지만 외부에서 관리하는 state 를 다시 컴포넌트 내부에서 state 로 선언하여 사용하는 등의 문제가 발생한다
- 또한, 액션이 발생한 컴포넌트는 리렌더링이 발생하지만 다른 컴포넌트에서 외부 상태값을 참조하고 있을 경우 해당 컴포넌트는 다시 렌더링이 발생하기 전까지는 변경된 상태값 적용이 안된다
- 이를 해결하기 위해서는 별도의 기능을 하는 createStore, useStore 등의 코드를 만들어서 사용해야만 한다
- 이와 비슷한 기능은 React 의 useSubscription 을 통해 구현되어 있다
** [p. 367] 이거 코드 길게 잘 써놨는데.... 흐음 애매하네요 ㅎㅎㅎ ** 막상 리덕스는 알알려주는....
5.2.3 useState 와 Context를 동시에 사용해 보기
- 스토어를 사용하는 경우 스토어가 여러개가 되면 컴포넌트에서 어떤 스토어를 사용해야하는지 판단하기 어려운 단점이 존재
- Context 를 이용하여 스토어를 컴포넌트에 주입하면, 위의 문제를 해결할 수 있다
- 대부분의 상태관리 라이브러리는 위와 같은 방식으로 구성되어 있으며, 아래의 장점을 가진다
- useState, sueReducer 와 같이 지역적 사용이 아닌 글로벌 활용이 가능하다
- 상태가 변경되면 참조하고 있는 모든 컴포넌트의 렌더링이 발생한다
5.2.4 상태관리 라이브러리 Recoil, Jotai, Zustand 알아보기
- Recoil, Jotai 는 Context 와 Provider 그리고 훗을 기반으로 작은 상태를 효율적으로 관리하기 좋음
- Zustand 는 하나의 큰 스토어를 기반으로 상태를 관리하기 좋음. Context 가 아닌 클로저를 기반으로 스토어가 생성됨
페이스북이 만든 상태 관리 라이브러리 Recoil
-
리액트를 위한 Atomic 상태 관리 라이브러리, 하지만 아직 베타
-
리액트의 v18 에 맞추어 동시성 렌더링, 서버 컴포넌트, Streaming SSR 지원 이후에 1.0.0 을 배포 예정
-
RecoilRoot 를 최상단에 선언에 하나의 스토어를 만들고 Atom 이라는 상태 단위를 스토어에 등록
-
Recoil 의 훅을 통해 상태 변화를 구독하고 값이 변하면 리렌더링을 통해 Atom 의 값을 참조
-
예시 코드
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from "recoil";
// Recoil 파트
const counterState = atom({
key: "counterState",
default: 0,
});
function Counter() {
const [, setCount] = useRecoilState(counterState);
function handleBtnClick() {
setCount((count) => count + 1);
}
return (
<>
<button onClick={handleBtnClick}>+</button>
</>
);
}
const isBiggerThen10 = selector({
key: "above10State",
get: ({ get }) => {
console.log(get(counterState));
return get(counterState) >= 10;
},
});
function Count() {
const count = useRecoilValue(counterState);
const biggerThen10 = useRecoilValue(isBiggerThen10);
return (
<>
<h3>{count}</h3>
<p>10 보다 큰가? : {JSON.stringify(biggerThen10)}</p>
</>
);
}
function App() {
return (
<div className="App">
<RecoilRoot>
<Counter />
<Count />
</RecoilRoot>
</div>
);
}
export default App;
특징
- 메타에서 만드는 만큼 리액트의 신기능들을 가장 잘 지원할 것으로 예상
- 리덕스와 달리 redux-saga 나 redux-thunk 를 사용하지 않아도 비동기 작업을 지원
- 아직 정식 버전이 아니므로 사용에 있어서 주의 필요
Recoil 에서 영감을 받은, 그러나 조금 더 유연한 Jotail
-
Recoil 과 마찬가지로 Atomic 스타일의 상태 관리 라이브러리이며 작은 단위의 상태를 위로 전파할 수 있는 구조
-
atom 을 통해 상태를 선언하면 해당 상태는 컴포넌트 외부에서도 사용이 가능하다. 따라서 RecoilRoot 로 하위 컴포넌트를 감쌀 필요가 없다
-
Recoil 대비 간결한 코드, 객체 참조를 통해 문자열 key 값 없이 상태값 관리 가능
-
그외의 API 및 localStorage 연동, Next, React Native 등 다양한 기능을 지원
-
정식 버전이므로 상용 어플리케이션에 적용 가능
-
예시 코드
import { atom, useAtom, useAtomValue } from "jotai";
const counterState = atom(0);
function Counter() {
const [, setCount] = useAtom(counterState);
const handleBtnClick = () => setCount((count) => count + 1);
return (
<>
<button onClick={handleBtnClick}>+</button>
</>
);
}
const isBiggerThen10 = atom((get) => get(counterState) >= 10);
function Count() {
const count = useAtomValue(counterState);
const biggerThen10 = useAtomValue(isBiggerThen10);
return (
<>
<h3>{count}</h3>
<p>10 보다 큰가? : {JSON.stringify(biggerThen10)}</p>
</>
);
}
export default function JotailComponent() {
return (
<div>
<h2>조타이</h2>
<Counter />
<Count />
</div>
);
}
** 리코일은 10보다 biggerThan 이 >= 10 이고 Jotai 는 왜 > 10 이죠? ㅋㅋㅋㅋㅋ ** [p. 383] 리코일 코드에서 key 는 또 above10State 고.... 오락가락 하시네여
작고 빠르면 확장에도 유연한 Zustand
-
Redux 에서 영감을 받아 만든 Flux 타입 상태 관리 라이브러리, 따라서 하나의 큰 스토어를 기반으로 상태를 관리한다
-
코드가 제일 간결하고 사용이 쉽다
-
라이브러리의 코드의 용량 자체도 작아서 작고 빠르다
-
API 가 간단한 구조를 가진다면 Zustand 는 좋은 선택지이다
-
예시 코드
import { create } from "zustand";
const useCounterStore = create((set) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set((state) => ({ count: state.count - 1 })),
}));
export default function ZustandComponent() {
const { count, inc, dec } = useCounterStore();
const biggerThan10 = String(count >= 10);
return (
<div>
<h2>Zustand</h2>
<button onClick={inc}>+</button>
<button onClick={dec}>-</button>
<h3>{count}</h3>
<p>10 보다 큰가? : {biggerThan10}</p>
</div>
);
}
** 전 redux 로 배워서 그런지 zustand 가 압도적으로 편해 보입니다! 다들 어찌 생각하시나요?
5.2.5 정리
- 상황에 맞는, 그리고 프레임워크의 변화에 따르게 대응하는 라이브러리를 선택하는 것이 유리하다!
** 근데 리덕스나 발티오 같은건 안가르쳐 주나요?? 그래도 쉐어 1위인데?
** Zustand 연습하자!