react hooks component에서 setInterval 사용의 문제점

useInterval이라는 커스텀 훅이 있다.

이 훅은 interval을 세팅하고, 컴포넌트가 언마운트 될 때 interval을 clera한다.
이렇게 함으로서 setInterval과 clearInterval 모두 컴포넌트 라이프 사이클에 종속된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useEffect, useRef } from "react";

function useInterval(callback, delay) {
const savedCallback = useRef();

// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);

// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}

setInterval + clearInerval의 단점

임피던스 불일치라는 용어를 쓴다.
리액트 프로그래밍 모델은 state를 기반으로 렌더링되고 기타 변수들을 관리한다.

하지만 setInterval은 일단 설정하고 나면 없애는 것 외에는 변경할 수 없다. 또한 react의 state 기반으로 동작하지도 않는다.

state나 props가 변하더라도 교체하기 전까지는 내부의 props와 state를 참조할 것이다.

ref의 사용

위에서 알아본 것 처럼 setInterval은 react의 동작과 다르게 동작한다.
이로 인해 발생하는 문제점은 다음과 같다.

  1. 첫 렌더에서 callback1을 가진 setInterval(callback1,delay)를 수행한다.
  2. 다음 렌더에서 새로운 props와 state를 거쳐서 만들어지는 callback2가 있다. 하지만 시간을 재설정하지 않고서는 callback을 대체할 수 없다.

이를 해결하려면 useRef를 사용하면 된다.

useRef는 hooks에서 렌더링과 관련없는 변수를 담는데에 이용되는 하나의 상자이다.

솔루션은 다음과 같다.

  1. setInterval(fn,delay)에서 함수가 savedCallback을 호출하게 만든다.
  2. 첫 렌더링에서 savedCallback을 callback1로 설정한다.
  3. 두 번째 렌더링에서 savedCallback을 callback2로 설정한다.

결과본은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState, useEffect, useRef } from "react";

function useInterval(callback, delay) {
const savedCallback = useRef();

// callback에 변경사항이 있을 경우 가장 최근의 함수를 저장한다
// 최근의 state나 props의 변경사항을 반영한 callback이다.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);

// 타이머를 교체하거나 하는 작업 없이 제일 최근에 들어온 함수를 실행한다.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}

Interval 일시정지하기

위의 useInterval hook에선 null을 delay에 전달하는 것으로 interval을 일시 정지할 수 있다.
delay가 null이 아닐경우 타이머를 설정하기 떄문이다..!

1
2
3
4
5
6
7
8
9
const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);

useInterval(
() => {
setCount(count + 1);
},
isRunning ? delay : null
);

ref

훅스 컴포넌트에서 setInterval 사용시의 문제점
Making setInterval Declarative with React Hooks

styled-component+ts props 전달하기

typescript환경에서 styled-components에 props를 전달하려면 제네릭을 활용하면 된다.
나는 next.js를 사용중이라 @emotion/styled를 사용하였다.

예시

1
2
3
4
5
6
7
8
9
10
11
12
interface blockType {
blockColor: string;
sizeOfRowCol: number;
}

const Block = styled.div<blockType>`
background: ${(props) => props.blockColor};
width: ${(props) => (BlockWrapperSize - 10) / props.sizeOfRowCol}px;
height: ${(props) => (BlockWrapperSize - 10) / props.sizeOfRowCol}px;
margin: 2px;
box-sizing: border-box;
`;

실제 사용할 때는 컴포넌트에 props를 내려주고 접근하여 사용하면 된다.

1
2
3
4
5
6
7
<BlockWrapper>
{colorTable.map((block, idx) => (
<Block blockColor={block.color} sizeOfRowCol={2} key={idx}>
&nbsp;
</Block>
))}
</BlockWrapper>

axios 요청 취소하기

axios는 요청을 취소할 수 있는 CancelToken을 제공한다.

1
2
3
4
5
6
cancelToken = axios.CancelToken.source();
// 토큰을 생성
axios.get(url, { cancelToken: cancelToken.token });
// 요청에 토큰을 넣어서 전송
cancelToken.cancel();
// 토큰의 cancel메서드를 이용하여 취소

토큰을 생성하여 axios의 config에 넣어주면 된다.
그 후 토큰의 cancel()메서드를 호출하면 된다.

아래의 코드는 리액트에서 사용한다고 가정하고 예시로 작성해본 코드이다..!

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
const Example = () => {
const ref = useRef();
const handleRequest = async () => {
try {
ref.current = axios.CancelToken.source();
const data = await axios.get(
url,
{
data:{/.../},
cancelToken: ref.current.token,
});
//...데이터 처리
}
catch (e) {
if (error.response) {
//서버에서 에러 메시지를 전달해주었을 경우
console.error(error.response.message);
}
else if (error.request) {
//서버에서 응답하지 않은 경우
console.error("request is failed!");
}
else if (axios.isCancel(error)) {
//요청을 취소한 경우
console.log("request is cancled!");
}
}
};
};

const handleRequestCancel=()=>{
res.current.cancel();
}

return(
<>
<button onClick={handleRequest}>요청</button>
<button onClick={handleRequestCancel}>취소</button>
</>
)

마지막 요청만 서버로 보내는 디바운싱과 함께 적용하면 요청을 효율적으로 관리할 수 있을 것 같다.

디바운싱을 적용하여 간단하게 작성해보았다.

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
const Example = () => {
const ref = useRef();
const timer=useRef();
const handleRequest = () => {
if(timer.current){
clearTimeout(timer);
}
const newTimer=setTimeout(async ()=>{
try {
ref.current = axios.CancelToken.source();
const data = await axios.get(
url,
{
data:{/.../},
cancelToken: ref.current.token,
});
//...데이터 처리
}
catch (e) {
if (error.response) {
//서버에서 에러 메시지를 전달해주었을 경우
console.error(error.response.message);
}
else if (error.request) {
//서버에서 응답하지 않은 경우
console.error("request is failed!");
}
else if (axios.isCancel(error)) {
//요청을 취소한 경우
console.log("request is cancled!");
}
}
};
},1300)
timer.current=(newTimer)
};

const handleRequestCancel=()=>{
res.current.cancel();
}

return(
<>
<button onClick={handleRequest}>요청</button>
<button onClick={handleRequestCancel}>취소</button>
</>
)

다음에 진행할 프로젝트에 꼭 한 번 적용해보고 싶다!

ref

axios 러닝 가이드

state 최적화

count에 따라 색을 다르게 하는 코드이다.

왼쪽 코드는 counter가 증가할 때마다 useEffect에서 color의 변화가 일어난다.
즉 렌더링이 두 번 일어난다.

그에 비해 오른쪽 코드는 counter가 변화할 떄마다 counter에 따라 color를 다르게 한다.
즉 한 번의 렌더링으로 같은 기능을 한다.

state를 새로 만들기 전에 기존 state로 처리할 수 있는지 생각하고 만들자!

state optimization

ref

https://twitter.com/asidorenko_/status/1483473130383450114?s=20

useState Hook의 setState에는 콜백함수가 없다!

지금까지 setState의 두 번째 파라메터로 콜백함수를 넣어 동기실행을 할 수 있다고 알고 있었는데
이는 클래스형 컴포넌트의 setState에만 존재한다.

즉 다음과 같은 실행은 불가능하다.

1
2
3
4
5
const [state, setState] = useState(0);

setState(1, () => {
return console.log("hi");
});

하지만 useEffect를 사용하여 다음과 같은 방법으로 대체할 수 있다.
useEffect의 의존성 배열에 state를 추가하여 실행하는 방법이다.

1
2
3
4
5
6
7
const [state, setState] = useState(0);

setState(1);

useEffect(() => {
console.log("hi");
}, [state]);

typescript useRef의 타입

antd+typescript+react를 사용하던 중 ref의 세 가지 타입에 대해 알게되어 포스팅한다.
react의 useRef는 세 가지 타입이 오버로딩 되어있다.

useRef

useRef는 initialValue를 useRef 객체의 current에 저장한다.
이 useRef의 current가 변경되어도 컴포넌트는 리렌더링 되지 않는다.
때문에 DOM 내부 객체를 직접 가리키거나 렌더링과 상관없는 변수를 저장하는데에 사용된다.

useRef의 타입

  1. MutableRefObject<T>
1
function useRef<T>(initialValue: T): MutableRefObject<T>;

initialValue의 타입과 제네릭의 타입이 T인경우 MutableRefObject<T>를 반환한다.
렌더링과 관련없는 값을 저장하는데에 사용하자.

  1. RefObject<T>
1
function useRef<T>(initialValue: T | null): RefObject<T>;

제네릭의 타입이 T이고 initialValue의 타입이 T 혹은 null인 경우 RefObject<T>를 반환한다.
RefObject<T>는 readonly로 current를 직접 수정할 수 없다.
하지만 current의 하위 프로퍼티는 조작 가능하다.

그렇기에 DOM 을 조작하기 위해 사용하자.

  1. MutableRefObject<T|undefined>
1
function useRef<T = undefined>(): MutableRefObject<T | undefined>;

ts+react에서 이벤트 객체의 타입 지정하기

ts+react를 사용할 때에 HTMLEvent의 타입을 지정하는 방법에 대해서 간단하게 정리해보았다.

React의 Event의 타입

Event의 타입은 React안에 정의되어있다.

onchange 이벤트의 타입은 React.ChangeEvent<T>이다.
onclick 이벤트의 경우 React.MouseEvent<T>이다.
제네릭에는 이벤트를 발생시키는 요소를 전달하면 된다.

예시

자주 쓰이는 useInput hooks이다.

이벤트의 타입을 React.ChangeEvent<HTMLInputElement>라고 명시해주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState, useCallback } from "react";

type UseInputTypes = [
string,
(e: React.ChangeEvent<HTMLInputElement>) => void,
React.Dispatch<React.SetStateAction<string>>
];
const useInput = (initialValue: string): UseInputTypes => {
const [value, setValue] = useState<string>(initialValue);

const onChangeValue = useCallback((e) => {
setValue(e.target.value);
}, []);
return [value, onChangeValue, setValue];
};

export default useInput;

next.config.js 여러 플러그인 연결하기

다음 플러그인 사용하기
https://www.npmjs.com/package/next-compose-plugins

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
const withTM = require("next-transpile-modules")([
"@fullcalendar/common",
"@babel/preset-react",
"@fullcalendar/common",
"@fullcalendar/daygrid",
"@fullcalendar/interaction",
"@fullcalendar/react",
"@fullcalendar/timegrid",
]);

const withPlugins = require("next-compose-plugins");

const nextConfig = {
async rewrites() {
return [
{
source: "/:path*",
destination: `https://dev.aistudios.com/:path*`,
},
];
},
};

module.exports = withPlugins([withTM({})], nextConfig);
// withPlugins([plugins],nextConfig);

nextjs css-npm 에러

next.js의 경우 node_modules내부의 패키지에서 전역으로 css를 import하면 에러를 발생시킨다.
구글링을 통해 해결하였다.

1
./node_modules/@fullcalendar/common/main.css Global CSS cannot be imported from within node_modules. Read more: https://err.sh/next.js/css-npm Location: node_modules/@fullcalendar/common/main.js

해결방법

1
npm i next-transpile-modules @babel/preset-react

next-transpile-modules 는 node_modules 내부의 패키지에 css/scss파일이 포함 될 수 있도록 transpile 하는 플러그인이다.

이후 next.config.js 파일을 다음과같이 작성해주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @type {import('next').NextConfig} */
const withTM = require("next-transpile-modules")([
"@fullcalendar/common",
"@babel/preset-react",
"@fullcalendar/common",
"@fullcalendar/daygrid",
"@fullcalendar/interaction",
"@fullcalendar/react",
"@fullcalendar/timegrid",
]);

module.exports = withTM({
// your custom config goes here
});

개선방안

공부할 예정인 next-compose-plugins를 통해 간편하게 여러 플러그인을 import 할 수 있다.

ref

next css-npm
How To Use Fullcalendar With Next.js (v10 or higher)

typescript ReactElement ReactNode JSX.Element 차이

render

render() 함수는 다음 중 하나를 반환하여야한다.

  1. React Element
  • JSX를 사용하여 생성된다.
  1. 배열과 Fragment
  • render()를 통하여 여러개의 Element를 반환한다.
  1. Portal
  • 별도의 DOM 하위트리에 자식 엘리먼트를 렌더링한다.
  1. 문자열과 숫자
  • 이 값들은 DOM상에 텍스트 노드로서 렌더링된다.
  1. Boolean 또는 null
  • 아무것도 랜더링 하지 않는다.

render()는 순수한 함수여야 한다.
컴포넌트의 state를 변경하지 않고 호출될 때마다 동일한 결과를 반환해야한다.
또한 브라우저와 직접적으로 상호작용해서는 안된다.

ReactNode

ReactNode는 ReactChild 등등 많은 것을 포함한다.
제일 범용적으로 사용할 수 있는 것 같다..!(다른 관점에선 리펙토링 할 수 있는 부분)

클래스형 컴포넌트의 반환값이다!

1
2
3
4
5
6
7
type ReactNode =
| ReactChild
| ReactFragment
| ReactPortal
| boolean
| null
| undefined;

ReactElement

ReactElement는 type, props, 그리고 key를 가진 객체이다.

예전 게시글에서 알아본 것 처럼 함수형 컴포넌트의 반환값이다!

1
2
3
4
5
6
7
8
9
10
interface ReactElement<
P = any,
T extends string | JSXElementConstructor<any> =
| string
| JSXElementConstructor<any>
> {
type: T;
props: P;
key: Key | null;
}

JSX.Element

JSX.Element는 any type의 generic type인 ReactElement이다.
전역으로 선언되어있어 라이브러리들이 구현하여 사용할 수 있다.

1
2
declare global { namespace JSX { interface Element extends React.ReactElement<any, any> { }

++추가 React.FC

React.FCchildren이 포함되어있어 컴포넌트의 props의 타입 처리가 어렵다.

차라리 위의 ReactElementReactNode를 적용 후 children의 타입을 명시하는 것이 좋다..!