The History of Frontend Development and React Troubleshooting
How did React come into existence, and how is it evolving?
1. History π©βπ»
- μΈν°λ· μΉμνμ μ΄μ°½κΈ°μ λΈλΌμ°μ λ μΉ νμ€μ΄ μ λλ‘ μ μλμ§ μμ ν¬λ‘μ€ λΈλΌμ°μ§ μ΄μκ° κ°λ°μ κ°μ₯ ν° λΆλΆ μ€ νλμλ€.
- jQuery μ λ±μ₯μΌλ‘ ν¬λ‘μ€ λΈλΌμ°μ§ μ΄μλ₯Ό ν΄κ²°ν¨μ λ¬Όλ‘ , λ°λ³΅μ μΈ UI μ²λ¦¬ μμ μ λΌμ΄λΈλ¬λ¦¬λ₯Ό ν΅ν΄ μ²λ¦¬ν μ μκ² λμλ€.
- μΉμ νΉμ±μ μμλ‘ μλ²μ λ°μ΄ν°λ₯Ό λ°μ μ¬λ λλ§μ ν΄μ€μΌνλ€. μ΄λ₯Ό μλμΌλ‘ μ²λ¦¬νκΈ° μν΄ Angular.JS μ Backbone.JS λ
λ°μ΄ν° λ°μΈλ©
μ λμ νκ³ , μ¬μ¬μ©μ μν΄μ»΄ν¬λνΈ λ¨μ κ°λ°
μ λμ νλ€. - μλ°©ν₯ λ°μ΄ν° λ°μΈλ©μΌλ‘ μΈν λΈλΌμ°μ μ μμ μλͺ¨ λ° νλ μμν¬λ³΄λ€
κ°λ²Όμ΄ λΌμ΄λΈλ¬λ¦¬
λ₯Ό λͺ©νλ‘λ¨λ°©ν₯ λ°μ΄ν° λ°μΈ
λ©μ΄ κ°λ₯ν React κ° μ겨λ¬λ€. λ λλ§μ μ±λ₯ μ νκ° ν¬λVirtual DOM
μ μ¬μ©ν΄ λΉκ΅νκ³ ν λ²μ λ λλ§νλλ‘ ν΄ μ»΄ν¬λνΈ λ¨μ κ°λ°μ λΈλΌμ°μ§ μ±λ₯μ λμλ€. 리μ‘νΈμ μν₯μΌλ‘ Angular.JS λ λ²μ 2λ‘ λμ΄μ€λ©° Angular λΌλ μ΄λ¦μΌλ‘ μ¬νμνκ³ , Angular μ νλ μμν¬μ μ₯μ κ³Ό React μ μ₯μ μ λͺ¨λ κ°λ λΌμ΄λΈλ¬λ¦¬μ κ°λ°μ λͺ©νλ‘ Vue.JS κ° μ겨λ¬λ€. - Svelte μ κ°μ λΌμ΄λΈλ¬λ¦¬λ λ€μ μ»΄ν¨ν μ±λ₯ μν₯ νμ€νλ‘ μΈν΄ Virtual DOM μ μ°λκ² μ€νλ € λ릴 μ μμΌλ©°, κΈΈκ³ λ³΅μ‘ν λ¬Έλ²λ€μ μ΅λν κ°μν νκ³ μ νλ λͺ©μ μΌλ‘ μ겨λ¬λ€.
νμ§λ§ μ΅κ·Ό νΈλ λλ₯Ό 보면 μ¬μ ν React λ μλμ μΈ μ μ μ¨μ 보μ¬μ€κ³Ό λμμ μ μΌνκ² κΎΈμ€ν μμΉμ νκ³ μμμ μ μ μλ€. μ§κΈκΉμ§, κ·Έλ¦¬κ³ μμΌλ‘λ κ°κΉμ΄ λ―Έλμλ μ¬μ ν React κ° μΉμλΌλ κ²μ μ μ μλ€.
2. Limitation of Class Components π©βπ»
ν΄λμ€ μ»΄ν¬λνΈμ μ£Όμ λ¬Έμ μ μ μ 리νλ©΄ λ€μκ³Ό κ°λ€.
- Boilerplate Code : λ€λ₯Έ μΈμ΄μ κ°μ Class κ° μ€μ λ‘λ JavaScript μ μ‘΄μ¬νμ§ μκΈ° λλ¬Έμ μ΄λ₯Ό ν΅ν΄ ꡬνν ν΄λμ€ μ»΄ν¬λνΈ μμ λ§μ Boilerplate Code λ₯Ό νμλ‘ νλ€.
this
λ°μΈλ© : JavaScript μ νΉμ΄νthis
λμ λ°©μμ λν μ΄ν΄λ₯Ό νμλ‘ ν¨μ λ°λΌ μλͺ»λ μ½λ©μ νλ κ²½μ°κ° λ§λ€.- λΉλκΈ° μ½λ°±μ κ° λ³μ‘° : instance μ¬νμ©μΌλ‘ μΈν λΉλκΈ° ν¨μ λ΄ props, state μ κ°μ΄ λ³μ‘°λ μ μλ€.
1. this binding of Event call
class MatchPage extends Component {
constructor(props) {
super(props);
this.state = {
currentCard: createMockCard(),
matches: [],
};
}
next() {
this.setState({
...this.state,
currentCard: createMockCard(),
});
}
like() {
this.next();
checkIsMatched().then(({ data: { isMatched } }) => {
if (isMatched) {
this.setState({
...this.state,
matches: [this.state.currentCard, ...this.state.matches],
});
}
});
}
render() {
const { state: { currentCard, matches }, next, like } = this;
const matchControllerProps = { next, like };
return (
<main style={commonStyles.flexCenter}>
<section style={pageStyles.pageWrap}>
<img src='/logo.png' alt='logo' style={pageStyles.logo} />
<MatchCard style={commonStyles.flex1} card={currentCard} />
<MatchController {...matchControllerProps} />
<MatchList matches={matches}/>
</section>
</main>
);
}
}
JavaScript βthisβ μμ μ΄ν΄λ³΄μλ―μ΄ μ΄λ²€νΈμ μν΄ νΈμΆλλ κ²½μ° this
κ° μ΄λ²€νΈ elements κ° νΈμΆνλ κ°μ²΄κ° λκΈ°
λλ¬Έμ this binding
μ΄ νμνλ€. λ°λΌμ μμ±μ ν¨μκ° Closures λ₯Ό μμ±ν΄ μκΈ° μμ μ properties κ° νμ μκΈ° μμ μ
κ°λ¦¬ν¬ μ μλλ‘ bind
λ₯Ό μ¬μ©ν΄ μꡬμ μΌλ‘ λ°μΈλ© λλλ‘ constructor
μ μ μν΄μ€λ€.
class MatchPage extends Component {
constructor(props) {
super(props);
this.state = {
currentCard: createMockCard(),
matches: [],
};
this.next = this.next.bind(this)
this.like = this.like.bind(this)
}
//...
}
λ€λ₯Έ λ°©λ²μΌλ‘λ Lexical Scope
λ₯Ό κ°λ Arrow Functions
λ₯Ό μ¬μ©νλ κ²μ΄λ€.
class MatchPage extends Component {
//...
next = () => {
this.setState({
...this.state,
currentCard: createMockCard(),
});
}
like = () => {
this.next();
checkIsMatched().then(({ data: { isMatched } }) => {
if (isMatched) {
this.setState({
...this.state,
matches: [this.state.currentCard, ...this.state.matches],
});
}
});
}
//...
}
μ΄ λ¬Έμ λ JavaScript βthisβ μμ μ΄ν΄λ³Έ κ²μ²λΌ
Object Literal
λ΄λΆμμ Arrow Functions λ₯Ό μ¬μ©νλ κ²μ λ¬Έμ κ° μλ κ²μ΄μ§Prototype
μ μ¬μ©νκ±°λES6 Class
λ¬Έλ²μ μ¬μ©ν κ²½μ° μΌμΌν΄bind()
λ₯Ό νλ κ² λ³΄λ€Arrow Functions
λ₯Ό μ¬μ©νλ κ²μ΄ μ’λ€.
2. Value Mutation in Asynchronous Callback
ν΄λμ€ μ»΄ν¬λνΈμμλ λΉλκΈ° μ½λ°±μ΄ ν¬ν¨λ κ²½μ° νμ¬ state κ°μ νμλ‘ νλλ° λΉλκΈ° μ½λ°±μ κ°μΌλ‘ state κ° λ³μ‘°λλ λ¬Έμ κ° μλ€.
μ¦, Like
λ²νΌμ λλ₯΄λ©΄ νμ¬ μ΄λ―Έμ§μ μ’μμλ₯Ό μ²λ¦¬ν λ€μ λ€μ μ¬μ§μ κ°μ ΈμμΌνλλ° λ€μ μ¬μ§μ κ°μ Έμ€λ λΉλκΈ° μ½λ°±μ΄ state λ₯Ό
λ³κ²½νκΈ° λλ¬Έμ μ’μμκ° λ³κ²½λ state κ°μ μ΄μ©ν΄ μ’μμλ₯Ό μ²λ¦¬ν΄λ²λ¦¬λ λ¬Έμ λ€. μ΄ λ¬Έμ λ₯Ό ν΄κ²°νκΈ° μν΄ λ©μλ μ μλ₯Ό render()
λ©μλ λ΄λΆλ‘ μ΄λμν¨λ€. μ°Έκ³ λ‘ render λ΄λΆμμ λ€μ bind λ₯Ό νμ§ μκΈ° μν΄ ν¨μλ Arrow Functions λ‘ μ μΈνλ€.
class MatchPage extends Component {
constructor(props) {
super(props);
this.state = {
currentCard: createMockCard(),
matches: [],
};
}
render() {
const { state: { currentCard, matches } } = this;
const next = () => {
this.setState({
...this.state,
currentCard: createMockCard(),
});
}
const like = () => {
next();
checkIsMatched().then(({ data: { isMatched } }) => {
if (isMatched) {
this.setState({
...this.state,
matches: [currentCard, ...this.state.matches],
});
}
});
}
const matchControllerProps = { next, like };
return (
<main style={commonStyles.flexCenter}>
<section style={pageStyles.pageWrap}>
<img src='/logo.png' alt='logo' style={pageStyles.logo} />
<MatchCard style={commonStyles.flex1} card={currentCard} />
<MatchController {...matchControllerProps} />
<MatchList matches={matches}/>
</section>
</main>
);
}
}
μ°Έκ³ λ‘ ν΄λμ€ μ»΄ν¬λνΈλ ν¨μ μ체κ°
render
λ€.
ν΄λμ€ μ»΄ν¬λνΈλ 리ν©ν λ§μ ν΅ν΄
render()
ν¨μ λ΄JSX
μ½λλ₯Ό Stateless νλλ‘ ν¨μν μ»΄ν¬λνΈλ‘ λΆλ¦¬μν€λ κ²μ΄ μ’λ€.class MatchPageComponent extends Component { //... render() { //... const props = { currentCard, matches, next, like }; return <MatchPage {...props} /> } } const MatchPage = ({currentCard, matches, next, like}) => ( <main style={commonStyles.flexCenter}> <section style={pageStyles.pageWrap}> <img src='/logo.png' alt='logo' style={pageStyles.logo}/> <MatchCard style={commonStyles.flex1} card={currentCard}/> <MatchController next={next} like={like}/> <MatchList matches={matches}/> </section> </main> )
3. React Hooks Make It Easy to Create Stateless Components
React Hooks λ₯Ό μ¬μ©νλ©΄ μμ κ°μ΄ state μ μ’ μλλ MatchPageComponent μ MatchPage λ₯Ό λλμ§ μμλ λλ€.
const MatchPage = () => {
const [currentCard, setCurrentCard] = useState(createMockCard())
const [matches, setMatches] = useState([])
const next = () => setCurrentCard(createMockCard())
const like = () => {
next();
checkIsMatched().then(({data: {isMatched}}) => {
if (isMatched) {
setMatches(prevState => [currentCard, ...prevState])
}
});
}
return (
<main style={commonStyles.flexCenter}>
<section style={pageStyles.pageWrap}>
<img src='/logo.png' alt='logo' style={pageStyles.logo}/>
<MatchCard style={commonStyles.flex1} card={currentCard}/>
<MatchController next={next} like={like}/>
<MatchList matches={matches}/>
</section>
</main>
)
}
3. React Hooks Action π©βπ»
ν΄λμ€ μ»΄ν¬λνΈμ νκ³λ₯Ό 극볡νλ κ³Όμ μμ ν¨μν μ»΄ν¬λνΈμ React Hooks κ° λμ
λλ©΄μ ν΄κ²°ν λ¬Έμ μ
μμ νλμ μ»΄ν¬λνΈ μ½λλ₯Ό μμ±νλ©΄μ μνκ΄λ¦¬λ₯Ό λΆλ¦¬μν€κ³ , μΆμν μν¨ useState
ν
μ μ΄ν΄λ³΄μλ€. μ΄ κ³Όμ μμ μ겨λ
React Hooks κ° μ΄λ€ λ°©μμΌλ‘ λμνλμ§ μμμΌ λ¦¬μ‘νΈ μ΅μ νλ₯Ό μ΄ν΄νκ³ μ΄μ λ°©ν΄λμ§ μλλ‘ μ½λλ₯Ό μμ±ν μ μλ€.
const ReactDOM = (function () {
let _container;
let _elementOrComponent;
function _render() {
const elementTree = React.render();
const run = (reactElement, parent) => {
parent = parent ?? document.createElement('div');
Object.entries(reactElement).forEach(([key, value]) => {
// JSX νκ·Έ
if (
key.includes('div') ||
key.includes('button') ||
key.includes('input')
) {
const _el = document.createElement(key.replace(/[0-9].?/, ''));
parent.appendChild(_el);
if (typeof value === 'object') {
run(value, _el);
}
} else if (key === 'text') {
parent.innerHTML = parent.innerHTML += value;
} else if (key === 'value') {
parent.value = value;
} else if (key === 'onClick') {
parent.addEventListener('click', value);
} else if (key === 'onChange') {
parent.addEventListener('change', value);
} else {
if (typeof value === 'function') {
// ν¨μν μ»΄ν¬λνΈμΌ κ²½μ°
run(value(), parent);
}
}
});
return parent;
};
_container.innerHTML = '';
_container.appendChild(run(elementTree, null));
}
return {
render(elementOrComponent, container) {
if (!container instanceof Element)
throw new Error('ReactDOM should be rendered on DOM Element');
_container = container;
_elementOrComponent = elementOrComponent;
React._setRenderer(ReactDOM);
React._setElementOrComponent(elementOrComponent);
_render();
},
_render,
};
})();
const React = (function () {
let _currentIndex = 0;
let _hooks = []; // νΉμ μ»΄ν¬λνΈλ₯Ό 컨ν
μ΄λλ‘ states λ€μ μ μ₯νλ λ°°μ΄μ closures λ‘ κ°λλ€.
let _renderer = null;
let _elementOrComponent = null;
return {
_setRenderer(renderer) {
_renderer = renderer;
},
_setElementOrComponent(elementOrComponent) {
_elementOrComponent = elementOrComponent;
},
render() {
let result = null;
// μ»΄ν¬λνΈ μΈμ€ν΄μ€ λλ ν¨μν μ»΄ν¬λνΈ μ€ν ...
if (typeof _elementOrComponent !== 'function') {
return _elementOrComponent;
}
const component = _elementOrComponent();
if (component.render !== undefined) {
result = component.render(); // ν΄λμ€ μ»΄ν¬λνΈλ render λ©μλλ₯Ό μ€ν
} else {
result = component; // ν¨μν μ»΄ν¬λνΈλ μ»΄ν¬λνΈ μμ²΄κ° render
}
// μ»΄ν¬λνΈλ₯Ό μλ‘ κ·Έλ¦΄ λλ§λ€ useState λ₯Ό λ€μ μννλ€.
// κ° states κ° μμλλ‘ μμ μ _hooks μ λ°°μ΄ index λ₯Ό κ°λλ‘ λ λλ§μ΄ λλλ©΄ μΈλ±μ€λ₯Ό μ΄κΈ°ν ν΄μΌνλ€.
_currentIndex = 0;
return result;
},
useState(initialValue) {
const currentIndex = _currentIndex;
_hooks[currentIndex] = _hooks[currentIndex] ?? initialValue; // μ΄κΈ°κ° μμ κ²½μ° ν λΉ
const setState = (cbOrValue) => {
if (typeof cbOrValue === 'function') {
_hooks[currentIndex] = cbOrValue(_hooks[currentIndex]);
} else {
_hooks[currentIndex] = cbOrValue;
}
// state κ° λ³κ²½λλ©΄ νμ render λ₯Ό νΈμΆνλλ‘ νλ€.
// Observable μ notify μν μ νλ©° νμ render μκ² μ리λλ‘ λ΄λΆμ μΌλ‘ μ μλ₯Ό νλ κ²μ΄λ€.
_renderer._render();
};
_currentIndex++;
return [_hooks[currentIndex], setState];
},
useEffect(cb, dependencies) {
const prevDependencies = _hooks[_currentIndex];
const isChanged =
prevDependencies &&
!dependencies.every((el, i) => el === prevDependencies[i]);
// μ΅μ΄ λ λλ§ || states κ° λ³ν κ²½μ°
if (isChanged || !prevDependencies) {
cb();
// states λ₯Ό λͺ¨λ _hooks μ μ μ₯ν ν λ§μ§λ§ μΈλ±μ€μ states λ₯Ό λ΄μ dependency λ°°μ΄μ μ μ₯νλ€.
_hooks[_currentIndex] = dependencies;
}
_currentIndex++; // useState μ λ§μ°¬κ°μ§λ‘ useEffect λ μ¬λ¬ κ° μ μΈν μ μμΌλ index λ₯Ό μ¦κ°μν¨λ€.
},
};
})();
const Greeting = () => ({
div: {
text: 'Hello World!!!!!!!!!!',
},
});
const App = () => {
const [foo, setFoo] = React.useState(0);
const [bar, setBar] = React.useState(0);
const [baz, setBaz] = React.useState('');
React.useEffect(() => {
console.log('something changed!!?!?', foo, bar, baz);
}, [foo, baz]);
/*
useEffect κ° states foo, baz λ₯Ό λ°°μ΄λ‘ κ°λ [foo, baz] λ₯Ό
dependencies λ‘ μΆκ°νκΈ° μ _hooks λ λ€μκ³Ό κ°λ€.
_hooks = [foo, bar, baz];
useEffect κ° dependency [foo, baz] λ₯Ό μΆκ°νλ©΄ μ΄μ _hooks λ λ€μκ³Ό κ°λ€.
_hooks = [foo, bar, baz, [foo, baz]];
*/
/*
<div>
<div>
foo : {foo} + {" "}
<button onclick={ () => setFoo((val) => val + 1) }>foo + 1</button>
</div>
<>
bar : {bar} + {" "}
<button onclick={ () => setBar((val) => val + 1) }>bar + 1</button>
</div>
<div>
baz : {baz} + {" "}
<input value={baz} onChange={ (event) => setBaz(event.target.value) } />
</div>
<Greeting />
</div>
*/
const reactElement = {
div: {
div1: {
text: `foo : ${foo} `,
button: {
onClick: () => setFoo((val) => val + 1),
text: 'foo + 1',
},
},
div2: {
text: `bar : ${bar} `,
button: {
onClick: () => setBar(bar + 1),
text: 'bar + 1',
},
},
div3: {
text: 'onChange ',
input: {
value: baz,
onChange: (event) => setBaz(event.target.value),
},
},
Greeting,
},
};
return reactElement;
};
ReactDOM.render(App, document.getElementById('root'));
μ¬κΈ°μ μ€μν κ²μ μ»΄ν¬λνΈλΌλ ν¨μ 컨ν μ΄λ μμ Closures λ₯Ό μ¬μ©ν΄ States λ₯Ό κ΄λ¦¬νλ€λ κ²μ΄λ€.
νμ§λ§ μ μμ μ½λλ λλ΅μ μΈ κ°λ μΌ λΏ μ€μ 리μ‘νΈλ μμ κ°μ΄ λ¨μνκ² μλνμ§ μλλ€. μ μμ μ½λλ νμ λͺ¨λ 리μ‘νΈ νΈλ¦¬λ₯Ό λ€μ λ λλ§ ν λΏ μλλΌ κ°μ΄ μ€μ λ‘ λ³νλμ§ μκ΄ μμ΄ μ΄λ²€νΈκ° λ°μν κ²½μ° λ€μ λ λλ§μ νλ νμμ νμΈν μ μλ€. μ΄μ κ°κ³Όμ λΉκ΅λ μ€μ§ useEffect μμλ§ μ²λ¦¬λκ³ μλλ°, μ΄ λ§μ λ root μ»΄ν¬λνΈ μ체λ₯Ό λͺ¨λ μλ‘ κ·Έλ €λΈλ€.
νμ§λ§ μ€μ 리μ‘νΈλ Fiber μ κ°μ Reconciliation μμ§λ μ‘΄μ¬νκ³ , λ μ½κ², λ μμ νκ² μ½λ μμ±μ ν μ μλλ‘ λ§μ κΈ°μ λ€μ΄
ν¬ν¨λμ΄μλ€. λΉμ·νκ² λμνμ§λ§ μ μμ μ½λμμ κ°λ λ¬Έμ μ μ μΉμ΄ 컀μ§μλ‘ μ±λ₯ μ νκ° μ¬κ°νκ² λ°μν κ²μμ μ μ μλ€.
리μ‘νΈμ λ λλ§ μ΅μ νκ° νμν μ΄μ
λ€.
4. Render Optimization π©βπ»
1. SCU and Virtual DOMs Equivalent
μ κ·Έλ¦Όμμ κΈμ¨λ κ·Έλ¦Όμ λ
Ήμμ true λ₯Ό λνλ΄λ©°, λΉ¨κ°μμ false λ₯Ό λνλΈλ€. SCU
μ vDOMEq
λΌλ 2κ°μ μ§νκ° μλ€.
SCU
λ shouldComponentUpdate λ‘ μ»΄ν¬λνΈκ° μ
λ°μ΄νΈ λμ΄μΌ νλμ§λ₯Ό λνλΈλ€. λ°λΌμ
λ
Ήμμ΄λ©΄ μ»΄ν¬λνΈλ₯Ό μ
λ°μ΄νΈ ν΄μΌν¨μ λνλΈλ€. λ°λ©΄ vDOMEq
λ Virtual DOM μ΄
μ΄μ κ³Ό κ°μμ§λ₯Ό λ°ννλ―λ‘ λΉ¨κ°μμ΄λ©΄ μ»΄ν¬λνΈλ₯Ό μ
λ°μ΄νΈλ₯Ό ν΄μΌν¨μ λνλΈλ€.
- C1 : SCU κ° true λ₯Ό, vDOMEs κ° false λ₯Ό λ°ννμΌλ―λ‘ μ»΄ν¬λνΈ μ λ°μ΄νΈλ₯Ό ν΄μΌνλ€.
- C2 : SCU κ° false λ₯Ό λ°ννκΈ° λλ¬Έμ μ»΄ν¬λνΈλ₯Ό μ λ°μ΄νΈ νμ§ μλλ€.
- C4, C5 : C2 μ SUC κ° false μ΄λ―λ‘ λμ΄μ νμμ νμ§ μλλ€.
- C3 : SCU κ° true λ₯Ό, vDOMEs κ° false λ₯Ό λ°ννμΌλ―λ‘ μ»΄ν¬λνΈ μ λ°μ΄νΈλ₯Ό ν΄μΌνλ€.
- C6 : SCU κ° true λ₯Ό, vDOMEs κ° false λ₯Ό λ°ννμΌλ―λ‘ μ»΄ν¬λνΈ μ λ°μ΄νΈλ₯Ό ν΄μΌνλ€.
- C7 : SCU κ° false λ₯Ό λ°ννκΈ° λλ¬Έμ μ»΄ν¬λνΈλ₯Ό μ λ°μ΄νΈ νμ§ μλλ€.
- C8 : SCU κ° true λ₯Ό λ°ννμ§λ§ vDOMEs κ° true λ₯Ό λ°ννμΌλ―λ‘ μ»΄ν¬λνΈ μ λ°μ΄νΈ νμ§ μλλ€.
리μ‘νΈμ λ λλ§ μ΅μ νμμ λ°λ‘ μ΄ C8 μ΄ ν₯λ―Έλ‘μ΄ λΆλΆμ΄λ€. μ΄λ‘μ¨ λ¦¬μ‘νΈλ C1, C3, C6 λ§ re-render λ₯Ό μννλ€.
μ¦, SCU && !vDOMEs
κ° true μΌ κ²½μ°μλ§ μ΅μ’
μ μΌλ‘ μ»΄ν¬λνΈμ re-render λ₯Ό μννλ€λ κ²μ μ μ μλ€.
2. Prevent Unnecessary Reconciliation
리μ‘νΈλ λ·°λ κΈ°λ³Έμ μΌλ‘ μ΅κ·€λ¬λ³΄λ€λ λΉ λ₯΄κΈ° λλ¬Έμ λλΆλΆμ μ΅μ νλ₯Ό ν¬κ² κ³ λ €νμ§ μμλ 300ms μ΄νμ λ°μ μλλ‘ μ μλνλ€.
νμ§λ§ μ΅κ·Ό λΈλΌμ°μ κ° ν μ μλ μΌμ΄ λ§μμ§λ©° three.js
μ κ°μ λ°μ΄ν° μκ°ν, Google Analytics μ κ°μ λλμ λ°μ΄ν°λ₯Ό
μ²λ¦¬ν΄μΌνκ³ κΈ°λ₯μ΄ λ§μ΄ νμν λ°±μ€νΌμ€, λ‘κ·Έ λ°μ΄ν° μκ°νμμ Fiber κ° ν΄λΉ λΌμ΄λΈλ¬λ¦¬μ λν μ΅μ νλ₯Ό μ§μνμ§ λͺ» ν κ²½μ°
reconciliation μ λ§μ μκ°μ΄ μμλ μ μμ΄ μ§μ μ΅μ νλ₯Ό ν΄μΌνλ κ²½μ°κ° λ°μν μ μλ€.
μλΉμ€ κ·λͺ¨κ° 컀μ§κΈ° μ μλ μ΅μ νμ λ¬Έμ κ° μλ μ½λλΌ νλλΌλ λΉ λ₯΄κ² μλν μ μλ€. νμ§λ§ μλΉμ€κ° 컀μ§κ³ λ¬Έμ κ° μκΈ΄ ν μ΄λ₯Ό κ³ μΉ λ λ무 λ§μ μκ°μ΄ μμλ μ μλ€. κ·Έλ κΈ° λλ¬Έμ κ°κΈμ 리μ‘νΈ λ λλ§ μ΅μ νλ₯Ό μ΄ν΄νκ³ μ’μ μ½λλ₯Ό μμ±νλλ‘ μ΅κ΄μ λ€μ΄λ κ²μ΄ μ€μνλ€.
1 ) νμ μ»΄ν¬λνΈλ‘ μ λ¬μ Reference μ μ§νκΈ°
useMemo
, useCallback
μ μ μ©νλ€. λ―Έμ μ©μ νμ μ»΄ν¬λνΈκ° λ§€λ² μ Reference λ₯Ό λ°μ λΆνμν λ λλ§μ΄ λ°μν
μ μλ€. νΉν Custom Input μ onChange λ₯Ό μ λ¬ν λ, memoization μ μ²λ¦¬νμ§ μμΌλ©΄ re-rendering λ νμκ°
μλ μμ μ»΄ν¬λνΈ λͺ¨λκ° λ€μ λ λλ§λλ€.
2 ) Object μν λΉκ΅
λΉκ΅ν΄μΌν λμμ νμ μ΄ Reference Types μΌ κ²½μ° Shallow Equality λ₯Ό λΉκ΅ν 건μ§, Deep Equality λ₯Ό λΉκ΅ν κ±΄μ§ μ νλ¨ν΄μΌνλ€.
Bad Case
const {
markers,
mapConfig: { markerScale, markerTextScale: textScale },
} = useSelector((state) => state);
Good Case 1
const markers = useSelector((state) => state.markers);
const markerScale = useSelector((state) => state.mapConfig.markerScale);
const textScale = useSelector((state) => state.mapConfig.markerTextScale);
Good Case 2
const { markers, markerScale, textScale } = useSelector(
(state) => ({
markers: state.markers,
markerScale: state.mapConfig.markerScale,
textScale: state.mapConfig.markerTextScale,
}),
shallowEqual,
);
useSelector
μ Object μ κ°μ Reference Types λ₯Ό μ¬μ©ν λλ Good Case 1 μ²λΌ κ° μμκ°λ³λ‘ λΆλ¦¬ν΄
μ¬μ©
νκ±°λ, Good Case 2 μ κ°μ΄ κ° μμκ°μ Object λ‘ μΆμΆν λ€μ λ°ννκ³ shallowEqual
λ₯Ό μ μ©ν΄μΌνλ€.
κ·Έλ μ§ μμΌλ©΄ 리μ€νΈμ κ°μ κ²μ λ λλ§ ν λ 무쑰건 λ λλ§μ μ€νν΄ μ¬κ°ν μ±λ₯ μ νλ₯Ό μΌκΈ°ν μ μλ€.
λ λ€λ₯Έ Bad Case μ μλ λ€μκ³Ό κ°λ€.
Bad Case
const App = () => {
// ...
const { map, view, vectorSource } = useContext(OlContext);
useEffect(() => {
map.setTarget("map");
view.setZoom(12);
view.setCenter(transform(SEOUL_CITY_HALL_LONLAT, "EPSG:4326", "EPSG:3857"));
}, [map, view]);
markers.forEach((marker) => {
console.log(`render ${marker.id} marker`);
renderMarker(marker, vectorSource, { markerScale, textScale });
});
//...
return (
<>
<div id="map" style={styles.MapRoot}></div>
</>
);
};
μ μ½λλ μ§λμ λ§μ»€λ₯Ό νλ μΆκ°ν λλ§λ€ μ»΄ν¬λνΈκ° re-rendering λκΈ° λλ¬Έμ μ 체 λ§μ»€λ₯Ό μλ‘ κ·Έλ¦°λ€. λ§μ»€κ° 500κ° μλλ° 1κ°λ₯Ό μΆκ°νλ©΄ 501κ°λ₯Ό μλ‘κ² κ·Έλ¦¬κ² λλ€.
Good Case
const Marker = ({ marker }) => {
const { vectorSource } = useContext(OlContext);
const { markerScale, textScale } = useSelector(
(state) => ({
markerScale: state.mapConfig.markerScale,
textScale: state.mapConfig.markerTextScale,
}),
shallowEqual,
);
useEffect(() => {
console.log(`render ${marker.id} marker`);
renderMarker(marker, vectorSource, { markerScale, textScale });
}, [marker, vectorSource, markerScale, textScale]);
return null;
};
const App = () => {
// ...
const { map, view } = useContext(OlContext);
useEffect(() => {
map.setTarget("map");
view.setZoom(12);
view.setCenter(transform(SEOUL_CITY_HALL_LONLAT, "EPSG:4326", "EPSG:3857"));
}, [map, view]);
// Good Case
const markerList = markers.map((marker) => (
<Marker key={marker.id} marker={marker} />
));
//...
return (
<>
<div id="map" style={styles.MapRoot}></div>
{markerList}
</>
);
};
λ§μ»€λ₯Ό Marker
μ»΄ν¬λνΈλ‘ λΆλ¦¬μν€κ³ , useEffect
λ‘ κ°μΈ Memoization μ²λ¦¬νλ€. λ°λΌμ λ§μ»€μ λ λλ§μ μ λΆ
νμ μ»΄ν¬λνΈμ μμνκ³ , μμ μ»΄ν¬λνΈμ λ¨μ μ reference μ£Όμ λ³κ²½μ λν΄ μν₯μ λ°μ§ μλλ‘ μ²λ¦¬νλ€. μ΄μ
κΈ°μ‘΄μ κ·Έλ €μ§ 500κ°μ λ§μ»€λ reference λ§ λ°λκ³ κ°μ΄ λ³κ²½λμ§ μμκΈ° λλ¬Έμ(=dependencies μ μλ
κΈ°μ‘΄μ marker λ κ·Έλλ‘ μ μ§) μλ‘κ² μΆκ°λ λ§μ»€λ§ λ λλ§ νλ©΄ λλ€.
3 ) Redux λ₯Ό λ체νλ λ€λ₯Έ μν λΌμ΄λΈλ¬λ¦¬ λμ μ κ³ λ €
κ³Όλνκ² λμ΄λ Redux μν νΈλ¦¬λ λΆνμν UI Elements λ₯Ό κ΄γΉνκ³ μμ μ μμΌλ©°, Popup, Toast, Toggle Button
κ³Ό κ°μ μ μ UI μ μνλ Context
, Atomic State
λ±μΌλ‘ λΆλ¦¬νλ€.
λν Redux λ₯Ό λ€λ₯Έ μνκ΄λ¦¬ λΌμ΄λΈλ¬λ¦¬λ‘ κ΅μ²΄νλ κ²μ΄ κ°λ₯νλ€λ©΄, Recoil, Jotai, Zustand μ κ°μ λΌμ΄λΈλ¬λ¦¬λ₯Ό λμ ν΄ μ ν λ°λ μνλ₯Ό μ€μ΄λ κ²μ΄ μ’λ€. λ¨, μν κ΄λ¦¬ λΌμ΄λΈλ¬λ¦¬λ₯Ό κ΅μ²΄νλ κ²μ κ²°μ½ μ¬μ΄ μμ μ΄ μλλ―λ‘ μ μ€ν΄μΌνλ€.
4 ) PureComponent, React.memo
- PureComponent λ λμ΄μ κΆμ₯λμ§ μλλ€. λ°λΌμ ν¨μν μ»΄ν¬λνΈλ‘ λ³κ²½νλ κ²μ΄ μ’λ€.
- React.memo λ μμ£Ό μ λ°μ΄νΈ λλ μ»΄ν¬λνΈμ μ μ©μ μ€νλ € μ±λ₯μ΄ μ νλλ€. λ°λΌμ λ°λμ νμνμ§λ₯Ό κ²ν ν΄μΌνλ€.
5. Redux π©βπ»
1. Classification of the State Management
1 ) Flux-based
μ μ, λ¨μΌ μ μ₯μλ₯Ό μ¬μ©νλ©° μ΅μμ Provider μ»΄ν¬λνΈμ Action μ€νμ μ μΈν μν λ³κ²½μ μ ννλ€.
λ¨μΌ Store λ₯Ό κ°μνλ―λ‘ λλ²κΉ
μ κΈ°μ€μ΄ Action μ μ€ν => μ μ State λ³κ²½μΌλ‘ μ μΌνλ€.
- Redux: Flux + Reducer
- Zustand: Flux + Context
2 ) Context-based
μ§μ μν μ νλ₯Ό μ¬μ©ν΄ Provider μ μμΉλΌ μ΅μμμ κ΅νλμ§ μμΌλ©° μμ λ‘λ€.
λ¨μΌ Store κ° μλλ―λ‘, Key λ₯Ό ν΅ν΄ μ΄λ€ state μΈμ§λ₯Ό ννν΄μΌνλ©°, λλ²κΉ
κΈ°μ€μ μ λ³λλ‘ μ 곡ν΄μΌνλ€.
- React Context API: id, displayName(optional)
- Jotai: debugLabel(optional)
- Recoil: State μμ± μμ μ Key κ°μ
3 ) Proxy-based
State μ¬ν λΉ κ°μ§ λ° μ νλ₯Ό μ§μ ν λΉ(state = newState)ν μ§ Action μ μ¬μ©ν μ§ μ νν μ μλ€.
- MobX
2. Redux Middleware
Redux Middleware λ dispatch λ₯Ό κ°μνλ μ dispatch ν¨μλ₯Ό νμ₯ν μ ν¨μλ₯Ό λ§λ€μ΄μ μ¬μ©νλ€(κΈ°μ‘΄μ ν¨μλ₯Ό μμ νκ±°λ μ¬μ μ ν κ²½μ° Monkeypatching λ¬Έμ κ° λ°μνλ€). μ΄λ₯Ό μν΄μ Redux Middleware λ Currying μ μ μ©ν΄ λ¬Έμ λ₯Ό ν΄κ²°νλ€.
λͺ¨λ Action μ λν΄ log λ₯Ό λ¨κΈ°λ κ³΅ν΅ λ‘μ§μ λ§λλ κ²μ κ°μ ν΄λ³΄μ.
Bad Case - Monkeypatching μ΄ λ°μν dispatch
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
Good Case - Currying μ μ μ©ν΄ ν¨μλ₯Ό Chaining
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
μ΄μ μ΄λ€μμΌλ‘ Middleware λ₯Ό λ§λ€μ΄μΌνλμ§ μμμΌλ λͺ¨λ Action μ λν΄ μλ¬λ₯Ό 리ν¬νΈνλ κ³΅ν΅ λ‘μ§μ λ§λλ κ²μ κ°μ ν΄λ³΄μ.
const crashReporter = store => next => action => {
try {
return next(action);
} catch (err) {
console.error('Caught an exception!', err);
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
});
throw err;
}
}
Redux μ applyMiddleware
λ createStore()
μμ μλνλ―λ‘
(...middlewares) => (createStore) => createStore
μ νλ¦μΌλ‘ μλνλ€. μ λ Middlewares λ₯Ό μ μ©ν΄λ³΄μ.
import { createStore, combineReducers, applyMiddleware } from 'redux';
let createStoreWithMiddleware = applyMiddleware(logger, crashReporter)(createStore);
let todoApp = combineReducers(reducers);
let store = createStoreWithMiddleware(todoApp);
μ΄μ Store λ λͺ¨λ Action μ νλ¦μ΄ logger
μ crashReporter
λ₯Ό μ§λκ² λλ€.
store.dispatch(addTodo('Use Redux'));
λ€μν Middleware μμλ Redux Middleware Examples μμ νμΈν μ μλ€. λν Redux Middleware μ λν GitHub μ€λͺ μ 보면 λ―Έλ€μ¨μ΄λ μ μ¬μ μΌλ‘ λΉλκΈ°μ μ΄κΈ° λλ¬Έμ Store μ κΈ°λ₯μ μΆκ°νκΈ° μν΄ Composition Chain μ ꡬμ±ν λ νμ μμ μμΉν΄μΌνλ€(First Store Enhancer)λ κ·μΉμ κ°λλ€.
3. Redux Middleware Libraries
Redux λ λκΈ° μμ λ§ μ²λ¦¬ν μ μλ€. νμ§λ§ μΉμ ν΅μ μΌλ‘ μλνκΈ° λλ¬Έμ λ§μ API λ λΉλκΈ°λ‘ μλνκ³ , νΉν μλ²μμ ν΅μ μ μμ΄ λΉλκΈ° μμ μ νμλ€. κ·Έλ¦¬κ³ μ΄λ¬ν λΉλκΈ° μμ μ νμν λ―Έλ€μ¨μ΄λ₯Ό λΌμ΄λΈλ¬λ¦¬ν ν κ²λ€μ΄ μ‘΄μ¬νλλ° λ€μκ³Ό κ°μ νΉμ§μ κ°λλ€.
- Redux-Saga: Generator κΈ°λ°μ Redux λΉλκΈ° λ―Έλ€μ¨μ΄ λΌμ΄λΈλ¬λ¦¬
- Redux-Thunk: async/await κΈ°λ°μ Redux λΉλκΈ° λ―Έλ€μ¨μ΄ λΌμ΄λΈλ¬λ¦¬λ‘
- Redux-Observable: Observable Sequence λ‘ κ΅¬νλ RxJS λ₯Ό κΈ°λ°μΌλ‘ νλ Redux λΉλκΈ° λ―Έλ€μ¨μ΄ λΌμ΄λΈλ¬λ¦¬
Reference
- κ°ν, βν λ²μ λλ΄λ Reactμ λͺ¨λ κ² μ΄κ²©μ°¨ ν¨ν€μ§, Part 5. νΈλ¬λΈμν β fastcampus.co.kr. last modified unknown, Fast Campus.
- βλ―Έλ€μ¨μ΄.β Redux Docs. accessed Dec. 19, 2023, Redux - λ―Έλ€μ¨μ΄.
- βreduxjs/redux.β, Redux GitHub. accessed Dec. 19, 2023, Redux - applyMiddleware.