DOM
은 document-object-model
로 HTML
문서를 JavaScript
에서 제어하기 위해서 Object
로 만든 것을 말합니다.
Virtual DOM
은 DOM
에서 렌더링에 필요한 요소를 제외하고 React
에서 필요한 데이터만 경량화한 형태로 구현한 DOM
입니다.
흔히 Virtual DOM
을 사용하는 이유가 DOM
이 무겁고 느리기 때문이라고 생각하기도 하지만 실제로 그런 것은 아닙니다.
DOM
이 무겁고 느리기보다는 DOM
의 변경에 의해서 발생하는 layout, reflow, repaint 때문입니다.
layout
, reflow
, repaint
는 실제로 브라우저 화면을 구성하기 위해서 계산하고 그리는 행위를 하므로 다른 행위보다 많은 시간을 할애합니다.
하나의 Node
가 변경되더라도 ( 어떤 값인지에 따라 다르지만 ) layout
, reflow
, repaint
가 발생하게 됩니다.
그 이후 즉시 다른 Node
가 변경되면 또다시 layout
, reflow
, repaint
가 발생하게 됩니다.
이런 작업을 하면서 불필요한 연산을 많이 처리하게 됩니다.
불필요한 연산을 줄이기 위해서 React
에서는 Virtual DOM
을 이용해서 변경된 값들만 감지하고 연속적인 변경은 한 번에 처리하기 때문에 layout
, reflow
, repaint
가 한 번만 발생하게 됩니다.
따라서 Virtual DOM
이 DOM
보다 더 빠르고 효율적으로 동작합니다.
컴포넌트는 객체지향프로그래밍에서 말하는 하나의 객체라고 생각해도 무방할 것 같습니다.
객체들을 하나하나 조립해서 프로그램을 만들듯이 원하는 위치에 원하는 컴포넌트를 조립해서 웹페이지를 만드는 것입니다.
코드를 명확하게 분리해서 작성할 수 있으며, 쉽게 재사용할 수 있습니다. 또한 가상 돔을 이용해서 변화를 감지하여 특정 컴포넌트의 특정 부분만 리랜더링 되도록 합니다.
React
에서는 변수와 함수를 메모이제이션 할 수 있습니다.
메모이제이션이란 어떤 실행에 대한 결괏값이 저장해두고 다음에 실행할 때 결과값을 재사용함으로써 불필요한 연산을 줄이는 것을 의미합니다.
변수를 메모이제이션해서 컴포넌트를 리렌더링할 때 새로운 변수를 만들지 않고 기존에 저장해놓을 값을 그대로 사용합니다.
물론 useEffect()
처럼 deps
의 값이 변경된다면 새로운 값을 메모이제이션 합니다.
함수를 메모이제이션해서 컴포넌트를 리렌더링할 때 새로운 함수를 선언하지 않고 기존에 선언한 함수를 그대로 사용합니다.
물론 useEffect()
처럼 deps
의 값이 변경된다면 새로운 함수를 메모이제이션 합니다.
컴포넌트를 메모이제이션 하는 방법입니다.
원래 컴포넌트는 아래 5가지 조건에 의해서 리렌더링 되게 됩니다.
하지만 React.memo()
를 사용하게 된다면 3번에 의해서 컴포넌트가 리렌더링 되지는 않습니다.
부모 컴포넌트가 변경되었다고 자식의 컴포넌트가 달라질 거라는 확신은 없기 때문에 개발자가 선택할 수 있게 만들어둔 것으로 생각합니다.
또한 React.memo()
는 HOC
( higher-order-component
)로 컴포넌트를 반환하는 컴포넌트입니다. 따라서 React.memo(Component)
형식으로 사용합니다.
state
변경props
변경shouldComponentUpdate
가 true
거나 forceUpdate()
가 실행될 때 ( 이 두 가지는 사용해본 적 없음 )여기서 한 가지 생각해보면 좋은 것이 모든 함수, 변수, 컴포넌트에 메모이제이션을 한다고 무조건 좋은 것은 아니라는 것입니다. 메모이제이션한다는 행위 자체는 그만큼 메모리를 더 사용한다는 의미이므로 성능상의 문제가 있거나 확실히 사용함에 대한 이점을 갖는 것이 아니라면 사용에 대해 충분히 고민하고 사용해야 한다고 생각합니다.
여태까지 저는 모든 함수에다가 아무 생각 없이 useCallback()
을 사용했었는데 이번에 공부하면서 잘못된 방식이라는 것을 알게 되었고 앞으로는 충분히 고민하고 적절한 곳에 사용할 생각입니다.
처음에
React
를 접할 때도 동작 원리가 궁금하긴 했지만, 그때는JavaScript
의 주요 개념들조차도 제대로 이해하지 못하는 상태였기 때문에React
의 개념들에 대해서 깊게 이해하지 못하고 넘어갔었지만, 현재는closure
같은 개념도 어느 정도 이해했으며,React
에 대해 많이 익숙해졌기 때문에 평소에 별생각 없이 가져다가 사용하기만 했던hook
들을 어떻게 사용하는지에 관해서 공부하고 정리해보려고 합니다.
React.useState()
와 React.useEffect()
를 간단하게 구현해보면서 내부적으로 어떤 방식에 의해 동작하는지에 대해 이해하려고 노력했습니다.
다른 포스트들을 보면 클로저부터 순차적으로 알려주는 포스트가 많아서 굳이 여기서는 그렇게 하지 않고 typescript
를 적용한 최종 결과물만 작성했습니다.
type Type = {
click: () => void;
render: () => void;
wrtie: (str: string) => void;
};
const React = ((): {
render: (Component: () => Type) => Type;
useState: <T>(initialValue: T) => [T, (newState: T) => void];
useEffect: (cb: () => void, depArray: any[]) => void;
} => {
// hook들의 값이 들어갈 배열 ( useState, useEffect 등 )
let states: any[] = [];
// 현재 어떤 hook인지 판단할 인덱스 ( 몇번째로 선언한 hook인지 )
let index = 0;
const useState = <T>(initialValue: T): [T, (newState: T) => void] => {
// setState()를 사용할 때 선언 시점의 index를 사용하기 위해 index값을 저장해둠
// 만약 _index를 선언하지 않았다면 여러 개의 hook을 사용하면서 증가한 index의 hook의 값을 변경시키게 됨
const _index = index;
// 현재 값 || 초기값을 가짐
const state = (states[_index] as T) || initialValue;
// state 변경 함수
const setState = <T>(newState: T) => (states[_index] = newState);
// 다음 훅으로 변환
index++;
return [state, setState];
};
const useEffect = (cb: () => void, depArray: any[]) => {
// 이전에 받았던 deps값의 배열
const oldDeps = states[index];
let hasChanged = true;
// deps값 배열중에 하나라도 값이 다르다면
if (oldDeps)
hasChanged = depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
// cb() 실행 ( 단, 초기에는 실행 )
if (hasChanged) cb();
// deps값 배열 최신화
states[index] = depArray;
// 다음 hook으로 변환
index++;
};
const render = (Component: () => Type): Type => {
index = 0;
const C = Component();
C.render();
return C;
};
return { render, useState, useEffect };
})();
const Component = (): Type => {
const [count, setCount] = React.useState<number>(1);
const [text, setText] = React.useState<string>("default");
React.useEffect(() => console.log("count 변경!"), [count]);
return {
render: () => console.log({ count, text }),
click: () => setCount(count + 1),
wrtie: (str: string) => setText(str),
};
};
let App: Type;
// "count 변경!"
App = React.render(Component); // { count: 1, text: "default" }
App.click(); // "count 변경!"
App.wrtie("blue");
App = React.render(Component); // ( count: 2, text: "blue" )
React
에서는 특정 노드를 선택할 때는 useRef()
를 이용해서 선택합니다.
하지만 useRef()
가 노드를 선택하기 위해서만 존재하지는 않습니다.
useRef()
로 생성한 변수는 값이 변경되어도 리렌더링이 되지 않습니다.
따라서 컴포넌트에서 사용은 하지만 값의 변화가 렌더링에 영향을 미치지 않는 변수를 useRef()
를 이용해서 사용하면 됩니다.
ref
속성값에 callback
함수를 넣어주는 방식을 말합니다.
이번에 공부하면서 알게 되었고 여태까지 합리적이지 못한 방식으로 hook
을 사용했다는 것을 깨달았습니다.
아래 두 가지의 예시를 비교해보면 callback ref
의 유용함을 알 수 있습니다.
( 좋은 예시가 떠오르지 않아서 autoFocus
속성을 못쓴다는 가정함 )
useRef()
사용 예시import React, { useEffect, useRef } from "react";
function App() {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => inputRef.current?.focus(), [inputRef]);
return (
<div className="App">
<input type="text" ref={inputRef} />
</div>
);
}
export default App;
callback ref
사용 예시import React from "react";
function App() {
return (
<div className="App">
<input type="text" ref={(node) => node?.focus()} />
</div>
);
}
export default App;
이전에는 특정 노드를 선택해서
ref
변수에 넣어서 사용했었습니다.
그렇게되면 불필요한useRef()
,useEffect()
를 사용해야하는데callback ref
를 이용해서 간단하게 처리할 수 있습니다.
forwardRef()
는 하위 컴포넌트에 ref
를 전달하는 방법입니다.
기본적으로 컴포넌트에 ref
를 전달하면 에러가 발생합니다.
그래서 forwardRef()
라는 특수한 방식을 이용해서 ref
를 하위 컴포넌트로 전달할 수 있습니다.
import React, { useEffect, useRef } from "react";
function App() {
const inputRef = useRef<HTMLInputElement | null>(null);
const buttonRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
setTimeout(() => {
console.log("타이머 실행! >> ", buttonRef);
buttonRef.current?.click();
}, 2000);
}, [buttonRef]);
return (
<div className="App">
{ /* 정상 작동 */ }
<Form title="제목" ref={inputRef} />
{ /* 비정상 작동 : 코드상에서 에러가 나지는 않지만 실행했을 때 콘솔창에 경고 메시지 띄워줌 + buttonRef에 값이 들어가지 않음 */ }
<Button ref={buttonRef} />
</div>
);
}
type FormProps = {
title: string;
};
const Form = React.forwardRef<HTMLInputElement, FormProps>((props, ref) => (
<form>
<h2>{props.title}</h2>
<input type="text" ref={ref} />
</form>
));
type ButtonProps = {
ref: React.MutableRefObject<HTMLButtonElement | null>;
};
const Button = ({ ref }: ButtonProps) => {
return (
<button type="button" ref={ref} onClick={() => console.log("click!!")}>
click me
</button>
);
};
export default App;
리액트는 단방향 데이터 흐름을 지킵니다.
각 컴포넌트는 useState()
나 useReducer()
를 이용해서 관리하며, 하위 컴포넌트로 데이터를 전달할 때는 props
를 이용합니다.
리액트의 단방향 데이터 흐름을 지키다보면 props drilling
이 발생하게 됩니다.
Context API
를 사용하면 props
를 이용하지 않고 다른 컴포넌트에서 데이터를 사용할 수 있습니다.
리액트에서 상태 관리를 하는 방법중 하나입니다. ( useState()
, useReducer()
)
Flux
패턴을 따르며 action
을 dispatch
하면 reducer
를 통해서 state
를 변경할 수 있습니다.
즉, 기존 state
+ action
dispatch
--reducer
--> 새로운 state
를 만들어냅니다.
기존에 사용하던
MVC
패턴의 문제점인 데이터가 여러 방향으로 흘러서 데이터 관리가 힘들다는 점을 보완하기 위해 나온Flux
패턴은 단방향으로 데이터가 흘러가고action
과state
를dispatch
하는 방법으로만 데이터를 조작할 수 있습니다.
/context/dotoProvider.tsx
import { createContext, useCallback, useReducer } from "react";
type Props = {
children: React.ReactNode;
};
type ContextType = {
todoList: StateType[];
onAddTodo: (todo: string) => void;
onRemoveTodo: (id: number) => void;
};
// 컨텍스트 생성 ( 여러 컴포넌트에서 공유할 데이터 )
export const todoContext = createContext<ContextType>({
todoList: [],
onAddTodo: () => {},
onRemoveTodo: () => {},
});
type StateType = {
id: number;
contents: string;
};
type ActionType =
| {
type: "ADD_TODO";
payload: string;
}
| {
type: "REMOVE_TODO";
payload: number;
};
// 리듀서 생성 ( 상태 변경 방법 작성 ( 기존 상태 + action => 새로운 상태 ) )
const todoReducer = (
initialState: StateType[],
action: ActionType
): StateType[] => {
switch (action.type) {
case "ADD_TODO":
return [
...initialState,
{
id: Date.now(),
contents: action.payload,
},
];
case "REMOVE_TODO":
return initialState.filter((todo) => todo.id !== action.payload);
default:
return initialState;
}
};
// Provider Wrapper
const PersonProvider = ({ children }: Props) => {
const [todoList, dispatch] = useReducer(todoReducer, []);
// todo 추가
const onAddTodo = useCallback(
(todo: string) => {
dispatch({
type: "ADD_TODO",
payload: todo,
});
},
[dispatch]
);
// todo 제거
const onRemoveTodo = useCallback(
(id: number) => {
dispatch({
type: "REMOVE_TODO",
payload: id,
});
},
[dispatch]
);
return (
// 상태가 변경되면 context를 구독하는 컴포넌트에게 변화를 알리는 역할을 하는 컴포넌트
<todoContext.Provider
value={{
todoList,
onAddTodo,
onRemoveTodo,
}}
>
{children}
</todoContext.Provider>
);
};
export default PersonProvider;
/src/App.tsx
import { useContext } from "react";
import { todoContext } from "./context/dotoProvider";
function App() {
const { todoList, onAddTodo, onRemoveTodo } = useContext(todoContext);
return (
// 대부분은 index.tsx 최상위에서 씌워줍니다.
// 그러면 하위의 모든 컴포넌트들에서 todoContext를 사용할 수 있게 됩니다.
<todoContext>
<div className="App">
<h1>ToDoList</h1>
<ul>
{todoList.map((todo) => (
<li key={todo.id}>
<span>{todo.contents}</span>
<button type="button" onClick={() => onRemoveTodo(todo.id)}>
remove todo
</button>
</li>
))}
</ul>
<button type="button" onClick={() => onAddTodo("추가" + Math.random())}>
add todo
</button>
</div>
</todoContext>
);
}
export default App;