개발/프론트엔드 Job Interview

리액트 기본 개념 QnA. 1탄 (feat. 면접 뽀개기)

차얀 2021. 12. 16. 17:08
    목차
리액트 면접 대비용으로 QnA 형태로 주요 개념을 나열한 글입니다.

 

React란 무엇일까요?

리액트는 페이스북이 만든 자바스크립트 라이브러리입니다.

주로, 사용자 View 영역을 만들기 위해 사용됩니다.

 

 

 

리액트는 라이브러리인가 프레임워크인가?

우선, 라이브러리와 프레임워크의 주요 차이점은 앱의 흐름 제어권을 누가 가지고 있는지에 따라 나누어집니다.

라이브러리는 내가 주도성을 가지고 가져와 호출해 사용하는 개념이고, 

프레임워크는 자체적으로 주도성을 가지고 있고, 내가 거기에 들어가서 사용하고 호출하는 개념입니다.

 

리액트는 UI 렌더링에만 관심을 가지고 있고

그 외 동작 (라우터, 상태 관리, 빌드, 테스트 등)들을 수행하기 위해서

다른 기술 스택들을 사용해야 한다는 관점에서 보면

리액트도 결국 뷰 관리를 위해 가져다 사용하는 도구이기 때문에

라이브러리라 할 수 있습니다.

 

 

 

리액트의 주요 특징은 뭐가 있을까요?

뷰 렌더링 성능 개선을 위해 virtual DOM을 활용합니다.

단방향 데이터 흐름 방식으로 데이터를 관리합니다.

 

 

 

Vue vs React

가장 주요한 차이점은, Vue는 프레임워크이고, React는 라이브러리라는 점입니다.

 

  1. 협업 시 코딩 컨벤션 맞추기
    Vue를 사용하는 사람들은 프레임워크 내 코드를 작성하는 방식이기 때문에,
    코드를 작성하는 방식이 비슷해 협업 측면에서 별도의 코딩 컨벤션에 대한 논의를 덜 해도 됩니다.
    반면, 리액트는 자유도가 높고, 기능을 구현하는 방법이 다양하기에,
    코딩 컨벤션을 맞추는데 많은 시간을 들여야 합니다.

  2. 컴포넌트 재활용
    리액트에서는 함수형 컴포넌트를 이용해 쉽고 깔끔하게 재활용 가능한 컴포넌트를 만들 수 있습니다.
    하지만, 뷰에서는 이를 위해서 별도의 파일 및 스크립트를 추가로 작성해야 될 필요가 있기에 불편합니다.

  3. 러닝 커브
    뷰는 자바스크립트를 잘 몰라도, 뷰에서 제공해주는 문법만 알면 빠르게 개발할 수 있다.
    반면 리액트는 자바스크립트 기반이라 이를 잘 알아야 하고, 뷰 이외의 기능(상태 관리, 라우터 등)을 위해선
    새로운 툴을 공부해야 된다는 단점이 있다.

  4. 타입스크립트
    리액트는 타입스크립트와 잘 호환이 되지만, 뷰는 2.x 버전 기준으로 지원이 완벽하지 않다.

결국, 규모 작고 빠르게 개발해야 하는 경우엔 Vue를 사용해도 좋지만,

프로젝트 규모가 커지고, 컴포넌트 재활용의 빈도가 많게끔 사용하기 위해선 리액트를 사용하는 것이 좋다.

 

 

 

리액트의 단점에 대해 말해주세요.

단순히 뷰를 관리하는 라이브러리이기 때문에,
그 외 동작 (라우터, 상태 관리, 빌드, 테스트 등)들을 수행하기 위해서

다른 기술 스택을 공부하고 사용해야 하는 측면에서 러닝 커브가 높아집니다.

 

또, 동일한 웹앱이라도 리액트로 개발된 웹앱의 비교적 느린 속도와 큰 용량이 단점으로 언급되곤 합니다.

이 때문에, 피쳐폰, 2G 환경 등 측면에서 접근성이 떨어지기에 이점을 고려해야 합니다.

 

 

 

데이터 단방향 바인딩과 양방향 바인딩의 차이가 뭔가요?

양방향 바인딩과 단방향 바인딩의 모습

우선, 데이터 바인딩이란 화면에 보이는 데이터와 메모리 내 데이터를 일치시키는 기법을 의미합니다.

 

양방향 데이터 바인딩은 사용자의 입력값이 곧바로 코드 상의 변수에 바인딩될 수 있지만,

단방향 데이터 바인딩은 적절한 이벤트를 통해야 코드 상의 변수에 바인딩될 수 있습니다.

 

양방향 데이터 바인딩의 방식이 이벤트 바인딩을 위한 코드량을 줄여주긴 하지만,

변에 따라 DOM 전체 렌더링 해주거나 데이터 변화하므로 성능 측면에선 단방향 데이터 바인딩 방식보다 불리합니다.

 

추가로, 뷰에서는 양방향 바인딩 방식을 사용하고 리액트에선 단방향 바인딩 방식을 사용합니다..

 

 

 

virtual DOM이란 무엇이고 왜 virtual DOM을 사용할까요?

DOM을 객체 형태로 만들어 둔 것이 virtual DOM입니다.

 

이후, 기존 vdom과 새로운 vdom을 비교해서 자체적인 재조정 알고리즘을 통해

변화된 부분을 모아 바뀐 부분을 통째로 변경하는 방식으로

재 렌더링 비용을 절약했습니다. (최소 수정, 최대 성능)

 

물론, 바닐라 JS에서도 DocumetFragment와 같은 인터페이스를 이용해 비슷한 방식으로
작동하게 할 수 있지만, virtual DOM을 사용하는 리액트에서는 이를 자동화해
사용자가 UI 구현에 보다 집중할 수 있게끔 하였습니다.

 

 

 

reconciliation(재조정) 알고리즘이란 무엇인가?

기존 vdom과 새로운 vdom을 비교하는 알고리즘을 의미합니다.

일반적인 비교 알고리즘은 O(n^3)의 비교 연산이 필요하기 때문에 재조정 알고리즘(O(n))을 선택했습니다.

 

재조정 알고리즘이 변경을 파악하는 방법으론 

우선, 엘리먼트 타입이 다른 경우, 이전 트리를 버리고 새로운 트리를 구축합니다 (새로 마운트 수행)

두 번째로, 엘리먼트의 속성이 다른 경우, 해당 속성만을 갱신합니다.

 

그리고 자식에 대해선, key를 이용해 변경된 자식 컴포넌트에 대해서만 갱신합니다.

(즉, key 값이 없으면 변화 발생 시 통째로 바꿔 끼어 비효율적입니다.)

 

이후, 해당 알고리즘을 통해 바뀌었다고 판단되는 부분을 통째로 변경하는 방식으로
재 랜더링 비용을 절약했습니다.

 

 

 

JSX란 무엇인가요?

주로 React와 함께 사용되는 자바스크립트 확장 문법으로, HTML의 형태를 띠고 있습니다.

즉, HTML의 형태로 보이지만 HTML이 아니고, React.createElement()의 시맨틱 슈가입니다.

(참고로 createElement는 virtual DOM을 만들어내는 역할을 수행합니다)

// React에서 사용되는 JSX의 형태
// HTML 문법의 형태를 띄고 있다.
const App = () => {
	return (
    	<h1 id="title">Hello World!</h1>
    )
}

// 하지만 실제론 아래와 같은 createElement의 시맨틱 슈가 역할이다
return React.createElement('h1', { id: "title" }, "Hello World!")

 

 

 

클래스 컴포넌트와 함수 컴포넌트의 차이는 무엇인가요?

리액트의 처음에는 클래스형 컴포넌트만 사용되었습니다.

이후 state가 필요 없는 컴포넌트(presentation의 역할)를 수행하는 컴포넌트를
함수 컴포넌트를 사용하는 방식으로 사용되기 시작했습니다.

 

그러다 리액트 훅이 나오고, 함수 컴포넌트에서도 state를 사용할 수 있게 되자

현재는 함수 컴포넌트로 컴포넌트를 구현하는 게 방식이 주가 되었습니다.

 

함수형 컴포넌트가 주가 된 원인은 기존 클래스 컴포넌트의

길고 복잡한 보일러 플레이트, 어려운 컴포넌트 및 로직 재활용와 같은 단점이 주된 원인이었습니다.

 

 

 

HOC란 무엇인가요?

고차 컴포넌트(Higher Order Component)의 줄임말로,

컴포넌트 재사용의 목적으로 사용되는 패턴으로, 다른 컴포넌트를 감싸는 리액트 컴포넌트입니다.
이제는, 커스텀 훅(custom Hook)을 이용해 이를 대체합니다.

 

보통 아래와 같이 반복될 수 있는 코드들을 HoC를 만들어 해결할 수 있습니다.

  • 로딩 중 스켈레톤 보여주다 완료 시 데이터 보여주기,
  • 로그인 여부 확인 후 처리
  • 에러 처리에 대한 분기 적용 및 에러 메시지 표시

with-* prefix를 붙여 사용되는 편입니다.

// 함수형 컴포넌트 HOC 예제
const withHOC = WrappedComponent => {
  const newProps = {
    loading: false,
  };
  return props => {
    return <WrappedComponent {...props} {...newProps} />
  }
};

// 클래스형 컴포넌트 HOC 예제
const withHOC = WrappedComponent => {
  const newProps = {
    loading: false,
  };
  return class extends React.Component {
    render() {
      return <WrappedComponent {...this.props} {...newProps} />
    }
  }
};


// 이런 HOC를 사용하는 법
export default withHOC(AnyComponent);

이때 render 메서드 안에서 HOC를 사용하게 되면 render 메서드 실행될 때마다, HOC를 새로 만드니 주의해야 해야 합니다.

 

 

 

HOC를 구현하는 방법에 대해 아시나요?

리액트에서 WrappedComponenet를 조작하는 HOC를 구현하는 주요 방법 2가지가 있습니다.

Prop Proxy(PP)와 Inheritance Inversion (II)가 있습니다.

 

우선 Prop Proxy 방식은

HOC가 받은 props를 전달하는 방식을 의미합니다.

// prop proxy 방식의 HOC
// 함수형 컴포넌트 HOC 예제
const withHOC = WrappedComponent => {
  const newProps = {
    loading: false,
  };
  return props => {
    return <WrappedComponent {...props} {...newProps} />
  }
};

// 클래스형 컴포넌트 HOC 예제
const withHOC = WrappedComponent => {
  const newProps = {
    loading: false,
  };
  return class extends React.Component {
    render() {
      return <WrappedComponent {...this.props} {...newProps} />
    }
  }
};


// 이런 HOC를 사용하는 법
export default withHOC(AnyComponent);

이를 이용해

props 조작, ref를 통한 인스턴스 접근, state 추상화와 같은 작업이 가능합니다.

 

다음으로 Inheritance Inversion(상속 역전) 방식은

해당 HOC를 사용하는 컴포넌트를 확장하는 방식을 의미합니다.

이 과정에서 HOC와 WrappComponent와의 관계가 역전되어 위와 같은 네이밍이 붙었습니다.

// ii HOC 기본 예제
function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      return super.render();
    }
  };
}

 

 

 

HOC의 기능을 커스텀 훅을 이용해 구현할 수도 있는 거 같은데 어떻게 생각하세요?

HOC 방식에는 몇 가지 문제가 있습니다.

 

우선, 구현한 HOC를 어느 컴포넌트에서 사용되는지 빠르게 알기가 힘듭니다.

또, HOC 컴포넌트를 사용할때, HOC 내부에서 선언되는 props에 대한 정보를 파악하기도 어렵습니다.

그리고 HOC 간에 조합이 있고, 서로 간 결과에 의존적일 때 데이터를 넘기는 과정이 블랙박스이기에 이 과정에서 문제가 발생하기도 합니다.

 

이제는 함수 컴포넌트가 주가 되었고, 커스텀 훅으로 HOC의 기능을 구현할 뿐만 아니라

위와 같은 문제를 해결할 수 있게 때문에 커스텀 훅을 사용하는 방식을 지향합니다.

 

 

 

pure componenet가 무엇인가? (feat. useMemo)

클래스 컴포넌트를 보통 기본 컴포넌트와 Pure Component로 나눈다.

함수형 컴포넌트에선 React.memo가 이 역할을 수행한다.

 

이 둘의 차이는 성능의 차이이다.

props와 state를 얕은 비교를 통해 불필요한 리 랜더링을 방지한다.

즉, 리 랜더링이 발생하는 경우 (setState 호출 등) props와 state를 얕은 비교 해 같은 경우 리 렌더링을 수행하지 않는다.

(useMemo는 depenedency에 넣어준 값이 동일하면 기존 값 재활용한다)

 

만약 state나 props가 항상 바뀌는 컴포넌트를 pure componenet로 선언하면,

비교 후 render 작업을 별도로 수행해야 하기 때문에 성능 감소를 일으킨다.

 

 

 

key가 존재하는 이유 (feat. index를 key값으로 사용하면 안 되는 이유)

리액트에서 key는 반복되는 요소에 대해 달아 줍니다.

이는 리 랜더링 되는 과정에서 반복되는 요소에 대해
이전 virtual DOM과 바뀐 virtual DOM을 비교하는 과정에서 사용되는데,

key값을 이용해 바뀐 자식 요소에 대해서만 리 랜더링을 수행합니다.

(즉, key값이 없으면 자식 컴포넌트 전부를 리 렌더링을 수행하기 때문에 성능에서 비효율적인 겁니다)

 

또, 이런 점 때문에 index 값을 key 값으로 사용하지 말라는 이야기가 나오는 것인데,

내부 요소의 추가, 삭제, 변경에 따라 각 요소에 달리는 index 값이 달라지면 문제가 생기게 되는 겁니다.

 

그래서 index를 key값으로 사용하려면,
반복되는 요소가 정적이고, 재정렬 및 필터링이 되지 않는 경우에만 사용을 해도 됩니다.

 

 

 

state와 props의 차이점은?

props는 부모 컴포넌트로부터 자식 컴포넌트로 전달되는 데이터입니다.

이는 수정될 수 없고 다른 값을 계산하거나 표시하는 데 사용됩니다.

 

state는 컴포넌트 내에서 수정될 수 있는 내부 데이터입니다.

 

 

 

setState를 이용해 state를 변경하는 이유

setState는 리액트에게 리 랜더링을 수행하라고 알려주는 역할을 수행합니다.

(즉, state를 직접 수정 하련 리 랜더링이 발생하지 않는다)

 

참고로, setState는 반드시 동기적으로 일어나지 않기 때문에,

비슷한 시점에 같은 state를 수정하면 오류가 발생하곤 합니다.

이를 방지하기 위해서 setState 내에서 콜백 함수처럼 내부 인자를 state와 props를 사용하면 됩니다.

// setState 동기적으로 하는법
const App = () => {
	const [number, setNumber] = useState(0);
	...
    const updateState = () => {
    	/* 둘다 정상적으로 실행된다는 보장이 없다.
    	setNumber(number + 1);
        setNumber(number - 1);
        */ 
        
        /* 적절한 대처법
        setNumber((state) => state + 1);
        setNumber((state) => state - 1);
        */
    }
 	...
}

 

또, 만약 state값이긴 한데, 수정 시에 리 랜더링이 발생할 필요가 없는 state의 경우엔,

컴포넌트 밖에 일반적인 변수 선언을 사용해 해당 값을 수정하는 방식으로 사용하면 됩니다.

// 변수 업데이트시 리랜더링 필요없는 변수 예제
let justInnerState = 0;

const App = () => {
	...
    justInnerState++  // 해당 변수는 아래 return내 사용되지 않고 내부적으로 이용됩니다
    ...
}

 

 

 

prop drilling이란 무엇이고 이를 회피하는 방법은 뭐가 있을까요?

부모 컴포넌트에서 하위 컴포넌트로 데이터를 전달할 때 발생하는 것으로,

props를 전달하는 목적 외 props를 필요로 하지 않는 컴포넌트가 발생하는 현상을 의미합니다.

 

이를 회피하기 위해선,

컴포넌트를 리팩터링 해 state의 위치를 적절히 조율하는 방법을 사용해야 하긴 하지만,

가장 최선의 방법은 전역 상태 관리 라이브러리를 사용하는 것입니다.

 

 

 

제어 컴포넌트와 비제어 컴포넌트가 무엇인지 말해주세요.

보통, input, textarea, select와 같은 폼 엘리먼트 내 입력 요소에서 데이터를 관리하는 방법을 의미합니다.

 

제어 컴포넌트는 이 데이터 state를 "신뢰 가능한 단일 출처 (single source of truth)"로 만듭니다.

즉, state를 보여주는 값으로 넘기고,

해당 state를 다룰 수 있는 이벤트 핸들러를 콜백으로 넘기는 방식으로 구현합니다.

// 일반적인 제어 컴포넌트 구조
const [name, setName] = useState("")

handleOnChange(event) {
	setName(event.target.value);
}
...

<input type="text" value={name} onChange={handleOnChange} />

 

반대로, 비제어 컴포넌트는

모든 state 업데이트에 대해 이벤트 핸들러를 작성하는 대신,

ref를 사용해 DOM에서 값을 가져와 필요한 연산 후, 적절한 시점에 보이는 영역에 데이터를 pull 합니다.

// 일반적인 비제어 컴포넌트 구조
const nameRef = useRef(null);

handleSubmit(event) {
	alert("이름 : " + nameRef.current.value)
	e.preventDefault()
}
...

<input defaultValue="기본값" type="text" ref={nameRef} />

 

 

 

제어 컴포넌트와 비제어 컴포넌트의 각각의 장단점을 말해주세요.

제어 컴포넌트는 데이터 무결성을 잘 지키기에 내부의 state값과 보여지는 값이 항상 일치합니다.

하지만, 이 state 값이 커질수록 성능상 문제가 발생할 수 있습니다.

 

반면 비제어 컴포넌트는, 데이터의 무결성에 대한 관심도는 떨어지지만,

입력된 값을 필요한 시점에 가져오기 때문에 성능상 유리합니다.

 

단, 즉각적인 유효성 검사, 조건부 버튼 비활성화와 같은 기능을 구현해야 하는 경우엔 제어 컴포넌트가 유리합니다.

 

 

 

그럼 제어 컴포넌트와 비제어 컴포넌트 둘 중 어느 것을 사용해야 할까요?

반드시 뭐가 더 좋다 라는 건 없습니다.

이 둘을 상황에 맞게 적절히 사용하면 됩니다. (위 문항 참고)

그리고 반드시 하나의 방법을 사용하지 않고,

두 개를 적절히 섞어 single source of truth를 지킨다는 생각으로 구현해도 좋습니다.

(내부적으론 제어 방식, 외부에 공개되는 건 비제어 방식과 같은 식으로...)

 

 

 

리액트 생명주기에 대해 아는 대로 말해주세요.

리액트 생명주기는 크게 3가지 과정으로 나누어 볼 수 있습니다.

 

  1. 초기화 과정 (mounting)
    말 그대로 컴포넌트가 초기화되는 과정을 의미합니다.
    prop와 state를 세팅하고, DOM을 렌더링 후, componentDidMount를 실행합니다.

     - constructor : 메서드 바인딩, state 초기화 작업 수행합니다.
     - getDerivedStateFromProps : 시간이 흐름에 따라 변하는 props에 state가 의존하는 아주 드문 경우를 위한 메서드
     - render
     - 실제 DOM 반영 및 ref 바인딩
     - 
    componentDidMount : 렌더링 후, 모든 ajax 요청, state 및 DOM 변화, 이벤트 리스너 설정 이후 실행됩니다.
  2. 업데이트 과정 (updating)
    컴포넌트가 업데이트되는 경우를 의미합니다.
    새로운 props를 보내거나, setState나 forceUpdate를 통해 업데이트되는 경우 실행됩니다.
     - getDerivedStateFromProps (새로운 props 들어오는 경우 실행됩니다)
     - shouldComponentUpdate : default true로 false 리턴하면 컴포넌트 업데이트를 실행하지 않습니다
     - render
     - getSnapshotBeforeUpdate
      : 새로운 DOM이 적용되기 직전에 호출되고, 이전 스크롤 위치 정보 등을 원할 때 사용됩니다.
      : return 하는 값이 componentDidUpdate에 snapshot 인자로 들어갑니다.
     - 실제 DOM 반영 및 ref 바인딩
     - componentDidUpdate : 이전 prop, state 및 앞서 반환된 snapshot 값을 이용해 필요한 DOM 작업을 수행합니다.
  3. 언마운트 과정 (unmounting)
    컴포넌트의 DOM이 더 이상 필요 없는 경우 실행됩니다.
     - componenetWillUnmount : 컴포넌트가 DOM 상에서 제거될 때에 호출됩니다.

리액트 16.3 이후 리액트 생명주기

 

 

 

함수형 컴포넌트에서 리액트 생명주기 메서드를 사용할 수 있나요?

함수형 컴포넌트는 앞서 본 메서드를 직접 가져와 생명주기를 사용하지 않습니다.

대신, useEffect를 사용해 구현합니다.

// 1. componentDidMount
// 초기화 되는 과정중, render 호출해 렌더링 이후 실행됩니다.
useEffect(() => {
	// ...
}, []}


// 2. componentDidUpdate, getDerivedStateFromProps
// 2번째 인자 배열의 내용이 변화할때만 실행됩니다.
useEffect(() => {
	// ...
}, [prop값, state값]}


// 3. componentWillUnmount
// 컴포넌트가 DOM에서 언마운트 될때 실행됩니다.
useEffect(() => {
	return () => {
    	// ...
    }
}, [...]}

 

 

 

리액트 훅이란?

리액트 v16.8 이후에 새롭게 도입된 기능으로,

함수 컴포넌트에서 state를 가지고 생명주기 관리 기능을 사용할 수 있게 해 줬습니다.

대표적으로 useState와 useEffect가 있습니다.

 

기존 클래스 컴포넌트가 있는데 함수 컴포넌트를 사용하는 이유는

클래스 컴포넌트와 함수 컴포넌트의 차이는 무엇인가요?

문항을 확인해주세요.

 

 

 

리액트 훅은 어떤 원리로 함수 컴포넌트에서 상태를 유지하나요?

우선 useState는 클로저를 활용해 그 상태를 유지합니다.

// useState 내부 구조 기본 예제
function useState(value) {
    const state = hooks[idx] || initVal // 훅들은 서로 의존성을 가집니다.
    const setState = newVal => {
    	hooks[_idx] = newVal
    }
    
    idx++; // 훅들은 서로 의존성을 가집니다.
    
    return [state, setState]
 }

 

useEffect는 의존성 배열에 있는 값을 최초에 저장합니다.
이후 새로 들어온 값과 비교해 변화 여부를 감지하고, 변화가 있으면 주어진 콜백 함수를 실행합니다.

function useEffect(cb, depArray) {
  const oldDeps = hooks[idx];  // 훅들은 서로 의존성이 존재합니다.
  let hasChanged = true
  
  if (oldDeps) {
    // 의존 값 배열의 값 중에서 차이가 있는지 확인합니다.
    hasChanged = depArray.some((dep, i) => !Object.is(dep, oldDeps[i]))
  }
  
  // 값이 바뀌었으면 주어진 콜백 함수를 실행합니다.
  if (hasChanged) {
    cb()
  }
  ...
  idx++;  // 훅들은 서로 의존성이 존재합니다.
}



 

리액트 훅을 사용하는 규칙에 대해서 아시나요?

컴포넌트 내 최상위에서만 리액트 훅을 호출해야 합니다.

 

보통, 리액트는 Hook이 호출되는 순서에 의존합니다.
useState나 useEffect를 보면,
컴포넌트 내 Hook들에 대한 정보(state값, dependecy에 있는 변수의 값)들을 배열 형태로 담고 있습니다.


즉, 훅마다 훅 정보 배열에 의존하기에 이를 참조하기 위한 index 정보가 매우 중요합니다.

만약 조건문 등 내에 훅이 위치하게 되면, 조건에 따라 index(Hook의 호출 순서)가 달라지기에, 
문제가 발생하게 됩니다.

 

 

 

 

다음 글에서 계속됩니다.

 

참고

https://yceffort.kr/2019/08/13/reactjs-interview-questions-1
https://velog.io/@dojunggeun/React-interview-questions-15#4-key%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%EB%90%98%EB%82%98%EC%9A%94
https://www.sitepoint.com/react-interview-questions-solutions/
https://velog.io/@endol007/%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8-React-%EA%B4%80%EB%A0%A8