์ปดํฌ๋ํธ์ ํจ์ ์ ๋ฌํ๊ธฐ
์ปดํฌ๋ํธ๋ก onClick๊ณผ ๊ฐ์ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์ด๋ป๊ฒ ์ ๋ฌ ํ ๊น์?
์์ ์ปดํฌ๋ํธ์ ํ๋กํผํฐ๋ก ์ด๋ฒคํธ ํธ๋ค๋ฌ์ ๋ค๋ฅธ ํจ์๋ค์ ์ ๋ฌํฉ๋๋ค.
<button onClick={this.handleClick}>
ํธ๋ค๋ฌ ์์์ ๋ถ๋ชจ ์ปดํฌ๋ํธ์ ์ ๊ทผํ ํ์๊ฐ ์์ผ๋ฉด ์ปดํฌ๋ํธ ์ธ์คํด์ค์ ํจ์๋ฅผ ๋ฐ์ธ๋ฉํด ์ฃผ์ด์ผ ํฉ๋๋ค.
์ปดํฌ๋ํธ ์ธ์คํด์ค๋ก ํจ์๋ฅผ ์ด๋ป๊ฒ ๋ฐ์ธ๋ฉํ ๊น์?
์ฌ์ฉํ๊ณ ์๋ ๋ฌธ๋ฒ๊ณผ ๋น๋ ๋จ๊ณ์ ๋ฐ๋ผ this.props
, this.state
์ ๊ฐ์ ์ปดํฌ๋ํธ์ ์ดํธ๋ฆฌ๋ทฐํธ์ ํจ์๋ค์ด ํ์คํ ์ ๊ทผํ ์ ์๋๋ก ๋ง๋๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ ๊ฐ์ง๊ฐ ์์ต๋๋ค.
์์ฑ์์์ ๋ฐ์ธ๋ฉํ๊ธฐ (ES2015)
class Foo extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Click happened');
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}
ํด๋์ค ํ๋กํผํฐ (ES2022)
class Foo extends Component {
handleClick = () => {
console.log('Click happened');
};
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}
render ๋ฉ์๋ ์์์ ๋ฐ์ธ๋ฉํ๊ธฐ
class Foo extends Component {
handleClick() {
console.log('Click happened');
}
render() {
return <button onClick={this.handleClick.bind(this)}>Click Me</button>;
}
}
์ฃผ์
Function.prototype.bind
๋ฅผ render ๋ฉ์๋์์ ์ฌ์ฉํ๋ฉด ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋งํ ๋๋ง๋ค ์๋ก์ด ํจ์๋ฅผ ์์ฑํ๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค.
render ๋ฉ์๋ ์์์ ํ์ดํ ํจ์ ์ฌ์ฉ
class Foo extends Component {
handleClick() {
console.log('Click happened');
}
render() {
return <button onClick={() => this.handleClick()}>Click Me</button>;
}
}
์ฃผ์
render ๋ฉ์๋ ์์์ ํ์ดํ ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋งํ ๋๋ง๋ค ์๋ก์ด ํจ์๋ฅผ ๋ง๋ค๊ธฐ ๋๋ฌธ์ ์๊ฒฉํ ๋น๊ต์ ์ํด ์ต์ ํ๊ฐ ๊นจ์ง ์ ์์ต๋๋ค.
render ๋ฉ์๋ ์์์ ํ์ดํ ํจ์๋ฅผ ์ฌ์ฉํด๋ ๊ด์ฐฎ์๊น์?
์ด ๋ฐฉ๋ฒ์ ๋์ฒด๋ก ์ฌ์ฉํด๋ ๊ด์ฐฎ๊ณ , ์ฝ๋ฐฑ ํจ์๋ก ๋งค๊ฐ๋ณ์๋ฅผ ์ ๋ฌํด ์ฃผ๋ ๊ฐ์ฅ ์ฌ์ด ๋ฐฉ๋ฒ์ ๋๋ค.
์ฑ๋ฅ ๋ฌธ์ ๊ฐ ์๋ค๋ฉด ๋ฐ๋์ ์ต์ ํ๋ฅผ ํด์ผ ํฉ๋๋ค.
๋ฐ์ธ๋ฉ์ด ํ์ํ ์ด์ ๋ ๋ฌด์์ผ ๊น์?
์๋ฐ์คํฌ๋ฆฝํธ์์ ์๋ ๋ ๊ฐ์ ์ฝ๋ ์กฐ๊ฐ์ ๋์ผํ์ง ์์ต๋๋ค.
obj.method();
var method = obj.method;
method();
๋ฐ์ธ๋ฉ ๋ฉ์๋๋ ๋ ๋ฒ์งธ ์ฝ๋ ์กฐ๊ฐ์ด ์ฒซ ๋ฒ์งธ ์ฝ๋์กฐ๊ฐ๊ณผ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์๋ํ๋๋ก ๋ง๋ค์ด ์ค๋๋ค.
์ผ๋ฐ์ ์ผ๋ก React์์ ๋ค๋ฅธ ์ปดํฌ๋ํธ์ ๋ฉ์๋๋ฅผ ์ ๋ฌํด ์ค ๋๋ง ๋ฐ์ธ๋ฉํด ์ฃผ๋ฉด ๋ฉ๋๋ค. ์๋ฅผ ๋ค์ด <button onClick={this.handleClick}>
๋ this.handleClick
์ ์ ๋ฌํ์ฌ ๋ฐ์ธ๋ฉํฉ๋๋ค. ๊ทธ๋ ์ง๋ง render
๋ฉ์๋๋ ์๋ช
์ฃผ๊ธฐ ๋ฉ์๋๋ ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ก ์ ๋ฌํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ฐ์ธ๋ฉํ ํ์๊ฐ ์์ต๋๋ค.
Yehuda Katz์ ๊ธ์์ ๋ฐ์ธ๋ฉ์ด ๋ฌด์์ธ์ง, JavaScript์์ ์ด๋ป๊ฒ ํจ์๊ฐ ์๋ํ๋์ง์ ๋ํด ์์ธํ ์ ์ ์์ต๋๋ค.
์ ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋งํ ๋๋ง๋ค ํจ์๊ฐ ํธ์ถ๋ ๊น์?
์ปดํฌ๋ํธ๋ก ํจ์๋ฅผ ์ ๋ฌํ ๋ ํธ์ถํ์ง ์๋์ง ํ์ธํฉ๋๋ค.
render() {
// ์๋ชป๋ ๋ฐฉ๋ฒ: handleClick์ ๋ ํผ๋ฐ์ค๋ก ์ ๋ฌ๋์ง ์๊ณ ํธ์ถ๋์์ต๋๋ค!
return <button onClick={this.handleClick()}>Click Me</button>
}
์์ ๊ฐ์ ๋ฐฉ์์ด ์๋๋ผ ๊ดํธ ์์ด ํจ์ ๊ทธ ์์ฒด๋ฅผ ์ ๋ฌํด์ผ ํฉ๋๋ค.
render() {
// ์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ : handleClick์ด ๋ ํผ๋ฐ์ค๋ก ์ ๋ฌ๋์์ต๋๋ค.
return <button onClick={this.handleClick}>Click Me</button>
}
์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ์ฝ๋ฐฑ์ ์ด๋ป๊ฒ ๋งค๊ฐ๋ณ์๋ฅผ ์ ๋ฌํ ๋์?
์ด๋ฒคํธ ํธ๋ค๋ฌ์ ํ์ดํ ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ์ผ ๋ค์์ ๋งค๊ฐ๋ณ์๋ฅผ ๋๊ฒจ์ค ์ ์์ต๋๋ค.
<button onClick={() => this.handleClick(id)} />
.bind
๋ฅผ ํธ์ถํ ๊ฒ๊ณผ ๊ฐ์ต๋๋ค.
<button onClick={this.handleClick.bind(this, id)} />
์์: ํ์ดํ ํจ์๋ฅผ ์ด์ฉํ์ฌ ๋งค๊ฐ๋ณ์ ์ ๋ฌํ๊ธฐ
const A = 65 // ASCII character code
class Alphabet extends React.Component {
constructor(props) {
super(props);
this.state = {
justClicked: null,
letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
};
}
handleClick(letter) {
this.setState({ justClicked: letter });
}
render() {
return (
<div>
Just clicked: {this.state.justClicked}
<ul>
{this.state.letters.map(letter =>
<li key={letter} onClick={() => this.handleClick(letter)}>
{letter}
</li>
)}
</ul>
</div>
)
}
}
์์: data-attributes๋ฅผ ์ฌ์ฉํด์ ๋งค๊ฐ๋ณ์ ์ ๋ฌํ๊ธฐ
๋ค๋ฅธ ๋ฐฉ๋ฒ์ผ๋ก ์ด๋ฒคํธ ํธ๋ค๋ฌ์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ธฐ ์ํด DOM API๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ด ๋ฐฉ๋ฒ์ ์์ฃผ ๋ง์ ์์๋ฅผ ์ต์ ํํ๊ฑฐ๋ React.PureComponent ๋์ผ์ฑ ๊ฒ์ฌ์ ์์กดํ๋ ๋ ๋๋ง ํธ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๋ ๊ณ ๋ คํด ๋ณผ ๋งํฉ๋๋ค.
const A = 65 // ASCII character code
class Alphabet extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
justClicked: null,
letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
};
}
handleClick(e) {
this.setState({
justClicked: e.target.dataset.letter
});
}
render() {
return (
<div>
Just clicked: {this.state.justClicked}
<ul>
{this.state.letters.map(letter =>
<li key={letter} data-letter={letter} onClick={this.handleClick}>
{letter}
</li>
)}
</ul>
</div>
)
}
}
์ด๋ป๊ฒ ํจ์๊ฐ ๋๋ฌด ๋นจ๋ฆฌ, ๋๋ฌด ๋ง์ด ํธ์ถ๋๋ ๊ฒ์ ๋ง์ ์ ์๋์?
onClick
๋๋ onScroll
๊ณผ ๊ฐ์ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํ๊ณ ์์ ๋ ์ฝ๋ฐฑ์ด ๋๋ฌด ๋น ๋ฅด๊ฒ ํธ์ถ๋์ง ์๋๋ก ์ฝ๋ฐฑ์ด ์คํ๋๋ ์๋๋ฅผ ์ ์ดํ ์ ์์ต๋๋ค. ๋ค์์ ํจ์๋ค์ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
- throttling: ์๊ฐ ๊ธฐ๋ฐ ๋น๋์ ๋ฐ๋ฅธ ๋ณ๊ฒฝ ์ํ๋ง (์์
_.throttle
) - debouncing: ๋นํ์ฑ ์ฃผ๊ธฐ ์ดํ์ ๋ณ๊ฒฝ ์ ์ฉ (์์
_.debounce
) requestAnimationFrame
throttling:requestAnimationFrame
(์์raf-schd
)์ ๊ธฐ๋ฐ์ผ๋ก ํ ๋ณ๊ฒฝ ์ํ๋ง
throttle
๊ณผ debounce
ํจ์๋ฅผ ๋น๊ตํ๊ณ ์ถ์ผ๋ฉด ์๊ฐํ๋ฅผ ํ์ธํ๋ฉด ๋ฉ๋๋ค.
์ฃผ์
_.debounce
,_.throttle
,raf-schd
๋ ์ง์ฐ๋๋ ์ฝ๋ฐฑ์ ์ทจ์ํ๋ ๋ฉ์๋cancel
์ ์ ๊ณตํฉ๋๋ค.componentWillUnmount
์์ ์ด ํจ์๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ ๋๋ ์ง์ฐ๋ ํจ์ ๋ด์์ ์ปดํฌ๋ํธ๊ฐ ๋ง์ดํธ๊ฐ ๋์ด์์์ ํ์ธํด์ผ ํฉ๋๋ค.
Throttle
Throttling์ ํจ์๊ฐ ์ฃผ์ด์ง ์๊ฐ ๋์์ ํ ๋ฒ ์ด์ ํธ์ถ๋๋ ๊ฒ์ ๋ง์ต๋๋ค. ์๋๋ โclickโ ํธ๋ค๋ฌ์ throttling์ ์ฌ์ฉํ์ฌ ์ด๋น ํ ๋ฒ๋ง ํธ์ถ๋๋๋ก ํ ์์์ ๋๋ค.
import throttle from 'lodash.throttle';
class LoadMoreButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleClickThrottled = throttle(this.handleClick, 1000);
}
componentWillUnmount() {
this.handleClickThrottled.cancel();
}
render() {
return <button onClick={this.handleClickThrottled}>Load More</button>;
}
handleClick() {
this.props.loadMore();
}
}
Debounce
Debouncing์ ํจ์๊ฐ ๋ง์ง๋ง์ผ๋ก ํธ์ถ๋ ํ ํน์ ์๊ฐ๊น์ง ์คํ๋์ง ์๋๋ก ํด์ค๋๋ค. ๋น ๋ฅด๊ฒ ๋ฐํํ๋ ์ด๋ฒคํธ(์์ ์คํฌ๋กค, ํค๋ณด๋ ์ด๋ฒคํธ)์ ์๋ต์ผ๋ก ์ด๋ค ๋น์ผ ๊ณ์ฐ์ ์ํํด์ผ ํ ๋ ์ฌ์ฉํ๋ฉด ์ข์ต๋๋ค. ์๋์ ์์๋ 250 ๋ฐ๋ฆฌ์ด ์ด๋ด์ ํ ์คํธ ์ ๋ ฅ์ Debouncingํ์ต๋๋ค.
import debounce from 'lodash.debounce';
class Searchbox extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.emitChangeDebounced = debounce(this.emitChange, 250);
}
componentWillUnmount() {
this.emitChangeDebounced.cancel();
}
render() {
return (
<input
type="text"
onChange={this.handleChange}
placeholder="Search..."
defaultValue={this.props.value}
/>
);
}
handleChange(e) {
this.emitChangeDebounced(e.target.value);
}
emitChange(value) {
this.props.onChange(value);
}
}
requestAnimationFrame
throttling
requestAnimationFrame
์ ๋ ๋๋ง ์ฑ๋ฅ์ ์ํด ๋ธ๋ผ์ฐ์ ์์ ์ต์ ํ๋ ์๊ฐ์ ํจ์๊ฐ ์คํ๋๋๋ก ํจ์๋ฅผ ํ์ํ๋ ๋ฐฉ๋ฒ์
๋๋ค. requestAnimationFrame
์ ํ๋ก ๋ค์ด๊ฐ ํจ์๋ ๋ค์ ํ๋ ์์์ ์คํ๋ฉ๋๋ค. ๋ธ๋ผ์ฐ์ ๋ 1์ด๋น 60 ํ๋ ์(60 fps)์ ๋ณด์ฅํ๊ธฐ ์ํด ์ด์ฌํ ์ผํฉ๋๋ค. ํ์ง๋ง ๋ธ๋ผ์ฐ์ ๊ฐ ์ด๋ฅผ ํ์ง ๋ชปํ ๋ ์ ์ ๋ก ํ๋ ์์ ์ ํํฉ๋๋ค. ์๋ฅผ ๋ค๋ฉด ํ ๊ธฐ๊ธฐ๊ฐ 30 fps๋ง ์ฒ๋ฆฌํ ์ ์๋ค๋ฉด 1์ด ๋์ 30 ํ๋ ์๋ง ์ป์ ์ ์์ต๋๋ค. throttling์ ์ํด requestAnimationFrame
์ ์ฌ์ฉํ๋ฉด 1์ด์ 60๋ฒ ์ด์ ์
๋ฐ์ดํธํ๋ ๊ฒ์ ๋ง์ ์ ์์ต๋๋ค. 1์ด๋น 100๋ฒ ์
๋ฐ์ดํธํ๋๋ก ๋ธ๋ผ์ฐ์ ์ ์ผ์ ๋ง๋ค์ด ์ฃผ์ด๋, ์ ์ ๋ ์ด๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
์ฃผ์
์ด ๊ธฐ๋ฒ์ ์ฌ์ฉํ๋ฉด, ํ๋ ์์ ๊ฐ์ฅ ๋ง์ง๋ง์ผ๋ก ๊ฒ์ฌ๋ ๊ฐ๋ง ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค. ์ต์ ํ๊ฐ ์ด๋ป๊ฒ ์๋ํ๋์ง์ ๋ํ ์์๋
MDN
์์ ํ์ธํ ์ ์์ต๋๋ค.
import rafSchedule from 'raf-schd';
class ScrollListener extends React.Component {
constructor(props) {
super(props);
this.handleScroll = this.handleScroll.bind(this);
// ์
๋ฐ์ดํธ ์ผ์ ์ ์ ํ๋ ํจ์๋ฅผ ๋ง๋ญ๋๋ค.
this.scheduleUpdate = rafSchedule(
point => this.props.onScroll(point)
);
}
handleScroll(e) {
// ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ ๋ฐ๊ฒ ๋๋ฉด ์
๋ฐ์ดํธ๋ฅผ ์ผ์ ์ ์ถ๊ฐํฉ๋๋ค.
// ํ ํ๋ ์ ์์ ๋ง์ ์
๋ฐ์ดํธ๋ฅผ ๋ฐ์ผ๋ฉด ์ค์ง ๋ง์ง๋ง ๊ฐ๋ง ๊ฒ์ฌํฉ๋๋ค.
this.scheduleUpdate({ x: e.clientX, y: e.clientY });
}
componentWillUnmount() {
// ๋ง์ดํธ ํด์ ์ค์ ์์์ํ์ ์
๋ฐ์ดํธ๋ค์ ๋ชจ๋ ์ทจ์ํฉ๋๋ค.
this.scheduleUpdate.cancel();
}
render() {
return (
<div
style={{ overflow: 'scroll' }}
onScroll={this.handleScroll}
>
<img src="/my-huge-image.jpg" />
</div>
);
}
}
์๋ ์ ํ ํ ์คํธ ๋ฐฉ๋ฒ
์๋ ์ ํ ์ฝ๋๊ฐ ์ ์๋ํ๋์ง ํ
์คํธํ ๋, ๋นจ๋ฆฌ ๊ฐ๊ธฐ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค. jest
๋ฅผ ์ฌ์ฉํ๋ค๋ฉด mock timers
๋ฅผ ๋นจ๋ฆฌ ๊ฐ๊ธฐ ๋๊ตฌ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค. requestAnimationFrame
throttling์ ์ฌ์ฉํ๋ค๋ฉด ์ ๋๋ฉ์ด์
ํ๋ ์์ ํฑ์ ์ ์ดํ๊ธฐ ์ํ ํด๋ก
raf-stub
๋ฅผ ๋ณด๋ฉด ์ข์ต๋๋ค.