(번역) 훌륭한 리액트 개발자가 하지 않는 다섯가지 실수들
@meric.emmanuel 의 Great React engineers don’t make these five mistakes 를 한국어로 번역한 글입니다.

1. React.memo를 인라인으로 사용하기
우리가 화살표 함수를 사용한 컴포넌트를 memo 내부에서 정의하게 된다면, 컴포넌트는 React 프로파일러에서 디스플레이 네임이 명시되지 않습니다.
import { memo } from 'react'
// This will show 'Anonymous' in the profiler
export const MyComponent = memo((props) => /* ... */)
대신, 컴포넌트는 Anonymous 로 명시되어 우리는 어떤 컴포넌트가 추가적인 렌더링을 발생하는지 알 수 없게 됩니다.

아쉬운 점은 memo 를 사용했다면 프로파일러로 메모이제이션이 올바르게 동작하는지 확인해보고 싶었을 텐데 그럴 수 없다는 것입니다.
컴포넌트를 분리된 변수로 정의한다면 이러한 현상을 피할 수 있습니다.
const MyComponentInternal = (props) => /* ... */
// This will show 'MyComponent' in the profiler
export const MyComponent = memo(MyComponentInternal)
참고로, 올바르게 수행되지 않은 메모이제이션은 당신의 앱을 더 느리게 만듭니다. 매 렌더마다 props 를 비교하는 건 비용이 발생합니다. 렌더링 절약 효과가 이 비용보다 크지 않다면 당신은 memo 를 사용하여 성능을 저하시키게 될 수 있습니다.
2. 각 <Route> 마다 동일한 에러 바운더리 사용하기
만일 당신의 앱이 여러 페이지로 구성이 되어 있고, 이를 하나의 에러 바운더리로 감싸게 된다면 사용자는 다른 페이지로 이동할 때 에러 화면이 사라지는 것을 보지 못할 수 있습니다.
// The error screen won't go away when navigating to another page
<Route path="books" element={
<ErrorBoundary>
<Books/>
</ErrorBoundary>
}/>
<Route path="users" element={
<ErrorBoundary>
<Users/>
</ErrorBoundary>
}/>
<Route path="settings" element={
<ErrorBoundary>
<Settings/>
</ErrorBoundary>
}/>
이는 에러 바운더리가 React와 동일하게 작동하기 때문에, 페이지를 이동할 때 컴포넌트 업데이트로 간주되어 내부 에러 상태가 그대로 유지되기 때문입니다.
이 경우에는 key props 를 사용하여 에러 바운더리를 동일하지 않게 만들 수 있습니다.
<Route path="books" element={
<ErrorBoundary key="books">
<Books/>
</ErrorBoundary>
}/>
<Route path="users" element={
<ErrorBoundary key="users">
<Users/>
</ErrorBoundary>
}/>
<Route path="settings" element={
<ErrorBoundary key="settings">
<Settings/>
</ErrorBoundary>
}/>
또 다른 옵션으로는, URL path 의 basename 를 key 로 받아 사용하는 커스텀 에러 바운더리 컴포넌트를 만들 수 있습니다.
const PageErrorBoundary = ({ children }) => {
const match = useMatch('*')
return <ErrorBoundary key={match.pathBasename}>{children}</ErrorBoundary>
}
3. .sort 를 가변적으로 사용하기
.sort 는 원본 배열을 직접 수정합니다. 항상 그렇듯이, 변경 가능한 업데이트는 React에서 온갖 종류의 버그를 만들어냅니다. 불변적인 업데이트만 사용해야 합니다.
const items = [3, 1, 2]
// This mutates the original array
items.sort((a, b) => a - b)
console.log(items)
// [1, 2, 3]
만일 .sort 를 상태에 사용하게 된다면 다른 컴포넌트는 이것을 정렬되지 않은 원본으로 읽으려 할 것이고, 이는 결국 버그를 만들어내게 됩니다.
물론 setItems(items.sort()) 처럼 상태 업데이트에 사용할 수 있지만 이는 참조값이 바뀌지 않는다면 아무 동작도 하지 않을 것입니다.
그 대신 불변성을 가진 .toSorted 를 사용해볼 수 있지만 이는 구형 브라우저에서 지원되지 않을 수 있습니다. 구형 브라우저에서는 스프레드 연산자를 사용하는 것도 하나의 방법입니다.
return (
// Use spread operator to avoid mutations
[...items]
.sort((i1, i2) => i1.name.localeCompare(i2.name))
)
4. items.length && <List items={items} />
리액트는 숫자를 렌더합니다. 그래서 배열의 길이가 0이라면 이는 배열의 목록 대신 UI 에 0 이라는 문자가 노출될 것입니다.
간단하게 items.length > 0 를 사용하여 조건을 대신할 수 있습니다.
items.length && <List items={items} /> // This will display 0
items.length > 0 && <List items={items} /> // This is fine
5. useLayoutEffect 대신 useEffect 사용하기
useEffect 는 각 렌더에 즉각적으로 반영되지 않고 DOM이 업데이트 된 이후 약간의 딜레이가 소요됩니다. 결과적으로 사용자는 최종으로 변경된 상태가 아닌 상태를 바로 보게 됩니다.
이는 우아하지 않은, 깜빡임을 발생시킵니다.
이를 대신하여 useLayoutEffect 는 DOM이 업데이트된 이후 동기적으로 실행되어 화면이 다시 로드될 시간이 생기기 전에 실행되어 사용자는 항상 최종으로 변경된 상태를 볼 수 있습니다.
또한 useLayoutEffect 는 깜빡임을 방지할 수 있습니다. 만일 UI 영향이 없는 네트워크 요청, 동기 이벤트 리스너 작업을 할 때에는 useEffect 를 사용해도 괜찮을 것입니다.