Front-end Developer

0%

원문 링크: https://mui.com/guides/migration-v4/

v4 에서 v5로 마이그레이션하기


마이그레이션 해야 하는 이유

버그 수정 및 새로운 스타일링 엔진과 같은 여러 개선사항의 이점을 얻으려면 마이그레이션 해야한다.

마이그레이션 단계

React 또는 TypeScript 버전 업데이트

  • React 최소지원버전: v16.8.0 to v17.0.0.
  • TypeScript 최소지원버전: v3.2 to v3.5.

ThemeProvider setup

v5로 업그레이드 전에 ThemeProvider 가 당신의 어플리케이션의 root에 정의되어 있고(default theme를 사용한다고 하더라도) useStyles<ThemeProvider> 전에 호출되진 않았는지 확인해 본다. 그 이유는 임시로 @mui/styles (JSS style-engine)을 사용해야 하는데, 이것이 ThemeProvider 를 요구한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {
ThemeProvider,
createMuiTheme,
makeStyles
} from '@material-ui/core/styles'

const theme = createMuiTheme()

const useStyles = makeStyles(theme => {
root: {
// some CSS that access to theme
}
})

function App() {
const classes = useStyles() // ❌ If you have this, consider moving it inside a component that wrapped with <ThemeProvider>
return <ThemeProvider theme={theme}>{children}</ThemeProvider>
}

MUI 버전 업데이트

v5 버전의 MUI core를 사용하려면 먼저 패키지 네임을 업데이트해야 한다.

1
2
3
4
npm install @mui/material @mui/styles

// or with `yarn`
yarn add @mui/material @mui/styles

만약 아래와 같은 패키지가 이미 있는 경우 새 패키지를 별도로 설치해라

  • @material-ui/lab@mui/lab
  • @material-ui/icons@mui/icons-material

  • 모든 변경된 패키지 리스트 보기

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @material-ui/core -> @mui/material
    @material-ui/system -> @mui/system
    @material-ui/unstyled -> @mui/base
    @material-ui/styles -> @mui/styles
    @material-ui/icons -> @mui/icons-material
    @material-ui/lab -> @mui/lab
    @material-ui/types -> @mui/types
    @material-ui/styled-engine -> @mui/styled-engine
    @material-ui/styled-engine-sc ->@mui/styled-engine-sc
    @material-ui/private-theming -> @mui/private-theming
    @material-ui/codemod -> @mui/codemod
    @material-ui/docs -> @mui/docs
    @material-ui/envinfo -> @mui/envinfo

org 또는 패키지 이름이 @material-ui에서 @mui로 변경된 것은 리브랜딩의 일환이다. 자세한 내용은 블로그 포스트 또는 #27803을 확인한다.

그 다음으로는 새로운 종속 디펜던시인 이모션 패키지를 설치해야 한다.

1
2
3
4
npm install @emotion/react @emotion/styled

// or with `yarn`
yarn add @emotion/react @emotion/styled

만약 MUI Core v5를 emotion 대신 styled-components와 함께 사용하고 싶다면 설치 가이드를 확인한다.

만약 @material-ui/pickers를 사용중이라면 이것은 @mui/lab으로 이동했다. 이 링크를 통해 @mui/lab으로 마이그레이션 하는 다음 단계를 확인할 수 있다.

지금쯤 @mui/styles를 설치했어야 한다. 여기에는 emotion을 복제하는 JSS를 포함하고 있다. 이는 v5로의 점진적인 마이그레이션을 허용하기 위한 것이다. 다음 단계에 따라 종속성을 제거할 수 있도록 한다.

어플리케이션이 에러없이 잘 실행되고 있는지 확인하고, 다음 단계를 이어가기 전에 변경 사항을 커밋하도록 한다.

어플리케이션이 완전히 MUI v5로 마이그레이션 되면 이전의 @material-ui/* 패키지를 yarn remove 또는 npm install로 제거할 수 있다.


codemod 실행

마이그레이션을 쉽게 할 수 있도록 codemod를 준비했다.

preset-safe

이 codemod에는 마이그레이션에 유용한 대부분의 transformer가 포함되어 있다. (이 codemod는 폴더당 한 번만 적용될 수 있도록 한다.)

1
npx @mui/codemod v5.0.0/preset-safe <path>

이 codemode를 일대일로 하나씩 실행하려면 preset-safe codemod에서 상세 내용을 확인한다.

variant-prop

어떠한 variant도 정의되지 않았다면 (default variant는 v4 standard에서 v5 outlined로 변경되었다.)<TextField/>, <FormControl/>, <Select/> 등의 컴포넌트에 variant=”standard”를 적용하여 변환할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
// if you have theme setup like this, ❌ don't run this codemod.
// these default props can be removed later because `outlined` is the default value in v5
createMuiTheme({
components: {
MuiTextField: {
defaultProps: {
variant: 'outlined'
}
}
}
})

만약 variant=”standard” 를 컴포넌트에서 유지하고 싶다면, 이 codemod를 실행하거나 theme default props를 구성한다.

1
npx @mui/codemod v5.0.0/variant-prop <path>

자세한 내용은 variant-prop codemod를 확인한다.

underliner prop이 정의되지 않았다면 컴포넌트에 underline=”hover”를 적용하면 변환할 수 있다. (default underline은 v4 “hover”에서 v5 “always”로 변경되었다.)

1
2
3
4
5
6
7
8
9
10
11
// if you have theme setup like this, ❌ don't run this codemod.
// this default props can be removed later because `always` is the default value in v5
createMuiTheme({
components: {
MuiLink: {
defaultProps: {
underline: 'always'
}
}
}
})

만약 variant=”hover” 를 컴포넌트에서 유지하고 싶다면, 이 codemod를 실행하거나 theme default props를 구성한다.

1
npx @mui/codemod v5.0.0/link-underline-hover <path>

자세한 내용은 link-underline-hover codemod에서 확인한다.

codemod 단계를 일단 완료했다면 어플리케이션을 다시 실행해본다. 이때 에러없이 실행되어야 한다. 그렇지 않을 경우 troubleshooting 섹션을 확인해본다. 다음 단계는 각 컴포넌트의 주요 변경사항을 처리하는 것이다.


Handling breaking changes

지원되는 브라우저 및 노드 버전

default 번들의 타겟이 변경되었다. 정확한 버전은 browserslist query가 릴리즈 될 때 고정된다.

"> 0.5%, last 2 versions, Firefox ESR, not dead, not IE 11, maintained node versions"

기존 번들은 아래와 같은 최소 버전을 지원한다.

  • Node 12 (up from 8)
  • Chrome 90 (up from 49)
  • Edge 91 (up from 14)
  • Firefox 78 (up from 52)
  • Safari 14 (macOS) and 12.5 (iOS) (up from 10)
  • and more (see .browserslistrc (stable entry))

더이상 IE 11을 지원하지 않기 때문에 IE 11을 지원해야 하는 경우는 lagacy bundle를 확인한다.

non-ref-forwarding class components

component prop의 non-ref-forwarding class components 또는 immediate children의 지원이 중단되었다. 만약 unstable_createStrictModeTheme를 사용하고 있거나 React.StrictMode의 findDOMNode 과 관련된 어떠한 경고도 표시되지 않았다면 아무런 작업도 수행할 필요가 없다. 그렇지 않다면 Caveat with refs 섹션에서 어떻게 마이그레이션 해야하는지 알아볼 필요가 있다. 이 변경 사항은 component prop를 사용하는 곳이나 children을 element로 필요로 하기 때문에 children을 components에 전달해야 하는 거의 모든 components에 영향을 미친다. (예를 들어, )

style library

v5에서 default로 사용하는 스타일 라이브러리는 [emotion](https://github.com/emotion-js/emotion)이다. JSS에서 emotion으로 마이그레이션 하는 동안 components에 JSS style을 사용하여 override하는 경우 (예를 들자면 makeStyles 통해 override하는 경우) CSS 삽입 순서 (CSS injection order)를 신경써야 한다. 이렇게 하려면 StyledEngineProviderinjectFirst 옵션이 컴포넌트 tree의 가장 최상단에 위치해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
//예시

import * as React from 'react';
import { StyledEngineProvider } from '@mui/material/styles';

export default function GlobalCssPriority() {
return (
{/* Inject emotion before JSS */}
<StyledEngineProvider injectFirst>
{/* Your component tree. Now you can override MUI's styles. */}
</StyledEngineProvider>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//예시
import * as React from 'react';
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';

const cache = createCache({
key: 'css',
+ prepend: true,
});

export default function PlainCssPriority() {
return (
<CacheProvider value={cache}>
{/* Your component tree. Now you can override MUI's styles. */}
</CacheProvider>
);
}

styled-components를 사용 중이고, 커스텀 타겟이 있는 StyleSheetManager를 사용중이라면 타겟이 HTML 엘리먼트의 첫번 째 요소인지 확인한다. 어떻게 이것을 수행하는지 확인하려면 @mui/styled-engine-sc package에 있는 [StyledEngineProvider implementation](https://github.com/mui-org/material-ui/blob/master/packages/mui-styled-engine-sc/src/StyledEngineProvider/StyledEngineProvider.js)을 살펴본다.

Theme structure

theme 구조는 v5에서 변경되었기 때문에 변경된 형태로 업데이트 해야 한다. 원활한 전환을 위해서 adaptV4Theme를 사용해서 일부 테마의 변경된 사항을 새로운 테마 구조로 반복적으로 업그레이드 할 수 있다.

1
2
3
4
5
6
7
8
-import { createMuiTheme } from '@mui/material/styles';
+import { createTheme, adaptV4Theme } from '@mui/material/styles';

-const theme = createMuiTheme({
+const theme = createTheme(adaptV4Theme({
// v4 theme
-});
+}));

adapther는 다음의 변경 사항을 지원한다.

  • “gutters” abstraction은 가치가 있다고 생각될 만큼 자주 사용되지 않는다고 입증되었다.
1
2
3
4
5
6
7
-theme.mixins.gutters(),
+paddingLeft: theme.spacing(2),
+paddingRight: theme.spacing(2),
+[theme.breakpoints.up('sm')]: {
+ paddingLeft: theme.spacing(3),
+ paddingRight: theme.spacing(3),
+},
  • theme.spacing 은 이제 default로 px 유닛과 단일 값을 반환한다. 이는 styled-compontnts와 emotion의 통합을 개선한다.
1
2
이는 `theme.spacing`이 template string로 호출되던 것으로부터 'px' 접미사를 제거해서
[preset-safe codemod](https://mui.com/guides/migration-v4/#preset-safe)에서 처리된다.
1
2
3
4
5
//before
theme.spacing(2) => 16

//after
theme.spacing(2) => '16px'
  • 이 기능을 설명하는 데 일반적으로 사용되는 “dark mode”라는 용어를 더 잘 따르기 위해서 theme.palette.type 키는 theme.palette.mode 로 이름이 변경되었다.
1
2
3
import { createTheme } from '@mui/material/styles';
-const theme = createTheme({palette: { type: 'dark' }}),
+const theme = createTheme({palette: { mode: 'dark' }}),
  • default [theme.palette.info](http://theme.palette.info) 색상은 라이트, 다크 모드 모두에서 AA 표준 명암비를 통과하도록 변경되었다.
1
2
3
4
5
6
7
8
9
10
info = {
- main: cyan[500],
+ main: lightBlue[700], // lightBlue[400] in "dark" mode

- light: cyan[300],
+ light: lightBlue[500], // lightBlue[300] in "dark" mode

- dark: cyan[700],
+ dark: lightBlue[900], // lightBlue[700] in "dark" mode
}
  • default theme.palette.succeess 색상은 색상은 라이트, 다크 모드 모두에서 AA 표준 명암비를 통과하도록 변경되었다.
1
2
3
4
5
6
7
8
9
10
success = {
- main: green[500],
+ main: green[800], // green[400] in "dark" mode

- light: green[300],
+ light: green[500], // green[300] in "dark" mode

- dark: green[700],
+ dark: green[900], // green[700] in "dark" mode
}
  • default theme.palette.warning 색상은 색상은 라이트, 다크 모드 모두에서 AA 표준 명암비를 통과하도록 변경되었다.
1
2
3
4
5
6
7
8
9
10
warning = {
- main: orange[500],
+ main: "#ED6C02", // orange[400] in "dark" mode

- light: orange[300],
+ light: orange[500], // orange[300] in "dark" mode

- dark: orange[700],
+ dark: orange[900], // orange[700] in "dark" mode
}
  • default theme.palette.text.hint 키는 MUI components에서는 사용되지 않기 때문에 제거되었다. 만약 dependency를 가지고 있다면 뒤에 아래 예시와 같은 내용을 덧붙이도록 한다.
1
2
3
4
5
6
import { createTheme } from '@mui/material/styles';

-const theme = createTheme(),
+const theme = createTheme({
+ palette: { text: { hint: 'rgba(0, 0, 0, 0.38)' } },
+});
  • theme의 components 정의는 어떠한 관련된 component에서도 정의를 쉽게 찾아볼 수 있도록 compontns key 아래에 새롭게 구성되었다.
  1. prop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createTheme } from '@mui/material/styles';

const theme = createTheme({
- props: {
- MuiButton: {
- disableRipple: true,
- },
- },
+ components: {
+ MuiButton: {
+ defaultProps: {
+ disableRipple: true,
+ },
+ },
+ },
});
  1. overrides
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createTheme } from '@mui/material/styles';

const theme = createTheme({
- overrides: {
- MuiButton: {
- root: { padding: 0 },
- },
- },
+ components: {
+ MuiButton: {
+ styleOverrides: {
+ root: { padding: 0 },
+ },
+ },
+ },
});

Styles

  • 기능을 더 잘 나타내기 위해 fade 에서 alpha 로 이름이 변경되었다. 이전의 이름은 입력 색상 값에 이미 alpha값이 있을 때 혼동을 야기했다. helper는 색상의 alpha 값을 override 한다.
1
2
3
4
5
6
7
- import { fade } from '@mui/material/styles';
+ import { alpha } from '@mui/material/styles';

const classes = makeStyles(theme => ({
- backgroundColor: fade(theme.palette.primary.main, theme.palette.action.selectedOpacity),
+ backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity),
}));
  • createStyles 함수는 @mui/material/styles 에서 export 되는 것에서 @mui/styles 에서 export되는 것으로 변경되었다. 디펜던시를 core 패키지의 @mui/styles 로 이동시키는 것이 필요하다.
1
2
-import { createStyles } from '@mui/material/styles';
+import { createStyles } from '@mui/styles';

@mui/styles

ThemeProvider

@mui/styles@mui/material를 함께 사용한다면, ThemeProvider@mui/material/styles에서 export 되던 것을 @mui/styles에서 export 되도록 변경해야 한다. 이 방법은 @mui/styles에서 export된 스타일링 유틸리티인 makeStyles, withStyles 등과 MUI component에서 context에서 제공되는 theme가 모두 사용되도록 할 수 있다.

1
2
-import { ThemeProvider } from '@mui/styles';
+import { ThemeProvider } from '@mui/material/styles';

Default Theme (TypeScript)

@mui/styles 패키지는 더이상 @mui/material/styles 의 일부가 아니다. @mui/styles@mui/material 과 함께 사용하는 경우에 DefaultTheme에 대해 모듈을 추가하여 적용해야 한다.

1
2
3
4
5
6
// in the file where you are creating the theme (invoking the function `createTheme()`)
import { Theme } from '@mui/material/styles';

declare module '@mui/styles' {
interface DefaultTheme extends Theme {}
}

@mui/material/colors

  • 1 레벨 이상의 중첩된 import는 private이다. @mui/material/colors/red 에서 색상을 가져올 수 없다.
1
2
-import red from '@mui/material/colors/red';
+import { red } from '@mui/material/colors';

@mui/material/styles

createGenerateClassName

createGenerateClassName 함수는 더이상 @mui/material/styles에서 export되지 않는다. @mui/styles에서 직접 import해야 한다.

1
2
-import { createGenerateClassName } from '@mui/material/styles';
+import { createGenerateClassName } from '@mui/styles';

@mui/styles를 사용하지 않고 커스텀 클래스네임을 생성하려면 ClassNameGenerator에서 세부내용을 확인한다.

createMuiTheme

createMuiTheme 함수는 ThemeProvider를 좀더 직관적으로 사용할 수 있도록 하기 위해서 createTheme로 이름이 변경되었다.

1
2
3
4
5
-import { createMuiTheme } from '@mui/material/styles';
+import { createTheme } from '@mui/material/styles';

-const theme = createMuiTheme({
+const theme = createTheme({

jssPreset

  • jssPreset 객체는 더이상 @mui/material/styles 에서 export되지 않는다. @mui/styles에서 바로 import해서 사용해야 한다.
1
2
-import { jssPreset } from '@mui/material/styles';
+import { jssPreset } from '@mui/styles';

makeStyles

  • makeStyles JSS 유틸리티는 더이상 @mui/material/styles 에서 export되지 않기 때문에 대신 @mui/styles/makeStyles를 사용해야 한다. defaultTheme 를 더이상 사용할 수 없기 때문에 ThemeProvider를 어플리케이션의 루트에 추가하도록 한다. 만약 @mui/material와 함께 사용하고 있다면 @mui/material/styles에 있는 ThemeProvider 컴포넌트를 사용하는 것을 추천한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-import { makeStyles } from '@mui/material/styles';
+import { makeStyles } from '@mui/styles';
+import { createTheme, ThemeProvider } from '@mui/material/styles';

+const theme = createTheme();
const useStyles = makeStyles((theme) => ({
background: theme.palette.primary.main,
}));
function Component() {
const classes = useStyles();
return <div className={classes.root} />
}

// In the root of your app
function App(props) {
- return <Component />;
+ return <ThemeProvider theme={theme}><Component {...props} /></ThemeProvider>;
}

MuiThemeProvider

MuiThemeProvider 컴포넌트는 더이상 @mui/material/styles에서 export되지 않으므로 ThemeProvider를 대신 사용한다.

1
2
-import { MuiThemeProvider } from '@mui/material/styles';
+import { ThemeProvider } from '@mui/material/styles';

serverStyleSheets

ServerStyleSheets 컴포넌트는 더이상 @mui/material/styles에서 export되지 않으므로 @mui/styles에서 직접 import하도록 한다.

1
2
-import { ServerStyleSheets } from '@mui/material/styles';
+import { ServerStyleSheets } from '@mui/styles';

Styles

styled JSS utility는 더이상 @mui/material/styles에서 export되지 않으므로 대신 @mui/styles에서 export된 것을 사용하도록 한다. defaultTheme 를 더이상 사용할 수 없기 때문에 ThemeProvider를 어플리케이션의 루트에 추가하도록 한다. 만약 @mui/material와 함께 사용하고 있다면 @mui/material/styles에 있는 ThemeProvider 컴포넌트를 사용하는 것을 추천한다.

1
2
3
4
5
6
7
8
9
10
11
-import { styled } from '@mui/material/styles';
+import { styled } from '@mui/styles';
+import { createTheme, ThemeProvider } from '@mui/material/styles';

+const theme = createTheme();
const MyComponent = styled('div')(({ theme }) => ({ background: theme.palette.primary.main }));

function App(props) {
- return <MyComponent />;
+ return <ThemeProvider theme={theme}><MyComponent {...props} /></ThemeProvider>;
}

StylesProvider

  • StylesProvider 컴포넌트는 더이상 @mui/material/styles에서 export되지 않으므로 @mui/styles에서 바로 export하여 사용하도록 한다.
1
2
-import { StylesProvider } from '@mui/material/styles';
+import { StylesProvider } from '@mui/styles';

useThemeVariants

  • useThemeVariants hook은 더이상 @mui/material/styles 에서 export 되지 않기 때문에 @mui/styles 에서 즉시 import하여 사용하도록 한다.
1
2
-import { useThemeVariants } from '@mui/material/styles';
+import { useThemeVariants } from '@mui/styles';

withStyles

innerRef prop은 ref prop으로 교체한다. Refs는 이제 자동으로 inner 컴포넌트에 전달된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import * as React from 'react';
import { withStyles } from '@mui/styles';

const MyComponent = withStyles({
root: {
backgroundColor: 'red',
},
})(({ classes }) => <div className={classes.root} />);

function MyOtherComponent(props) {
const ref = React.useRef();
- return <MyComponent innerRef={ref} />;
+ return <MyComponent ref={ref} />;
}

withStyles JSS utility는 더이상 @mui/material/styles 에서 export 되지 않기 때문에 @mui/styles/withStyles 를 대신 쓰도록 한다. defaultTheme 를 더이상 사용할 수 없기 때문에 ThemeProvider를 어플리케이션의 루트에 추가하도록 한다. 만약 @mui/material와 함께 사용하고 있다면 @mui/material/styles에 있는 ThemeProvider 컴포넌트를 사용하는 것을 추천한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-import { withStyles } from '@mui/material/styles';
+import { withStyles } from '@mui/styles';
+import { createTheme, ThemeProvider } from '@mui/material/styles';

+const defaultTheme = createTheme();
const MyComponent = withStyles((props) => {
const { classes, className, ...other } = props;
return <div className={clsx(className, classes.root)} {...other} />
})(({ theme }) => ({ root: { background: theme.palette.primary.main }}));

function App() {
- return <MyComponent />;
+ return <ThemeProvider theme={defaultTheme}><MyComponent /></ThemeProvider>;
}

withTheme

withTheme HOC utility는 @mui/material/styles 패키지에서 제거되었기 때문에 대신 @mui/styles/withTheme를 사용할 수 있다. defaultTheme 를 더이상 사용할 수 없기 때문에 ThemeProvider를 어플리케이션의 루트에 추가하도록 한다. 만약 @mui/material와 함께 사용하고 있다면 @mui/material/styles에 있는 ThemeProvider 컴포넌트를 사용하는 것을 추천한다.

1
2
3
4
5
6
7
8
9
10
11
-import { withTheme } from '@mui/material/styles';
+import { withTheme } from '@mui/styles';
+import { createTheme, ThemeProvider } from '@mui/material/styles';

+const theme = createTheme();
const MyComponent = withTheme(({ theme }) => <div>{props.theme.direction}</div>);

function App(props) {
- return <MyComponent />;
+ return <ThemeProvider theme={theme}><MyComponent {...props} /></ThemeProvider>;
}

innerRef prop를 ref prop으로 바꾼다. Ref는 이제 자동으로 내부 컴포넌트에 전달된다.

1
2
3
4
5
6
7
8
9
10
import * as React from 'react';
import { withTheme } from '@mui/styles';

const MyComponent = withTheme(({ theme }) => <div>{props.theme.direction}</div>);

function MyOtherComponent(props) {
const ref = React.useRef();
- return <MyComponent innerRef={ref} />;
+ return <MyComponent ref={ref} />;
}

withWidth

이 HOC는 제거되었고, 대안으로 [useMediaQuery hook](https://mui.com/components/use-media-query/#migrating-from-withwidth)을 쓸 수 있다.

mui/icons-matetial

Github

Github icon은 24px에서 22px로 크기가 감소되었다.

@material-ui/pickers

@material-ui/pickers를 마이그레이션 하는 방법은 다음 페이지를 참조하도록 한다.


System

다음의 system function 및 속성은 사용되지 않는 css로 간주되어 이름이 변경되었다.

  • gridGap to gap
  • gridRowGap to rowGap
  • gridColumnGap to columnGap

간격을 조정하는 unit인 gap, rowGap, columnGap 등을 사용할 때, 이전에 숫자로 사용하고 있었다면 theme.spacing과 더불어 새롭게 변환되기 위해 px를 덧붙여서 사용해야 한다.

1
2
3
4
<Box
- gap={2}
+ gap="2px"
>

css prop은 styled-components와 emotion의 css prop이 충돌되는 것을 막기 위해 sx로 변경한다.

1
2
;-(<Box css={{ color: 'primary.main' }} />) +
<Box sx={{ color: 'primary.main' }} />

Core components

core components는 스타일 엔진으로 emotion을 사용하기 때문에 emotion에 의해 사용되는 props들은 전파되지 않는다. 아래의 코드 스니펫처럼 props asSomeOtherComponent로 전파되지 않는다.

1
<MuiComponent component={SomeOtherComponent} as='button' />

References
Migration from v4 to v5 - MUI

Keyof Type Operator

Keyof Type Operator는 객체타입을 취하여 key인 stirng 또는 numeric 리터럴 유니언을 생성한다. 아래 예시에서 type P“x” | “y”인 타입과 같다.

1
2
3
type Point = { x: number; y: number };
type P = keyof Point;
//type P = “x” | “y”

type이 string, 또는 number 인덱스 시그니처가 있으면 keyof가 이 types를 대신 반환한다.

1
2
3
4
5
6
7
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;

type A = number;

type Mapish = { [k: string]: boolean };
type M = keyof Mapish;

이 예시에서 Mstring | number이다. 이는 JavaScript object keys가 언제나 string로 강제전환되기 때문이다. 그래서 obj[0]은 언제나 obj["0"]과 같다. keyof타입은 mapped types와 결합해서 사용할 때 특히 유용하다.


Omit<Type, Keys>

Type에서 type을 구성하는 모든 속성을 선택하여 keys에 의해 제거될 수 있도록 한다. (key는 string 리터럴이거나 string 리터럴의 유니언이다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}

type TodoPreview = Omit<Todo, 'description'>;

const todo: TodoPreview = {
title: 'Clean room',
completed: false,
createdAt: 1615544252770,
};

todo;

const todo: TodoPreview;

type TodoInfo = Omit<Todo, 'completed' | 'createdAt'>;

const todoInfo: TodoInfo = {
title: 'Pick up kids',
description: 'Kindergarten closes at 5pm',
};

todoInfo;

const todoInfo: TodoInfo;

Exclude<Type, ExcludedUnion>

Type으로부터 type을 제외하고 모든 union 멤버가 ExcludedUnion으로 할당될 수 있도록 한다.

1
2
3
4
5
6
7
8
9
type T0 = Exclude<'a' | 'b' | 'c', 'a'>;

type T0 = 'b' | 'c';
type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>;

type T1 = 'c';
type T2 = Exclude<string | number | (() => void), Function>;

type T2 = string | number;

Omit vs Exclude

아래 StudentInfo에 대해 Omit과 Exclude를 사용해보면 어떻게 될까?

1
2
3
4
5
interface StudentInfo {
isStudent: boolean;
firstName?: string;
lastName?: string;
}

StudentInfo라는 type에 Omit을 사용해보면 다음과 같다.

1
2
3
4
5
interface OmitStudentData {
age: number;
studentName: keyof Omit<StudentInfo, 'lastName'>;
memo: string;
}

type Omit<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P];}

Omit은 object type인 StudentInfo를 취해서 type 중 특정한 타입을 제거한다. 여기서 StudentInfo의 속성 { isStudent: boolean, firstName?: string, lastName?: string }중 key가 ‘lastName’인 속성을 제거하고자 했다. 따라서 결과는 아래와 같다.

1
2
3
4
interface StudentInfo {
isStudent: boolean;
firstName?: string;
}

StudentInfo라는 type에 Exclude를 사용해보면 다음과 같다.

1
2
3
4
5
interface ExcludeStudentData {
age: number;
studentName: Exclude<keyof StudentInfo, 'lastName'>;
memo: string;
}

type Exclude<T, U> = T extends U ? never : T

Exclude는 union 멤버의 구성요소를 제거한다. 여기서 Exclude는 Union Type을 취해서 StudentInfo의 Union Type은 Exclude<'isStudent' | 'firstName' | 'lastName' | 'lastName'>와 같은 형태가 되는데, 'lastName'을 제외한 나머지 <'isStudent' | 'firstName'>이 union 멤버가 된다. 따라서 결과는 아래와 같다.

1
2
3
4
interface StudentInfo {
isStudent: boolean;
firstName?: string;
}

Omit, Exclude를 쓴 결과는 동일하지만 과정상 Omit은 특정 속성을 제거한 결과를 제공한다는 점, Exclude는 속성을 제거하는 것이 아니라 특정 Union Type을 제외한 나머지 Union Type을 제거한다는 차이가 있다.


References
The keyof type operator
Omit
Exclude
Difference b/w only Exclude and Omit (Pick & Exclude) Typescript

원문 링크: https://mui.com/styles/advanced/#string-templates

CSS injection order

CSS가 어떻게 브라우저에 의해 계산되는지 이해하는 것은 언제 스타일 오버라이딩이 되는지 알기 위한 키포인트이기 때문에 매우 중요하다. 이와 관련하여 MDN의 어떻게 계산되는가?를 읽어보는 것을 추천한다.

기본적으로 style 태그는 페이지의 <head> 엘리먼트의 가장 마지막에 주입된다. 이 style 태그는 페이지의 다른 어떤 style 태그 (e.g. CSS module, styled components) 보다도 특수함을 가진다.

injectFirst

StylesProvider 컴포넌트는 injectFirst prop를 가지고 있어서 head(낮은 우선순위)에서 가장 먼저 주입되는 style tag이다.

1
2
3
4
5
6
import { StylesProvider } from '@mui/styles'

;<StylesProvider injectFirst>
{/* Your component tree.
Styled components can override MUI's styles. */}
</StylesProvider>

makeStyles / withStyles / styled

주입되는 style tag는 makeStyles / withStyles / styled가 발생하는 같은 순위에서 발생한다. 예를 들어 color red가 아래 예시에서 우세하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import clsx from 'clsx'
import { makeStyles } from '@mui/styles'

const useStylesBase = makeStyles({
root: {
color: 'blue' // 🔵
}
})

const useStyles = makeStyles({
root: {
color: 'red' // 🔴
}
})

export default function MyComponent() {
// Order doesn't matter
const classes = useStyles()
const classesBase = useStylesBase()

// Order doesn't matter
const className = clsx(classes.root, classesBase.root)

// color: red 🔴 wins.
return <div className={className} />
}

hook의 호출 순서와 class name의 연속적인 순서는 상관 없다.

insertionPoint

JSS는 이러한 상황을 해결할 수 있는 매커니즘을 제공한다. HTML내에 삽입점을 추가하는 것으로 CSS 규칙이 components에 적용되는 순서를 제어할 수 있다.


References
CSS injection order

원문링크: https://nextjs.org/docs/basic-features/typescript#incremental-type-checking

create-next-app support

TypeScript project를 create-next-app과 함께 --ts, --typescript 를 사용하여 아래와 같이 생성할 수 있다.

1
2
3
npx create-next-app --ts
# or
yarn create next-app --typescript

Existing projects

이미 존재하는 project에 적용하려면 root folder에 빈 tsconfig.json file을 생성한다.

1
touch tsconfig.json

Next.js는 자동으로 이 파일 안에 default values를 설정한다. custom complier optionstsconfig.json을 작성하는 것 또한 지원된다.

Next.js는 TypeScript를 핸들링하기 위해 Babel을 사용하는데 몇가지 주의사항이 있고 일부 컴파일러 옵션은 다르게 처리된다.

next(npm run dev or yarn dev)를 실행하면, Next.js가 setup을 완료하기 위해 설치가 필요한 패키지를 안내한다.

1
2
3
4
5
6
7
8
9
npm run dev

# You'll see instructions like these:
#
# Please install typescript, @types/react, and @types/node by running:
#
# yarn add --dev typescript @types/react @types/node
#
# ...

이렇게 하면 .js 파일을 .tsx로 전환할 준비가 완료되었고, TypeScript를 사용할 수 있게 된다.


next-env.d.ts라는 이름의 파일이 프로젝트의 root에 생성된다. 이 파일은 TypeScript complier가 Next.js의 type을 선택하도록 한다. 이는 매번 변경되므로 임의로 삭제하거나 편집하지 않도록 한다.

TypeScript strict 모드는 기본적으로 꺼져있다. TypeScript를 편하게 사용하려면 tsconfig.json파일 내에 이 모드를 켜도록 한다.

next-env.d.ts를 편집하는 대신 additional.d.ts와 같은 새 파일을 추가한 다음, tsconfig.jsoninclude 배열에서 참조하여 새로운 타입 유형을 추가할 수 있다.

기본적으로 Next.js는 next build의 일부로 type checking를 수행한다. 개발하는 동안에는 code editior를 이용하여 type checking을 할 것을 추천한다.

만약 error report를 무시하고 싶다면, Ignoring TypeScript errors와 관련된 문서를 확인하도록 한다.


Static Generation and Server-side Rendering

getStaticProps, getStaticPaths, getServerSideProps을 위해 각각 getStaticProps, getStaticPaths, getServerSideProps type을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { GetStaticProps, GetStaticPaths, GetServerSideProps } from 'next';

export const getStaticProps: GetStaticProps = async (context) => {
// ...
};

export const getStaticPaths: GetStaticPaths = async () => {
// ...
};

export const getServerSideProps: GetServerSideProps = async (context) => {
// ...
};

만약 getInitialProps를 사용하려면 이 링크를 참조한다.


API Routes

아래 예제는 API routes의 빌트인 타입을 사용하는 방법이다.

1
2
3
4
5
import type { NextApiRequest, NextApiResponse } from 'next';

export default (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).json({ name: 'John Doe' });
};

reponse data에 type을 사용할 수도 있다.

1
2
3
4
5
6
7
8
9
import type { NextApiRequest, NextApiResponse } from 'next';

type Data = {
name: string,
};

export default (req: NextApiRequest, res: NextApiResponse<Data>) => {
res.status(200).json({ name: 'John Doe' });
};

Custom App

만약 custom App을 사용한다면 빌트인 타입인 AppProps를 사용할 수 있고 파일 이름을 ./pages/_app.tsx와 같이 변경할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// import App from "next/app";
import type { AppProps /*, AppContext */ } from 'next/app';

function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}

// Only uncomment this method if you have blocking data requirements for
// every single page in your application. This disables the ability to
// perform automatic static optimization, causing every page in your app to
// be server-side rendered.
//
// MyApp.getInitialProps = async (appContext: AppContext) => {
// // calls page's `getInitialProps` and fills `appProps.pageProps`
// const appProps = await App.getInitialProps(appContext);

// return { ...appProps }
// }

export default MyApp;

Path aliases and baseUrl

Next.js는 자동으로 tsconfig.json pathsbaseUrl 옵션을 제공한다. Module Path aliases와 관련된 특징을 더 알아보고 싶다면 이 링크를 참조한다.

Type checking next.config.js

next.config.js 파일은 반드시 JavaScript 파일이어야 하고, Babel이나 TypeScript로 parse되어서는 안된다.하지만 아래와 같이 JSDoc를 이용해서 IDE에서의 몇 가지 type checking를 추가할 수는 있다.

1
2
3
4
5
6
7
8
9
10
// @ts-check

/**
* @type {import('next').NextConfig}
**/
const nextConfig = {
/* config options here */
}

module.exports = nextConfig

Incremental type checking

v10.2.1부터 Next.js는 incremental type checkingtsconfig.json에서 사용할 수 있도록 지원한다. 이는 대규모 application에서 type checking을 빠르게 할 수 있도록 돕는다. 최소한 v4.3.2이상의 TypeScript를 사용하고 있다면 최고의 성능을 경험하기 위해 이를 사용하는 것이 추천된다.


References
Next.js - TypeScript

원문링크: https://axios-http.com/

Axios란

node.js와 브라우저를 위한 promise 기반의 HTTP Client. 브라우저와 nodejs에서 동일한 코드가 동일하게 동작한다. server-side에서는 native 기반의 node.js httop 모듈을 사용하고, client(브라우저)에서는 XMLHttpRequests를 사용한다.

특징

  • 브라우저로부터 XMLHttpRequests를 만든다.
  • node.js로부터 http 요청을 만든다.
  • Promise API를 보조한다.
  • request와 response를 중간에서 가로채어 관리한다.
  • request와 response 데이터를 전송한다.
  • request를 취소한다.
  • JSON data에 대한 자동 변환.
  • XSRF로부터 보호하기 위해 client side를 보조한다.

설치

1. using npm

1
$ npm install axios

2. using bower

1
$ bower install axios

3. using yarn

1
$ yarn add axios

4. using jsDelivr CDN

1
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

5. Using unpkg CDN

1
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

예시

CommonJS usage

TypeScript typings (for intellisense / autocomplete)이 가능하게 하기 위해 CommonJS를 사용할 때는 아래 예시처럼 require()를 import한다.

1
2
3
const axios = require('axios').default;

// axios.<method>를 쓰게 되면 autocomplete과 parameter typings의 사용이 가능해진다.

GET request를 수행하는 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const axios = require('axios');

// Make a request for a user with a given ID
axios
.get('/user?ID=12345')
.then(function (response) {
// handle success
console.log(response);
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// 항상 실행된다.
});

// 선택적으로 위의 요청을 아래와 같이 수행할 수 있다.
axios
.get('/user', {
params: {
ID: 12345,
},
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
})
.then(function () {
// 항상 실행된다.
});

// async/await를 쓰고 싶다면 'async' 키워드를 function/method의 바깥쪽에 붙여준다.
async function getUser() {
try {
const response = await axios.get('/user?ID=12345');
console.log(response);
} catch (error) {
console.error(error);
}
}

POST Requests

Axios를 가지고 POST request를 요청하는 방법

POST request를 수행하는 예제

1
2
3
4
5
6
7
8
9
10
11
axios
.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone',
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

여러 요청을 동시에 수행하는 예제

1
2
3
4
5
6
7
8
9
10
11
12
function getUserAccount() {
return axios.get('/user/12345');
}

function getUserPermissions() {
return axios.get('/user/12345/permissions');
}

Promise.all([getUserAccount(), getUserPermissions()]).then(function (results) {
const acct = results[0];
const perm = results[1];
});

Axios API

request는 관련된 config를 통해 axios로 전달된다.

axios(config)

1
2
3
4
5
6
7
8
9
// POST request를 보낸다.
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone',
},
});
1
2
3
4
5
6
7
8
// node.js remote image에 대한 GET request
axios({
method: 'get',
url: 'http://bit.ly/2mTM3nY',
responseType: 'stream',
}).then(function (response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'));
});

axios(url[, config])

1
2
// Send a GET request (default method)
axios('/user/12345');

Request method aliases

편의를 위해 지원되는 모든 요청 메서드에 대한 alias가 제공된다. alias method인 url, method, data 속성을 쓸 때는 config에 작성할 필요없다.

1
2
3
4
5
6
7
8
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])

The Axios Instance

Creating an instance

custom config를 통해 axios의 새로운 인스턴스를 생성할 수 있다.

axios.create([config])

1
2
3
4
5
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: { 'X-Custom-Header': 'foobar' },
});

Instance methods

사용 가능한 인스턴스 메소드는 아래와 같다. 명시된 config는 instance config와 병합된다.

1
2
3
4
5
6
7
8
9
axios#request(config)
axios#get(url[, config])
axios#delete(url[, config])
axios#head(url[, config])
axios#options(url[, config])
axios#post(url[, data[, config]])
axios#put(url[, data[, config]])
axios#patch(url[, data[, config]])
axios#getUri([config])

References
Axios

원문링크: https://redux-toolkit.js.org/rtk-query/usage/mutations

Mutation

Overview

mutation은 서버에 데이터 업데이트를 보낼 때 사용하고, 이를 로컬 캐시에 적용하도록 한다. 또 유효하지 않은 데이터를 걸러내고, 강제로 re-fetch할 수 있도록 한다.

Defining Mutation Endpoints

Mutation Endpoints는 createApi의 endpoints 섹션에 return 되는 객체로 정의된다. 정의되는 곳에서는 builder.mutation() 메소드를 통해 정의한다.

Mutation Endpoints는 URL(URL query params를 포함하는)의 구조의 query 콜백이나 queryFn 콜백에 정의해야 되는데, queryFn callback은 임의로 async 로직을 구성하고 결과를 반환한다. query 콜백은 URL, request 메소드에 사용하는 HTTP 메소드를 포함한 객체를 반환한다.

query 콜백이 URL을 생성하기 위한 추가 데이터가 필요한 경우에는 단일 인수로 작성되어야 하고, 만약 여러 파라미터를 전달할 예정이라면 단일 옵션 개체(option object)의 형태로 전달되어야 한다.

Mutation endpoints는 결과가 캐시되기 전에 response 내용을 수정하고, tags를 정의하여 캐시 무효화를 식별하고, 캐시 항목이 추가 및 제거될 때까지 전체 라이프 사이클 콜백을 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//Example of all mutation endpoint options

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import { Post } from './types'

const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
updatePost: build.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
// note: an optional `queryFn` may be used in place of `query`
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
// Pick out data and prevent nested properties in a hook or selector
transformResponse: (response: { data: Post }) => response.data,
invalidatesTags: ['Post'],
// onQueryStarted is useful for optimistic updates
// The 2nd parameter is the destructured `MutationLifecycleApi`
async onQueryStarted(
arg,
{ dispatch, getState, queryFulfilled, requestId, extra, getCacheEntry }
) {},
// The 2nd parameter is the destructured `MutationCacheLifecycleApi`
async onCacheEntryAdded(
arg,
{
dispatch,
getState,
extra,
requestId,
cacheEntryRemoved,
cacheDataLoaded,
getCacheEntry,
}
) {},
}),
}),
})

onQueryStarted는 optimistic update에 사용할 수 있다.


Performing Mutations with React Hooks

Mutation Hook Behavior

useQuery와 달리 useMutation는 tuple을 반환한다. tuple의 첫 번째 아이템은 ‘trigger’ 함수이고, 두 번째 아이템은 status, error, data를 포함하는 객체이다.

useQuery hook과 달리 useMutation은 자동으로 실행되지 않는다. mutation을 실행하기 위해서 hook의 첫 번째 tuple 값인 trigger 함수를 호출해야 한다. useMutaion과 관련된 자세한 내용은 링크를 참조한다.

Frequently Used Mutation Hook Return Values

useMutation hook은 mutaion trigger 함수를 포함하는 tuple과 mutation 결과와 관련된 속성을 포함하는 객체를 반환한다 하였다.

mutaion trigger 함수가 호출될 때 해당 엔드포인트에 대한 요청을 시작한다. mutation trigger를 호출하면 upwarp 속성이 포함된 프로미스를 반환하는데, 이 속성을 호출하는 것을 통해 mutation 호출을 풀고, raw한 reponse와 error를 제공할 수 있다. 이는 mutation의 호출되는 영역에서 성공/실패 여부를 결정하는 경우에 유용하다.

mutation result는 mutation 요청에 대한 가장 최신의 data와 같은 속성을 포함하는 객체인데, 이 뿐 만 아니라 현재 요청한 라이프사이클 state에 대한 boolean 속성도 포함하고 있다.

아래는 mutation result 객체에서 가장 자주 사용하는 속성들이다. useMutation가 return하는 전체 속성에 대한 내용은 링크를 참조한다.

  • data: 존재하는 경우에, 가장 최신의 trigger response로 부터 반환된 데이터. 만약 동일한 hook의 인스턴스에서 후속 trigger가 호출되면 새로운 데이터를 받기 전에는 undefined를 반환한다. 이전 데이터에서 새로운 데이터로의 원활한 전환을 위해서 컴포넌트 레벨의 고려해야 한다.
  • error: 존재하는 경우 에러의 결과.
  • isUninitialized: true일 때, mutation이 아직 발생하지 않았음을 나타낸다.
  • isLoading: true일 경우에 mutation이 아직 발생했고, reponse를 기다리는 중임을 나타낸다.
  • isSuccess: true일 경우에 마자믹으로 실행된 mutation에서 성공적인 요청으로 부터 얻은 데이터가 있음을 나타낸다.
  • isError: true일 때, 마지막으로 실행된 mutation에서 error 상태를 얻었음을 나타낸다.

note: RTK 쿼리에서 mutation은 query가 그런 것 처럼 ‘loading’와 ‘fetching’ 사이에 시멘틱한 구분을 포함하고 있지 않다. mutation은 후속 call이 반드시 관련이 있는 것이라고 가정하지 않기 때문에, mutation은 ‘no fetching’과 같은 컨셉이 없고, ‘loading’ 또는 ‘not loading’의 컨셉을 가진다.


Standard Mutation Example

페이지 하단의 updataPost mutation의 완전한 수정된 예시이다. 이 시나리오에서 post는 useQuery로 fetch되었고, post의 이름을 수정할 수 있는 EditablePostName 컴포넌트가 렌더된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//src/features/posts/PostDetail.tsx

export const PostDetail = () => {
const { id } = useParams<{ id: any }>()

const { data: post } = useGetPostQuery(id)

const [
updatePost, // This is the mutation trigger
{ isLoading: isUpdating }, // This is the destructured mutation result
] = useUpdatePostMutation()

return (
<Box p={4}>
<EditablePostName
name={post.name}
onUpdate={(name) => {
// If you want to immediately access the result of a mutation, you need to chain `.unwrap()`
// if you actually want the payload or to catch the error.
// Example: `updatePost().unwrap().then(fulfilled => console.log(fulfilled)).catch(rejected => console.error(rejected))

return (
// Execute the trigger with the `id` and updated `name`
updatePost({ id, name })
)
}}
isLoading={isUpdating}
/>
</Box>
)
}

Advanced Mutations with Revalidation

일반적으로 개발자가 mutation(revalidation)을 수행한 후 로컬 데이터 캐시를 서버와 재동기화하려는 경우가 매우 흔하다. RTK Query는 이에 대해 보다 중앙집권화 된 접근방법을 취함으로써 API 정의에서 invalidation 동작을 구성해야 한다. Advanced Invalidation with abstract tag IDs를 통해 RTK Query와 Invalidation에 대한 자세한 내용을 참조하라.

Revalidation Example

post의 CRUD service 예시이다. 이는 Selectively invalidating lists의 전략을 구현하고, 실제 어플리케이션을 위한 좋은 기반을 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
//src/app/services/posts.ts

// Or from '@reduxjs/toolkit/query' if not using the auto-generated hooks
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export interface Post {
id: number
name: string
}

type PostsResponse = Post[]

export const postApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query<PostsResponse, void>({
query: () => 'posts',
// Provides a list of `Posts` by `id`.
// If any mutation is executed that `invalidate`s any of these tags, this query will re-run to be always up-to-date.
// The `LIST` id is a "virtual id" we just made up to be able to invalidate this query specifically if a new `Posts` element was added.
providesTags: (result) =>
// is result available?
result
? // successful query
[
...result.map(({ id }) => ({ type: 'Posts', id } as const)),
{ type: 'Posts', id: 'LIST' },
]
: // an error occurred, but we still want to refetch this query when `{ type: 'Posts', id: 'LIST' }` is invalidated
[{ type: 'Posts', id: 'LIST' }],
}),
addPost: build.mutation<Post, Partial<Post>>({
query(body) {
return {
url: `post`,
method: 'POST',
body,
}
},
// Invalidates all Post-type queries providing the `LIST` id - after all, depending of the sort order,
// that newly created post could show up in any lists.
invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
}),
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
providesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
updatePost: build.mutation<Post, Partial<Post>>({
query(data) {
const { id, ...body } = data
return {
url: `post/${id}`,
method: 'PUT',
body,
}
},
// Invalidates all queries that subscribe to this Post `id` only.
// In this case, `getPost` will be re-run. `getPosts` *might* rerun, if this id was under its results.
invalidatesTags: (result, error, { id }) => [{ type: 'Posts', id }],
}),
deletePost: build.mutation<{ success: boolean; id: number }, number>({
query(id) {
return {
url: `post/${id}`,
method: 'DELETE',
}
},
// Invalidates all queries that subscribe to this Post `id` only.
invalidatesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
}),
})

export const {
useGetPostsQuery,
useAddPostMutation,
useGetPostQuery,
useUpdatePostMutation,
useDeletePostMutation,
} = postApi

Optimistic Updates

https://redux-toolkit.js.org/rtk-query/usage/optimistic-updates

useMutation을 통해 이미 존재하고 있는 일부 데이터에 대한 업데이트를 수행하고자 할 때 RTK Query는 optimistic 업데이트를 실행하기 위한 몇가지 tool을 제공한다. 이는 사용자에게 변경사항이 즉시 적용된다는 인상을 주고 싶을 때 유용한 패턴이다.

핵심 개념은 다음과 같다:

  • quey 또는 mutation이 시작 될 때 , onQueryStarted가 실행된다.
  • api.util.updateQueryData 를 통해 캐시된 데이터를 수동으로 업데이트한다.
  • promiseResult가 reject일 때 .undo 를 통해 이전 디스패치에서 반환된 객체의 속성으로 롤백한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

// Optimistic update mutation example (async await)

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import { Post } from './types'

const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
providesTags: ['Post'],
}),
updatePost: build.mutation<void, Pick<Post, 'id'> & Partial<Post>>({
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
invalidatesTags: ['Post'],
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, patch)
})
)
try {
await queryFulfilled
} catch {
patchResult.undo()
}
},
}),
}),
})

References
RTK Query Mutations

원문링크: https://redux-toolkit.js.org/rtk-query/overview

RTK Query는 강력한 data fetching과 caching 툴이다. 웹 어플리케이션 내에서 데이터를 로딩할 때 사용되는 common case들을 단순화 하기 위해 고안되었기 때문에 data fetching과 캐싱 로직을 직접 작성할 필요가 없다. Redux Tookit 패키지에서 선택적 옵션으로 포함되어 있고, Redux Tookit 기능적으로 다른 api위에 구축된다.

Motivation

웹 어플리케이션은 보통 data를 화면에 그리기 위해 서버로부터 data를 fetch해서 가져와야 한다. 또 이 data를 업데이트 하거나 업데이트할 내용을 서버에 보내고, 서버의 데이터와 싱크를 맞추기 위해 client측에서 데이터를 캐시해서 가지고 있기도 한다. 오늘날을 어플리케이션 내에서 아래와 같은 다양한 행동이 실행되기 때문에 이 과정이 더욱 복잡하다.

  • UI spinners를 보여주기 위해 loading 상태일 때를 추적한다.
  • 같은 데이터를 사용할 때 중복 요청을 막는다
  • UI가 보다 빠르게 느끼도록 하는 updates
  • UI와 유저 인터페이스의 상호작용을 위한 캐시 수명(사이클) 매니징

Redux 코어는 늘 최소화되어 왔기 때문에 실제 로직을 작성하는 것은 개발자에게 달려있다. 이는 리덕스가 위와 같은 사용 사례를 해결하기 위한 어떠한 것도 내부에 포함하고 있지 않다는 의미이다. 리덕스 도큐먼트에서는 로딩 스테이트와 요청 결과를 트래킹하기 위한 요청 수명(사이클)이 몇 가지 일반적인 패턴이 있다고 가르쳐왔다. 그리고 Redux Toolkit의 createAsyncThunk는 이러한 전형적인 패턴을 추상화하여 고안된 것이다. 그러나 유저는 아직도 상당한 리듀서 로직을 로딩 스테이트와 데이터 캐시를 위해 사용해야 한다.

지난 몇 년간, 리액트 커뮤니티는 “data fetching과 캐싱’이 state management와 완전히 다른 관심사라는 것을 깨달았다. Redux와 같은 상태관리 라이브러리를 사용해서 데이터를 캐시할 수 있자만, 데이터 fetching과 같은 상황에서 쓸 목적으로 만들어진 별도의 툴을 사용해도 될 만큼 의미있다.

RTK Query는 Apollo Client, React Query, Urql 및 SWR과 같이 data fetching을 위한 선구적인 솔루션 툴들로부터 영감을 받았지만, API 디자인에 고유한 접근 방식을 추가했다.

  • data fetching과 캐싱 로직은 Redux toolkit의 createSlice와 createAsyncThunk api를 기반으로 구축된다.
  • 왜냐하면 redux toolkit은 UI에 구애받지 않는 UI-agnostic이기 때문에, RTK 쿼리는 모든 UI layer에서 사용할 수 있다.
  • api endpoint는 인수를 통해 어떻케 쿼리 파라미터를 생성할 것인지와 캐싱을 위해 어떻게 응답을 변환할 것인지를 포함하여 미리 정의된다.
  • RTK Query는 react hook을 생성하여 전체 data fetching과정을 캡슐화하고, 컴포넌트에 data와 isLoading필드를 제공하고, 컴포넌트가 마운트되거나 언마운트될 때 캐시된 데이터의 수명을 관리한다.
  • RTK Query는 “cache entry lifecycle”옵션을 제공하는데, 이 옵션을 사용하면 초기 데이터를 fetching한 후에 웹소켓 메세지를 통해 스트리밍 캐시 업데이트 등과 같은 경우에 사용할 수 있도록 활성화 된다.
  • openAPI 또는 GraphQL 스키마에서 API slice의 코드 생성에 대한 예를 찾을 수 있다.
  • RTK Query는 완전히 TypeScript로 쓰여져 있기 때문에 TS 사용에 대한 좋은 경험을 제공할 수 있다.

포함된 것

APIs

RTK Query는 Redux Toolkit 패키지의 코어를 설치하면 그곳에 포함되어 있다. 아래 두 엔트리 포인트 중 한 가지로 사용할 수 있다.

1
2
3
4
5
import { createApi } from '@reduxjs/toolkit/query';

/* React-specific entry point that automatically generates
hooks corresponding to the defined endpoints */
import { createApi } from '@reduxjs/toolkit/query/react';

RTK Query에는 다음의 API가 포함된다.

  • createApi(): RTK Query의 핵심기능이다. 여러 엔드포인트들로부터 데이터를 어떻게 회수할 것인지가 작성된 여러 엔드포인트들을 정의하고, 여기에는 데이터를 어떻게 fetch하거나 변환할 것인지에 대한 내용이 담긴다. 대부분의 경우 app에서 “하나의 base URL 당 하나의 API 슬라이스”를 사용하여 한 번만 작성되어야 한다.
  • fetchBaseQuery(): 요청을 단순화하는 것을 목표로 하는 fetch를 감싸는 작은 Wrapper이다. 대부분의 사용자에게는 createApi에서 baseQuery로 사용할 것이 권장된다.
  • : Redux 스토어가 없는 경우에도 Provider로써 사용할 수 있다.
  • setupListeners(): refetchOnMounte와 refetchOnReconnect를 사용할 수 있게 하는 유틸리티이다.

번들 크기

RTK 쿼리는 앱의 번들 사이즈에 고정된 일회성 사이즈로 추가된다. RTK Query는 Redux Toolkit과 React-Redux의 가장 상단에서 빌드되기 때문에 이렇게 추가된 사이즈는 이미 사용자가 app에서 이것을 쓰고 있는지 아닌지에 따라 다르다. 예상되는 min+gzip 번들 크기는 다음과 같다.

  • RTK를 이미 사용하고 있는 경우: RTK 쿼리의 경우 ~9kb, hooks의 경우 ~2kb.
  • RTK를 이미 사용하지 않는 경우:
    • React 없음: RTK+dependencies+RTK 쿼리의 경우 17kB
    • React 사용 시: 19kB + React-Redux, which is a peer dependency

endpoint들의 정의를 추가적으로 포함하고자 하면 보통 몇 바이트에 불과한 endpoints 정의 내부의 실제 코드에 근거하여 크기만 증가해야 한다

RTK 쿼리에 포함된 기능은 추가된 번들 크기에 대한 비용을 빠르게 지불하는 것을 포함하기 때문에 직접 작성한 data fetching 로직을 제거하면 대부분의 의미있는 응용 프로그램에서 size가 향상된다.


기본 사용법

Create an API Slice

RTK Query는 Redux Toolkit 패키지의 코어를 설치하면 그곳에 포함되어 있다. 아래 두 엔트리 포인트 중 한 가지로 사용할 수 있다.

1
2
3
4
5
6
7
import { createApi } from '@reduxjs/toolkit/query';

/* React-specific entry point that automatically generates
hooks corresponding to the defined endpoints */

// export hooks를 쓰려면 반드시 /react로 사용
import { createApi } from '@reduxjs/toolkit/query/react';

React에서 일반적인 사용법은 createApi를 임포트하는 것으로 시작하여 서버의 base url을 정리하고, 접근하고자 하는 엔드포인트를 정리하기 위한 “API slice”를 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { Pokemon } from './types'

// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (builder) => ({
getPokemonByName: builder.query<Pokemon, string>({
query: (name) => `pokemon/${name}`,
}),
}),
})

// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByNameQuery } = pokemonApi

스토어 구성

“API slice”에는 자동으로 생성되는 slice 리듀서와 구독 수명주기를 매니지 할 수 있는 커스텀 미들웨어를 포함하고 있다. 이 두 가지 모두 Redux 스토어에 추가해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { configureStore } from '@reduxjs/toolkit';
// Or from '@reduxjs/toolkit/query/react'
import { setupListeners } from '@reduxjs/toolkit/query';
import { pokemonApi } from './services/pokemon';

export const store = configureStore({
reducer: {
// Add the generated reducer as a specific top-level slice
[pokemonApi.reducerPath]: pokemonApi.reducer,
},
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(pokemonApi.middleware),
});

// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
setupListeners(store.dispatch);

컴포넌트에 hooks사용

마지막으로 API slice에서 자동 생성된 react hook을 컴포넌트 파일로 임포트 하고, 컴포넌트에서 필요한 파라미터를 사용하여 hooks를 호출한다. RTK Query는 mount될 때 자동으로 data를 fetch하고, 파라미터가 변경될때 re-fetch하며, 결과에 {data, isFetching} 값을 제공한다. 그리고 이 값들이 변경되면 컴포넌트를 리렌더링한다.

1
2
3
4
5
6
7
8
9
10
11
import * as React from 'react';
import { useGetPokemonByNameQuery } from './services/pokemon';

export default function App() {
// Using a query hook automatically fetches data and returns query values
const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur');
// Individual hooks are also accessible under the generated endpoints:
// const { data, error, isLoading } = pokemonApi.endpoints.getPokemonByName.useQuery('bulbasaur')

// render UI based on data and loading state
}

사용예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//dosApiSlice.ts

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const DOGS_API_KEY = '4902a53c-cfda-44e4-a5d7-35b9bd5b6cb9';

interface Breed {
id: string;
name: string;
image: {
url: string;
};
}

//api slice

export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://api.thedogapi.com/v1',
prepareHeaders(headers) {
headers.set('x-api-key', DOGS_API_KEY);

return headers;
},
}),
//builder에 의해 엔트리 포인트 생성
endpoints(builder) {
return {
fetchBreeds: builder.query<Breed[], number | void>({
query(limit = 10) {
return `/breeds?limit=${limit}`;
},
}),
};
},
});

export const { useFetchBreedsQuery } = apiSlice;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//store.ts
//configureStore: basic redux create store function의 wrapper
//create store

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counter-slice';
import { apiSlice } from '../features/dogs/dogsApiSlice';

//combine reducer
export const store = configureStore({
reducer: {
counter: counterReducer,
[apiSlice.reducerPath]: apiSlice.reducer,
},
middleware: (getDefaultMiddleware) => {
return getDefaultMiddleware().concat(apiSlice.middleware);
},
});

//ts thing
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

//사용하고자 하는 곳에서 사용
import { useFetchBreedsQuery } from './features/dogs/dogsApiSlice';
import logo from './logo.svg';
import './App.css';

function App() {
//take selector function
const count = useAppSelector((state) => state.counter.value);
const dispatch = useAppDispatch();

const [numDogs, setNumDogs] = useState(10);
const { data = [], isFetching } = useFetchBreedsQuery(numDogs);

console.log(useFetchBreedsQuery);

References
RTK Query Overview

Redux-toolkit-typescript 원문 번역

원문링크: https://redux-toolkit.js.org/tutorials/typescript

Usage With TypeScript

Redux Toolkit은 이미 TypeScript로 작성되었으므로 TS 유형 정의가 내장되어 있다. React Redux 는 NPM 의 별도 @types/react-reduxtypedef 패키지 에 type에 대한 정의가 있다. 라이브러리 함수를 작성하는 것 외에도 type은 리덕스 스토어와 리액트 컴포넌트 사이의 인터페이스를 보다 typesage하게 작성하는 것을 쉽게 하도록 도와주는 helper를 export한다.

React Redux v7.2.3부터는 react-redux 패키지가 @types/react-redux에 종속되어서 type 정의는 자동으로 라이브러리와 함께 설치된다. 그렇지 않은 경우에는 직접 수동으로 설치해야 한다. (npm install @types/react-redux)

The Redux+TS template for Create-React-App comes with a working example of these patterns already configured.

Project Setup

Root State 및 Dispatch Types 정의

configureStore를 사용하면 추가 입력이 필요하지 않다. 하지만 추후에 필요에 따라 참조할 수 있도록 RootState type과 Dispatch type을 추출하고자 하게 된다. 스토어 자체에서 이러한 타입들을 추론한다는 것은 state slice를 추가하거나 미들웨어 세팅을 수정할 때 올바르게 업데이트 된다는 것을 의미한다.

이것들은 모두 type이기 때문에 app/store.ts와 같이 스토어 셋업 파일에서 직접 export하여 다른 파일로 import하는 것이 안전하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { configureStore } from '@reduxjs/toolkit';
// ...

export const store = configureStore({
reducer: {
posts: postsReducer,
comments: commentsReducer,
users: usersReducer,
},
});

// Infer the `RootState` and `AppDispatch` types from the store itself
//RooteState와 AppDispatch가 스토어 자체에서 추론된다.
export type RootState = ReturnType<typeof store.getState>;

// 추론된 타입:
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

Define Typed Hooks

RooteState와 AppDispatch를 type들을 각각 컴포넌트로 import할 수 있지만, 어플리케이션에서 사용할 수 있도록 타입이 지정된 버전의 useDispatch, useSelector 훅을 사용하는 것이 좋다. 그 이유는 아래와 같다.

  • useSelector를 쓰면 매번 (state:RootState)를 사용할 필요가 없다.
  • useDispatch를 쓰면 기본 dispatch 타입은 thunk에 대해 알지 못한다. thunk를 올바르게 쓰기 위해서는thunk middlewate type을 포함한 스토어의 특정하게 커스텀된 AppDispatch를 useDispatch와 함께 사용해야 한다. 미리 유형이 정의된 useDispatch 훅을 추가하고자 할 때는 사용하고자 하는 곳에 AppDispatch를 import해야 하는 것을 잊지 않도록 한다.

이들은 type이 아니라 실제 변수이기 때문에 store setup 파일보다는 app/hooks.ts와 같은 별도의 파일에 정의하는 것이 중요하다. 이를 통해 hook을 쓰고자 하는 어떠한 파일에서든 hook을 import할 수 있게하고, 잠재적인 import 디펜던시 이슈를 피할 수 있다.

1
2
3
4
5
6
7
8
9
//app/hook.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
// 원형의 useDispatch, useSelector 대신 아래를 사용한다.
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

Application Usage

slice state 및 액션 타입의 정의

각각의 slice file은 초기 상태값에 대한 type을 정의해야 createSlice가 각각의 case 리듀서에 있는 state type을 올바르게 추론할 수 있다.

생성된 모든 액션들은 action.payload를 일반적인 인수로 사용하는 Redux Toolkit의 PayloadAction 타입을 사용해서 정의해야 한다.

여기서 store 파일로부터 RootState를 안전하게 import 할 수 있다. circular import이긴 하지만 typsScript 컴파일러가 type을 올바르게 처리할 수 있다. 이는 selector 함수를 작성할 때 필요할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../../app/store';

// Define a type for the slice state
interface CounterState {
value: number;
}

// Define the initial state using that type
const initialState: CounterState = {
value: 0,
};

export const counterSlice = createSlice({
name: 'counter',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// Other code such as selectors can use the imported `RootState` type
export const selectCount = (state: RootState) => state.counter.value;

export default counterSlice.reducer;

생성된 action creator는 작성자가 reducer에 제공한 PayloadAction의 타입 기반으로 payload의 인수를 수락하도록 올바르게 입력됩니다. 예를 들어, 위 코드에서 incrementByAmountsms 인수로 number를 필요로 한다.

몇몇 케이스에선 typeScript가 초기 상태의 type을 불필요하게 tigth하게 만들 수 있다. 만약 이런 일이 발생한다면 변수의 type을 선언하는 대신 as 를 이용해서 초기 상태를 캐스팅하는 방법으로 해결할 수 있다.

1
2
3
4
// Workaround: cast state instead of declaring variable type
const initialState = {
value: 0,
} as CounterState

Use Typed Hooks in Components

컴포넌트 파일에서 React-Redux의 표준 hook 대신 미리 입력된 hook을 가져온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { useState } from 'react';

import { useAppSelector, useAppDispatch } from 'app/hooks';

import { decrement, increment } from './counterSlice';

export function Counter() {
// The `state` arg is correctly typed as `RootState` already
const count = useAppSelector((state) => state.counter.value);
const dispatch = useAppDispatch();

// omit rendering logic
}

Usage With TypeScript

https://redux-toolkit.js.org/usage/usage-with-typescript

리덕스 툴킷은 typeScript로 작성되었으며, 해당 API는 TypeScript 응용 프로그램과의 뛰어난 통합을 가능하게 하도록 설계되었다.

configureStore

state type 가져오기

state type을 가져오는 가장 쉬운 방법은 roote reducer를 미리 정의하고, returnType을 추출하는 것이다. 쇼type 이름인 state는 일반적으로 남용되기 깨문에 혼동을 막기 위해 RootState와 같이 type에 다른 이름을 주는 것을 추천한다.

1
2
3
import { combineReducers } from '@reduxjs/toolkit';
const rootReducer = combineReducers({});
export type RootState = ReturnType<typeof rootReducer>;

혹은 rootReducer를 직접 생성하지 않고 직접 configureStore()에 slice reducer 전달 할 수 있다. 이 경우 root reducer를 올바르게 추론하기 위해 Type을 약간 수정해야 한다.

1
2
3
4
5
6
7
8
9
10
11
import { configureStore } from '@reduxjs/toolkit';
// ...
const store = configureStore({
reducer: {
one: oneSlice.reducer,
two: twoSlice.reducer,
},
});
export type RootState = ReturnType<typeof store.getState>;

export default store;

Dispatch 타입 가져오기

스토어에서 Dispatch 타입을 가져오려면 store를 생성한 후에 추출해야 한다. 이 역시 Dispatch는 남용되기 때문에 혼동을 막기 위해 AppDispatch처럼 다른 type 이름을 지정할 것을 추천한다. 혹은 아래처럼 useAppDispatch hook을 추출하여 useDispatch를 호출할 때 사용하는 것이 더 편할 수 도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { configureStore } from '@reduxjs/toolkit'
import { useDispatch } from 'react-redux'
import rootReducer from './rootReducer'

const store = configureStore({
reducer: rootReducer,
})

export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()
// Export a hook that can be reused to resolve types

export default store

Dispatch 타입에 대한 올바른 입력

dispatch 함수의 typedms 미들웨어 옵션에 의해 직접 추론될 수 있다. 만약 미들웨어에 올바른 타입을 추가하고 싶다면 dispatch는 이미 올바르게 입력되어 있어야 한다.

typeScript는 스프레드 연산자를 사용해서 배열을 합칠 때 array type을 확장하는 경우가 많기 때문에 getDefaultMiddleware()에 의해 리턴되는 MiddlewareArray 메소드에서 .concat(…) 또는 .prepend(…)를 사용하는 것이 추천된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { configureStore } from '@reduxjs/toolkit'
import additionalMiddleware from 'additional-middleware'
import logger from 'redux-logger'
// @ts-ignore
import untypedMiddleware from 'untyped-middleware'
import rootReducer from './rootReducer'

export type RootState = ReturnType<typeof rootReducer>
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.prepend(
// correctly typed middlewares can just be used
additionalMiddleware,
// you can also type middlewares manually
untypedMiddleware as Middleware<
(action: Action<'specialAction'>) => number,
RootState
>
)
// prepend and concat calls can be chained
.concat(logger),
})

export type AppDispatch = typeof store.dispatch

export default store

getDefaultMiddleware 없이 MiddlewareArray 사용

getDefaultMiddleware의 사용을 완전히 스킵할 경우, 미들웨어 배열의 안전한 타입 연결에 MiddlewareArray를 사용할 수 있다. 이 class는 .concat(…) 및 추가적인 .prepend(…) 메서드에 대한 수정된 유형만 사용하여 기본 JavaScript 배열 유형을 확장한다.

하지만 const로 사용하고, 스프레드 연산자를 사용하지 않는 한 배열 유형을 확장하는 문제가 발생하지 않기 때문에 일반적으로 필요하지는 않다.

다음의 두 가지 호출은 동일하다.

1
2
3
4
5
6
7
8
9
10
11
import { configureStore, MiddlewareArray } from '@reduxjs/toolkit'

configureStore({
reducer: rootReducer,
middleware: new MiddlewareArray().concat(additionalMiddleware, logger),
})

configureStore({
reducer: rootReducer,
middleware: [additionalMiddleware, logger] as const,
})

React Redux에서 추출한 Dispatch type 사용

기본적으로 React Redux useDispatch 훅에는 미들웨어를 고려하는 type이 포함되어 있지 않다. 만약 dispatching을 할 때 dispatch 함수에 대해 더 구체적인 type이 필요한 경우에는 dispatch 함수의 type을 지정하거나, custom-type이 된 버전의 useSelector를 만들어서 사용한다. (자세한 내용은 이곳 참조)

createAction

대부분의 케이스에서 action.type에 대한 리터럴 정의는 필요하지 않기 때문에 아래와 같은 코드를 사용할 수 있다.

1
createAction<number>('test');

이렇게 하면 생성된 action이 PayloadActionCreator의 타입이 된다.

일부 설정에서는 action.type에 대한 리터럴 설정이 필요할 수도 있다. 하지만 typeScript는 수동으로 정의된 타입 매개변수와 추론된 타입 매개변수의 혼합을 허용하지 않기 때문에 일반 정의와 실제 JavaScript 코드 모두에서 타입을 지정해야 합니다.

1
createAction<number, 'test'>('test');

중복 없이 이것을 작성하는 다른 방법을 찾고 있다면, callback을 준비하여 두 type 파라미터를 인수에서 추론할 수 있으므로 action type을 특정할 필요가 없다.

1
2
3
4
function withPayloadType<T>() {
return (t: T) => ({ payload: t });
}
createAction('test', withPayloadType<string>());

리터럴하게 입력된 action.type의 대안

예를 들어 case문에서 payload type을 올바르게 작성하기 위해 action.type을 유니온 식별을 위한 식별자로 사용하는 경우 이 대안에 관심이 있을 수 있다.

생성된 action creators는 type predicate로써 match 메소드를 가진다.

1
2
3
4
5
6
7
const increment = createAction<number>('increment');
function test(action: Action) {
if (increment.match(action)) {
// action.payload inferred correctly here
action.payload;
}
}

이 match 메소드는 redux-observable과 RxJS의 filter 메소드를 조합하여 사용하는 경우에도 유용하다.

createReducer

createReducer의 기본 호출방법은 아래와 같이 “lookup table” / “map object”를 사용하는 것이다.

1
2
3
createReducer(0, {
increment: (state, action: PayloadAction<number>) => state + action.payload,
});

하지만 key는 오직 문자열이기 때문에, API TypeScript를 사용할 경우 action type을 추론하거나 validate할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
{
const increment = createAction<number, 'increment'>('increment');
const decrement = createAction<number, 'decrement'>('decrement');
createReducer(0, {
[increment.type]: (state, action) => {
// action is any here
},
[decrement.type]: (state, action: PayloadAction<string>) => {
// even though action should actually be PayloadAction<number>, TypeScript can't detect that and won't give a warning here.
},
});
}

대안으로 RTK에 type-safe reducer builder API가 포함되어 있다.


Building Type-Safe Reducer Argument Objects

createReducer에 대한 인수로 간단한 객체를 사용하는 대신, ActionReducerMapBuilder 인스턴스를 받는 콜백을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
const increment = createAction<number, 'increment'>('increment');
const decrement = createAction<number, 'decrement'>('decrement');
createReducer(0, (builder) =>
builder
.addCase(increment, (state, action) => {
// action is inferred correctly here
})
.addCase(decrement, (state, action: PayloadAction<string>) => {
// this would error out
})
);

리듀서 인수 객체를 정의할 때 보다 엄격한 type 안정성이 필요한 경우에 사용이 추천된다.

builder.addMatcher

builder.addMatcher의 첫 번째 matcher에 type predicate 함수를 사용할 수 있다. 결과적으로 두번째 reducer 인수에 대한 action 인수는 typeScript에 의해 유츄될 수 있다.

1
2
3
4
5
6
7
8
9
function isNumberValueAction(action: AnyAction): action is PayloadAction<{ value: number }> {
return typeof action.payload.value === 'number'
}

createReducer({ value: 0 }, builder =>
builder.addMatcher(isNumberValueAction, (state, action) => {
state.value += action.payload.value
})
})

createSlice

createSlice는 action 뿐만 아니라 reducer도 만드는데 여기서 type safety에 대해 걱정할 필요는 없다. action types는 인라인으로 제공될 수 있다.

1
2
3
4
5
6
7
8
9
10
11
const slice = createSlice({
name: 'test',
initialState: 0,
reducers: {
increment: (state, action: PayloadAction<number>) => state + action.payload,
},
});
// now available:
slice.actions.increment(2);
// also available:
slice.caseReducers.increment(0, { type: 'increment', payload: 5 });

케이스 리듀서가 너무 많아서 인라인으로 정의하는 것이 지저분하거나 슬라이스 전체에서 케이스 리듀서를 재사용하려는 경우 createSlice 외부에서 정의하여 호출하고 caseReducer처럼 입력할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
type State = number;
const increment: CaseReducer<State, PayloadAction<number>> = (state, action) =>
state + action.payload;

createSlice({
name: 'test',
initialState: 0,
reducers: {
increment,
},
});

References

Getting Started | Redux Toolkit
Let’s Learn Modern Redux! (with Mark Erikson) - Learn With Jason

Redux-toolkit 원문 번역

원문링크: https://redux-toolkit.js.org/introduction/getting-started#rtk-query

Redux toolkt은 리덕스 로직을 표준화하기 위한 패키지이다. 원래는 리덕스가 가지고 있는 세가지 문제점을 지원하기 위해 만들어졌다.

  • 리덕스 스토어를 구성하는 방법이 지나치게 복잡하다.
  • 리덕스를 사용하기 위해 많은 패키지를 설치해야 한다
  • 리덕스는 지나치게 많은 보일러 플레이트 코드를 필요로 한다.

리덕스가 가진 이러한 문제점에 대헤 모든 것을 커버할 순 없지만 create-react-app과 appllo-boost를 근간으로 하여 보다 추상화된 셋업 프로세스와 가장 많이 사용되는 use cases를 처리하기 위한 방법을 제공할 수 있을 뿐 아니라 사용자가 어플리케이션 코드를 단순화할 수 있는 유용한 유틸리티를 제공한다.

리덕스 툴킷은 또한 data fetching과 캐싱을 할 수 있는 ‘RTK Query’를 포함하는데, 별도의 엔트리포인트 set로 패키지 안에 포함되어 있다. 옵셔널이지만 직접 data fetching 로직을 작성하지 않아도 된다.
이 툴은 리덕스 유저들에게 유용하다. 만약 당신이 프로젝트에 리덕스를 처음 적용해보는 유저거나 어플리케이션 내에 존재하는 코드를 단순화 시키고자 하는 익숙한 리덕스 유저든, 리덕스 툴킷은 당신의 리덕스 코드를 훨씬 좋게 만들 것이다.


설치방법

Create React App을 이용

가장 추천되는 방법으로 official Redux + JS template 또는 create React App용 Redux + Ts template을 사용하는 것이다. 이 템플릿은 redux toolkit 및 react redux 구성 요소의 통합을 활용한다.

1
2
3
4
5
# Redux + Plain JS template
npx create-react-app my-app --template redux

# Redux + TypeScript template
npx create-react-app my-app --template redux-typescript

기존 앱

redux toolkit은 모듈 번들러용 npm 패키지로 또는 노드 어플리케이션에서 사용 가능하다.

1
2
3
4
5
# NPM
npm install @reduxjs/toolkit

# Yarn
yarn add @reduxjs/toolkit

또는 window.RTK의 전역 변수로 정의된 precompiled UMD package로 사용가능하다. UMD package는 <script>태그로 바로 사용할 수 있다.


Redux-toolkit에 포함된 것

  • [configureStore()](https://redux-toolkit.js.org/api/configureStore): 단순화된 구성옵션과 좋은 기본값을 제공하기 위해 createStore 로 감싼다. 이는 자동으로 slice reducer와 결합할 수 있고, 사용자게 제공하는 리덕스 미들웨어가 무엇이든 간에 redux-thunk를 디폴트로 제공한다. 또 redux devtools extension을 사용할 수 있다.
  • [createReducer()](https://redux-toolkit.js.org/api/createReducer): switch 문으로 액션 타입의 목록을 작성하는 대신, case reducer funxtion들로 액션타입의 목록을 제공한다. 추가로 자동으로 immer 라이브러리를 사용하여 immutable한 업데이트를 normal mutative 코드를 통해 단순화한다. 예를 들어 state.todos[3].completed = true.처럼.
  • [createAction()](https://redux-toolkit.js.org/api/createAction): 주어진 액션 타입 문자열에 대한 action creator 함수를 생성한다. 이 함수는 스스로 toString()으로 정의되기 때문에 type 상수대신 사용할 수 있다.
  • [createSlice()](https://redux-toolkit.js.org/api/createSlice): reducer 함수의 객체, a slice name, 초기값을 받아들여서 이와 상응하는 action creators, action types을 가진 slice reducer를 자동으로 생성한다.
  • [createAsyncThunk](https://redux-toolkit.js.org/api/createAsyncThunk): 액션 타입 문자열을 받아들여서 promise를 return하는 함수를 반환하고, 이 promise에 근거하여 pending/fulfilled/rejected 의 액션 타입을 디스패치하는 thunk를 생성한다.
  • [createEntityAdapter](https://redux-toolkit.js.org/api/createEntityAdapter): 스토어에 있는 정규화된 data(normalized date)를 관리하기 위해 재사용 가능한 리듀서와 selectors set를 생성한다.
  • The [createSelector utility](https://redux-toolkit.js.org/api/createSelector) from the Reselect library, re-exported for ease of use.

RTK Query

@reduxjs/toolkit package 에 옵셔널로 포함되어 제공되는 것이다. data fetching과 캐싱의 사용 사례를 해결하기 위해 제작되었고, app의 API 인터페이스 계틍을 정의하기 위해 작지만 강력한 툴셋을 제공한다. 웹 어플리케이션에서 데이터를 로딩하는 보통의 경우를 단순화하도록 만들어졌기 때문에 직접 data fetching과 캐싱 로직을 작성할 필요가 없다.

RTK 쿼리는 실행을 위해 Redux Toolkit 코어 위에 구축되는데, 아키텍처 내부에서 redux를 내부적으로 사용하기 위함이다. RTK 쿼리를 쓰기 위해서 리덕스나 RTK에 대한 지식이 필요하지는 않지만 RTK 쿼리가 제공하는 모든 추가적인 global store 관리 기능을 탐색할 필요가 있고, RTK 쿼리를 완전하게 사용하여 요청 및 캐시 동작의 타임라인을 재현하고, 횡단하기 위해 Redux DevTools 브라우저 확장 프로그램을 설치해야한다.

RTK 쿼리는 core Redux Toolkit package 내부에 설치되어있다. 아래 두 가지 엔트리 포인트를 통해 사용할 수 있다.

1
2
3
4
5
import { createApi } from '@reduxjs/toolkit/query';

/* React-specific entry point that automatically generates
hooks corresponding to the defined endpoints */
import { createApi } from '@reduxjs/toolkit/query/react';

RTK 쿼리안에 들어 있는 것

다음의 API가 포함된다.

  • [createApi()](https://redux-toolkit.js.org/rtk-query/api/createApi): RTK 쿼리 기능의 핵심. 일련의 엔드포인트로부터 데이터를 어떻게 회수해오는지와 관련된 방법, 데이터를 fetch하고 transtorm하는 방법을 설명하는 엔드리 포인트 set를 정의할 수 있다. 보통 기본 URL당 하나의 API 슬라이스를 사용하여 app에서 한 번만 쓰여야 한다.
  • [fetchBaseQuery()](https://redux-toolkit.js.org/rtk-query/api/fetchBaseQuery): 요청을 단순화하는 목적을 가진 작은 wrapper이다. 대부분의 사용자에게 createApi에서 권장되는 baseQuery로 사용된다.
  • [<ApiProvider />](https://redux-toolkit.js.org/rtk-query/api/ApiProvider): 리덕스 스토어가 아직 없는 상태에서 Provider로 사용할 수 있다.
  • [setupListeners()](https://redux-toolkit.js.org/rtk-query/api/setupListeners): refetchOnMount, refetchOnReconnect 동작을 활성화하는 데 사용되는 유틸리티이다.

References

Getting Started | Redux Toolkit
Let’s Learn Modern Redux! (with Mark Erikson) - Learn With Jason

headless UI

실제 UI 요소를 제공하거나 렌더하지 않지만 라이브러리가 제공하는 state와 callback hooks를 통해서 테이블 마크업을 커스텀할 수 있다.

headless user interface component는 아무런 인터페이스를 제공하지 않음으로써 최대의 시각적 유연성을 제공하는 컴포넌트이다. 유저 인터페이스가 없는 유저 인터페이스라고 할 수 있다. 이는 컴포넌트의 로직과 동작을 시각적 표현에서 분리하는 것이다.

<CoinFlip/>를 예로 들자면 이 컴포넌트를 child component의 함수로 쓰거나 prop을 렌더하는 식으로 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const flip = () => ({
flipResults: Math.random(),
});

class CoinFlip extends React.Component {
state = flip();

handleClick = () => {
this.setState(flip);
};

render() {
return this.props.children({
rerun: this.handleClick,
isHeads: this.state.flipResults < 0.5,
});
}
}

위 함수는 headless이다. 아무것도 렌더하지 않기 때문이다. 이 함수는 다양한 consumer들이 여러 논리적인 처리를 하면서 프레젠테이션 작업도 할 것을 기대하고 있다. 따라서 어플리케이션 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<CoinFlip>
{({ rerun, isHeads }) => (
<>
<button onClick={rerun}>Reflip</button>
{isHeads ? (
<div>
<img src='/heads.svg' alt='Heads' />
</div>
) : (
<div>
<img src='/tails.svg' alt='Tails' />
</div>
)}
</>
)}
</CoinFlip>

여기서는 render prop을 받는 것으로 headless가 구현되었는데, HOC로 구현될 수도 있다.
혹은 View와 Controller, ViewModel과 View의 패턴으로 구현될 수도 있다. 여러 방법이 있겠지만 여기서 중요한 점은 동전을 뒤집는 매커니즘과 그 매커니즘의 인터페이스를 분리했다는 것이다.

react-table

모든 React Table에서는 useTable hook과 table instance 객체가 리턴된다. 이 table instance 객체는 table을 만들고, table의 state와 상호작용할 수 있는 모든 것을 포함한다.

getting your data

테이블 구조를 생각해보면 보통 rows(행)과 columns(열)로 구성된 구조를 떠올린다. 그렇지만 테이블 구성이 단순히 행과 열의 조합으로 생각하기에는 중첩된 columns(열)과 행 등의 구조를 가질 수 있는 등 점점 복잡한 구조를 가지기 때문에 아래와 같은 구조로 기본 데이터 구조를 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const data = React.useMemo(
() => [
{
col1: 'Hello',
col2: 'World',
},
{
col1: 'react-table',
col2: 'rocks',
},
{
col1: 'whatever',
col2: 'you want',
},
],
[]
);

여기서 중요한 점은 useMemo를 사용했다는 것이다. 이는 데이터가 매번 렌더될 때마다 새롭게 생성되지 않음을 보장하는 것이다. 만약 useMemo를 쓰지 않는다면 table은 렌더될 때마다 새로운 데이터를 받았다고 생각하기 때문에 매번 많은 로직은 새로 계산하려고 시도하게 된다.

Define Columns

useTable hook에 전달하기 위해 column에 대한 정의를 한다. 역시 마찬가지로 useMemo를 사용했기 때문에 매번 렌더될 때마다 새롭게 계산하지 않고, value를 기억해뒀다가 메모제이션된 value와 실제 value에 차이가 있을 때만 변화시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
const columns = React.useMemo(
() => [
{
Header: 'Column 1',
accessor: 'col1', // accessor is the "key" in the data
},
{
Header: 'Column 2',
accessor: 'col2',
},
],
[]
);

Using the useTable hook

위에서 작성한 data와 column에 대한 정의를 가지고 useTable hook에 전달하여 새로운 table instance를 생성할 수 있다. 즉 useTable은 최소 메모제이션된 columns와 data를 포함한 객체를 제공받아야 한다.

1
const tableInstance = useTable({ columns, data });

Building a basic table UI

table instance는 만들었지만 아직 테이블 마크업과 스타일은 하지 않은 상태이므로 기본적인 테이블 구조를 작성해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
return (
<table>
<thead>
<tr>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
);

Applying the table instance to markup

이제 기본적인 테이블 구조를 작성했으니 이를 이용해서 tableInstance를 얻을 수 있다. 이를 이용해 테이블 작성을 완료할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const tableInstance = useTable({ columns, data });

const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
tableInstance;

return (
// apply the table props
<table {...getTableProps()}>
<thead>
{
// Loop over the header rows
headerGroups.map((headerGroup) => (
// Apply the header row props
<tr {...headerGroup.getHeaderGroupProps()}>
{
// Loop over the headers in each row
headerGroup.headers.map((column) => (
// Apply the header cell props
<th {...column.getHeaderProps()}>
{
// Render the header
column.render('Header')
}
</th>
))
}
</tr>
))
}
</thead>
{/* Apply the table body props */}
<tbody {...getTableBodyProps()}>
{
// Loop over the table rows
rows.map((row) => {
// Prepare the row for display
prepareRow(row);
return (
// Apply the row props
<tr {...row.getRowProps()}>
{
// Loop over the rows cells
row.cells.map((cell) => {
// Apply the cell props
return (
<td {...cell.getCellProps()}>
{
// Render the cell contents
cell.render('Cell')
}
</td>
);
})
}
</tr>
);
})
}
</tbody>
</table>
);

Final Result

앞의 모든 과정을 한번에 표현해보면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import { useTable } from 'react-table';

function App() {
const data = React.useMemo(
() => [
{
col1: 'Hello',
col2: 'World',
},
{
col1: 'react-table',
col2: 'rocks',
},
{
col1: 'whatever',
col2: 'you want',
},
],
[]
);

const columns = React.useMemo(
() => [
{
Header: 'Column 1',
accessor: 'col1', // accessor is the "key" in the data
},
{
Header: 'Column 2',
accessor: 'col2',
},
],
[]
);

const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable({ columns, data });

return (
<table {...getTableProps()} style={{ border: 'solid 1px blue' }}>
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th
{...column.getHeaderProps()}
style={{
borderBottom: 'solid 3px red',
background: 'aliceblue',
color: 'black',
fontWeight: 'bold',
}}
>
{column.render('Header')}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td
{...cell.getCellProps()}
style={{
padding: '10px',
border: 'solid 1px gray',
background: 'papayawhip',
}}
>
{cell.render('Cell')}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
);
}

Hook Usage

React Table은 리액트 훅은 내외부에서 사용하여 구성하고, lifecycle를 관리한다. 그리고 기본적으로 custom react hook과 호환되는 hook을 가진다.

useTable은 가장 기본적으로 사용되는 훅인데, 모든 옵션과 플러그인 훅의 스타팅 포인트로 제공된다. 옵션이 useTable로 전달되면 제공받은 순서대로 모든 플러그인 훅으로 전달되고, 최종 instance를 만들어 결과적으로 table state와 상호작용하는 나만의 table UI를 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
const instance = useTable(
{
data: [...],
columns: [...],
},
useGroupBy,
useFilters,
useSortBy,
useExpanded,
usePagination
)

The stages of React Table and plugins

  1. useTable이 호출되어 table instance가 만들어진다.
  2. instance.state는 custom user state나 자동으로 생성된 것으로 resolve된다.
  3. 플러그인 포인트들의 컬렉션은 instance.hooks에 생성된다.
  4. 각 플러그인은 instance.hook에 hook을 추가할 수 있다.
  5. useTable 로직이 실행되면서 각 플러그인 훅 type은 등록된 순서대로 개별 hook 함수가 실행되는 순서에 따라 특정 시점에서 사용된다.
  6. useTable로부터 최종적인 instance 객체를 얻고, 이를 이용해 개발자가 자신만의 테이블을 만들 수 있다.

Plugin Hook Order & Consistency

플러그인 hook의 순서와 사용은 다른 custom hooks가 그러하듯 항상 같은 순서로 호출되어야 한다는 hooks의 법칙에 따른다.

Option Memoization

React Table은 state와 사이드 이펙트를 업데이트하거나 계산해야하는 타이밍을 메모제이션에 의존한다. 이 말은 모든 옵션이 useTable에 전달될 때 useMemo 또는 useCallback으로 메모제이션되어야 한다는 의미이다.


References
HEADLESS USER INTERFACE COMPONENTS
React Table - overview
React Table - Quick Start
React Table - Overview