리액트를 사용하며 겪었던 문제들을 풀어내 리액트의 이해를 돕습니다.
하루 한 문제 목표 🤔
컨트리뷰트는 대환영입니다. 🤗
const MyInput = (props) => {
console.log('why render every times T.T')
return (
<input {...props} />
)
}
const App = () => {
const [text, setText] = useState("")
const handleChangeText = (e) => {
setText(e.target.value)
}
return (
<>
<MyInput onChange={handleChangeText} />
</>
)
}
정답
컴포넌트가 렌더링 될 때는 state, props가 변할 때입니다. javascript에서는 object를 주소값으로 비교합니다. handleChangeText
는 Function object입니다. 매 렌더링시에 handleChangeText
는 새로운 주소값에 할당되어 MyInput
의 props(onChange)가 변한 것으로 React는 판단합니다.
이 문제를 해결하기 위해 (일반적으로)React.useCallback 혹은 React.memo을 사용할 수 있습니다.
const MyList = ({datas}) => {
return (
<ul>
{datas.map(x => (
<li key={`li-${x}`}>
{x}
</li>
))}
</ul>
)
}
const App = () => {
const [datas, setDatas] = useState(['hello', 'hola', 'bonjour'])
const addData = () => {
datas.push('안녕')
setDatas(datas)
}
return (
<>
<button onClick={addData}>add</button>
<MyList datas={datas} />
</>
)
}
정답
datas
에 push를 해도 datas
의 주소값이 바뀌진 않습니다. 하지만, React는 주소값이 바뀌는 걸 통해 변화를 감지합니다.*1번 문제 참고 즉, 리액트는 datas
가 바뀌지 않은 걸로 판단하여 똑똑하게 렌더링하지 않은 겁니다.
이 문제를 해결하기 위해선 불변성에 대해 알아야합니다. 또, 전개 구문은 불변성을 지키는 똑똑한 방법입니다.
const MyTodoList = ({todos}) => {
return (
<>
{todos.map((todo, index) => (
<li key={index}>
{todo.name}
<input type="checkbox" />
</li>
))}
</>
)
}
const App = () => {
const [todos, setTodos] = useState([
{ name: 'homework' },
{ name: 'washing dishes' },
{ name: 'make todoList' },
])
const handleClick = () => {
setTodos([...todos].reverse())
}
return (
<>
<button onClick={handleClick}>I don't care, just shake!</button>
<ul>
<MyTodoList todos={todos} />
</ul>
</>
)
}
정답
React는 리스트 렌더링에서 key
로 어떤 항목을 변경, 추가 또는 삭제할지 판단합니다. index를 key로 사용한다면, 우리가 배열을 reverse
했을 때에 React는 이렇게 판단합니다. 아하! homework
가 make todoList
라는 문자열로 바뀌었구나? 그럼 문자열만 바꿔주면 되겠다!
이 문제를 해결하기 위해 key
에 id
를 넘겨주세요. 고유한 id
값을 넘겨주면 원하는 대로 작동할 것 입니다. 읽어보면 좋은 글로는, 왜 key가 필요한가에 대한 더 자세한 설명 그리고 인덱스를 key로 사용할 경우 부정적인 영향에 대한 상세 설명이 있습니다.
const App = () => {
const [title, setTitle] = useState("is the title")
if (title !== "") {
useEffect(() => {
console.log(title)
})
}
const [content, setContent] = useState("is the content")
if (content !== "") {
useEffect(() => {
console.log(content)
})
}
return (
...
)
}
정답
정확한 의미로는, 무조건 함수의 최상위에서만 Hook
을 사용해야만 합니다. 이는 React
가 Hook
의 상태를 올바르게 유지할 수 있게 도와줍니다. 더 설명하자면, React는 Hook
의 실행 순서에 의존해서 state
(어떨땐 useEffect
의 callback)를 저장합니다.
예를 들어, 위의 예제에서는(조건문을 빼면), ["is the title", () => { console... }, "is the content", () => { console... }]
의 상태로 React는 저장합니다. 만약 조건문의 조건이 false
가 되어 Hook
의 실행 순서가 변한다면 어떨까요? state
와 Hook
을 연결시켜주지 못해 버그가 발생합니다.
이 문제를 해결하려면 조건문을 useEffect
안으로 넣으세요! 오직 그것 뿐입니다. 더 중요한 건, 모든 Hook
은 최상위에서만 호출하세요! 읽어보면 좋은 글로는, Hook의 첫번째 규칙 그리고 Hook의 규칙이 있습니다.
const myMessage = () => {
return (
<p>
Hi there, is my message
</p>
)
}
const App = () => {
return (
<div>
<myMessage></myMessage>
</div>
)
}
정답
만약 컴포넌트의 첫 글자를 소문자로 작성한다면, 리액트는 해당 컴포넌트를 built-in
요소(e.g. div, p, span)로 판단합니다.
Fun Fact
: 당신이 div
라는 이름을 가진 아주 멋진 컴포넌트를 만들었어도, 리액트는 이 컴포넌트를 built-in
요소로 판단해서 그저 빈 div
태그를 출력해줄 뿐입니다. 이 경우, 리액트는 어떠한 에러 혹은 경고를 개발자에게 알려주지 않습니다.
이 문제를 해결하려면 컴포넌트의 첫 글자를 대문자로 작성하세요! 오직 그것 뿐입니다. 읽어보면 좋은 글로는, 케이스 네이밍 컨벤션이 있습니다.