React 면접에서 가장 자주 묻는 질문 30선: 합격을 위한 완벽 가이드

2026년 React 면접에서 가장 자주 출제되는 30가지 질문을 정리했습니다. 상세한 답변과 코드 예제로 React 개발자 합격을 준비할 수 있습니다.

React 면접 질문 일러스트. 컴포넌트와 훅이 서로 연결된 모습

React 기술 면접에서는 핵심 개념, 고급 패턴, 그리고 모범 사례에 대한 이해도를 평가합니다. 이 가이드는 가장 자주 출제되는 30가지 질문을 정리하고, 효과적인 준비를 돕기 위해 상세한 답변과 코드 예제를 함께 제공합니다.

준비를 위한 조언

질문은 난이도 순으로 정리되어 있습니다. 고급 개념을 다루기 전에 기초를 충분히 익히면 보다 체계적인 준비가 가능합니다.

React 기초

1. Virtual DOM이란 무엇이며 React는 왜 사용하는가

Virtual DOM은 실제 DOM을 가볍게 표현한 JavaScript 객체입니다. React는 이 추상화를 통해 인터페이스 업데이트를 최적화합니다.

처리 과정은 세 단계로 이루어집니다. React는 먼저 DOM의 가상 복사본을 만들고, 변경이 발생하면 이전 버전과 비교하며(diffing 알고리즘), 마지막으로 필요한 변경 사항만 실제 DOM에 적용합니다(reconciliation).

jsx
// Simplified example of the concept
// When state changes, React doesn't recreate the entire DOM
function Counter() {
  const [count, setCount] = useState(0)

  // Only the span containing count will be updated in the real DOM
  // The rest of the component is untouched
  return (
    <div>
      <h1>Counter</h1>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  )
}

이러한 방식 덕분에 비용이 큰 DOM 작업을 피할 수 있고, 복잡한 인터페이스에서도 빠른 업데이트가 가능합니다.

2. 함수형 컴포넌트와 클래스 컴포넌트의 차이는 무엇인가

함수형 컴포넌트는 props를 받아 JSX를 반환하는 JavaScript 함수입니다. React 16.8부터 훅이 도입되면서 함수형 컴포넌트에서도 상태와 라이프사이클을 다룰 수 있게 되었습니다.

jsx
// Functional component (recommended)
// More concise, easier to test, supports hooks
function Welcome({ name }) {
  const [visits, setVisits] = useState(0)

  useEffect(() => {
    setVisits(v => v + 1)
  }, [])

  return <h1>Hello {name}, visit #{visits}</h1>
}

// Class component (legacy)
// More verbose, requires this binding
class WelcomeClass extends React.Component {
  state = { visits: 0 }

  componentDidMount() {
    this.setState(prev => ({ visits: prev.visits + 1 }))
  }

  render() {
    return <h1>Hello {this.props.name}, visit #{this.state.visits}</h1>
  }
}

오늘날에는 함수형 컴포넌트가 표준입니다. 클래스 컴포넌트는 여전히 지원되지만, 신규 프로젝트에서는 권장되지 않습니다.

3. JSX는 어떻게 동작하는가

JSX는 JavaScript의 구문 확장으로, 코드 안에 마크업을 작성할 수 있게 해줍니다. HTML이 아니라 JavaScript를 보기 좋게 표현한 것입니다.

jsx
// What we write (JSX)
const element = (
  <div className="container">
    <h1>Title</h1>
    <p>Paragraph</p>
  </div>
)

// What Babel compiles (pure JavaScript)
const element = React.createElement(
  'div',
  { className: 'container' },
  React.createElement('h1', null, 'Title'),
  React.createElement('p', null, 'Paragraph')
)

HTML과의 차이점으로는 class 대신 className, for 대신 htmlFor, 속성에 카멜케이스 사용(onClick, tabIndex), 자체 닫힘 태그의 명시적 종료 등이 있습니다.

4. state와 props의 차이는 무엇인가

Props는 부모 컴포넌트에서 자식 컴포넌트로 전달되는 데이터로, 읽기 전용입니다. State는 컴포넌트 내부의 상태이며 setter를 통해 변경할 수 있습니다.

UserCard.jsxjsx
// name and role are props (immutable)
function UserCard({ name, role }) {
  // isExpanded is state (mutable)
  const [isExpanded, setIsExpanded] = useState(false)

  return (
    <div className="card">
      <h2>{name}</h2>
      <p>{role}</p>

      {/* Modifying state triggers a re-render */}
      <button onClick={() => setIsExpanded(!isExpanded)}>
        {isExpanded ? 'Collapse' : 'Details'}
      </button>

      {isExpanded && <UserDetails name={name} />}
    </div>
  )
}

// Usage
<UserCard name="Alice" role="Developer" />

기본 원칙은 props는 위에서 아래로(부모에서 자식으로) 흐르고, state는 각 컴포넌트에 국한된다는 점입니다.

5. 리스트에서 key가 중요한 이유는 무엇인가

Key는 리스트의 어떤 요소가 변경, 추가, 제거되었는지 React가 식별할 수 있도록 돕습니다. 고유하고 안정적인 key가 없다면 React는 예상치 못한 동작을 보일 수 있습니다.

jsx
// ❌ Bad practice: index as key
// Problem: if order changes, React loses tracking
{items.map((item, index) => (
  <ListItem key={index} item={item} />
))}

// ✅ Good practice: unique and stable identifier
{items.map(item => (
  <ListItem key={item.id} item={item} />
))}

// Concrete example of the problem with indices
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React' },
    { id: 2, text: 'Create a project' }
  ])

  // When deleting the first element with key={index}
  // React will think element 0's content changed
  // instead of understanding an element was removed

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

React Hooks

6. useState와 자주 발생하는 함정에 대해 설명한다

useState는 함수형 컴포넌트의 로컬 상태를 관리합니다. setter는 값 또는 업데이트 함수를 인자로 받을 수 있습니다.

jsx
// Declaration with initial value
const [count, setCount] = useState(0)

// ❌ Pitfall: multiple updates in the same cycle
function increment() {
  setCount(count + 1) // count = 0, sets 1
  setCount(count + 1) // count = 0 still, sets 1
  setCount(count + 1) // count = 0 still, sets 1
  // Final result: 1 (not 3)
}

// ✅ Solution: use the update function
function incrementCorrect() {
  setCount(prev => prev + 1) // 0 → 1
  setCount(prev => prev + 1) // 1 → 2
  setCount(prev => prev + 1) // 2 → 3
  // Final result: 3
}

// ❌ Pitfall: object mutation
const [user, setUser] = useState({ name: 'Alice', age: 25 })
user.age = 26 // Direct mutation, no re-render

// ✅ Solution: create a new object
setUser({ ...user, age: 26 })
// or
setUser(prev => ({ ...prev, age: 26 }))

7. useEffect는 의존성 배열과 함께 어떻게 동작하는가

useEffect는 렌더링 이후에 사이드 이펙트를 실행합니다. 의존성 배열은 이펙트가 언제 다시 실행될지를 제어합니다.

jsx
// Executed on every render (rare, usually avoid)
useEffect(() => {
  console.log('Render completed')
})

// Executed only on mount (equivalent to componentDidMount)
useEffect(() => {
  console.log('Component mounted')

  // Cleanup on unmount (equivalent to componentWillUnmount)
  return () => {
    console.log('Component unmounted')
  }
}, [])

// Executed when userId changes
useEffect(() => {
  async function fetchUser() {
    const response = await fetch(`/api/users/${userId}`)
    const data = await response.json()
    setUser(data)
  }

  fetchUser()
}, [userId])

// ❌ Missing dependency - subtle bug
useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1) // count is "captured" at its initial value
  }, 1000)
  return () => clearInterval(timer)
}, []) // count is missing from dependencies

// ✅ Fix with update function
useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1) // No need for count in deps
  }, 1000)
  return () => clearInterval(timer)
}, [])
ESLint 규칙

누락된 의존성을 감지하기 위해 eslint-plugin-react-hooks를 항상 활성화하는 것이 좋습니다. 이 규칙은 진단하기 어려운 많은 버그를 사전에 방지해 줍니다.

8. useMemo와 useCallback은 언제 사용해야 하는가

이 두 훅은 메모이제이션을 통해 불필요한 재계산이나 함수 재생성을 피하는 데 사용됩니다. 다만 과도하게 사용하지 않도록 주의해야 합니다.

jsx
// useMemo: memoizes a computed value
function ProductList({ products, filter }) {
  // Recalculated only if products or filter change
  const filteredProducts = useMemo(() => {
    console.log('Filtering...')
    return products.filter(p => p.category === filter)
  }, [products, filter])

  return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}

// useCallback: memoizes a function
function ParentComponent() {
  const [count, setCount] = useState(0)

  // Without useCallback, handleClick is recreated on every render
  // Causing unnecessary re-renders of ExpensiveChild
  const handleClick = useCallback((id) => {
    console.log('Clicked:', id)
  }, []) // Empty deps = stable function

  return (
    <>
      <span>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      {/* React.memo on ExpensiveChild for this to be effective */}
      <ExpensiveChild onClick={handleClick} />
    </>
  )
}

// ❌ Over-optimization: not needed here
const SimpleComponent = () => {
  // This calculation is trivial, useMemo adds overhead
  const doubled = useMemo(() => 2 * 2, [])
  return <span>{doubled}</span>
}

이 훅들은 명확한 성능 문제가 확인되었거나, 메모이제이션된 컴포넌트로 전달되는 참조를 안정화해야 할 때만 사용해야 합니다.

9. useRef는 어떻게 동작하며 어떤 용도로 사용하는가

useRef는 렌더링 사이에서 유지되는 가변 참조를 만듭니다. 값이 바뀌어도 재렌더링은 발생하지 않습니다.

jsx
// Case 1: Access a DOM element
function TextInput() {
  const inputRef = useRef(null)

  const focusInput = () => {
    inputRef.current.focus()
  }

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </>
  )
}

// Case 2: Store a mutable value without re-render
function Timer() {
  const [seconds, setSeconds] = useState(0)
  const intervalRef = useRef(null)

  const start = () => {
    // Store the interval ID to be able to stop it
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1)
    }, 1000)
  }

  const stop = () => {
    clearInterval(intervalRef.current)
  }

  return (
    <div>
      <span>{seconds}s</span>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  )
}

// Case 3: Keep the previous value
function usePrevious(value) {
  const ref = useRef()

  useEffect(() => {
    ref.current = value
  }, [value])

  return ref.current
}

10. useContext를 설명하고 언제 사용하는지 알아본다

useContext는 prop drilling 없이 React 컨텍스트에 접근할 수 있게 해줍니다. 테마나 로그인 사용자처럼 전역 데이터에 적합합니다.

1. Create the contextjsx
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {}
})

// 2. Create the provider
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  const toggleTheme = useCallback(() => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }, [])

  // Memoize the value to avoid unnecessary re-renders
  const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme])

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

// 3. Use the context
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext)

  return (
    <button
      onClick={toggleTheme}
      className={theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-white text-gray-800'}
    >
      {theme === 'dark' ? 'Light' : 'Dark'} mode
    </button>
  )
}

// 4. Wrap the application
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
      <Footer />
    </ThemeProvider>
  )
}

React / Next.js 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

고급 패턴

11. Higher-Order Component(HOC)란 무엇인가

HOC는 컴포넌트를 인자로 받아 강화된 새 컴포넌트를 반환하는 함수입니다. 훅이 등장한 이후로 사용 빈도는 줄었지만, 일부 라이브러리에서는 여전히 사용됩니다.

jsx
// HOC that adds logging
function withLogging(WrappedComponent) {
  return function WithLogging(props) {
    useEffect(() => {
      console.log(`${WrappedComponent.name} mounted with props:`, props)

      return () => {
        console.log(`${WrappedComponent.name} unmounted`)
      }
    }, [])

    return <WrappedComponent {...props} />
  }
}

// HOC that handles authentication
function withAuth(WrappedComponent) {
  return function WithAuth(props) {
    const { user, isLoading } = useAuth()

    if (isLoading) return <LoadingSpinner />
    if (!user) return <Navigate to="/login" />

    return <WrappedComponent {...props} user={user} />
  }
}

// Usage
const ProtectedDashboard = withAuth(Dashboard)
const LoggedButton = withLogging(Button)

12. Render Props 패턴을 설명한다

Render Props 패턴은 함수 형태의 prop을 통해 컴포넌트 간에 로직을 공유하는 방식입니다.

jsx
// Component with render prop
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 })

  useEffect(() => {
    const handleMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY })
    }

    window.addEventListener('mousemove', handleMove)
    return () => window.removeEventListener('mousemove', handleMove)
  }, [])

  // Call the render function with data
  return render(position)
}

// Usage
function App() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <div>
          Position: {x}, {y}
        </div>
      )}
    />
  )
}

// Modern version with custom hook (preferred)
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 })

  useEffect(() => {
    const handleMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY })
    }

    window.addEventListener('mousemove', handleMove)
    return () => window.removeEventListener('mousemove', handleMove)
  }, [])

  return position
}

function App() {
  const { x, y } = useMousePosition()
  return <div>Position: {x}, {y}</div>
}

13. 커스텀 훅을 만드는 방법

커스텀 훅은 상태가 있는 로직을 컴포넌트 간에 추출하여 재사용할 수 있게 해줍니다.

useLocalStorage.jsjsx
function useLocalStorage(key, initialValue) {
  // Initialize with localStorage value or default
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error(error)
      return initialValue
    }
  })

  // Setter wrapper that syncs with localStorage
  const setValue = useCallback((value) => {
    try {
      // Support update functions
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)
      window.localStorage.setItem(key, JSON.stringify(valueToStore))
    } catch (error) {
      console.error(error)
    }
  }, [key, storedValue])

  return [storedValue, setValue]
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light')
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 16)

  return (
    <div>
      <select value={theme} onChange={e => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>

      <input
        type="range"
        min="12"
        max="24"
        value={fontSize}
        onChange={e => setFontSize(Number(e.target.value))}
      />
    </div>
  )
}

14. Compound Components 패턴이란 무엇인가

이 패턴은 <select><option>처럼 암묵적으로 함께 동작하는 컴포넌트를 만드는 방식입니다.

jsx
// Shared context between components
const TabsContext = createContext()

function Tabs({ children, defaultValue }) {
  const [activeTab, setActiveTab] = useState(defaultValue)

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  )
}

function TabList({ children }) {
  return <div className="tab-list" role="tablist">{children}</div>
}

function Tab({ value, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext)
  const isActive = activeTab === value

  return (
    <button
      role="tab"
      aria-selected={isActive}
      className={`tab ${isActive ? 'active' : ''}`}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  )
}

function TabPanel({ value, children }) {
  const { activeTab } = useContext(TabsContext)

  if (activeTab !== value) return null

  return (
    <div role="tabpanel" className="tab-panel">
      {children}
    </div>
  )
}

// Attach sub-components
Tabs.List = TabList
Tabs.Tab = Tab
Tabs.Panel = TabPanel

// Intuitive usage
function App() {
  return (
    <Tabs defaultValue="profile">
      <Tabs.List>
        <Tabs.Tab value="profile">Profile</Tabs.Tab>
        <Tabs.Tab value="settings">Settings</Tabs.Tab>
        <Tabs.Tab value="billing">Billing</Tabs.Tab>
      </Tabs.List>

      <Tabs.Panel value="profile">Profile content...</Tabs.Panel>
      <Tabs.Panel value="settings">Settings content...</Tabs.Panel>
      <Tabs.Panel value="billing">Billing content...</Tabs.Panel>
    </Tabs>
  )
}

15. Controlled vs Uncontrolled 패턴은 어떻게 구현하는가

Controlled 컴포넌트는 React가 상태를 관리하고, Uncontrolled 컴포넌트는 DOM을 직접 사용합니다.

jsx
// CONTROLLED component
// State is in React, component reflects that state
function ControlledInput() {
  const [value, setValue] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('Submitted value:', value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  )
}

// UNCONTROLLED component
// State is in the DOM, we read it when needed
function UncontrolledInput() {
  const inputRef = useRef(null)

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('Submitted value:', inputRef.current.value)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        ref={inputRef}
        defaultValue=""
      />
      <button type="submit">Submit</button>
    </form>
  )
}

// Component that supports BOTH modes
function FlexibleInput({ value, defaultValue, onChange }) {
  const isControlled = value !== undefined
  const [internalValue, setInternalValue] = useState(defaultValue || '')

  const currentValue = isControlled ? value : internalValue

  const handleChange = (e) => {
    if (!isControlled) {
      setInternalValue(e.target.value)
    }
    onChange?.(e.target.value)
  }

  return (
    <input
      type="text"
      value={currentValue}
      onChange={handleChange}
    />
  )
}

성능과 최적화

16. React.memo는 어떻게 동작하는가

React.memo는 props가 변경되지 않으면 재렌더링을 건너뛰도록 컴포넌트를 메모이제이션하는 HOC입니다.

jsx
// Memoized component
const ExpensiveList = React.memo(function ExpensiveList({ items, onItemClick }) {
  console.log('ExpensiveList rendered')

  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  )
})

// Parent using the memoized component
function Parent() {
  const [count, setCount] = useState(0)
  const [items] = useState([
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' }
  ])

  // ❌ This function is recreated on every render
  // So ExpensiveList re-renders despite memo
  const handleClick = (id) => console.log(id)

  // ✅ Stable function with useCallback
  const handleClickStable = useCallback((id) => {
    console.log(id)
  }, [])

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <ExpensiveList items={items} onItemClick={handleClickStable} />
    </div>
  )
}

// Custom comparison
const MemoWithCustomCompare = React.memo(
  function Component({ user, onClick }) {
    return <div onClick={onClick}>{user.name}</div>
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return prevProps.user.id === nextProps.user.id
  }
)

17. 코드 스플리팅과 React.lazy란 무엇인가

코드 스플리팅은 번들을 청크로 분할하여 필요할 때 로드함으로써 초기 로딩 시간을 단축합니다.

jsx
// Lazy loading components
const Dashboard = lazy(() => import('./Dashboard'))
const Settings = lazy(() => import('./Settings'))
const Profile = lazy(() => import('./Profile'))

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/profile" element={<Profile />} />
      </Routes>
    </Suspense>
  )
}

// Conditional lazy loading
function FeaturePanel({ showAdvanced }) {
  // Component is only loaded if needed
  const AdvancedOptions = lazy(() => import('./AdvancedOptions'))

  return (
    <div>
      <BasicOptions />

      {showAdvanced && (
        <Suspense fallback={<Skeleton />}>
          <AdvancedOptions />
        </Suspense>
      )}
    </div>
  )
}

// Named exports with lazy
const { Chart } = lazy(() =>
  import('./Charts').then(module => ({ default: module.Chart }))
)

18. 긴 리스트는 어떻게 최적화하는가

긴 리스트는 성능 문제를 일으킬 수 있습니다. 가상화(virtualization)는 화면에 보이는 요소만 렌더링합니다.

jsx
// With react-window (virtualization library)
import { FixedSizeList } from 'react-window'

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style} className="list-item">
      {items[index].name}
    </div>
  )

  return (
    <FixedSizeList
      height={400}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  )
}

// Optimization without library: pagination
function PaginatedList({ items, pageSize = 20 }) {
  const [page, setPage] = useState(0)

  const paginatedItems = useMemo(() => {
    const start = page * pageSize
    return items.slice(start, start + pageSize)
  }, [items, page, pageSize])

  const totalPages = Math.ceil(items.length / pageSize)

  return (
    <div>
      <ul>
        {paginatedItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>

      <div className="pagination">
        <button
          onClick={() => setPage(p => p - 1)}
          disabled={page === 0}
        >
          Previous
        </button>
        <span>{page + 1} / {totalPages}</span>
        <button
          onClick={() => setPage(p => p + 1)}
          disabled={page >= totalPages - 1}
        >
          Next
        </button>
      </div>
    </div>
  )
}
가상화의 적용 기준

가상화는 요소가 수백 개를 넘기 시작할 때 의미가 있습니다. 그보다 작은 리스트에서는 페이지네이션이나 점진적 로딩만으로도 충분한 경우가 많습니다.

19. 불필요한 재렌더링을 어떻게 피할 수 있는가

불필요한 재렌더링을 식별하고 제거하는 것은 성능에 매우 중요합니다. 다양한 기법으로 렌더링을 최적화할 수 있습니다.

jsx
// Technique 1: Separate components
// ❌ Everything re-renders when count changes
function BadExample() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <ExpensiveComponent /> {/* Unnecessary re-render */}
    </div>
  )
}

// ✅ ExpensiveComponent no longer re-renders
function GoodExample() {
  return (
    <div>
      <Counter />
      <ExpensiveComponent />
    </div>
  )
}

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

// Technique 2: Pass children instead of rendering directly
// ✅ children is stable, no re-render of children
function ContextProvider({ children }) {
  const [value, setValue] = useState(0)

  return (
    <Context.Provider value={value}>
      {children} {/* children doesn't re-render when value changes */}
    </Context.Provider>
  )
}

// Technique 3: Use DevTools to identify re-renders
// React DevTools > Profiler > "Highlight updates when components render"

20. React 18+의 자동 배칭(automatic batching)은 무엇인가

React 18은 재렌더링 횟수를 줄이기 위해 상태 업데이트를 자동으로 묶어 처리합니다.

jsx
// Before React 18: two re-renders
function handleClick() {
  setCount(c => c + 1) // Re-render
  setFlag(f => !f)      // Re-render
}

// React 18+: single re-render (automatic batching)
function handleClick() {
  setCount(c => c + 1) // Batched
  setFlag(f => !f)      // Batched
  // Single re-render at the end
}

// Even in async callbacks (React 18 novelty)
async function handleSubmit() {
  const response = await fetch('/api/submit')

  // These two updates are batched
  setData(response.data)
  setLoading(false)
  // Single re-render
}

// To force immediate re-render (rare)
import { flushSync } from 'react-dom'

function handleClick() {
  flushSync(() => {
    setCount(c => c + 1)
  })
  // DOM updated here

  flushSync(() => {
    setFlag(f => !f)
  })
  // DOM updated here
}

React / Next.js 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

모던 React (18+)

21. useTransition과 useDeferredValue를 설명한다

이 두 훅은 업데이트를 긴급하지 않은 것으로 표시하여 인터페이스의 응답성을 유지합니다.

jsx
// useTransition: mark an update as non-urgent
function SearchResults() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()

  const handleChange = (e) => {
    // Urgent update: input stays responsive
    setQuery(e.target.value)

    // Non-urgent update: can be interrupted
    startTransition(() => {
      const filtered = filterLargeDataset(e.target.value)
      setResults(filtered)
    })
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />

      {isPending && <span>Searching...</span>}

      <ul>
        {results.map(r => <li key={r.id}>{r.name}</li>)}
      </ul>
    </div>
  )
}

// useDeferredValue: defer a value
function DeferredSearch() {
  const [query, setQuery] = useState('')
  // deferredQuery is "behind" during rapid updates
  const deferredQuery = useDeferredValue(query)

  // Show indicator during the "lag"
  const isStale = query !== deferredQuery

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />

      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        {/* Uses deferred value, fewer calculations */}
        <ExpensiveList filter={deferredQuery} />
      </div>
    </div>
  )
}

22. 데이터 페칭에서 Suspense는 어떻게 동작하는가

Suspense는 로딩 상태를 선언적으로 관리합니다. React 18부터는 데이터 페칭에도 확장 적용됩니다.

With a compatible library (React Query, Relay, etc.)jsx
function UserProfile({ userId }) {
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <ProfileContent userId={userId} />
    </Suspense>
  )
}

// The component "suspends" during loading
function ProfileContent({ userId }) {
  // This function suspends if data isn't ready
  const user = useUserData(userId)

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  )
}

// Nested Suspense boundaries
function Dashboard() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <Header />

      <div className="grid">
        <Suspense fallback={<StatsSkeleton />}>
          <Stats />
        </Suspense>

        <Suspense fallback={<ChartSkeleton />}>
          <Chart />
        </Suspense>

        <Suspense fallback={<TableSkeleton />}>
          <RecentActivity />
        </Suspense>
      </div>
    </Suspense>
  )
}

23. useActionState (React 19)는 무엇인가

useActionState(이전 명칭 useFormState)는 Server Action과 함께 사용하는 폼 처리를 단순화합니다.

actions.jsjsx
'use server'

async function submitForm(prevState, formData) {
  const email = formData.get('email')
  const password = formData.get('password')

  // Validation
  if (!email || !password) {
    return { error: 'All fields are required' }
  }

  try {
    await createUser({ email, password })
    return { success: true, message: 'Account created!' }
  } catch (e) {
    return { error: e.message }
  }
}

// Component
function SignupForm() {
  const [state, formAction, isPending] = useActionState(submitForm, {})

  return (
    <form action={formAction}>
      {state.error && (
        <div className="error">{state.error}</div>
      )}

      {state.success && (
        <div className="success">{state.message}</div>
      )}

      <input name="email" type="email" placeholder="Email" />
      <input name="password" type="password" placeholder="Password" />

      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create account'}
      </button>
    </form>
  )
}

24. Server Component는 어떻게 동작하는가

Server Component는 서버에서만 실행되며, 클라이언트에 전송되는 JavaScript 양을 줄여줍니다.

ServerComponent.jsxjsx
// No "use client" = Server Component by default
import { prisma } from '@/lib/prisma'

// async/await directly in the component
export default async function ProductList() {
  // Direct database call (no API)
  const products = await prisma.product.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10
  })

  return (
    <div>
      <h2>Recent Products</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name} - ${product.price}
            {/* Can include Client Components */}
            <AddToCartButton productId={product.id} />
          </li>
        ))}
      </ul>
    </div>
  )
}

// AddToCartButton.jsx
'use client'

// This component needs interactivity
export function AddToCartButton({ productId }) {
  const [isPending, startTransition] = useTransition()

  const handleClick = () => {
    startTransition(async () => {
      await addToCart(productId)
    })
  }

  return (
    <button onClick={handleClick} disabled={isPending}>
      {isPending ? '...' : 'Add'}
    </button>
  )
}

25. React 18의 SSR 스트리밍을 설명한다

SSR 스트리밍은 전체 렌더링이 끝날 때까지 기다리지 않고, HTML을 점진적으로 브라우저에 전달합니다.

app/page.jsx (Next.js App Router)jsx
import { Suspense } from 'react'

export default function Page() {
  return (
    <div>
      {/* Immediate render */}
      <Header />

      {/* Streamed when ready */}
      <Suspense fallback={<MainContentSkeleton />}>
        <MainContent />
      </Suspense>

      {/* Streamed independently */}
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>

      {/* Immediate render */}
      <Footer />
    </div>
  )
}

// The browser receives:
// 1. HTML shell with Header, skeletons, Footer
// 2. MainContent when its request completes
// 3. Sidebar when its request completes

// loading.jsx for route-level streaming
export default function Loading() {
  return <PageSkeleton />
}

상태 관리

26. Redux, Context API, 또는 다른 솔루션은 언제 사용하는가

선택은 상태의 복잡도와 프로젝트의 요구사항에 따라 달라집니다.

jsx
// Context API: simple state, few updates
// ✅ Good for: theme, user, preferences
const ThemeContext = createContext()

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

// Redux / Zustand: complex state, frequent updates
// ✅ Good for: cart, filters, complex business data
import { create } from 'zustand'

const useCartStore = create((set, get) => ({
  items: [],
  total: 0,

  addItem: (product) => set((state) => {
    const existing = state.items.find(i => i.id === product.id)
    if (existing) {
      return {
        items: state.items.map(i =>
          i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i
        )
      }
    }
    return { items: [...state.items, { ...product, quantity: 1 }] }
  }),

  removeItem: (id) => set((state) => ({
    items: state.items.filter(i => i.id !== id)
  })),

  getTotal: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0)
}))

// React Query / SWR: server state (cache, refetch)
// ✅ Good for: API data, server synchronization
import { useQuery, useMutation } from '@tanstack/react-query'

function Products() {
  const { data, isLoading } = useQuery({
    queryKey: ['products'],
    queryFn: () => fetch('/api/products').then(r => r.json())
  })

  const mutation = useMutation({
    mutationFn: (newProduct) => fetch('/api/products', {
      method: 'POST',
      body: JSON.stringify(newProduct)
    })
  })
}

27. Reducer 패턴은 어떻게 구현하는가

Reducer 패턴은 복잡한 상태 업데이트 로직을 한곳에 중앙화합니다.

jsx
// Define actions
const ACTIONS = {
  ADD_TODO: 'ADD_TODO',
  TOGGLE_TODO: 'TOGGLE_TODO',
  DELETE_TODO: 'DELETE_TODO',
  SET_FILTER: 'SET_FILTER'
}

// Pure reducer (no side effects)
function todoReducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD_TODO:
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: Date.now(), text: action.payload, completed: false }
        ]
      }

    case ACTIONS.TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      }

    case ACTIONS.DELETE_TODO:
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      }

    case ACTIONS.SET_FILTER:
      return { ...state, filter: action.payload }

    default:
      return state
  }
}

// Usage with useReducer
function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [],
    filter: 'all'
  })

  const addTodo = (text) => {
    dispatch({ type: ACTIONS.ADD_TODO, payload: text })
  }

  const toggleTodo = (id) => {
    dispatch({ type: ACTIONS.TOGGLE_TODO, payload: id })
  }

  const filteredTodos = useMemo(() => {
    switch (state.filter) {
      case 'completed':
        return state.todos.filter(t => t.completed)
      case 'active':
        return state.todos.filter(t => !t.completed)
      default:
        return state.todos
    }
  }, [state.todos, state.filter])

  return (
    <div>
      <TodoForm onAdd={addTodo} />
      <TodoList todos={filteredTodos} onToggle={toggleTodo} />
      <FilterButtons
        current={state.filter}
        onChange={(f) => dispatch({ type: ACTIONS.SET_FILTER, payload: f })}
      />
    </div>
  )
}

테스트

28. React 컴포넌트는 어떻게 테스트하는가

컴포넌트 테스트는 렌더링과 인터페이스 동작을 검증합니다.

Button.test.jsxjsx
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Button from './Button'

describe('Button', () => {
  // Basic rendering test
  it('renders with correct text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByRole('button')).toHaveTextContent('Click me')
  })

  // Interaction test
  it('calls onClick when clicked', async () => {
    const handleClick = jest.fn()
    render(<Button onClick={handleClick}>Click</Button>)

    await userEvent.click(screen.getByRole('button'))

    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  // Disabled state test
  it('does not call onClick when disabled', async () => {
    const handleClick = jest.fn()
    render(<Button disabled onClick={handleClick}>Click</Button>)

    await userEvent.click(screen.getByRole('button'))

    expect(handleClick).not.toHaveBeenCalled()
  })

  // Async state test
  it('shows loading state', async () => {
    render(<Button isLoading>Submit</Button>)

    expect(screen.getByRole('button')).toBeDisabled()
    expect(screen.getByTestId('spinner')).toBeInTheDocument()
  })
})

// Testing a custom hook
import { renderHook, act } from '@testing-library/react'
import useCounter from './useCounter'

describe('useCounter', () => {
  it('increments counter', () => {
    const { result } = renderHook(() => useCounter(0))

    act(() => {
      result.current.increment()
    })

    expect(result.current.count).toBe(1)
  })
})

29. 테스트에서 API 호출은 어떻게 모킹하는가

모킹은 외부 의존성으로부터 테스트를 분리하는 데 사용됩니다.

jsx
// With MSW (Mock Service Worker) - recommended
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'

const server = setupServer(
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ])
  }),

  http.post('/api/users', async ({ request }) => {
    const body = await request.json()
    return HttpResponse.json({ id: 3, ...body }, { status: 201 })
  })
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

test('loads and displays users', async () => {
  render(<UserList />)

  expect(screen.getByText('Loading...')).toBeInTheDocument()

  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument()
    expect(screen.getByText('Bob')).toBeInTheDocument()
  })
})

// Override for a specific test
test('handles error state', async () => {
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json({ error: 'Server error' }, { status: 500 })
    })
  )

  render(<UserList />)

  await waitFor(() => {
    expect(screen.getByText('Loading error')).toBeInTheDocument()
  })
})

30. 최적의 커버리지를 위해 테스트는 어떻게 구성하는가

균형 잡힌 테스트 전략은 서로 다른 수준의 테스트를 조합합니다.

jsx
// Recommended testing pyramid:
// - Many unit tests (fast, isolated)
// - Some integration tests (components + hooks)
// - Few E2E tests (critical scenarios)

// Unit tests: pure logic
// utils/formatPrice.test.js
describe('formatPrice', () => {
  it('formats with 2 decimals', () => {
    expect(formatPrice(10)).toBe('$10.00')
  })

  it('handles zero', () => {
    expect(formatPrice(0)).toBe('$0.00')
  })
})

// Integration tests: complete component
// features/Checkout/Checkout.test.jsx
describe('Checkout', () => {
  it('completes purchase flow', async () => {
    render(
      <CartProvider>
        <Checkout />
      </CartProvider>
    )

    // Fill the form
    await userEvent.type(screen.getByLabelText('Email'), 'test@example.com')
    await userEvent.type(screen.getByLabelText('Card'), '4242424242424242')

    // Submit
    await userEvent.click(screen.getByRole('button', { name: 'Pay' }))

    // Verify result
    await waitFor(() => {
      expect(screen.getByText('Order confirmed')).toBeInTheDocument()
    })
  })
})

// E2E tests with Playwright
// e2e/checkout.spec.ts
test('user can complete checkout', async ({ page }) => {
  await page.goto('/products')
  await page.click('[data-testid="add-to-cart-1"]')
  await page.click('[data-testid="checkout-button"]')

  await page.fill('[name="email"]', 'test@example.com')
  await page.fill('[name="card"]', '4242424242424242')
  await page.click('button[type="submit"]')

  await expect(page.locator('text=Order confirmed')).toBeVisible()
})

결론

여기에 정리한 30가지 질문은 React 면접에서 요구되는 핵심 지식을 모두 다룹니다. 반드시 익혀두어야 할 주요 영역은 다음과 같습니다.

  • 기초: Virtual DOM, JSX, props와 state, 컴포넌트
  • Hooks: useState, useEffect, useMemo, useCallback, useRef, useContext
  • 패턴: HOC, Render Props, Compound Components, 커스텀 훅
  • 성능: React.memo, lazy loading, 가상화
  • 모던 React: Suspense, Transitions, Server Components
  • 상태 관리: Context, Redux/Zustand, React Query
  • 테스트: React Testing Library, 모킹, 테스트 전략

React 면접 준비는 단순한 암기가 아닙니다. 실제 프로젝트에서 손으로 직접 다뤄볼수록 이러한 개념이 자연스럽게 체화되며, 면접에서도 자신 있게 설명할 수 있게 됩니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

태그

#react interview
#frontend interview
#react questions
#javascript
#technical interview

공유

관련 기사