0117 - 0123

0117

React 훅

등장 배경

컴포넌트에 보다 많은 로직을 작성해 래퍼 지옥 문제를 해결하려고 하면 컴포넌트가 커지고, 리팩토링 하기 어려워진다.
반면, 컴포넌트를 더 작은 조각으로 나눠 재사용하려고 하면 컴포넌트 트리보다 훨씬 많은 중첩이 생겨 래퍼 지옥이 다시 나타난다.

레거시 설계

  • 리팩토링, 테스트가 어려운 규모가 큰 컴포넌트
  • 컴포넌트 간 중복된 상태, 라이프 사이클
  • Render Props, HOC와 같은 복잡한 패턴
    • React 컴포넌트 간에 로직을 재사용하기 위해 사용된 Render Props, HOC 패턴은 코드 추적을 어렵게 한다.
    • 컨텍스트 공급/수요자, 고차 컴포넌트 등을 포함한 수많은 추상 레이어로 복잡하게 엮여진 래퍼 지옥을 보게 된다.

-> 문제의 원인을 리액트 팀은 클래스 컴포넌트에서 로직 재사용을 위해 사용되었던 복잡한 방식보다 간단한 방법을 제공하지 못했기 때문이라고 분석했다.

  • 클래스 컴포넌트를 사용할 때 JS의 this가 어떻게 동작하는지 이해해야 하지만, 여타 언어와 다르게 작동함에 따라 사용자의 혼란을 부추김
  • 이벤트 핸들러를 등록하기 위한 다양한 방식을 이해해야 했고, 클래스 필드 구문의 도움이 없을 경우 코드는 매우 장황해졌다.
  • 클래스는 번들 과정에서 파일의 크기를 증가시켜 성능 문제를 가져온다.

React 훅의 장점

  • 클래스를 사용하지 않아도 함수 컴포넌트를 중심으로 앱 개발이 가능
  • 훅을 사용해 컴포넌트에서 상태 및 로직을 추출한 후, 다른 컴포넌트에서 재사용 할 수 있다.
  • 서로 관련 있는 코드들을 한군데에 모아 작성할 수 있다.

React 훅의 특징

캡슐화

  • 훅은 완전하게 캡슐화 처리되므로, 현재 실행 중인 컴포넌트에서 훅을 호출할 때에도 격리된 로컬 상태를 유지한다.
  • 훅은 상태를 공유하는 방법이 아니라, 상태 저장 로직을 공유하는 방법이다. 결과적으로 리액트의 단방향 데이터 흐름을 깨트리지 않는다.

클린 트리

  • 훅 간에 데이터를 전달하는 기능은 Hooks를 애니메이션, 구독, 폼 관리 등을 처리하는데 매우 적합하다.
  • Hooks는 컴포넌트 트리를 어지럽게 만드는 거짓 된 레이어를 추가하지 않는다. 컴포넌트에 연결된 메모리 셀의 단순한 리스트와 유사하다.

상태 유지 위치

  • 리액트가 Hooks에 대한 상태를 유지하는 위치는 클래스 컴포넌트 상태를 유지하는 방법과 똑같이 유지한다.
  • 리액트에는 컴포넌트를 어떻게 정의하든 관계없이 모든 상태에 대한 내부 업데이트 대기열이 있다.

보다 작은 번들 크기

  • Hooks는 클래스를 사용할 때 보다 번들된 파일의 크기를 줄여 다소 성능 향상을 기대할 수 있다.

Hooks의 사용 규칙

Hooks는 조금 특별할 뿐인 일반 JS 함수로, 다음 2가지 규칙을 준수해야 한다.

  1. React 함수 컴포넌트, 다른 커스텀 Hook 함수 안에서만 사용 가능하다. 일반 함수, 또는 클래스 컴포넌트 안에서 호출하면 안 된다.
  2. Hook은 문 또는 중첩 된 함수 안에서 사용할 수 없다. 컴포넌트, 커스텀 훅 함수 최상위에만 사용




0118

useState

함수 컴포넌트에서 상태를 관리할 때는 React.useState() 훅을 사용한다.

상태 값, 업데이트 함수

  • useState() 훅은 상태와 상태 업데이트를 담당하는 함수를 반환한다.
  • 훅에 최초 전달된 값이 컴포넌트의 초기 상태 값이 되며, 함수 컴포넌트가 다시 렌더링 될 때는 항상 이전의 업데이트 된 상태 값이 최신 상태 값이 된다.
    const [stateValue, stateUpdater] = useState(initialState);

지연된 초기화

  • initialState 인자는 함수 컴포넌트 초기 렌더링 시에만 사용되는 state 초깃값을 설정한다.
  • 이후 다시 렌더링 될 때는 이 값이 무시된다.
  • 만약 state 초깃값을 계산하는데 많은 시간이 필요한 경우는 초기 렌더링 시에만 실행될 함수를 설정해 지연된 초기화 처리가 가능하다.
const [stateValue, setUpdater] = useState(() => {
  const initialState = localStorage.getItem("item");
  return JSON.parse(initialState);
});

객체 타입 상태 관리

  • useState() 훅에서 클래스의 state, setState()와 동일하게 작동되도록 하려면 합성된 객체를 반환해야 한다.
const [state, updateState] = useState({
  key1: false,
  key2: true,
});

updateState({
  ...state,
  key2: true,
});
  • useState()는 상태를 합성하는 것이 아니라 대체한다.
  • 클래스 컴포넌트의 setState 메서드와 다르게, 이전 상태를 새로운 상태가 대체한다.

useEffect

함수 컴포넌트에서 발생 가능한 부수 효과는 React.useEffect 훅으로 관리한다.

이펙트 함수

  • 사이드 이펙트(비동기 통신 요청/응답, DOM 조작, 구독/취소 등)는 클래스 컴포넌트의 render 메서드에서는 다룰 수 없다.
  • 이를 다룰 수 있는 라이프 사이클 메서드 내에서만 다뤄야 한다.
  • useEffect 훅은 클래스 컴포넌트의 사이드 이펙트 관리 라이프 사이클 메서드 기능을 모두 처리한다.
  • 렌더링 이후에 발생한다. 즉, effect가 수행되는 시점에 이미 DOM이 업데이트 되었음을 보장한다.
  • useEffect에 전달된 함수는 모든 렌더링에서 다르다.
useEffect(() => {
  // DOM 마운트(렌더링) 이후 콜백
});

이펙트 조건 처리

  • 특정 조건에 따라 이펙트 함수를 실행해야 할 경우, useEffect() 훅의 2번째 인자로 종속성 배열을 설정한다.
useEffect(() => {
  // componentDidMount
}, []);
  • 종속성 배열에 관리할 상태가 추가되면, 해당 상태가 변경될 때에만 이펙트 함수가 실행된다. (조건 처리)

클린업

  • 이펙트 함수를 통해 설정된 사이드 이펙트(구독)는 컴포넌트가 UI에서 제거될 때 정리되어야 한다.
  • 이를 수행하려면 이펙트 함수에서 클린업 함수를 반환하고 그 함수 내부에서 정리를 실행한다.
  • effect가 함수를 반환하면 리액트는 그 함수를 정리가 필요한 때에 실행시킨다.
useEffect(() => {
  // 구독
  return () => {
    // 정리 함수
    // 구독 취소
  };
}, []);
  • 클린업 함수는 메모리 방지를 위해 UI에서 컴포넌트를 제거하기 직전 수행된다.
  • 이는 클래스 컴포넌트의 componentWillUnmount와 유사하게 동작한다고 볼 수 있지만, 실상은 다음 이펙트 함수가 실행될 때마다 클린업 함수가 먼저 실행되어 정리한다.

effect가 업데이트 시마다 실행되는 이유

  • 컴포넌트가 화면에 표시되어 있는 동안 prop이 변한다면 무슨 일이 일어날까?
    • 버그가 발생하고, 마운트 해제가 일어날 동안에 잘못 호출되어 메모리 누수나 충돌이 발생할 수 있다.
    • 클래스 컴포넌트에서는 이를 해결하기 위해 componentDidUpdate를 사용하는데, 리액트 애플리케이션의 흔한 버그 중의 하나가 이를 제대로 다루지 않아서 발생한다.
  • useEffect는 기본적으로 업데이트를 다루기 때문에 더는 업데이트를 위한 특별한 코드가 필요 없다.

Effect를 건너뛰어 성능 최적화하기

  • 모든 렌더링 이후에 effect를 정리하거나 적용하는 것이 때때로 성능 저하를 발생시키는 경우도 있다.
  • 클래스 컴포넌트의 경우에는 componentDidUpdate에서 prevProps나 prevState와의 비교를 통해 문제를 해결할 수 있다.
  • useEffect의 두 번째 인수로 배열을 넘기면 내부 값이 변경되지 않는다면 리액트로 하여금 effect를 건너뛰도록 할 수 있다.
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // count가 바뀔 때만 effect를 재실행합니다.
  • 리렌더링 이후에 count가 변하지 않았다면 effect를 건너뛰게 된다.
  • 이 최적화 방법을 사용한다면 배열이 컴포넌트 범위 내에서 바뀌는 값들과 effect에 의해 사용되는 값들을 모두 포함하는 것을 기억해야 한다.
    • 그렇지 않으면 현재 값이 아닌 이전의 렌더링 때의 값을 참고하게 된다.
  • effect를 실행하고 이를 정리하는 과정을 딱 한 번씩만 실행하고 싶다면, 빈 배열을 두 번째 인수로 넘기면 된다.
    • 이렇게 함으로써 리액트로 하여금 effect가 props나 state 그 어떤 값에도 의존하지 않으므로 재실행되어야 할 필요가 없음을 알게 하는 것이다.




0119

카카오 FE 플랫폼 1차 면접 회고

사실 처음 면접을 시작하기 직전에는 걱정되는 마음 뿐이었다.
개발은 워낙 범위가 넓다보니 나올 예상 질문들을 대비해도 끝이 없다는 생각에 이 모든 것을 다 대비할 수는 없다고 생각했었다.

가끔은 멈출 수 없는 상상력 때문에 스트레스를 받기도 하지만, 그만큼 여러 상황에 대비할 수 있어서 좋은 결과가 따라오는 것 같다.

면접을 보고 난 직후의 느낌

나는 정말 인복을 타고난 사람이라고 느낀다.
이번 1차 면접에서도 좋은 면접관님들을 만나서 긴장하지 않고 내가 아는 것들을 잘 전달할 수 있는 시간이었다.

면접 보는 동안 시간 가는 줄 몰랐고, 선배 개발자들과 개발에 대해 대화하고 인사이트를 얻는다는 느낌을 받았다.
사용자 관점에 관한 질문들이 기억에 남는데, 이에 대해서 생각지 못했던 것들을 생각해 볼 수 있었다.

면접 분위기는 정말 좋았지만 막상 면접을 마치고 나니 답변을 못했던 것들만 생각이 났다.
특히 충분히 답변할 수 있었음에도 불구하고 순간 긴장해서 답변을 제대로 하지 못했던 질문들에 대해서는 아쉬움이 남았다.

면접 보기 전에는 ‘후회없이 아는 것을 말하고 나오자’라는 생각으로 면접에 임했다면, 면접을 보고 난 후에는 더욱 간절함이 커지고 꼭 이 팀에서 함께 일하고 싶다는 생각이 크게 들었다.

현재와 앞으로는?

정말 기쁘게도 1차 면접 결과는 합격이었다.

지금까지의 노력들이 인정받는다는 기분이 들어서 특히 더 좋았던 것 같다.
최종면접까지 약 3주간의 시간이 남았는데, 그 동안 해야할 일들을 정리해보았다.

  • 자기소개서와 프로젝트 더욱 꼼꼼하게
  • 인성 질문 대비
  • 타입스크립트 학습

타입스크립트는 어느 부서에 들어가든 두루두루 사용된다는 생각이 들어 미리 학습해야겠다고 생각했다.
현재에 안주하지말고 남은 기간 최선을 다해 후회없는 시간을 보내자




0120

이벤트 핸들러 내부의 this

  • 유사한 여러 개의 요소가 있고, 모두 같은 이벤트 핸들러가 등록되었을 때 그 이벤트 핸들러는 이벤트가 일어난 요소를 참조하는 것이 편할 것이다.
  • 따라서 이벤트 핸들러 내부의 this는 이벤트 객체의 currentTarget 프로퍼티와 같다.
  • 그러므로 이벤트 핸들러 프로퍼티 방식과 addEventListener 메서드 방식 모두 이벤트 핸들러 내부의 this는 이벤트를 바인딩한 DOM 요소를 가리킨다.
<body>
  <button type="button" id="button">click me</button>
  <script>
    const $button = document.getElementById("button");

    $button.addEventListener("click", function () {
      console.log(this); // $button 요소를 가리킨다.
    });
  </script>
</body>
  • 다만 이벤트 핸들러를 화살표 함수로 등록하게 된다면 이는 렉시컬 this를 가리키므로 this는 전역 객체인 window를 가리키게 된다.

drop & drop 구현 시, 이벤트 핸들러에서 preventDefault를 호출해야 하는 이유

  • 웹 페이지나 애플리케이션의 대부분의 구역들은 데이터를 드롭하기에 유효하지 않다.
  • drop 이벤트를 사용하기 위해서는 dragenter와 dragover의 이벤트에서 preventDefault 메서드를 호출하여 drop을 허용하지 않는 기본 동작을 방지해야 한다.
  • 또한 링크를 드래그 드롭한 경우 링크가 열리는데, 이를 방지하기 위해서는 drop 이벤트 핸들러에서 preventDefault를 호출해줘야 한다.

dragenter 이벤트와 dragover 이벤트 차이

  • dragenter 이벤트는 마우스가 대상 객체의 위로 처음 진입할 때 발생
  • dragover 이벤트는 드래그하면서 마우스가 대상 객체 위에 자리 잡고 있을 때 주기적으로 발생

기존에 구현했을 때는 dragenter 사용 시, 드롭 이벤트가 정상적으로 호출되지 않아서 dragover 이벤트를 사용하여 코드를 작성하였다.
하지만 dragenter로 다시 이벤트 핸들러를 작성하고, dragover 이벤트 핸들러에서는 preventDefault 메서드를 호출하니 drop 이벤트가 잘 동작하는 것을 확인하였다.
즉, 불필요하게 이벤트핸들러가 자주 호출되었던 것을 이벤트의 타입을 바꿈으로써 성능을 향상시킬 수 있음을 발견했다.




0121

이슈 수정, 삭제 시 모든 리스트가 재렌더링 되는데 이를 막을 방법

innerHTML 프로퍼티에 HTML 마크업 문자열을 할당하면 기존의 자식 노드까지 모두 제거하고 다시 처음부터 새롭게 자식 노드를 생성하여 DOM에 반영한다. 이는 효율적이지 않다.

insertAdjacentHTML 메서드는 기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다.
화면에 렌더링할 때 innerHTML로 모든 요소를 다시 그리는 것이 아니라 이러한 적절한 메서드를 사용하여 렌더링한다면 좀 더 효율적으로 렌더링 할 수 있을 것 같다.

DOM 엘리먼트 삭제 시, 이벤트리스너도 같이 삭제될까?

삭제한 DOM 요소를 참조하는 대상이 모두 사라진다면, 가비지 컬렉터에 의해 DOM 요소와 더불어 이벤트 리스너도 같이 삭제될 것이다.

kanban 보드를 구현하면서는 각 이슈가 아닌 이슈 리스트에 이벤트 리스너를 바인딩 했기에 이벤트 리스너를 해제해야할 이유가 없지만, 만약 각 이슈에 이벤트 리스너를 바인딩 했고, DOM 요소를 참조하는 대상이 남아있다면 메모리 누수가 발생하기에 이벤트리스너를 삭제해줘야 한다.

태그:

카테고리:

업데이트: