리액트에 대한 개요
2022 stackoverflow web trend 를 살펴보면, React 의 사용비율이 흔히 알려진 프론트엔드 4대장(React, Vue, Angular, Svelte)중에서 압도적으로 높다고 할 수 있다. 이렇게 많이 사용된다는 것은 그 만큼의 생태계가 발달되었음을 의미하고, 개발시에 정보들을 빠르게 얻을 수 있음을 의미한다.
라이브러리 vs 프레임워크
라이브러리
개발 편의를 위한 도구의 모음이다. 공구에 비유를 할 수 있다.
즉, 라이브러리는 그냥 함수들이나 기능 모음을 가져다 쓰는 것이다.
프레임워크
프레임 워크는 뼈대나 기반 구조를 뜻한다. 프로그래밍을 진행할 때 필수적인 구조를 제공해주기 때문에 프레임워크를 사용하는 프로그래머는 이 프레임 워크의 뼈대 위에서 코드를 작성하여 프로그램을 개발하게 된다.
차이점
라이브러리와 프레임워크의 차이는 어플리케이션의 Flow 를 누가 쥐고 있느냐에 달려 있다.
프레임워크는 전체적인 흐름을 스스로가 쥐고 있으며 사용자는 그 안에서 필요한 코드를 짜 넣는다. 반면에 라이브러리는 사용자가 전체적인 흐름을 만들며 라이브러리를 가져다 쓰는 것이라 할 수 있다.
정리하면, 라이브러리는 개발자에게 주도성이 있으며 프레임워크는 그 틀안에 이미 제어흐름에 대한 주도권이 내재되어 있다.
생태계
프론트엔드 개발시에 리액트를 사용하는 사람들이 많기 때문에 그만큼 해당 기슬에 대한 관심도 / 실제 사용 빈도 / 사용자 수가 많다.
이러한 요소들이 모여 풍부한 생태계를 만든다. 생태계가 풍부하다는 것은 구글링 하기 편하다라는 뜻이다
생태계가 큰 리액트를 사용하면 어떤 장점이 있을까?
- 관련 라이브러리가 많다.
- 문제를 해결할 방법을 찾기 쉽다.
- 나와 같은 고민을 하는 / 했던 사람이 많다.
- 실무에서 사용할 확률이 높다.
기술적 근간
많은 사람들이 사용한다 !== 기술적 근간이 좋다.
사용하는 사람이 많다고 해서 그 기술이 매우 좋다라고 할 수는 없다.
하지만 리액트는 기술적으로 확실한 장점이 있다.
Virtual DOM / JSX / Flux / Functional Programming
위 Virtual DOM / JSX / Flux 에 대해서 설명할 수 있으면 리액트를 왜 쓰는가에 대해서도 설명할 수 있을 듯하다. 함수형 프로그래밍은 익숙해지려면 시간이 많이 걸릴 것 같다.
또한 생태계가 성숙해가면서 고민의 깊이 또한 성숙해졌다.
리액트를 풍성하게 해주는 라이브러리들이 계속 나오고 있다. (React Query , framer motion)
DOM 다루기 Element 생성하기
DOM: 웹페이지를 프로그래밍적으로 제어할 수 있게 해주는 객체모델
컴포넌트: 엘리먼트들의 집합
엘리먼트: HTML 태그 요소
ReactDOM.render
render 는 공식문서에 다음과 같이 설명되어있다. 첫번째 파라미터는 새롭게 생성된 엘리먼트를 두번째 파라미터에는 그 엘리먼트를 담을 엘리먼트를 작성해주면된다.
React.createElement
바닐라 자바스크립트를 사용하여 엘리먼트를 추가하는 방법은 다음과 같다.
리액트로 엘리먼트를 추가하는 방법은 다음과 같다. 이 내용은 공식문서에서도 참고할 수 있다.
<!DOCTYPE html>
<html lang="en">
<body>
<script
crossorigin
src="https://unpkg.com/react@18/umd/react.development.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
></script>
<div id="root"></div>
<script>
const rootEl = document.getElementById("root");
const el = React.createElement("h1", { children: "Hello, world" });
ReactDOM.render(el, rootEl);
</script>
</body>
</html>
바닐라 자바스크립트의 createElement를 사용한 코드와 React.createElement를 사용한 코드로 만들어진 엘리먼트의 차이점을 살펴보자.
바닐라로 만든거는 엘리먼트만 만들어졌고, 리액트로 만든건 객체다. 위 코드로 볼때 아래 코드에 작성된 두번째 인자는 props 를 의미하는 것을 알 수 있다.
const el = React.createElement("h1", { children: "Hello, world" });
위 코드는 아래 코드와 같은 의미를 가진다.
const el = React.createElement("h1", null, "Hello, world");
위 코드는 공식문서의 내용이다. 두번째 파라미터는 props에 관한 것이다. className, chindren 과 같이 프로퍼티명을 작성해주어야한다. 세번째는 children만을 위한 파라미터다.
두번째 인자에 작성한 childeren 프로퍼티에는 여러개의 데이터를 넘겨줄 수 있다. 세번째 인자는 children을 위한 공간이므로 똑같이 동작한다.
그런데 생각해보면 지금까지는 React.createElement와 바닐라 자바스크립트로 엘리먼트를 추가하는 방법이 큰 차이가 없어보인다. 코드 길이도 비슷하고 말이다. 새로운 기술을 도입하려할 때 기존의 사용하던 방식과 크게 차이가 없다면 도입할 필요가 없다. 리팩토링 비용이 훨씬 크기 때문이다. 하지만 무엇인가 드라마틱한 변화가 있었기에 리액트라는 라이브러리가 대중화가 되지 않았을까?
JSX
그러한 변화중 하나가 JSX 이다. JSX란 문자도 HTML도 아닌 JavaScript의 확장 문법이다.
Babel
바벨은 자바스크립트 컴파일러로 JSX의 문법을 자바스크립트의 문법으로 해석할 수 있도록 해준다.
예제에 바벨을 적용해보자.
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel">
const rootEl = document.getElementById("root");
// const el = React.createElement("h1", {
// className: title,
// children: "Hello, world"
// });
const el = <h1 className="title">Hello, world</h1>;
ReactDOM.render(el, rootEl);
</script>
바벨 CDN을 추가한다. script의 type이 text/babel 일때 적용이 된다는 것에 유의해야한다. (default 는 application/javascript이다.)
꽤나 멋지다. 마치 HTML 마크업과 굉장히 유사하다.
하지만 이것외에 매력적인 것은 JS 변수, 배열, 함수를 바로 사용할 수 있다는 것이다. 바닐라 자바스크립트를 사용하여 DOM 을 얻고 해당 엘리먼트에 innerHTML 과 같은 메서드를 사용하여 새로운 엘리먼트를 추가하는 방식보다 훨씬 간단해졌다.
스프레드 연산자를 사용하여 props를 한번에 내려주는 방법이다.
멀티 Element 생성하기
지금까지의 예제에서는 단일 엘리먼트만 props의 children 으로 넘겨주었다. 그렇다면 여러요소를 넘기는 방법은 없을까?
아래의 HTML 코드 처럼 말이다.
당연히 방법이 있다.
위처럼 여러개의 엘리먼트를 생성하여 배열로 묶어 children 으로 내려주면된다. 그러나 children 요소들이 묶이는 태그가 div 이기 때문에 의도치 않게 div 가 HTML에 반영되는 부작용이 있다.
이 현상의 해결방법으로 리액트에서는 엘리먼트가 HTML에 반영이 되지않고 children을 넘겨줄 수 있는 React.Fragment 라는 태그를 만들었다.
React.createElement 는 JSX 로 바꿀 수 있다.
위 코드의 children 은 아래 코드처럼 생략할 수 있다.
React.Fragment 또한 <> </> 로 축약할 수 있다.
Element 찍어내기
JSX 문에서는 자바스크립트 코드를 간단하게 사용할 수 있다. 아래는 paint 라는 함수를 사용하여 중복되는 엘리먼트를 추상화한 코드이다.
그래서 뭐 어쩌라고? 라는 생각이 들 수도 있다. 하지만 다음 코드를 보면 생각이 달라질 수 있다. 리액트에서 흔히 사용하는 컴포넌트와 비슷한 코드가 되고있지 않은가?
아래코드는 좀 더 JSX 를 반환하는 컴포넌트와 비슷한 코드로 작성된 코드이다. 이때는 함수명의 첫글자를 대문자로 작성해주어야한다. 기존의 사용되고 있는 HTML 태그와 중복되는 경우에 없게 하기 위함이다.
물려준 children 을 사용하는 코드를 작성할 수도 있다.
리액트의 렌더링
바닐라 자바스크립트를 사용해서 버튼을 생성해보자.
아래 영상에서 버튼의 focus가 풀리는 요소는 버튼이 다시 생성되기 때문이다.
반면에 리액트를 사용한 코드는 어떨까?
리액트는 변경된 부분만 다시그린다.
리액트에서는 어떻게 변경된 부분만 다시 그릴 수 있는 걸까?
그 과정을 알기전에 정리해야할 지식들을 몇개 살펴보자.
Virtual DOM
JSX와 더불어 Virtual DOM은 리액트를 수많은 개발자에게 사랑받을 수 있게한 이유이다. Virtual DOM은 DOM을 복사하여 메모리에 저장되어있는 자바스크립트 객체이다.
리액트는 어떻게 Virtual DOM을 활용하여 실제 DOM을 조작할까?
리액트는 두개의 Virtual DOM 객체를 가지고 있다.
- 렌더링 이전 화면 구조를 나타내는 Virtual DOM
- 렌더링 이후에 보이게 될 화면 구조를 나타내는 Virtual DOM
리액트는 state가 변경될 때마다 렌더링이 발생한다. 이 시점마다 새로운 내용이 담긴 Virtual DOM을 생성하게 된다. 실제 브라우저가 그려지기 이전에 말이다.
렌더링 이전에 화면의 내용을 담고있는 첫번째 Virtual DOM과 업데이트 이후에 발생할 두번째 Virtual DOM을 비교해 정확히 어떤 Element가 변했는지를 비교한다. 리액트는 이를 통해 차이가 발생한 부분만 실제 DOM에 적용한다. 이 과정을 Reconciliation(재조정)이라고 한다. 이것이 매우 효율적인 이유는 Batch Update 때문이다. 변경된 모든 Element들을 queue에 저장한다음 한번에 실제 DOM에 적용하는 방식이다.
바닐라 자바스크립트로 구현한 결과물과 리액트로 구현한 결과물의 렌더링 차이가 발생하는 이유를 조금이나마 이해할 수 있었다. 여기서 더 나아가 Reconciliation(재조정)에 대해 알아보자.
Reconciliation(재조정)
리액트에서는 이전 Virtual DOM 과 업데이트 이후의 Virtual DOM을 비교해 차이가 발생한 부분만 실제 DOM에 적용한다. 이 과정을 Reconciliation(재조정)이라고 한다. 두 Virtual DOM의 차이를 비교할 때 리액트는 Diffing Algorithm(비교 알고리즘)을 사용한다. Diffing Algorithm(비교 알고리즘)은 휴리스틱 알고리즘으로 구현되며 O(n³) 으로 구현해아하지만 리액트는 대신, 두가지 가정을 기반하여 O(n) 복잡도의 휴리스틱 알고리즘을 구현했다.
- 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
- 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.
엘리먼트의 타입이 다른 경우
두 루트 엘리먼트의 타입이 다르면, 리액트는 이전 트리를 버리고 완전히 새로운 트리를 구축한다. 예를 들어 아래와 같은 비교가 일어나면 이전 Counter는 사라지고 새로 다시 마운트가 될 것이다.
<div>
<Counter />
</div>
<span>
<Counter />
</span>
DOM 엘리먼트의 타입이 같은 경우
같은 타입의 두 리액트 DOM 엘리먼트를 비교할 때 리액트는 두 엘리먼트 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신한다.
<div className="before" title="stuff" />
<div className="after" title="stuff" />
이 두 엘리먼트를 비교하면, React는 현재 DOM 노드 상에 className 만 수정한다.
style이 갱신될 때 리액트는 또한 변경된 속성만 갱신한다. 예를 들어서 다음과 같은 코드가 있다.
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
위 두 엘리먼트 사이에서 변경될 때 , 리액트는 fontWeight는 수정하지 않고 color 속성만 수정한다.
DOM 노드의 처리가 끝나면, 리액트는 이어서 해당 노드의 자식들을 재귀적으로 처리한다.
자식에 대한 재귀적 처리
DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다.
예를 들어, 자식의 끝에 엘리먼트를 추가하면, 두 트리 사이의 변경은 잘 작동할 것이다.
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
하지만 아래처럼 리스트의 맨 앞에 엘리먼트를 추가하는 경우는 어떨까?
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
리액트는 트리가 변경되었다고 판단하여 변경을 생성한다. Connecticut 만 새로 추가하고 Duke와 Villanova 는 이동만 하면되는데도 말이다.
이러한 비효율마저 인정하기 싫었던 리액트 개발자들은 key prop을 만들어 기존의 트리와 이후 트리의 자식들이 일치하는지 확인했다. 예로 다음코드를 보자.
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
"2014" key를 가진 엘리먼트가 새로 추가되었고, "2015"와 "2016" key를 가진 엘리먼트는 이동만 하면 된다는 것을 리액트는 알 수 있게 되었다.
어떤 값이 key 값으로 좋을까?
key 값은 형제 사이에서만 유일하면 된다. 전역에서 유일할 필요는 없다. 보통은 서버에서 넘겨주는 데이터의 id나 해당 데이터를 식별해줄 값을 지정한다.
최후의 수단으로 배열의 인덱스를 key로 사용할 수 있다. 하지만 이 경우는 리스트가 추가되는 경우에만 정상적으로 동작할 것이다.
리액트에서 Key 값이 필요한 이유
우리가 개발시에 한번 씩은 봤을 법한 key prop 에러는 엘리먼트 변경을 최소화 하기위한 리액트 팀의 배려랄까?
정리하면, 리액트에서 key를 사용하는 이유는 엘리먼트 혹은 컴포넌트의 변화를 감지하기 위함이다. 이것은 효율적인 DOM 사용으로 귀결된다.
Reconciliation 프로세스 정리
- 엘리먼트 타입이 다르다. -> 기존의 트리를 제거하고 새로운 트리를 생성
- 엘리먼트 타입이 같다. -> key prop을 확인한다. -> prop 을 확인한다.
이벤트 핸들러
바닐라 자바스크립트의 이벤트 핸들러 등록은 다음와 같다.
리액트에서는 어떻게 이벤트 핸들러를 등록할까? 간단하다 JSX의 prop으로 바닐라 자바스크립트의 이벤트를 카멜 케이스로 작성해주면 된다.
const rootElement = document.getElementById("root");
const element = (
<button onClick={() => alert("pressed")} onMouseOut={() => alert("bye")}>
Press
</button>
);
ReactDOM.render(element, rootElement);
다음은 간단한 예제이다.
<!DOCTYPE html>
<html lang="en">
<body>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel">
const rootEl = document.getElementById("root");
const state = { keyword: "", typing: false, result: "" };
const App = () => {
function handleChange(e) {
setState({ keyword: e.target.value, typing: true });
}
function handleClick(e) {
setState({ typing: false, result: `Found: ${state.keyword}` });
console.log(state);
}
return (
<>
<input onChange={handleChange} />
<button onClick={handleClick}>Search</button>
<p>
{state.typing ? `Looking for ${state.keyword}` : state.result}
</p>
</>
);
};
function setState(newState) {
Object.assign(state, newState);
render();
}
function render() {
ReactDOM.render(<App />, rootEl);
}
render(); // 초기 렌더
</script>
</body>
</html>
컴포넌트 사이드 이펙트 다루기
사이드 이펙트란 프로그래밍에서는 부수 효과란 개념으로 더 많이 사용된다. 어떠한 값의 변경으로 인해 추가적으로 발생하게 된다.
React 에서 사이드 이펙트를 다루기 위한 방법은 어떤것이 있을까? 바로 useEffect 가 존재한다. 아래 코드의 useEffect는 keyword가 변경될 때마다 발생한다. deps 흔히 불리는 의존성 배열에 부수 효과를 발생시키기 원하는 상태값이나 함수를 넣을 수 있다.
useEffect(() => {
window.localStorage.setItem("keyword", keyword);
}, [keyword]);
lazy init
useState 의 초기값으로 할당한 값이 비용이 큰 함수이거나, localStorage 같은 IO 작업이 발생하는 경우 딜레이가 발생할 수 있다.
import { useState } from "react";
import "./styles.css";
const lazyFunc = () => {
console.log("lazy");
for (let i = 0; i < 10_000; i++) {}
};
export default function App() {
const [state, setState] = useState(lazyFunc());
const [rerender, setRerender] = useState("");
const handleChange = (e) => {
setRerender(e.target.value);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<input onChange={handleChange} value={rerender} />
</div>
);
}
위 코드의 useState 내부에 있는 lazyFunc 함수는 렌더가 발생할 때 마다 실행된다.
문제라고까지는 할 수 없지만, 프로덕트에 영향을 끼칠 수 있으므로 리액트에서는 이것을 컨트롤 할 수 있도록 lazy init 을 제공한다.
import { useState } from "react";
import "./styles.css";
const lazyFunc = () => {
console.log("lazy");
for (let i = 0; i < 10_000; i++) {}
};
export default function App() {
const [state, setState] = useState(lazyFunc);
const [rerender, setRerender] = useState("");
const handleChange = (e) => {
setRerender(e.target.value);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<input onChange={handleChange} value={rerender} />
</div>
);
}
커스텀 훅 만들기
아래의 코드를 보면 "keyword" 와 "result" 의 상태가 바뀔 때 마다 각각 사이드 이펙트가 발생한다. 두가지의 로직은 상태값을 제외하면 똑같다. 이는 커스텀 훅을 사용하여 중복을 제거할 수 있다.
<script type="text/babel">
const el = document.getElementById("root");
const App = () => {
const [keyword, setKeyword] = React.useState(() =>
window.localStorage.getItem("keyword")
);
const [result, setResult] = React.useState("");
const [typing, setTyping] = React.useState(false);
React.useEffect(() => {
window.localStorage.setItem("keyword", keyword);
}, [keyword]);
React.useEffect(() => {
window.localStorage.setItem("result", result);
}, [result]);
function handleChange(e) {
setTyping(true);
setKeyword(e.target.value);
}
function handleClick(e) {
setResult(`Found: ${keyword}`);
setTyping(false);
}
return (
<>
<input onChange={handleChange} value={keyword} />
<button onClick={handleClick}>Search</button>
<p>{typing ? `Looking for ${keyword}` : result}</p>
</>
);
};
ReactDOM.render(<App />, el);
</script>
아래 코드는 위 코드에 커스텀 훅을 사용하여 중복을 제거한 코드이다.
<script type="text/babel">
const rootEl = document.getElementById("root");
function useLocalStorage(itemName, value = "") {
const [state, setState] = React.useState(() => {
return window.localStorage.getItem(itemName) || value;
});
React.useEffect(() => {
window.localStorage.setItem(itemName, state);
}, [state]);
return [state, setState];
}
const App = () => {
const [keyword, setKeyword] = useLocalStorage("keyword");
const [result, setResult] = useLocalStorage("result");
const [typing, setTyping] = useLocalStorage("typing", false);
function handleChange(e) {
setTyping(true);
setKeyword(e.target.value);
}
function handleClick(e) {
setResult(`Found: ${keyword}`);
setTyping(false);
}
return (
<>
<input onChange={handleChange} value={keyword} />
<button onClick={handleClick}>Search</button>
<p>{typing ? `Looking for ${keyword}` : result}</p>
</>
);
};
ReactDOM.render(<App />, rootEl);
</script>
커스텀 훅을 사용했을 때 이점?
중복 제거
중복을 제거할 수 있다 라는 것 자체만으로 큰 이점을 가진다. 조금 더 나아가서 어떤 이점들이 더 있는지 알아보자.
UI 와 비즈니스 로직의 분리
하나의 코드내에 UI 와 비즈니스 로직이 존재하는 것 보다 관심사를 분리해내어 컴포넌트는 state 를 사용하여 화면에 그리는 것에만, 커스텀 훅에서는 비즈니스 로직만 다룰 수 있게하여 유지보수에 도움이 될 수 있다.
캡슐화
객체 지향의 캡슐화처럼 커스텀 훅을 사용할 수 있다. 예를 들어 모달의 토글을 관리하기 위한 다음 코드가 있다.
const [isLight, setIsLight] = useState(false)
const onToggle = () => {
setLight(prev=>!prev)
}
위 처럼 사용할 수 있지만 아래 코드처럼 커스텀 훅으로 분리해낼 수도 있다.
export const useLight = () => {
const [isLight, setIsLight] = useState(false);
const turnOnLight = () => {
setIsLight(true);
}
const turnOffLight = () => {
setIsLight(false);
}
const toggleLight = () => {
setIsLight(prev => !prev);
}
return { turnOnLight, turnOffLight, toggleLight};
};
위 코드는 다음과 같은 이점을 가진다.
- 외부에서 조작 가능한 interface 만 남기고 내부 구현을 숨겨 이를 통해 데이터를 예측 가능한 방법으로 컨트롤이 되도록 한다. 결국에 state 를 바꾸는 함수는 turnOnLight, turnOffLight, toggleLight 같은 함수에 의해서만 state가 제어되기 때문이다.
- 다른 로직에서 커스텀 훅을 재사용 할 수 있다.
- 내부 로직을 수정해도 인터페이스를 호출하는 로직에 영향을 끼치지 않는다.
Hook Flow 이해하기
리액트의 렌더링
Hook Flow 를 이해하기에 앞서 리액트의 렌더링 트리거는 다음과 같다.
- 내부 상태값(state) 변경
- 부모가 전해준 속성(props) 변경
- 중앙 상태값(redux store 등) 변경
- 부모 컴포넌트가 다시 렌더링 되는 경우, 자식 컴포넌트도 다시 렌더링된다.
위의 샌드박스 코드를 살펴보자. 여기서 눈여겨 볼점은 UI 의 렌더와 함께 useState 가 실행 된 후 useEffect 가 실행된다는 점이다.
그렇다면 자식 요소가 있을 때는 어떠한 Hook Flow를 가질까?
// 첫 렌더링
!======= App render start =======!
App useState
======= App render end =======
App useEffect: No DEPS
App useEffect: EMPTY DEPS
App useEffect: [show]
// 부모의 show 상태가 변경되어 자식 컴포넌트가 렌더링 된다.
!======= App render start =======!
======= App render end =======
!======= Child render start =======!
Child useState
======= Child render end =======
Child useEffect: No DEPS
Child useEffect: EMPTY DEPS
Child useEffect: [text]
App useEffect: No DEPS
App useEffect: [show]
// show 상태가 변경되어 자식 컴포넌트가 제거됨
!======= App render start =======!
======= App render end =======
App useEffect: No DEPS
App useEffect: [show]
위 코드에서 요점은 다음과 같다. 부모의 useEffect 는 자식의 useEffect 후에 실행된다.
- 부모의 렌더링과 useState 가 실행된다.
- 자식의 렌더링과 useState 가 실행된다.
- 자식의 useEffect 가 실행된다.
- 부모의 useEffect 가 실행된다.
이번에는 useEffect 의 cleanUp 이 있을 경우에 어떠한 Hook Flow가 발생하는지 알아보자.
// 처음 렌더링
!======= App render start =======!
App useState
======= App render end =======
App useEffect: No DEPS
App useEffect: EMPTY DEPS
App useEffect: [show]
// show 상태 변경 , 자식 컴포넌트 mount
!======= App render start =======!
======= App render end =======
!======= Child render start =======!
Child useState
======= Child render end =======
[CLEAN UP] App useEffect: No DEPS
[CLEAN UP] App useEffect: [show]
Child useEffect: No DEPS
Child useEffect: Empty DEPS
Child useEffect: [text]
App useEffect: No DEPS
App useEffect: [show]
// show 상태 변경 , 자식 컴포넌트 unmount
!======= App render start =======!
======= App render end =======
[CLEAN UP] Child useEffect: No DEPS
[CLEAN UP] Child useEffect: Empty DEPS
[CLEAN UP] Child useEffect: [text]
[CLEAN UP] App useEffect: No DEPS
[CLEAN UP] App useEffect: [show]
App useEffect: No DEPS
App useEffect: [show]
위 코드의 요점은 다음과 같다. cleanUp 은 첫 렌더링시에는 발생하지 않는다. 그리고 후에는 useEffect 보다 앞에 발생한다.
- 부모의 렌더링과 useState 가 실행된다.
- 자식의 렌더링과 useState 가 실행된다.
- 자식의 useEffect cleanUp 이 실행된다. (첫번째 렌더링이 아닐 경우)
- 부모의 useEffect cleanUp 이 실행된다. (첫번째 렌더링이 아닐 경우)
- 자식의 useEffect 가 실행된다.
- 부모의 useEffect 가 실행된다.
cleanUp은 컴포넌트가 unmount 될때 처리해줘야하는 작업을 할때 유용하게 사용된다. 소켓 연결을 끊는다거나 unsubscribe 등에 사용된다.
조금만 더 확인해보자.
- 부모의 state가 변경될 때
- 자식의 state만 변경될 때
- 자식의 state만 변경될 때의 경우에 useLayoutEffect를 포함시킨 case
자식의 state만 변경될 때는 다음과 같은 Hook Flow 를 가진다.
// 렌더링, 부모와 자식 모두 렌더링 된다.
!======= App render start =======!
App useState
======= App render end =======
!======= Child render start =======!
Child useState
======= Child render end =======
Child useEffect: No DEPS
Child useEffect: Empty DEPS
Child useEffect: [text]
App useEffect: No DEPS
App useEffect: EMPTY DEPS
App useEffect: [show]
// 자식의 text state 가 변경되어 자식 컴포넌트가 렌더링 된다.
!======= Child render start =======!
======= Child render end =======
[CLEAN UP] Child useEffect: No DEPS
[CLEAN UP] Child useEffect: [text]
Child useEffect: No DEPS
Child useEffect: [text]
// 자식의 text state 가 변경되어 자식 컴포넌트가 렌더링 된다.
!======= Child render start =======!
======= Child render end =======
[CLEAN UP] Child useEffect: No DEPS
[CLEAN UP] Child useEffect: [text]
Child useEffect: No DEPS
Child useEffect: [text]
자식의 state만 변경되므로 자식만 다시 렌더링이 된다.
부모의 state가 바뀔 때는 어떤 Hook Flow를 가질까?
// 첫 렌더링
!======= App render start =======!
App useState
App text useState
======= App render end =======
!======= Child render start =======!
======= Child render end =======
Child useEffect: No DEPS
Child useEffect: Empty DEPS
App useEffect: No DEPS
App useEffect: EMPTY DEPS
App useEffect: [text]
// 부모의 state 변경
!======= App render start =======!
======= App render end =======
!======= Child render start =======!
======= Child render end =======
[CLEAN UP] Child useEffect: No DEPS
[CLEAN UP] App useEffect: No DEPS
[CLEAN UP] App useEffect: [text]
Child useEffect: No DEPS
App useEffect: No DEPS
App useEffect: [text]
// 부모의 state 변경
!======= App render start =======!
======= App render end =======
!======= Child render start =======!
======= Child render end =======
[CLEAN UP] Child useEffect: No DEPS
[CLEAN UP] App useEffect: No DEPS
[CLEAN UP] App useEffect: [text]
Child useEffect: No DEPS
App useEffect: No DEPS
App useEffect: [text]
부모가 다시 렌더링 될때에 자식도 다시 렌더링이 된다.
이제 useLayoutEffect 이 포함되어 있는 경우에는 어떻게 될지 알아보기전에, useLayoutEffect가 무엇인지, 언제 사용해야할지에 대해서 알아보자.
리액트의 렌더링은 다음 두 단계로 나뉜다.
- Render phase : 변경사항을 감지하여 Virtual DOM을 만드는 단계
- Commit phase : Virtual DOM 을 real DOM에 반영하는 단계
내가 생각하는 리액트를 사용했을 때 브라우저 렌더링 플로우다. (정확하지 않다.)
useLayoutEffect 는 Commit Phase 이후에 실제 DOM 에 변경사항이 반영되고 Paint 이전에 동기적으로 실행된다. 동기적으로 실행된다는 특성 때문에 비용이 비싼 연산이 수행될 경우 Paint 가 발생하지 않아 흰화면 밖에 나오지 않는다.
그래서 보통 Paint 이전에 DOM 크기를 읽어야 된다던가, useEffect 를 사용했을 때 깜빡이는게 거슬릴때 사용하는 것 같다. useEffect도 가끔 Paint 이전에 실행될 수 있다고 듣고 그에 관한 자료도 봤긴했지만, 아직 어디에 쓰일지는 모르겠어서 언급만 한다.
그래서 정리하면 useLayoutEffect 는 Paint 전에 발생하고 , useEffect 는 Paint 이후에 발생한다. 따라서 DOM 을 건들어야한다면, useEffect 보다는 useLayoutEffect에서 다루는 게 나을 수 있다. useEffect 에서 DOM 고치면 다시 paint 해야하니까.
이제 useLayEffect 에 대해서 간단하게 알아봤으니 이것이 있을 때는 어떤 Hook Flow 를 가질지 알아보자.
See the Pen Untitled by ehddud1006 (@ehddud1006-the-looper) on CodePen.
// 첫 렌더링
!======= App render start =======!
App useState
======= App render end =======
!======= Child render start =======!
======= Child render end =======
Child useLayEffect: No DEPS
Child useLayEffect: Empty DEPS
App useLayEffect: No DEPS
App useLayEffect: Empty DEPS
Child useEffect: No DEPS
Child useEffect: Empty DEPS
App useEffect: No DEPS
App useEffect: EMPTY DEPS
App useEffect: [show]
// 부모의 text state 변경
!======= App render start =======!
======= App render end =======
!======= Child render start =======!
======= Child render end =======
[CLEAN UP] Child useLayEffect: No DEPS
[CLEAN UP] App useLayEffect: No DEPS
Child useLayEffect: No DEPS
App useLayEffect: No DEPS
[CLEAN UP] Child useEffect: No DEPS
[CLEAN UP] App useEffect: No DEPS
Child useEffect: No DEPS
App useEffect: No DEPS
// 부모의 text state 변경
!======= App render start =======!
======= App render end =======
!======= Child render start =======!
======= Child render end =======
[CLEAN UP] Child useLayEffect: No DEPS
[CLEAN UP] App useLayEffect: No DEPS
Child useLayEffect: No DEPS
App useLayEffect: No DEPS
[CLEAN UP] Child useEffect: No DEPS
[CLEAN UP] App useEffect: No DEPS
Child useEffect: No DEPS
App useEffect: No DEPS
위 코드는 다음과 같은 플로우를 가진다.
- 부모의 렌더링과 useState 가 실행된다.
- 자식의 렌더링과 useState 가 실행된다.
- 자식의 useLayEffect cleanUp 이 실행된다. (첫번째 렌더링이 아닐 경우)
- 부모의 useLayEffect cleanUp 이 실행된다. (첫번째 렌더링이 아닐 경우)
- 자식의 useLayoutEffect 가 실행된다.
- 부모의 useLayoutEffect 가 실행된다.
- 자식의 useEffect cleanUp 이 실행된다. (첫번째 렌더링이 아닐 경우)
- 부모의 useEffect cleanUp 이 실행된다. (첫번째 렌더링이 아닐 경우)
- 자식의 useEffect 가 실행된다.
- 부모의 useEffect 가 실행된다.
useEffect는 deps 에 있는 상태 또는 함수가 변경될 때마다 실행된다. 또 처음 렌더링될때 한번은 실행된다. 만약에 이렇게 처음 렌더링될때 실행되지 않게 하는 방법이 있을까?
그렇다. 있으니까 말했다. 다음 패턴을 사용하면 된다.
const mounted = useRef(false);
useEffect(()=> {
if (!mounted.current){
mounted.current = true;
} else {
}
},[]);
후.. 이제 거의 다왔다. 이제 Hook Flow 진짜 마지막 문제를 내도록 하겠다.
컴포넌트의 구조는 다음과 같다. App > OuterBox > InnerBox
export default function App() {
return (
<div className="App">
<h1>useEffect 순서 테스트</h1>
<OuterBox />
</div>
);
}
const OuterBox: FC = () => {
return (
<>
<h2>Outer BOX</h2>
<InnerBox />
</>
);
};
const InnerBox: FC = () => {
return <h2>Inner Box</h2>
};
다음과 같은 useEffect 있을때 실행순서는 어떻게 될까?
function App() {
useEffect(() => {
console.log(1);
}, []);
return ...
}
const OuterBox: FC = () => {
useEffect(() => {
console.log(2);
}, []);
return ...
};
const InnerBox: FC = () => {
useEffect(() => {
console.log(3);
}, []);
return ...
};
3 -> 2 -> 1 이다.
useEffect .. Suspense 가 있을 때는 어떨까? 머리가 아파진다.
function App() {
useEffect(() => {
console.log(1);
}, []);
return ...
}
const OuterBox: FC = () => {
useEffect(() => {
console.log(2);
}, []);
return (
<>
<h2>Outer BOX</h2>
<Suspense fallback={<div>loading...</div>}>
<InnerBox />
</Suspense>
</>
);
};
const InnerBox: FC = () => {
//깃허브 stars 수를 갖고오는 비동기 요청
const repoStars = useRecoilValue(getStars);
useEffect(() => {
console.log(3);
}, []);
return ...
};
2 -> 1 -> 3 의 순서를 가진다.
useEffect는 컴포넌트 렌더링이 완료가 되면 실행이 된다. InnerBox는 Suspense 에게 렌더링을 interrupt 당하고 있다. 따라서 OuterBox , App 이 먼저 실행되고 비동기 요청이 완료된 시점에 InnerBox가 렌더링 되어 3이 출력된다.
결국 기억할 것은 useEffect는 컴포넌트의 렌더링이 끝나면 실행된다는 것이다.
만약 방금전 InnerBox가 이렇게 생겼다면 어떨까?
const InnerBox: FC = () => {
console.log(5);
// 비동기 요청
const repoStars = useRecoilValue(getStars);
console.log(4);
useEffect(() => {
console.log(3);
});
...
};
5 -> 2 -> 1 -> 4 -> 3 의 순서를 가진다.
이 결과로 인해 Suspense 는 비동기 요청을 만나는 그 순가 interrupt 해 간다는 것을 알 수 있다.
InnerBox 내부에서 로딩처리를 했을 때는 어떨까?
const InnerBox: FC = () => {
const repoStarsLodable = useRecoilValueLoadable(getStars);
console.log(4);
useEffect(() => {
console.log(3);
});
return (
<>
{repoStarsLodable.state === "loading" && <div>loading...</div>}
{repoStarsLodable.state === "hasValue" && (
<>
<h2>Inner Box</h2>
<h3>내 레포 star 개수는 {repoStarsLodable.contents}</h3>
</>
)}
</>
);
};
//실행결과
4 //
3 // loading...일 때 출력되는 로그
2
1
4 //
3 // 로딩 후 출력되는 로그
이제 Hook Flow 는 많이 살펴본 것 같다. 이제 위에 나온 Suspense가 궁금해지지 않나?
Suspense
아래와 같이 Suspense 를 사용했을 때 이점은 무엇일까?
const OuterBox: FC = () => {
useEffect(() => {
console.log(2);
}, []);
return (
<>
<h2>Outer BOX</h2>
<Suspense fallback={<div>loading...</div>}>
<InnerBox />
</Suspense>
</>
);
};
이것외에 Error Boudary와 함께 Suspense 를 사용하면 선언적 프로그래밍이 가능해진다. 프론트엔드 개발자는 컴포넌트에서 사용할 데이터가 정상적으로 로드되었을 때의 UI만 고려하면 되고, 로딩 중이나 에러의 상태에서의 UI는 위임해버리면 되기 때문이다. (하나의 컴포넌트에서 각각의 상태에 대한 관리를 할 필요가 없어진다.)
Suspense를 구현한 원리는 Promise를 Throw 하는 것이다. promise가 throw 되면 이 promise가 resolve되기 전까지 Fallback UI를 보여주고 resolve되면 hidden 상태였던 Child Component를 보여준다.
리액트 Element에 스타일 입히기
다른 속성들과 같이 props에 style을 넘겨줌으로써 요소에 스타일을 적용시킬 수 있다.
function Button({ className = "", color, style, ...rest }) {
return (
<button
className={`button ${className} ${color}`}
style=
{...rest}
/>
);
}
const element = (
<>
<Button stlye=>Green</Button>
<Button color="blue" stlye=>
Blue
</Button>
<Button color="red">Red</Button>
</>
);
Ref로 DOM 다루기
바닐라 JS에서 document.get~ 이나 document.query~ 를 이용하여 DOM에 접근하는 것 처럼 리액트에서도 useRef를 이용하여 돔에 접근하고 다룰 수 있다.
const rootElement = document.getElementById("root");
const App = () => {
const inputRef = React.useRef();
const divRef = React.useRef();
React.useEffect(() => {
inputRef.current.focus();
setTimeout(() => {
divRef.current.style.backgroundColor = "pink";
}, 1000);
});
return (
<>
<input ref={inputRef} />
<div
ref={divRef}
style=
/>
</>
);
};
ReactDOM.render(<App />, rootElement);
useRef 의 다른 용도
useRef는 DOM 엘리먼트에 접근하기 위해 주로 사용한다. 이외에 변수 처럼 사용할 수도 있다.
useRef 로 선언한 값이 변경될 때는 state 처럼 렌더링이 발생하지 않기 때문에 때로 유용하게 쓰일 수 있다. 또한 변수안에 함수를 할당하는등 다양한 용도로 사용이 가능하다.
또한 최신값을 추적하는데 사용할 수도 있다. 다음 두가지 예시를 보자. input 을 통해 텍스트를 입력한 후 send 를 클릭하면 setTimeout에 설정된 시간 이후에 alert 가 발생한다. 이때 함수 컴포넌트는 매 렌더링마다 마치 스냅샷처럼 각자의 state와 prop를 가진다. (클로저로 인하여) (실은 state와 prop 외에 이벤트핸들러, useEffect 모두 고유하게 가진다.) 따라서 두번째 발생한 state 값에 의해 첫번째 발생한 alert 값의 state가 변경되지 않는다. 독립적이라고 할 수 있다.
하지만 만약에 최신값을 참조해야한다면 이러한 클로저의 특성때문에 해답을 찾기 어려울 수 있다.
조금 더 프로젝트에 사용될 만한 유용한 예제를 찾고 싶었으나 식견이 짧다. ㅠㅠ
이때 useRef 는 유용한 대안이 될 수있다.
렌더링이 발생할 때마다 사이드 이펙트가 발생하여 useRef 는 최신값을 추적하게 된다. 따라서 위 예제에 두번의 연속한 send가 발생할 경우에는 첫번째 alert 에 두번째 send 에 입력한 값이 출력되게 된다. 따라서 ref 를 통해 렌더링의 일관성을 극복할 수 있게된다.
많이 사용하지는 않겠지만, 알아둬서 나쁠 건 없다.
Form 다루기
- onSubmit( )의 기본 이벤트는 event.preventDefault( )로 막을 수 있다.
- event.target.elements 는 console.dir 를 사용하여 확인할 수 있다. (객체는 주로 console.dir을 사용)
const rootElement = document.getElementById("root");
const App = () => {
const handleSubmit = (event) => {
event.preventDefault();
console.dir(event.target.elements);
alert(
`FirstName: ${event.target.elements.fname.value}, LastName: ${event.target.elements.lname.value}`
);
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="fname">First name:</label>
<br />
<input type="text" id="fname" name="fname" defaultValue="John" />
<br />
<label htmlFor="lname">Last name:</label>
<br />
<input type="text" id="lname" name="lname" defaultValue="Doe" />
<br />
<br />
<input type="submit" value="Submit" />
</form>
);
};
ReactDOM.render(<App />, rootElement);
- validation: onChange를 통해 입력을 확인하며 유효성 검사를 한다.
- input의 value 를 state로 관리한다.
const rootElement = document.getElementById("root");
const App = () => {
const [phoneNumber, setPhoneNumber] = React.useState("");
const [message, setMessage] = React.useState("");
const handleSubmit = (event) => {
event.preventDefault();
alert(phoneNumber);
};
const handleChange = (event) => {
if (event.target.value.startsWith(0)) {
setMessage("Phone Number is valid");
setPhoneNumber(event.target.value);
} else if (event.target.value.length === 0) {
setPhoneNumber("");
setMessage("");
} else {
setMessage("Phone Number should starts with 0");
}
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="phone">Phone Number: </label>
<br />
<input
id="phone"
name="phone"
onChange={handleChange}
value={phoneNumber}
/>
<p>{message}</p>
<br />
<br />
<button
type="submit"
disabled={
phoneNumber.length === 0 || message !== "Phone Number is valid"
}
>
Submit
</button>
</form>
);
};
ReactDOM.render(<App />, rootElement);
Error 다루기
ErrorBoundary를 통해 컴포넌트에서 에러가 발생했을 때 이를 캐치하여 리포팅하고 사용자들에게 에러가 발생하여 앱이 중단되는 것이 아닌 대체 화면을 보여줄 수 있다.
const rootElement = document.getElementById("root");
class ErrorBoundary extends React.Component {
state = { error: null };
static getDerivedStateFromError(error) {
// 에러바운더리의 state를 변경하는데 사용합니다.
return { error };
}
componentDidCatch(error, errorInfo) {
// 에러 리포팅 서비스에 에러를 기록할 수도 있습니다.
// 보통 에러메세지를 사용자에게 렌더할 경우에 사용하는 것 같습니다.
logErrorToMyService(error, errorInfo);
}
render() {
const { error } = this.state;
if (error) {
return <this.props.fallback error={error} />;
}
return this.props.children;
}
}
const Fallback = () => {
alert(error.message);
return <p>There is some ERROR...</p>;
};
const Child = () => {
throw new Error("Something Wrong....");
return <p>Child...</p>;
};
const App = () => {
return (
<>
<p>App</p>
<ErrorBoundary fallback={Fallback}>
<Child />
</ErrorBoundary>
</>
);
};
ReactDOM.render(<App />, rootElement);
상태 끌어올리기
상태 내려주기가 더 맞지 않나? 라는 생각이 드는 용어다.
부모의 state 나 handler 를 자식에게 물려주어 자식 컴포넌트에서 사용하는 것을 의미한다.
과도한 상태 끌어올리기는 props drilling을 발생시키기 때문에 과하게 깊어지면 context api 나 전역상태관리를 고려해볼 수 있다.
const rootElement = document.getElementById("root");
const Id = ({ handleIdChange }) => {
return (
<>
<label>ID: </label>
<input onChange={handleIdChange} />
</>
);
};
const Password = ({ handlePasswordChange }) => {
return (
<>
<label>PW: </label>
<input type="password" onChange={handlePasswordChange} />
</>
);
};
const App = () => {
const [id, setId] = React.useState("");
const [password, setPassword] = React.useState("");
const handleIdChange = (event) => {
setId(event.target.value);
};
const handlePasswordChange = (event) => {
setPassword(event.target.value);
};
const handleLoginClick = () => {
alert(`id: ${id}, pw: ${password}`);
};
return (
<>
<Id handleIdChange={handleIdChange} />
<br />
<Password handlePasswordChange={handlePasswordChange} />
<button
disabled={id.length === 0 || password.length === 0}
onClick={handleLoginClick}
>
LOGIN
</button>
</>
);
};
ReactDOM.render(<App />, rootElement);
데이터 fetch
const rootElement = document.getElementById("root");
const App = () => {
const [data, setDate] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
fetch("https://example.com")
.then(function (response) {
return response.json();
})
.then(function (myJson) {
setDate(myJson);
})
.catch((error) => setError(error.message));
}, []);
if (error != null) {
return <p>{error}</p>;
}
if (data == null) {
return <p>Loading...</p>;
}
return (
<div>
<p>People</p>
{data.people.map((person) => {
<div>
<span>name: {person.name} </span>
<span>age: {person.age}</span>
</div>;
})}
</div>
);
};
ReactDOM.render(<App />, rootElement);
리액트에서 불변성을 지켜야 하는 이유
불변성이란 메모리영역에서 값이 변하지 않는다 라는 의미를 가지고 있다. 자세한 설명은 이 글의 불변성 & 가변성 파트를 참고하길 바란다.
리액트는 불변성을 지켜야한다. 라는 말은 리액트를 사용한 사람이라면 한번 씩 들어봤을 말이다. 그 이유는 리액트가 상태 업데이트를 하는 원리 때문이다. 리액트는 상태값을 업데이트할 때 얕은 비교를 수행한다. 즉 배열이나 객체의 속성을 하나하나 비교하는 것이 아닌, 이전 참조값과 현재 참조값만을 비교하여 상태 변화를 감지한다.
불변성을 지킴으로써 얻게 되는 또 다른 이점은 사이드 이펙트를 방지하는 것이다. 즉 외부에 존재하는 원본데이터를 직접 수정하지 않고, 원본 데이터의 복사본을 만들어서 값을 사용하기 때문에 원본데이터를 참조하고 있는 다른 함수에서 사이드 이펙트가 발생할 가능성을 낮춘다.
어떻게 불변성을 지키야하나?
스프레드 연산자, map, filter, slice, reduce 등 새로운 배열을 반환하는 메서드를 활용한다.
리액트의 setState를 사용할 때에 원시타입은 값을 그대로 넣어주어도 괜찮지만, 참조타입인 경우에는 새로운 객체나 배열을 생성하고 값을 넣어주어야한다.
// 원시타입
const [number, setNumber] = useState(0)
setState(3)
// 참조타입
const [person, setPerson] = useState({ name: '', age: 30 })
setState({...person, name: 'pyo'})
불변성을 지키지 않았을 경우
useEffect의 deps 비교 연산도 얕은 비교를 수행한다.
import { useEffect, useState } from "react";
export default function App() {
const [todos, setTodos] = useState([
{ text: "hi1", completed: false },
{ text: "hi2", completed: false },
{ text: "hi3", completed: false }
]);
const click = () => {
todos[2].completed = true; // 불변성 X
setTodos(todos);
// const newTodos = [...todos]; // 불변성 O
// todos[2].completed = true;
// setTodos(newTodos);
};
useEffect(() => {
console.log("변했다.");
}, [todos]);
return (
<>
<button onClick={click}>click</button>
</>
);
}
위 코드와 같이 불변성을 지키지 않은 경우 click을 눌러도 useEffect가 변화를 감지하지 못해 useEffect가 동작하지 않는다.
setState도 마찬가지다. 참조 타입의 불변성을 지켜주지 않는다면 렌더링이 발생하지 않아서 데이터를 변경을 화면에 보여줄 수 없다.
클래스 컴포넌트에서 함수 컴포넌트를 사용하게 된 이유
다음 예제를 보면 첫번째 follow 를 클릭하였을 때 발생하는 alert 가 페이지를 이동하면 주체가 Dan -> Sophie 로 바뀌는 것을 볼 수 있다. (원래 원하는 결과는 Follwed Dan 이다.)
왜 이런 현상이 발생했을까? 그것은 클래스의 this 때문이다. 페이지 이동으 인해 this.prop에 대한 정보가 Dan 에서 User 로 바뀌었기 때문에 의도하지 않는 결과를 초래한다.
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
그래서 리액트 개발진들은 이것에 대한 해결방법으로 자바스크립트의 클로저 개념을 사용하였다. 다음 처럼 render 함수안에 렌더링시에 this.props를 저장해둔 것이다.
class ProfilePage extends React.Component {
render() {
// props의 값을 고정!
const props = this.props;
// Note: 여긴 *render 안에* 존재하는 곳이다!
// 클래스의 메서드가 아닌 render의 메서드
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
}
그런데 이렇게되다보니 결국에는 모든 상태와 함수가 render 스코프 안에 들어가버리는 현상이 발생하였고, 이러한 render 라는 껍질을 벗기기위해 함수 컴포넌트로 점점 변화하게 되었다.
이것도 중요하진 않다. 그냥 재밌어서 써봤다.
useCallback, useMemo, memo 언제 써야할까?
useMemo
리렌더링시에 계산 결과를 캐싱할 수 있는 리액트 hook 이다. 성능을 최적화하기 사용하고, 비싼 연산에 사용하라고 알려져왔다. 비싼 연산이란 뭘까? 주관적이다.
최근 리뉴얼된 공식문서는 나의 가려운 부분을 긁어주었다. 1초 이상이 걸리는 연산을 비싼 연산이라고 한다. 이에는 useMemo 를 적용하라고 되어있다.
console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');
useCallback
useCallback은 함수를 캐싱하는 훅이다. 여기서 주의점은 useCallback으로 함수를 감싼다고 해서 함수를 재생성하지 않는것이 아니라는 것이다. useCallback은 함수를 재생성해놓고 그것을 쓸지 말지 결정하는 메모이제이션이다.
그럼 언제 사용해야할까?
- useEffect의 의존성에 들어가야할 때
- memo 로 감싼 컴포넌트의 props 으로 들어갈 때
위 코드를 보게되면 text 의 state가 변경될 때마다 렌더링이 발생한다. 그때마다 Func 함수가 재생성되기 때문에 useEffect가 실행된다. 리액트는 얕은 비교를 하기 때문에 참조값을 비교하게 된다. 함수의 내용은 같지만 다른 함수로 판단한다.
이러한 현상을 막을 때 useCallback을 사용해야한다. 첫번째 케이스에 해당한다. 다음 코드는 useCallback 을 적용한 코드다. text 를 입력할 때 첫번째 샌드박스의 콘솔과 다른 것을 알 수 있을 것이다.
또한 props로 내려줄 때도 useCallback을 사용하면 불필요한 리렌더링을 제거할 수 있다. memo 로 감싼 컴포넌트의 prop으로 넘겨줄 때도 똑같은 이유이다. memo는 props 가 바뀌는가에 따라 메모이징된 렌더링 결과를 재사용할까 말까를 결정하기 때문이다.
memo
memo는 props 가 바뀌는가에 따라 메모이징된 렌더링 결과를 사용하거나 재생성한다. 다음은 memo를 사용하기에 적합한 예시이다.
// Initial render
<MovieViewsRealtime
views={0}
title="Forrest Gump"
releaseDate="June 23, 1994"
/>
// After 1 second, views is 10
<MovieViewsRealtime
views={10}
title="Forrest Gump"
releaseDate="June 23, 1994"
/>
// After 2 seconds, views is 25
<MovieViewsRealtime
views={25}
title="Forrest Gump"
releaseDate="June 23, 1994"
/>
// etc
만약 view 만 값이 바뀌고 title, releaseDate 는 그대로라면 어떻게 할까? 다음처럼 자주 바뀌는 view와 releaseDate, views 를 구분하여 컴포넌트를 만들 수 있다. 자주 바뀌는 view에는 memo 를 적용하나마나니 그대로 두고 releaseDate, views 에는 memo 를 적용하는 방식이다.
function MovieViewsRealtime({ title, releaseDate, views }) {
return (
<div>
<MemoizedMovie title={title} releaseDate={releaseDate} />
Movie views: {views}
</div>
);
}
컴포넌트를 나뉠때 자주 쓰이는 props 과 그렇지 않은 props 으로 나누어 memo 를 적용하는 것을 고려해볼 수 있다.
'카카오 테크 캠퍼스 > HTML CSS JS' 카테고리의 다른 글
[카테캠 7주차] JS 기초 모듈 (0) | 2023.05.29 |
---|---|
[카테캠 7주차] JS 기초 Events (0) | 2023.05.28 |
[카테캠 7주차] JS 기초 DOM (0) | 2023.05.28 |
[카테캠 7주차] JS 기초 비동기 (0) | 2023.05.28 |
[카테캠 6주차] 자바스크립트 기초 (1) | 2023.05.21 |