React 미드레벨 코딩 및 리팩토링 면접
새 면접Q. React 컴포넌트에서 이미지 갤러리를 구현할 때, 100개 이상의 고해상도 이미지를 한 번에 렌더링하면 초기 로딩 시간과 스크롤 성능이 저하됩니다. Intersection Observer API를 활용한 이미지 lazy loading을 구현하는 방법과, 이를 Custom Hook으로 추상화할 때 고려해야 할 사항들을 설명해주세요.
Intersection Observer API를 사용하면 뷰포트에 진입하는 요소를 감지하여 실제로 보이는 이미지만 로드할 수 있습니다. useIntersectionObserver 같은 Custom Hook을 만들어 ref와 isIntersecting 상태를 반환하도록 구현하고, threshold와 rootMargin 옵션으로 로딩 시점을 제어합니다. cleanup 함수에서 observer.disconnect()를 호출해 메모리 누수를 방지하고, 이미지 로드 후에는 observer를 해제하여 불필요한 감지를 중단해야 합니다. 또한 placeholder나 skeleton UI를 제공하고, loading='lazy' 속성과 병행하여 네이티브 lazy loading을 지원하는 브라우저에서는 추가 최적화를 얻을 수 있습니다.
- • Intersection Observer로 뷰포트 진입 감지 및 동적 로딩
- • Custom Hook으로 재사용 가능한 로직 추상화 (threshold, rootMargin 옵션)
- • cleanup에서 observer.disconnect() 호출로 메모리 누수 방지
- • 이미지 로드 완료 후 observer 해제로 성능 최적화
- • 네이티브 loading='lazy' 속성과 병행 사용
Intersection Observer를 사용할 수 없는 구형 브라우저를 지원해야 한다면 어떤 폴백 전략을 사용하시겠습니까? 또한 이미지가 캐시된 경우와 아닌 경우를 어떻게 구분하여 처리하시겠습니까?
Q. 여러 컴포넌트에서 공유되는 복잡한 폼 상태를 관리할 때, 중첩된 객체 구조의 특정 필드만 업데이트하는 경우가 빈번합니다. immer 라이브러리를 사용하는 것과 immutable update pattern을 직접 구현하는 것의 차이를 설명하고, 각각의 성능 특성과 trade-off를 분석해주세요. 특히 대규모 폼에서 어느 방식이 더 효율적인지 설명해주세요.
immer는 Proxy 기반으로 동작하여 draft 객체를 변경하는 것처럼 작성하면 내부적으로 structural sharing을 통해 변경된 부분만 새 객체를 생성합니다. 직접 구현하는 immutable update는 spread operator나 Object.assign을 사용하며 중첩이 깊어질수록 코드가 복잡해지고 실수하기 쉽습니다. immer는 개발 생산성이 높고 코드 가독성이 좋지만, Proxy 생성과 내부 로직으로 인한 오버헤드가 있어 작은 객체 업데이트에서는 오히려 느릴 수 있습니다. 대규모 중첩 폼에서는 immer의 이점이 크며, 특히 배열 내 객체 업데이트나 조건부 업데이트가 많은 경우 코드 복잡도를 크게 줄일 수 있습니다. 성능이 critical한 부분에서는 프로파일링 후 판단해야 합니다.
- • immer는 Proxy 기반 structural sharing으로 변경 부분만 새 객체 생성
- • 직접 구현은 중첩 깊이에 따라 코드 복잡도 급증 및 버그 위험
- • immer는 Proxy 오버헤드로 작은 객체에서는 느릴 수 있음
- • 대규모 중첩 폼에서는 immer의 생산성과 유지보수성 이점이 큼
- • 성능 critical 부분은 프로파일링 후 선택적 최적화 필요
immer를 사용할 때 draft 객체를 return하는 경우와 return하지 않는 경우의 차이는 무엇이며, 어떤 상황에서 예상치 못한 동작이 발생할 수 있나요?
Q. React 애플리케이션에서 사용자가 입력한 텍스트에서 특정 키워드를 하이라이팅하는 기능을 구현해야 합니다. 여러 개의 키워드를 동시에 하이라이팅하고, 대소문자를 구분하지 않으며, 겹치는 키워드도 처리해야 합니다. 단순 문자열 replace를 반복하는 방식의 문제점과, 효율적인 구현 방법을 알고리즘 관점에서 설명해주세요.
단순 replace 반복은 O(n*m*k) 시간 복잡도를 가지며 이미 하이라이팅된 HTML 태그 내부를 다시 매칭하는 문제가 발생합니다. 효율적인 방법은 모든 키워드의 매칭 위치를 먼저 찾아 배열에 저장하고, 위치 기준으로 정렬한 뒤 겹치는 구간을 병합하여 하나의 순회로 처리하는 것입니다. 정규식을 사용할 경우 모든 키워드를 OR로 결합한 하나의 패턴으로 만들어 한 번의 matchAll 호출로 모든 매칭을 찾을 수 있습니다. React 컴포넌트에서는 매칭된 구간과 일반 텍스트 구간을 분리하여 각각 다른 스타일의 span으로 렌더링하며, useMemo로 파싱 결과를 캐싱하여 불필요한 재계산을 방지합니다. 매우 긴 텍스트의 경우 Web Worker로 파싱 작업을 오프로드할 수도 있습니다.
- • 단순 replace 반복은 O(n*m*k) 복잡도와 HTML 태그 오염 문제
- • 모든 매칭 위치를 찾고 정렬 후 구간 병합으로 한 번에 처리
- • 정규식 OR 패턴으로 한 번의 matchAll 호출로 모든 키워드 매칭
- • useMemo로 파싱 결과 캐싱하여 리렌더링 시 재계산 방지
- • 긴 텍스트는 Web Worker로 파싱 작업 오프로드 고려
하이라이팅된 텍스트에서 사용자가 특정 하이라이트를 클릭했을 때 해당 키워드 정보를 얻어야 한다면, 데이터 구조를 어떻게 설계하시겠습니까?
Q. useEffect 내부에서 비동기 함수를 호출할 때, 컴포넌트가 언마운트된 후에도 setState가 호출되어 'Can't perform a React state update on an unmounted component' 경고가 발생합니다. 이 문제의 원인을 설명하고, cleanup 함수를 사용한 해결 방법과 AbortController를 활용한 fetch 취소 방법을 각각 설명해주세요.
비동기 작업이 완료되는 시점에 컴포넌트가 이미 언마운트되어 있으면 setState 호출이 메모리 누수를 유발하고 경고가 발생합니다. cleanup 함수에서 boolean flag(isMounted, isCancelled)를 사용하여 setState 전에 컴포넌트 마운트 상태를 확인하는 방법이 일반적입니다. 더 나은 방법은 AbortController를 생성하여 fetch 호출 시 signal을 전달하고, cleanup 함수에서 abort()를 호출하여 진행 중인 요청을 취소하는 것입니다. 이렇게 하면 네트워크 요청 자체가 중단되어 불필요한 데이터 전송과 처리를 방지할 수 있습니다. axios의 경우 CancelToken을 사용하며, React Query 같은 라이브러리는 이런 처리를 자동으로 해줍니다.
- • 언마운트 후 setState 호출로 메모리 누수 및 경고 발생
- • cleanup 함수의 boolean flag로 마운트 상태 확인 후 setState
- • AbortController로 fetch 요청을 cleanup에서 취소하여 근본적 해결
- • axios는 CancelToken, 라이브러리는 자동 처리 제공
- • 네트워크 요청 취소로 불필요한 데이터 전송 방지
여러 개의 비동기 요청을 병렬로 실행하는 경우, AbortController를 어떻게 관리해야 하며, Promise.all을 사용할 때 취소 처리는 어떻게 구현하시겠습니까?
Q. 코드 리뷰 중 한 컴포넌트에서 여러 개의 useEffect가 서로 연관된 상태를 각각 업데이트하고 있어, 상태 업데이트 순서에 따라 예상치 못한 동작이 발생하는 코드를 발견했습니다. 이런 경우 useReducer로 리팩토링하는 것이 좋은 이유와, 언제 useState 대신 useReducer를 선택해야 하는지 판단 기준을 설명해주세요.
여러 상태가 서로 의존적이고 복잡한 업데이트 로직이 있을 때 useReducer를 사용하면 모든 상태 변경을 하나의 reducer 함수에서 처리하여 일관성을 보장할 수 있습니다. 여러 useEffect가 상태를 순차적으로 업데이트하면 race condition이나 무한 루프 위험이 있지만, useReducer는 action 기반으로 상태 전환을 명확히 정의하여 예측 가능한 동작을 만듭니다. useState 대신 useReducer를 선택하는 기준은 1) 다음 상태가 이전 상태에 복잡하게 의존하는 경우, 2) 여러 하위 값을 포함하는 복잡한 상태 객체인 경우, 3) 상태 업데이트 로직을 컴포넌트 외부로 분리하여 테스트하고 싶은 경우입니다. 또한 dispatch는 identity가 보장되어 자식 컴포넌트에 전달 시 불필요한 리렌더링을 방지할 수 있습니다.
- • 여러 의존적 상태를 reducer에서 일관되게 처리하여 race condition 방지
- • action 기반 상태 전환으로 예측 가능하고 명확한 로직
- • 복잡한 상태 의존성, 중첩 객체, 테스트 필요 시 useReducer 선택
- • dispatch의 identity 보장으로 자식 컴포넌트 최적화 가능
- • 상태 업데이트 로직을 컴포넌트 외부로 분리하여 유지보수성 향상
useReducer와 Redux의 차이점은 무엇이며, 컴포넌트 로컬 상태로 useReducer를 사용하다가 전역 상태 관리가 필요해진 경우 어떻게 마이그레이션하시겠습니까?
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!