๐ป ๋ฆฌ์กํธ์์ DOM ์ง์ ์กฐ์: ๊ฐ์ DOM ๋ถ์ผ์น์ ์์ธก ๋ถ๊ฐ๋ฅํ UI
Summary
๋ฆฌ์กํธ์์ document.getElementById
์ ๊ฐ์ ์ง์ DOM ์กฐ์์ ๊ฐ์ DOM๊ณผ์ ๋ถ์ผ์น๋ฅผ ์ผ๊ธฐํ์ฌ ์์ธก ๋ถ๊ฐ๋ฅํ UI ๋ฒ๊ทธ์ ์ฑ๋ฅ ๋ฌธ์ ๋ฅผ ์ด๋ํฉ๋๋ค. ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ์ํ(state)์ ์์ฑ(props)์ ํตํด UI๋ฅผ ์ ์ธ์ ์ผ๋ก ๊ด๋ฆฌํ๊ณ , ์์ธ์ ์ธ ์ํฉ์์๋ง useRef
์ useEffect
๋ฅผ ์ฌ์ฉํ์ฌ ์ ํ์ ์ผ๋ก DOM์ ์ ๊ทผํด์ผ ํฉ๋๋ค.
Why Wrong?
๋ฆฌ์กํธ(React)๋ ํจ์จ์ ์ธ UI ์ ๋ฐ์ดํธ๋ฅผ ์ํด ๊ฐ์ DOM(Virtual DOM)์ด๋ผ๋ ์ถ์ํ ๊ณ์ธต์ ์ฌ์ฉํฉ๋๋ค. ๊ฐ๋ฐ์๊ฐ ์ปดํฌ๋ํธ์ ์ํ(state)๋ ์์ฑ(props)์ ๋ณ๊ฒฝํ๋ฉด, ๋ฆฌ์กํธ๋ ์๋ก์ด ๊ฐ์ DOM์ ์์ฑํ๊ณ ์ด์ ๊ฐ์ DOM๊ณผ ๋น๊ต(Reconciliation)ํ์ฌ ์ค์ DOM์ ์ต์ํ์ ๋ณ๊ฒฝ๋ง ์ ์ฉํฉ๋๋ค. ์ด๋ฌํ ๋ฆฌ์กํธ์ ์ ์ธ์ (Declarative) ๋ฐฉ์์ ๊ฐ๋ฐ์๊ฐ UI์ '์ต์ข ์ํ'๋ง ์ ์ํ๋ฉด ๋๋ฏ๋ก ์ฝ๋์ ์์ธก ๊ฐ๋ฅ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ๋์ฌ์ค๋๋ค.
ํ์ง๋ง ์ฝ๋ ๋ด์์ document.getElementById
, querySelector
๋ฑ์ ํตํด ์ค์ DOM ์์๋ฅผ ์ง์ ์กฐ์ํ๊ฒ ๋๋ฉด, ๋ฆฌ์กํธ์ ๊ฐ์ DOM์ ์ด๋ฌํ ์ธ๋ถ ๋ณ๊ฒฝ ์ฌํญ์ ์์ง ๋ชปํด ๋ด๋ถ ์ํ์ ์ค์ DOM ๊ฐ์ ๋ถ์ผ์น๊ฐ ๋ฐ์ํฉ๋๋ค. ์ด๋ก ์ธํด ๋ค์๊ณผ ๊ฐ์ ์ฌ๊ฐํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค:
- ์์ธก ๋ถ๊ฐ๋ฅํ UI ๋์: ๋ฆฌ์กํธ๊ฐ ๋ค์ ๋ ๋๋ง ์ฃผ๊ธฐ์์ ์์ ์ ๊ฐ์ DOM์ ๋ฐ๋ผ ์ค์ DOM์ ๋ค์ ์ ๋ฐ์ดํธํ ๋, ๊ฐ๋ฐ์๊ฐ ์ง์ ์กฐ์ํ ๋ด์ฉ์ด ์๋์น ์๊ฒ ๋ฎ์ด์์์ง๊ฑฐ๋, ์ฌ๋ผ์ง๊ฑฐ๋, ์๋ชป๋ ์ํ๋ก ๋๋์๊ฐ ์ ์์ต๋๋ค.
- ์ฑ๋ฅ ์ ํ: ๋ฆฌ์กํธ์ ์ต์ ํ๋ Reconciliation ํ๋ก์ธ์ค๊ฐ ์ง์ ์ ์ธ DOM ๋ณ๊ฒฝ์ผ๋ก ์ธํด ๋ฐฉํด๋ฐ์ ์ ์์ต๋๋ค. ๋ฆฌ์กํธ๊ฐ ๋ณ๊ฒฝ์ ๊ฐ์งํ์ง ๋ชปํด ๋ถํ์ํ ์ ์ฒด ์ฌ๋ ๋๋ง์ ์ ๋ฐํ๊ฑฐ๋, ์๋๋ฉด ๋ฐ๋๋ก ํ์ํ ์ ๋ฐ์ดํธ๋ฅผ ๋์น ์ ์์ต๋๋ค.
- ๋๋ฒ๊น ์ ์ด๋ ค์: UI๊ฐ ์์๋๋ก ๋์ํ์ง ์์ ๋, ๋ฆฌ์กํธ์ ์ํ์ ์ค์ DOM์ ๋ถ์ผ์น๋ก ์ธํด ๋ฒ๊ทธ์ ์์ธ์ ํ์ ํ๊ธฐ๊ฐ ๋งค์ฐ ๋ณต์กํด์ง๋๋ค. ๊ฐ๋ฐ์ ๋๊ตฌ์ ์ปดํฌ๋ํธ ํญ์์ ๋ณด๋ ์ํ์ ์ค์ ํ๋ฉด์ด ๋ค๋ฅผ ์ ์์ต๋๋ค.
- ํ๋ ์์ํฌ์ ์ด์ ์์ค: ๋ฆฌ์กํธ์ ๊ฐ์ ์ ์ธ์ UI ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ์ฃผ๋ ์ด์ ์ค ํ๋์ธ 'DOM ์กฐ์์ ์ถ์ํ' ์ด์ ์ ์์คํ๊ณ , ๋ณต์กํ ์ ์์ค DOM API๋ฅผ ์ง์ ๊ด๋ฆฌํด์ผ ํ๋ ๋ถ๋ด์ด ์๊น๋๋ค.
How to Fix?
๋ฆฌ์กํธ ์ฑ์์๋ ํญ์ '๋ฆฌ์กํธ ๋ฐฉ์'์ผ๋ก DOM์ ์กฐ์ํด์ผ ํฉ๋๋ค. ์ฆ, ์ปดํฌ๋ํธ์ ์ํ(state)๋ ์์ฑ(props)์ ๋ณ๊ฒฝํ์ฌ ๋ฆฌ์กํธ๊ฐ ์๋์ผ๋ก ์ค์ DOM์ ์ ๋ฐ์ดํธํ๋๋ก ํ๋ ์ ์ธ์ ๋ฐฉ์์ ์ฌ์ฉํด์ผ ํฉ๋๋ค. ์ด๋ ๋ฆฌ์กํธ๊ฐ ๊ฐ์ฅ ํจ์จ์ ์ด๊ณ ์์ธก ๊ฐ๋ฅํ๊ฒ ์๋ํ๋ ๋ฐฉ์์ ๋๋ค.
- ์ํ(State)๋ฅผ ํตํ UI ์ ์ด: ๋๋ถ๋ถ์ UI ๋ณ๊ฒฝ์ ์ปดํฌ๋ํธ์ ์ํ๋ฅผ ์ ๋ฐ์ดํธํจ์ผ๋ก์จ ์ด๋ฃจ์ด์ ธ์ผ ํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ์์์ ์คํ์ผ์ ๋ณ๊ฒฝํ๊ณ ์ถ๋ค๋ฉด CSS ํด๋์ค๋ฅผ ํ ๊ธํ๊ฑฐ๋ ์ธ๋ผ์ธ ์คํ์ผ์ ์ํ ๊ฐ์ ๋ฐ๋ผ ๋ณ๊ฒฝํฉ๋๋ค. ํ ์คํธ ์ฝํ ์ธ , ์์์ ๊ฐ์์ฑ ๋ฑ ๋ชจ๋ ๋์ ์ธ UI ์์๋ ์ํ์ ์ข ์๋์ด์ผ ํฉ๋๋ค.
- Refs๋ฅผ ์ด์ฉํ ์ง์ ์ ๊ทผ (์ตํ์ ์๋จ): ๋ฆฌ์กํธ ์ปดํฌ๋ํธ ์ธ๋ถ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(์: D3.js, Chart.js)์ ์ฐ๋ํ๊ฑฐ๋, ํน์ DOM ๋
ธ๋์ ํฌ๊ธฐ๋ฅผ ์ธก์ ํด์ผ ํ๋ ๊ฒฝ์ฐ, ํน์
<input>
์์์ ์ง์ focus()
๋ฅผ ์ฃผ๋ ๋ฑ ์ ๋ง๋ก ์ค์ DOM ๋ ธ๋์ ์ง์ ์ ๊ทผํด์ผ ํ๋ ๊ทนํ ์์ธ์ ์ธ ์ํฉ์์๋useRef
ํ ์ ์ฌ์ฉํฉ๋๋ค.useRef
๋ก DOM ๋ ธ๋์ ๋ํ ์ฐธ์กฐ๋ฅผ ์ป์ ํ,useEffect
ํ ๋ด์์๋ง ํด๋น ์ฐธ์กฐ๋ฅผ ํตํด ์ต์ํ์ imperative(๋ช ๋ นํ) ์์ ์ ์ํํด์ผ ํฉ๋๋ค. ์ด ๊ฒฝ์ฐ์๋ ๋ฆฌ์กํธ๊ฐ ํด๋น DOM ๋ ธ๋๋ฅผ ๋ ๋๋งํ๊ธฐ ์ ์ด๋ ํ์๋ง ์์ ์ ์ํํ๊ณ , ๋ฆฌ์กํธ๊ฐ ๊ด๋ฆฌํ๋ ๋ค๋ฅธ ๋ถ๋ถ๊ณผ ์ถฉ๋ํ์ง ์๋๋ก ์ฃผ์ํด์ผ ํฉ๋๋ค.
Before Code (Bad)
```jsx
import React from 'react';
function DirectDomManipulator() {
const handleClick = () => {
// ๐จ ์ํฐํจํด: ๋ฆฌ์กํธ ์ปดํฌ๋ํธ ๋ด์์ document ๊ฐ์ฒด๋ฅผ ํตํ ์ง์ DOM ์กฐ์
const element = document.getElementById('my-styled-div');
if (element) {
element.style.backgroundColor = 'lightblue';
element.textContent = 'DOM ์ง์ ๋ณ๊ฒฝ๋จ!';
// ๋ง์ฝ ์ด ์ปดํฌ๋ํธ๊ฐ ๋ค์ ๋ ๋๋ง๋๋ฉด,
// ๋ฆฌ์กํธ๋ ์์ ์ ์ํ๊ฐ ์๋ ์ด ์ง์ ๋ณ๊ฒฝ๋ ๋ด์ฉ์ ๋ฎ์ด์์ธ ์ ์์ต๋๋ค.
}
};
return (
<div>
<div id="my-styled-div" style={{ backgroundColor: 'lightgray', padding: '20px' }}>
์๋ ๋ด์ฉ
</div>
<button onClick={handleClick}>DOM ์ง์ ๋ณ๊ฒฝ</button>
</div>
);
}
export default DirectDomManipulator;
```
**๋ฌธ์ ์ **: ์ ์ฝ๋์์ `my-styled-div` ์์๋ ๋ฆฌ์กํธ์ ์ํด ๋ ๋๋ง๋์ง๋ง, ๋ฒํผ ํด๋ฆญ ์ `document.getElementById`๋ฅผ ํตํด ์ง์ ์กฐ์๋ฉ๋๋ค. ์ด๋ ๋ฆฌ์กํธ์ ๊ฐ์ DOM๊ณผ ์ค์ DOM ๊ฐ์ ๋ถ์ผ์น๋ฅผ ์ด๋ํ๋ฉฐ, ์ถํ `DirectDomManipulator` ์ปดํฌ๋ํธ๊ฐ ๋ค์ ๋ ๋๋ง๋ ๋(`props`๋ `state` ๋ณ๊ฒฝ ๋ฑ) ์ง์ ๋ณ๊ฒฝ๋ ๋ด์ฉ์ด ๋ฆฌ์กํธ์ ๊ด๋ฆฌํ์ ์๋ ์ด๊ธฐ ์ํ๋ก ๋๋์๊ฐ๊ฑฐ๋, ์์์น ๋ชปํ ๋์์ ํ์ฌ ๋๋ฒ๊น
์ด ๋งค์ฐ ์ด๋ ค์์ง๋๋ค.
After Code (Good)
```jsx
import React, { useState, useRef, useEffect } from 'react';
// โ
์ฌ๋ฐ๋ฅธ ํจํด: ์ํ(state)๋ฅผ ํตํด UI ๋ณ๊ฒฝ ์ ์ด
function StateControlledComponent() {
const [bgColor, setBgColor] = useState('lightgray');
const [content, setContent] = useState('์๋ ๋ด์ฉ');
const handleClick = () => {
setBgColor('lightblue');
setContent('์ํ๋ฅผ ํตํด ๋ณ๊ฒฝ๋จ!');
};
return (
<div>
<div style={{ backgroundColor: bgColor, padding: '20px' }}>
{content}
</div>
<button onClick={handleClick}>์ํ๋ฅผ ํตํด ๋ณ๊ฒฝ</button>
</div>
);
}
// โก๏ธ (์ฐธ๊ณ ) Refs๋ฅผ ์ด์ฉํ ์ ํ์ ์ธ DOM ์ง์ ์ ๊ทผ ์์ (์ตํ์ ์๋จ)
function RefDomManipulator() {
const inputRef = useRef(null);
useEffect(() => {
// ์ปดํฌ๋ํธ ๋ง์ดํธ ์ input์ ํฌ์ปค์ค
// ์ด์ฒ๋ผ ๋ฆฌ์กํธ๊ฐ ์ง์ ์ ๊ณตํ์ง ์๋ ์ ์์ค DOM API๊ฐ ํ์ํ ๋ useRef๋ฅผ ์ฌ์ฉํฉ๋๋ค.
if (inputRef.current) {
inputRef.current.focus();
}
}, []); // ๋ง์ดํธ ์ ํ ๋ฒ๋ง ์คํ๋๋๋ก ๋น ์์กด์ฑ ๋ฐฐ์ด ์ฌ์ฉ
const handleButtonClick = () => {
if (inputRef.current) {
inputRef.current.value = 'Refs๋ก ๊ฐ ์ค์ '; // input์ value ์์ฑ๋ state๋ก ๊ด๋ฆฌํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ด๋, ์์๋ฅผ ์ํด ์ฌ์ฉ
}
};
return (
<div>
<input ref={inputRef} type="text" placeholder="์ฌ๊ธฐ์ ํฌ์ปค์ค" />
<button onClick={handleButtonClick}>Input ๊ฐ ์ค์ </button>
<hr />
<StateControlledComponent />
</div>
);
}
export default RefDomManipulator;
```
**์ค๋ช
**: ์ฒซ ๋ฒ์งธ `StateControlledComponent` ์์๋ `useState` ํ
์ ์ฌ์ฉํ์ฌ ๋ฐฐ๊ฒฝ์๊ณผ ๋ด์ฉ์ ์ํ๋ก ๊ด๋ฆฌํฉ๋๋ค. ๋ฒํผ ํด๋ฆญ ์ ์ํ๋ง ์
๋ฐ์ดํธํ๋ฉด ๋ฆฌ์กํธ๊ฐ ์์์ ํด๋น ์ํ์ ๋ง์ถฐ UI๋ฅผ ํจ์จ์ ์ผ๋ก ๋ค์ ๋ ๋๋งํฉ๋๋ค. ์ด๋ ๋ฆฌ์กํธ์ ์ ์ธ์ ํจ๋ฌ๋ค์์ ๋ถํฉํ๋ ์ฌ๋ฐ๋ฅธ ๋ฐฉ์์ด๋ฉฐ, ๊ฐ์ DOM๊ณผ์ ๋ถ์ผ์น ๋ฌธ์ ๋ฅผ ์์ฒ์ ์ผ๋ก ๋ฐฉ์งํฉ๋๋ค.
๋ ๋ฒ์งธ `RefDomManipulator` ์์๋ `useRef`๋ฅผ ์ฌ์ฉํ์ฌ ํน์ DOM ๋
ธ๋(input)์ ์ง์ ์ ๊ทผํ๋ ๊ฒฝ์ฐ์
๋๋ค. ํ์ง๋ง ์ด ๊ฒฝ์ฐ์๋ `useEffect` ํ
๋ด์์๋ง DOM ์กฐ์์ ์ํํ์ฌ ๋ฆฌ์กํธ์ ๋ผ์ดํ์ฌ์ดํด์ ๋ง์ถฐ ์คํ๋๋๋ก ํฉ๋๋ค. ์ด๋ฐ ๋ฐฉ์์ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฐ๋, ํฌ์ปค์ค ๊ด๋ฆฌ, ๋ฏธ๋์ด ์ฌ์ ์ ์ด ๋ฑ ๋ฆฌ์กํธ๊ฐ ์ง์ ์ ๊ณตํ์ง ์๋ ์ ์์ค DOM API๊ฐ ํ์ํ ๊ฒฝ์ฐ์๋ง ์ ํ์ ์ผ๋ก ์ฌ์ฉํด์ผ ํฉ๋๋ค. ๋๋ถ๋ถ์ UI ์ํธ์์ฉ์ ์ํ๋ฅผ ํตํด ๊ด๋ฆฌ๋์ด์ผ ํฉ๋๋ค.