(번역) 훌륭한 리액트 개발자가 하지 않는 다섯가지 실수들

reactGreat React engineers don’t make these five mistakesuseMemokr-only
2025-09-13

@meric.emmanuelGreat React engineers don’t make these five mistakes 를 한국어로 번역한 글입니다.

first

1. React.memo를 인라인으로 사용하기

우리가 화살표 함수를 사용한 컴포넌트를 memo 내부에서 정의하게 된다면, 컴포넌트는 React 프로파일러에서 디스플레이 네임이 명시되지 않습니다.

import { memo } from 'react'

// This will show 'Anonymous' in the profiler

export const MyComponent = memo((props) => /* ... */)

대신, 컴포넌트는 Anonymous 로 명시되어 우리는 어떤 컴포넌트가 추가적인 렌더링을 발생하는지 알 수 없게 됩니다.

second

아쉬운 점은 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 를 사용해도 괜찮을 것입니다.