Skip to main content

React

기본 개념

Q. JSX와 내부 동작원리에 대해 설명해주세요.
  • JSX는 JavaScript 코드 내에서 HTML과 유사한 마크업 문법을 사용할 수 있게 해주는 문법 확장으로, UI 구조를 보다 직관적으로 작성하고 가독성을 높이는 데 도움을 줍니다.

    개발자가 JSX로 UI 코드를 작성하면 Babel과 같은 트랜스파일러가 이를 React.createElement() 함수 호출로 변환합니다. React.createElement()는 UI 구조 정보를 담은 React 엘리먼트 객체를 생성하고, React는 이 객체를 사용해 가상 DOM을 만든 뒤 실제 DOM과 비교하여 변경된 부분만 효율적으로 업데이트합니다.

Q. Element와 Component의 차이에 대해 설명해주세요
  • 엘리먼트는 React App의 가장 작은 단위로, 화면에 보이는 것을 기술하는 JavaScript 객체입니다. <div><MyComponent />와 같은 JSX 표현식은 결국 엘리먼트 객체로 변환됩니다.

    컴포넌트는 재사용 가능한 UI 로직과 구조를 캡슐화한 것으로, props라는 입력을 받아 특정 엘리먼트 트리를 생성하는 일종의 청사진입니다.

Q. props와 state에 대해서 설명해주세요.
  • props는 부모 컴포넌트가 자식 컴포넌트에 인자로 전달하는 데이터로, 일반적으로 자식 컴포넌트는 props를 수정할 수 없습니다.

    state는 컴포넌트 내부에서 관리되는 데이터입니다. 동적으로 변경될 수 있으며 변경 시 컴포넌트가 다시 렌더링되어 UI가 업데이트됩니다. 주로 사용자 입력이나 네트워크 요청 응답에 따라 변하는 데이터를 관리할 때 사용합니다.

Q. 제어 컴포넌트와 비제어 컴포넌트의 차이에 대해 설명해주세요.
  • 이 둘은 주로 리액트에서 폼 요소의 값을 누가 관리하느냐의 차이입니다.

    제어 컴포넌트는 리액트 컴포넌트가 폼 요소의 값을 직접 제어하는 방식입니다. Input의 value를 리액트 state와 연결하고, 값이 바뀔 때마다 onChange 이벤트 핸들러를 통해 state를 업데이트합니다. 이렇게 하면 리액트 state가 항상 최신 값을 가지고 있어서 값 검증이나 조건부 로직을 구현하기 좋고 데이터 흐름이 명확해집니다.

    비제어 컴포넌트는 폼 요소의 값을 DOM 자체에 맡겨두는 방식입니다. 리액트는 값의 변경을 직접 추적하지 않고, 필요할 때 ref를 사용해 DOM에 직접 접근하여 값을 가져옵니다. 구현이 더 간단하고 파일 입력처럼 리액트가 직접 제어하기 어려운 경우에 유용합니다.

    보통은 데이터 흐름을 명확하게 하고 제어하기 쉬운 제어 컴포넌트를 더 자주 사용하는 편입니다.

Q. Ref의 용도에 대해 설명해주세요.
  • Ref는 주로 DOM 노드나 React 엘리먼트에 직접 접근해야 할 때 사용합니다. ref.current 프로퍼티는 변경되어도 컴포넌트를 리렌더링시키지 않기 때문에, 타이머 ID처럼 렌더링과 직접적인 관련은 없지만 컴포넌트 인스턴스 내에서 계속 유지되어야 하는 값을 저장하는 용도로도 사용됩니다. DOM 요소 크기·위치 측정, 포커스 관리, 타이머 등이 대표적인 사용 예시입니다.
Q. HTML과 React의 이벤트 처리 차이점에 대해 설명해주세요.
  • 이벤트 핸들러 전달 방식에서 HTML은 문자열로 코드를 직접 전달하지만, React는 함수 참조를 중괄호 {} 안에 전달합니다.

    이벤트 객체 측면에서도 차이가 있는데, React는 브라우저 고유의 이벤트 대신 합성 이벤트(SyntheticEvent) 를 전달하여 크로스 브라우징 호환성을 제공합니다.

    기본 동작 방지 방식에서도 HTML에서는 return false로 기본 동작을 막을 수 있지만, React에서는 반드시 **event.preventDefault()**를 명시적으로 호출해야 합니다.

Q. Single Page Application에 대해서 설명해주세요.
  • SPA는 하나의 HTML 페이지로 애플리케이션을 구성하고, 사용자 상호작용에 따라 필요한 데이터만 서버로부터 비동기적으로 받아와 현재 페이지의 일부분만 동적으로 업데이트하는 방식의 웹 애플리케이션입니다.

    페이지 전체 리로딩이 없어 빠르고 부드러운 화면 전환을 제공하며, 필요한 데이터만 주고받으므로 서버 트래픽이 줄어드는 장점이 있습니다. 반면 앱의 규모가 크면 첫 로딩 시 많은 리소스를 받아야 할 수 있고, SEO 처리가 복잡해질 수 있다는 단점이 있습니다.

컴포넌트 생명주기 및 패턴

Q. 리액트에서 컴포넌트를 어떻게 생성하나요?
  • 리액트 컴포넌트는 주로 두 가지 방식으로 생성합니다.

    함수 컴포넌트는 JavaScript 함수를 사용하여 만드는 방식으로, props를 인자로 받고 JSX를 반환하며 Hook을 통해 상태와 생명주기 기능을 사용합니다.

    클래스 컴포넌트는 ES6 클래스로 React.Component를 상속받아 만들며, render() 메서드에서 JSX를 반환하고 this.state와 생명주기 메서드를 사용합니다.

Q. Class 컴포넌트와 Function 컴포넌트의 차이를 설명해주세요.
  • 과거에는 상태 관리나 라이프사이클 관리를 위해 클래스 컴포넌트 사용이 강제적이었지만, Hook이 등장하면서 함수형 컴포넌트가 클래스 컴포넌트의 모든 기능을 더 효율적이고 직관적으로 사용할 수 있게 되었습니다.

    상태 관리 측면에서 클래스형은 this.state 객체 안에 상태를 선언해야 하고 메서드에서 this 바인딩 문제가 발생할 수 있었지만, 함수형은 useState 훅 하나로 직관적이고 간결하게 사용할 수 있습니다.

    라이프사이클 측면에서 클래스형은 각 메서드에 로직이 분산되어 코드 추적이 어렵지만, 함수형은 useEffect 훅 하나로 마운트·업데이트·언마운트 시점을 모두 처리할 수 있어 관련 로직이 응집됩니다.

    성능 측면에서도 함수형 컴포넌트는 메모리 자원을 클래스형보다 덜 사용하며 빌드 사이즈도 더 작습니다.

Q. 라이프 사이클에 대해 설명해주세요.
  • 라이프사이클은 컴포넌트가 생성되고, 업데이트되고, 소멸되기까지의 과정을 말합니다. 모든 리액트 컴포넌트는 마운트 → 업데이트 → 언마운트의 라이프사이클을 갖습니다.

    마운트는 컴포넌트가 생성되는 시점으로, constructor → getDerivedStateFromProps → render → componentDidMount 순서로 호출됩니다.

    업데이트는 컴포넌트가 업데이트되는 시점으로, getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate 순서로 호출됩니다.

    언마운트는 컴포넌트가 화면에서 사라지는 시점이며, 사라지기 직전에 componentWillUnmount가 호출됩니다. 함수 컴포넌트에서는 useEffect 훅을 통해 유사한 동작을 구현합니다.

Q. React Portal에 대해 설명해주세요.
  • React Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 다른 위치로 자식 컴포넌트를 렌더링할 수 있게 해주는 기능입니다.

    컴포넌트 계층 구조는 그대로 유지하지만 실제 DOM 렌더링 결과는 document.body의 직속이나 특정 다른 DOM 노드 아래에 배치할 수 있습니다. 모달, 툴팁, 팝업과 같이 부모 컴포넌트의 CSS 영향을 받지 않고 최상단에 표시되어야 하는 UI를 구현할 때 유용합니다.

Q. Error Boundary에 대해 설명해주세요.
  • 에러 바운더리는 하위 컴포넌트 트리에서 발생하는 JavaScript 에러를 잡아내고, 에러로 인해 전체 애플리케이션이 중단되는 것을 방지하며 대체 UI를 보여줄 수 있게 하는 React 컴포넌트입니다.

    구현은 클래스 컴포넌트로만 가능하며, 두 가지 생명주기 메서드를 사용합니다. getDerivedStateFromError는 에러 발생 후 대체 UI를 렌더링하기 위해 상태를 업데이트하는 데 사용되며 렌더 단계에서 호출됩니다. componentDidCatch는 에러 정보를 로깅하는 등의 부수 효과를 처리하는 데 사용되며 커밋 단계에서 호출됩니다.

Q. Error Boundary는 어떤 에러를 잡을 수 있나요?
  • React가 컴포넌트 트리를 만들고 화면에 그리는 과정 중에 발생하는 에러를 주로 처리합니다.

    다만 이벤트 핸들러 내부의 에러, 비동기 코드, 서버 사이드 렌더링 중 발생한 에러, 에러 바운더리 컴포넌트 자체에서 발생한 에러는 잡지 못합니다. 이벤트 핸들러 등은 try-catch 구문을 사용하여 에러를 처리해야 합니다.

Q. HOC (Higher-Order Components)에 대해 설명해주세요.
  • HOC컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환하는 함수입니다.

    주된 목적은 여러 컴포넌트에서 반복되는 로직을 재사용하기 위해서입니다. 인증, 데이터 fetching, 로깅 같은 공통 로직을 HOC 안에 구현해두고 필요한 컴포넌트를 감싸주면, HOC가 해당 로직을 처리하고 원본 컴포넌트에 필요한 props를 전달하거나 렌더링을 제어합니다. 로직을 한 곳에서 관리할 수 있어 코드 중복을 줄이고 유지보수성을 높일 수 있다는 것이 장점입니다.

    다만 요즘에는 커스텀 훅이 HOC의 많은 역할을 대체하고 있습니다. 훅이 더 직관적이고 컴포넌트 트리가 깊어지는 Wrapper Hell 문제를 피할 수 있어 선호되는 경향이 있습니다.

Q. React Server Components에 대해서 설명해주세요.
  • React Server Components는 서버에서만 렌더링되고, 렌더링에 필요한 자바스크립트 코드가 클라이언트로 전송되지 않는 컴포넌트입니다.

    서버 컴포넌트 자체의 코드는 브라우저로 가지 않기 때문에 클라이언트 측 자바스크립트 번들 크기를 크게 줄여 초기 로딩 성능을 향상시킵니다. 서버에서 실행되므로 별도의 API 없이 데이터베이스나 파일 시스템 등에 직접 접근하여 데이터를 가져올 수 있습니다. 또한 데이터 fetching이나 정적인 구조는 서버 컴포넌트가, 상태 관리나 이벤트 처리 등은 클라이언트 컴포넌트가 담당하도록 역할을 분담할 수 있습니다. Next.js 같은 프레임워크에서 적극적으로 활용되고 있습니다.

렌더링 과정

⭐ Q. React의 주요 특징에 대해 설명해주세요.
  • React는 UI를 만들기 위한 JavaScript 라이브러리입니다. 스스로 상태를 관리하는 캡슐화된 컴포넌트를 조합해 복잡한 UI를 만들 수 있도록 지원하며, 데이터가 변경됨에 따라 적절한 컴포넌트만 효율적으로 갱신하고 렌더링합니다.

    React의 큰 특징으로는 Virtual DOM단방향 데이터 흐름이 있습니다.

    Virtual DOM은 Real DOM의 인메모리 표현으로 UI 표현을 메모리에 저장하고 Real DOM과 동기화합니다.

    단방향 데이터 바인딩은 데이터가 부모 컴포넌트에서 자식 컴포넌트로 한 방향으로만 흐르는 것으로, 주로 props를 통해 전달됩니다.

    라이브러리와 프레임워크의 차이는 제어 흐름의 주도권이 어디에 있느냐입니다. 프레임워크는 전체적인 흐름을 자체적으로 가지고 있어 개발자가 그 안에 코드를 작성하는 반면, 라이브러리는 개발자가 흐름을 제어하며 필요한 상황에 가져다 쓸 수 있습니다.

⭐ Q. Virtual DOM에 대해 설명해주세요.
  • Virtual DOM은 실제 브라우저의 DOM에 대응하는 가벼운 복사본을 인메모리로 만들어두고 사용하는 개념입니다.

    상태가 변경될 때마다 실제 DOM을 직접 조작하는 것은 비용이 많이 들고 성능 저하를 유발할 수 있습니다. 특히 변경이 잦을 경우 브라우저가 Reflow와 Repaint를 계속 반복해야 해서 느려지기 쉽습니다. 가상 DOM을 생성하면 이전 가상 DOM과 현재 가상 DOM을 diffing 알고리즘으로 비교해 변경된 부분만 찾아내고 한 번에 실제 DOM에 적용하는 방식으로 효율적으로 렌더링합니다. 이 과정을 재조정(Reconciliation) 이라고 합니다.

⭐ Q. 재조정(Reconciliation)에 대해 알고있는대로 설명해주세요.
  • 재조정(Reconciliation) 은 React가 가상 DOM을 사용하여 실제 DOM을 효율적으로 업데이트하는 과정입니다.

    상태나 props가 변경되면 React는 새로운 가상 DOM 트리를 생성하고 이전 트리와 비교합니다. 이 diffing 알고리즘을 통해 최소한의 변경사항만 감지하여 실제 DOM에 적용함으로써 성능을 최적화합니다.

Q. Diffing 알고리즘에 대해 알고있는대로 설명해주세요.
  • Diffing 알고리즘은 React가 이전 가상 DOM 트리와 새로운 가상 DOM 트리를 비교하여 실제 DOM에 어떤 변경사항을 적용해야 할지 결정하는 알고리즘입니다.

    React는 효율성을 위해 몇 가지 규칙을 사용합니다.

    다른 타입의 엘리먼트는 하위 트리까지 모두 새로 만들고, 같은 타입의 DOM 엘리먼트는 속성만 비교하고 업데이트합니다.

    리스트 형태의 자식을 비교할 때는 key prop을 사용하여 각 엘리먼트의 변경, 추가, 제거 여부를 효율적으로 파악합니다.

Q. Fiber에 대해 알고있는 대로 설명해주세요.
  • React Fiber는 React의 핵심 알고리즘인 재조정 알고리즘을 재구현한 것으로, 각 컴포넌트를 Fiber라는 작업 단위로 나누어 처리하는 것이 핵심입니다.

    이전 버전의 React는 재조정 과정이 동기적이고 중단될 수 없었기 때문에, 복잡한 컴포넌트 트리를 업데이트할 때 메인 스레드를 오랫동안 점유하여 애니메이션 끊김이나 사용자 입력 반응 지연 같은 성능 문제가 발생할 수 있었습니다.

    Fiber는 렌더링 작업을 작은 단위로 나누어 필요에 따라 작업을 중단, 재개, 또는 우선순위를 변경할 수 있게 합니다. 전체 작업을 여러 프레임에 걸쳐 분할 실행함으로써 메인 스레드를 차단하지 않고 부드러운 사용자 경험을 제공합니다.

Q. Render Phase와 Commit Phase에 대해 설명해주세요
  • Render Phase에서는 React가 컴포넌트를 호출해서 어떤 변경사항이 필요한지 계산합니다. 이전 렌더 결과와 비교해서 DOM에 어떤 변화를 주어야 할지 결정하는 단계로, 이 단계에서는 부수 효과가 없어야 합니다. Strict Mode가 함수를 두 번 호출하는 이유도 이 단계의 순수성을 검증하기 위함입니다.

    Commit Phase는 Render Phase에서 계산된 변경사항들을 실제 DOM에 적용하는 단계입니다. DOM 노드가 추가, 수정, 삭제되며 한번 시작하면 끝까지 실행되어야 UI의 일관성이 보장됩니다. 클래스 컴포넌트의 생명주기 메서드와 함수형 컴포넌트의 useEffect 훅 실행 및 클린업 함수 호출이 이 단계에서 이루어집니다.

Q. 리액트의 Strict Mode에 대해 설명해주세요
  • Strict Mode는 개발 환경에서 잠재적인 문제를 미리 발견하도록 도와주는 리액트의 도구입니다.

    주요 기능 중 하나는 컴포넌트의 렌더링 단계나 useState의 업데이트 함수 등 일부 함수들을 의도적으로 두 번 호출하는 것입니다. 함수가 순수하다면 두 번 실행해도 최종 결과는 동일하기 때문에, 이를 통해 예상치 못한 부수 효과를 쉽게 찾도록 도와줍니다. 그 외에도 오래된 API 사용이나 안전하지 않은 생명주기 메서드 사용 등에 대한 경고를 콘솔에 보여줍니다.

Q. 'React 18'의 주요 변경점에 대해서 알고 계신가요?
  • React 18의 가장 핵심적인 변화는 동시성 도입입니다. 이를 통해 여러 상태 업데이트를 동시에 처리하고 렌더링 중에도 사용자 입력에 반응할 수 있어 사용자 경험이 크게 향상되었습니다.

    자동 배치는 여러 상태 업데이트를 자동으로 묶어 처리하여 불필요한 리렌더링을 줄이며, 이전에는 이벤트 핸들러 내에서만 가능했지만 이제 Promise, setTimeout 등에서도 기본 적용됩니다.

    startTransition과 useTransition은 긴 렌더링을 유발하는 업데이트를 전환으로 표시하여 급한 업데이트가 중간에 끼어들 수 있도록 해 UI 반응성을 유지합니다.

    createRoot는 React 18의 동시성 기능을 사용하기 위한 새로운 진입점입니다.

React Hooks

Q. React Hooks에 대해 간단하게 설명해주세요.
  • React Hooks는 클래스 컴포넌트를 작성하지 않고도 함수 컴포넌트 안에서 상태 관리생명주기 기능 등을 사용할 수 있게 해주는 함수들입니다.
Q. Hooks를 조건문 내에서 사용하면 안되는 이유에 대해 설명해주세요.
  • 리액트에서 훅은 호출되는 순서에 의존합니다. 그 이유는 state가 자바스크립트의 클로저를 이용하여 구현되었기 때문입니다. 클로저 내에서 해당 state의 Index를 기록하고, 이 Index 값을 추적할 수 있도록 배열 내에서 상태값들을 관리합니다.

    따라서 호출 순서가 바뀔 가능성이 있는 반복문, 조건문, 중첩 함수 내에서 훅을 사용하면 에러가 발생할 수 있습니다.

Q. 메모이제이션에 대해 설명해주세요.
  • 메모이제이션(Memoization) 은 동일한 입력에 대해 동일한 결과를 반환하는 함수의 실행 결과를 저장했다가, 나중에 동일한 입력이 들어오면 다시 계산하지 않고 저장된 결과를 바로 반환하는 최적화 기법입니다. 주로 불필요한 연산이나 렌더링을 줄여 성능을 향상시키기 위해 사용됩니다.
Q. React.memo, useMemo, useCallback의 차이점에 대해 설명해주세요.
  • 셋 다 메모이제이션을 통해 성능을 최적화합니다.

    React.memo는 HOC로 컴포넌트를 감싸 props가 바뀌지 않으면 리렌더링을 방지합니다.

    useMemo는 계산 비용이 큰 값을 메모이제이션하여 동일한 입력에 대해 재계산을 방지합니다.

    useCallback은 동일한 함수 인스턴스를 유지해 자식 컴포넌트에 콜백을 전달할 때 불필요한 리렌더링을 줄입니다.

Q. useLayoutEffect에 대해서 설명해주세요.
  • useEffect는 렌더링 결과가 화면에 그려진 후 비동기적으로 실행되며 브라우저 렌더링을 블록하지 않습니다. 네트워크 요청, DOM 접근, 비동기 작업 등에 사용합니다.

    useLayoutEffect는 렌더링 결과가 paint되기 전에 동기적으로 실행됩니다. 브라우저 페인팅이 지연될 수 있지만 DOM 직접 조작, 레이아웃을 먼저 읽어야 하는 flicker 방지, 애니메이션 구현 등에 유용합니다.

Q. 리액트에서 순수함수의 Side Effect는 어떻게 처리되나요?
  • React 컴포넌트 자체는 순수 함수처럼 동작하는 것을 지향합니다. 즉, 동일한 props와 state에 대해 항상 동일한 UI를 렌더링해야 합니다. API 호출, 구독 설정, DOM 직접 조작 등의 Side Effect는 렌더링 과정에서 직접 수행하면 안 됩니다.

    React에서는 이러한 Side Effect를 처리하기 위해 주로 useEffect Hook을 사용합니다. useEffect는 컴포넌트 렌더링이 완료된 이후에 비동기적으로 실행되어, 렌더링 자체는 순수하게 유지하면서 필요한 Side Effect를 수행할 수 있습니다.

Q. 바닐라 JS로 useState를 어떻게 구현했는지 설명해주세요.
  • useState의 핵심 로직을 구현하기 위해서는 클로저를 활용합니다.

    함수 외부 스코프에 상태값을 저장할 배열과 현재 상태의 인덱스를 두고, 훅이 호출될 때 인덱스를 사용하여 배열에서 해당 상태값을 찾거나 초기화합니다. 상태값을 변경하는 setState는 클로저를 통해 자신이 관리할 상태의 인덱스를 기억하며, 해당 인덱스 값을 업데이트하고 리렌더링을 트리거합니다. 이후 [value, setter]를 반환하고 다음 useState 호출을 대비하여 인덱스를 증가시킵니다.

상태 관리

Q. 왜 state를 직접 변경하면 안될까요?
  • React가 상태 변화를 감지하고 화면을 리렌더링하도록 알려주기 위해서입니다. state 객체를 직접 수정하면 React는 변화를 알 수 없어 UI가 업데이트되지 않습니다.

    또한 React는 성능 최적화를 위해 여러 setState 호출을 하나로 묶어서 배치 처리하는 경우가 많습니다. setState를 사용해야 React가 이러한 최적화 과정을 관리하고 상태 업데이트 시점을 제어할 수 있습니다. 직접 수정하면 이런 최적화가 불가능하고 예측 불가능한 동작을 유발할 수 있습니다.

Q. React에서 State의 불변성은 어떻게 유지할 수 있나요?
  • 가장 중요한 원칙은 기존 상태 객체나 배열을 직접 수정하지 않는 것입니다.

    React는 상태의 참조값이 변경되었는지를 비교해서 리렌더링 여부를 결정하기 때문에, 원본을 직접 수정하면 변경을 제대로 감지하지 못할 수 있습니다. 그래서 상태를 업데이트할 때는 항상 새로운 객체나 배열을 만들어서 반환하는 방식으로 불변성을 유지합니다.

    객체의 경우 주로 스프레드 문법을 사용해 기존 객체의 속성들을 복사한 새로운 객체를 만들고 변경할 속성만 덮어씁니다. setState(prevState => ({ ...prevState, keyToUpdate: newValue }))

    배열의 경우 push, pop, splice처럼 원본 배열을 직접 수정하는 메서드 대신 새로운 배열을 반환하는 메서드를 사용합니다. setState(prevArray => [...prevArray, newItem])

Q. setState는 동기적으로 동작하나요? 혹은 비동기적으로 동작하나요?
  • setState는 비동기적으로 동작합니다. 다만 비동기 함수는 아닙니다.

    그 이유는 리액트의 Fiber 아키텍처와 밀접한 관련이 있습니다. Fiber는 재조정 알고리즘을 구현할 때 변경된 부분을 찾는 작업과 실제 DOM에 변경사항을 반영하는 작업을 나누어 진행합니다. 이 과정을 동기적으로 진행한다면 메인 스레드가 차단되어 프레임 드롭이나 응답 지연으로 이어져 UX를 저해하기 때문에 비동기적으로 처리됩니다.

⭐ Q. 부모 컴포넌트가 리렌더링되면, 자식 컴포넌트도 리렌더링 되나요?
  • 기본적으로 부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 함께 리렌더링됩니다.

    React는 부모의 상태나 props가 변경되어 리렌더링이 발생하면, 해당 부모가 반환하는 모든 자식 컴포넌트에게도 변경 사항이 전파될 수 있다고 가정하고 다시 렌더링을 시도합니다.

꼬리질문: 자식 컴포넌트의 리렌더링을 방지하는 방법은 무엇이 있을까요? (최적화)
  • React.memo를 사용하여 자식 컴포넌트를 감싸주면, props가 변경되지 않았을 때 리렌더링을 방지할 수 있습니다.
Q. Prop Drilling에 대해 설명해주세요.
  • Prop Drilling은 상위 컴포넌트의 상태나 데이터를 오직 하위 컴포넌트에 전달할 목적으로, 중간의 여러 컴포넌트를 거쳐 props를 전달하는 패턴을 말합니다.

    해당 데이터가 필요 없는 중간 컴포넌트들도 props를 받아서 넘겨줘야 하므로 코드가 복잡해지고 유지보수가 어려워진다는 문제점이 있습니다. 해결 방법으로는 Context API상태 관리 라이브러리를 사용하여 데이터를 필요한 컴포넌트에서 직접 접근하도록 하는 것이 일반적입니다.

Q. Context API에 대해 설명해주세요.
  • Context API는 React 컴포넌트 트리 안에서 데이터를 전역적으로 공유할 수 있도록 도와주는 기능입니다.

    다만 Context API는 상태 관리 라이브러리를 완전히 대체할 수 없습니다. 너무 자주 변경되는 데이터를 Context로 관리하면 해당 Context를 사용하는 모든 컴포넌트가 리렌더링될 수 있어 성능 문제가 발생할 수 있습니다. 테마, 언어 설정, 사용자 인증 정보처럼 자주 변하지 않는 데이터에 적합합니다.

꼬리질문: Context API와 상태 관리 라이브러리(Redux, Zustand)와의 비교를 더 자세히 말해주세요.
  • Context의 값이 변경되면 해당 Context를 구독하는 모든 컴포넌트가 기본적으로 리렌더링됩니다. 반면 Redux와 같은 상태관리 라이브러리는 useSelector 등을 통해 상태의 특정 부분만 구독하여 부분적인 변경에 최적화된 대응이 가능합니다.

    또한 Context API는 주로 데이터 전달에만 초점을 맞춘 간단한 기능이지만, 상태 관리 라이브러리는 미들웨어를 통한 비동기 처리, 로직 분리 및 DevTools 지원 같은 복잡한 상태 관리를 위한 다양한 기능과 패턴을 제공합니다.

Q. Context API, 전역 상태 관리 라이브러리를 사용하지 않고 Prop Drilling을 개선하는 방법은 무엇이 있을까요?
  • 컴포넌트 합성 패턴을 사용하는 방법이 있습니다.

    데이터가 필요한 하위 컴포넌트를 상위 컴포넌트에서 직접 생성하여 children prop이나 다른 이름의 prop으로 내려주는 방식입니다. 중간 컴포넌트는 데이터를 알 필요 없이 전달받은 컴포넌트를 렌더링하기만 합니다. <Page user={user} avatar={<Avatar user={user} />} />처럼 레이아웃 자체를 넘겨주는 방식이 그 예시입니다.

    이 패턴은 간단하고 React의 기본 원리에 충실하지만, 상위 컴포넌트에서 하위 컴포넌트 구조를 미리 정의하고 조합해야 하므로 상위 컴포넌트의 렌더 로직이 복잡해질 수 있다는 단점이 있습니다.

꼬리질문: '컴포넌트 합성 패턴 도입 vs 상태관리 라이브러리' 선택의 기준을 어떻게 할 것인가요?
  • 간단하고 지역적인 상태 전달 문제는 컴포넌트 합성으로 해결하고, 복잡하고 전역적인 상태 관리, 빈번한 업데이트, 고급 기능이 필요하다면 상태 관리 라이브러리나 Context API를 상황에 맞게 도입하는 것을 고려합니다.
Q. Flux 패턴에 대해서 설명해주세요.
  • Flux 패턴은 아키텍처 패턴 중 하나로, Action → Dispatcher → Store → View 순서의 단방향 데이터 흐름을 통해 데이터 흐름을 예측 가능하고 관리하기 쉽게 해줍니다.
Q. Client-Side 상태 관리와 Server-Side 상태 관리에 대해 설명해주세요.
  • 클라이언트 사이드 상태 관리는 사용자의 브라우저 환경 내에서 관리되는 상태를 의미합니다. UI의 표시 여부, 사용자의 입력 값, 테마 설정 등 해당 사용자의 인터페이스 경험에 직접 관련된 상태들이 여기에 해당합니다. 일반적으로 일시적이며 페이지를 새로고침하면 초기화될 수 있고, 즉각적인 UI 반응성과 사용자 경험 향상이 주된 목적입니다.

    서버 사이드 상태 관리는 서버와 DB에 있는 실제 데이터를 관리하는 것입니다. 사용자 계정 정보, 게시글 내용, 상품 목록 등 여러 사용자가 공유하거나 영구적으로 보존되어야 하는 데이터가 이에 해당합니다. 최근에는 TanStack QuerySWR과 같은 라이브러리를 사용하여 비동기 서버 상태를 효율적으로 관리하는 추세이며, 이 라이브러리들은 로딩 및 에러 상태 처리, 캐싱, 데이터 동기화 등의 복잡한 로직을 간편하게 구현하도록 돕습니다.

Q. (TanStack Query) staleTime과 gcTime의 차이점을 설명해주세요.
  • staleTime은 가져온 데이터가 신선하다고 판단하는 시간입니다. 이 시간이 지나면 데이터는 오래된 상태가 되어 다음에 필요할 때 백그라운드에서 다시 가져오려고 시도합니다. 기본값은 0초입니다.

    gcTime은 쿼리가 비활성 상태가 된 후 캐시에서 얼마 동안 데이터를 유지할지를 정하는 시간입니다. 아무 컴포넌트도 해당 데이터를 사용하지 않을 때 gcTime이 지나면 캐시에서 완전히 제거됩니다. 기본값은 5분입니다.

    핵심 차이는 staleTime은 데이터의 신선도와 재요청 시점에 관한 것이고, gcTime은 사용하지 않는 데이터의 캐시 유지 시간과 제거 시점에 관한 것입니다.

Q. (TanStack Query) 쿼리 키를 기반으로 API 호출을 캐싱하여 동일 키에 대한 중복 호출을 방지하려면 어떻게 해야할까요?
  • TanStack Query는 쿼리 키를 캐싱의 기본 단위로 사용합니다. 동일한 쿼리 키를 가진 useQuery가 여러 컴포넌트에서 호출되더라도 하나의 요청으로 처리하고 캐시된 데이터를 공유합니다.

    staleTime을 활용하면 데이터가 신선한 상태로 간주되는 시간 동안 동일한 키의 쿼리가 호출되어도 네트워크 요청 없이 캐시 데이터를 반환합니다. 배열 형태의 쿼리 키를 활용하면 ['users', userId]처럼 특정 파라미터를 포함해 쿼리를 구분할 수 있어, 각 userId별로 캐시를 독립적으로 관리하면서 동일 userId에 대한 중복 호출은 방지할 수 있습니다.

Q. Context API와 Redux의 차이점에 대해 설명해주세요.
  • 둘 다 상태 관리, 특히 props drilling을 피하기 위해 사용한다는 공통점이 있습니다.

    Context API는 리액트에 내장된 기능입니다. 테마, 로그인 정보처럼 자주 바뀌지 않는 비교적 간단한 데이터를 전역적으로 공유할 때 편리합니다. 다만 Context 값이 변경되면 이를 사용하는 모든 컴포넌트가 리렌더링될 수 있어 복잡한 상태 관리 시에는 성능에 신경 써야 합니다.

    Redux별도의 라이브러리입니다. 크고 복잡한 애플리케이션의 상태를 예측 가능하게 관리하는 데 강점이 있으며, 미들웨어를 통한 비동기 처리와 Redux DevTools 같은 강력한 개발 도구를 지원합니다. useSelector 등을 통해 필요한 상태만 구독하여 불필요한 리렌더링을 막는 데도 최적화되어 있습니다.

    결론적으로 Context API간편함과 내장 기능이 장점이고, Redux크고 복잡한 상태를 체계적으로 관리하고 강력한 개발 도구를 지원하는 데 강점이 있습니다.

최적화

Q. 렌더링 최적화를 위해 적용할 수 있는 방법들에 대해 설명해주세요.
  • 가장 중요한 건 불필요한 리렌더링을 줄이는 것이라고 생각합니다.

    React.memo, useCallback, useMemo와 같은 메모이제이션 방법이 있습니다. 관련 있는 상태는 가깝게 배치하고, 너무 많은 컴포넌트가 하나의 거대한 상태 객체에 의존하지 않도록 분리하면 불필요한 상태 업데이트가 전파되는 것을 막을 수 있습니다. 리스트 아이템을 렌더링할 때는 각 아이템에 고유하고 안정적인 key prop을 반드시 지정해야 합니다.

꼬리질문: 리스트 아이템에만 key prop을 지정하는 것이 좋나요?
  • 리스트가 아닌 경우 React는 컴포넌트 타입과 트리에서의 위치를 보고 컴포넌트의 고유성을 판단합니다. 리스트인 경우 아이템의 순서가 바뀌거나 추가·삭제될 가능성이 높기 때문에 위치만으로는 안정적인 추적이 어렵습니다. 따라서 고유한 key를 명시적으로 부여하여 변경을 추적하도록 돕는 것입니다.

    다만 리스트가 아닌 경우에도 key prop을 부여하는 것이 가능합니다. 주로 완전히 새로운 컴포넌트 인스턴스로 교체하고 싶을 때 사용합니다. 예를 들어 사용자 프로필 페이지에서 다른 사용자의 프로필로 전환될 때 key를 사용자 ID로 설정하면 프로필 컴포넌트가 완전히 새로 렌더링되어 이전 사용자의 상태가 남지 않게 할 수 있습니다.

Q. 성능(Performance) 최적화를 위해 적용할 수 있는 방법들에 대해 설명해주세요.
  • 불필요한 작업을 줄이고, 초기 로딩 속도를 개선하며, 리소스 사용을 효율화하는 것이 React 성능 최적화의 핵심이라고 생각합니다.

    코드 스플리팅을 통해 초기 로딩 속도를 크게 개선할 수 있습니다. React.lazy와 Suspense를 사용해 컴포넌트를 필요할 때만 로드하면 초기 로딩 시 필요한 코드의 양이 감소합니다. 번들 분석 도구를 사용해 번들 파일에서 많은 용량을 차지하는 라이브러리를 파악하고 더 가벼운 대체 라이브러리를 사용하거나 필요한 부분만 가져오도록 수정할 수도 있습니다. 또한 이미지 최적화로 파일 크기를 줄이고, WebP와 같은 최신 포맷을 사용하며, 지연 로딩을 적용하는 것도 중요합니다.

Q. 렌더링 최적화와 퍼포먼스 최적화의 차이점에 대해 설명해 주세요.
  • 렌더링 최적화는 React가 화면을 그리는 과정, 즉 UI를 업데이트하는 과정 자체를 효율화하는 데 초점을 맞춥니다.

    퍼포먼스 최적화는 좀 더 포괄적인 개념입니다. 렌더링 최적화를 포함해서 애플리케이션의 전반적인 성능, 즉 사용자가 느끼는 속도와 반응성을 개선하기 위한 모든 활동을 의미합니다. 초기 로딩 속도 개선, 데이터 로딩 및 처리 효율화, 메모리 사용량 최적화, 자바스크립트 실행 시간 단축 등이 여기에 포함됩니다.

    즉, 렌더링 최적화는 퍼포먼스 최적화를 달성하기 위한 중요한 방법 중 하나이며, 퍼포먼스 최적화는 로딩, 데이터 처리 등 애플리케이션 전반을 다루는 더 넓은 개념입니다.

⭐ Q. key값 사용 이유에 대해 설명해주세요.
  • React가 리스트 형태의 자식 요소들을 렌더링할 때 key prop을 권장하는 주된 이유는 Reconciliation 과정에서 각 요소를 효율적으로 식별하고 추적하기 위해서입니다.

    리스트 아이템이 추가, 삭제, 또는 재정렬될 때 key가 없으면 React는 index만으로 비교하여 어떤 요소가 변경되었는지 정확히 파악하기 어렵습니다. 이는 불필요한 DOM 조작이나 컴포넌트 재생성을 유발하여 성능 저하로 이어질 수 있습니다. 각 요소에 고유하고 안정적인 key를 제공하면 React가 이전 트리와 새 트리의 요소들을 정확히 매칭하여 최소한의 변경만으로 DOM을 업데이트할 수 있게 됩니다.

⭐ Q. 리액트에서 index를 key값으로 사용하면 안되는 이유에 대해서 설명해주세요.
  • index를 key로 사용하면 배열의 항목이 추가, 삭제, 또는 재정렬될 때 문제가 발생합니다.

    React는 key를 통해 각 요소를 식별하는데, index는 항목의 내용과 관계없이 위치에 따라 변경됩니다. 이 때문에 React가 변경 사항을 잘못 해석하여 불필요하게 DOM을 업데이트하거나 컴포넌트의 상태가 꼬이는 문제가 발생할 수 있으며, 성능 저하예측 불가능한 버그로 이어질 수 있습니다. 따라서 각 항목을 고유하게 식별할 수 있는 안정적인 값을 key로 사용하는 것이 좋습니다.