- 리액트를 공부하면서 메모한 내용들입니다. 리액트의 모든 내용을 담진 않았으며 기억해야할 부분, 어렵게 느껴진 부분들을 위주로 정리했습니다.
1. 리액트 소개
1-1. 리액트의 탄생
- 대규모 애플리케이션에서 자주 발생하는 사용자 인터랙션과 UI 업데이트는 코드를 복잡하게 만들고 관리를 어렵게 합니다. 이에 대응하기 위해 리액트는 상태 변경 시 전체 UI를 처음부터 다시 렌더링하는 방식을 채택합니다. 이 접근 방식은 개발자가 UI 업데이트 방법에 대해 고민하지 않도록 하여, 애플리케이션 개발을 단순화합니다.
- Virtual DOM 도입: 처음부터 다시 렌더링하는 방식은 성능 저하를 야기합니다. 이를 해결하기 위해, 리액트는 실제 DOM을 추상화한 메모리 상의 자바스크립트 객체인 Virtual DOM을 사용합니다. 리액트는 상태 변화가 발생했을 때 Virtual DOM을 사용하여 UI의 변경 사항을 빠르게 렌더링하고, 리액트의 효율적인 비교 알고리즘을 통해 실제 DOM과 Virtual DOM을 비교합니다. 그리고 차이가 있는 부분만 실제 DOM에 반영(패치)합니다.
- 결론적으로 리액트는 Virtual DOM을 사용하여 상태 변화에 따른 UI 업데이트를 효율적으로 처리함으로써, 복잡한 동적 UI를 갖는 웹 애플리케이션 개발을 간소화하고, 성능을 유지할 수 있게 하는 JavaScript 라이브러리입니다.
1-2. 라이브러리와 프레임워크
- 라이브러리는 개발자가 제어를 유지하면서 특정 작업을 수행하기 위해 필요할 때 선택적으로 사용하는 도구들입니다.
- 개발자가 그 안에서 작업을 수행하도록 하는, 작업의 흐름과 구조를 제공하는 체계적인 도구와 규칙의 집합입니다.
- 결론적으로 라이브러리와 프레임워크의 차이는 개발의 흐름을 주도하는 주체입니다. 라이브러리는 개발자, 프레임워크는 프레임워크 구조가 개발 흐름을 주도하고 제어합니다.
1-3. JSX
- JSX는 JavaScript를 확장한 문법으로(Superset) React에서 UI 컴포넌트를 정의하는 데 사용됩니다. 이는 JavaScript의 모든 기능을 포함하며, UI 구조를 직관적으로 표현할 수 있도록 설계되었습니다.
1-4. 리액트는 선언적인 프로그래밍 스타일
- 개발자가 원하는 상태를 선언하고, 리액트가 이를 기반으로 자동으로 UI를 관리(실제 DOM업데이트)하기 때문에 가독성이 높고 유지 보수가 간편합니다.
- 그러나 때로는 선언적인 스타일을 벗어나 DOM 요소에 직접적으로 접근하는 상황이 발생할 수 있습니다.(useRef를 사용하여 특정 DOM요소를 참조하고 조작.)
1-5. 리액트 'Strict Mode'
- 개발 과정에서 리액트 애플리케이션에서 잠재적인 문제를 미리 감지하고 수정할 수 있도록 돕는 개발 도구입니다.
- 컴포넌트의 재렌더링 감지: 엄격 모드는 컴포넌트의 마운트, 업데이트 과정을 두 번씩 실행하여 부수 효과(side effects)와 같은 연산들이 예상대로 동작하는지 확인합니다. 이 과정은 실제로 DOM에 변화를 두 번 적용하는 것은 아니고, 생명주기 메소드나 Hooks가 예상대로 안전하게 사용되는지 검사하기 위함입니다.
2. 리액트 컴포넌트, 상태, props
2-1. 컴포넌트
- 리액트의 컴포넌트는 독립적으로 동작하며 재사용 가능한 코드 조각입니다. 이들은 특정 기능을 수행하도록 설계되며, 하나의 컴포넌트 함수를 사용하여 여러 인스턴스를 생성할 수 있습니다. 생성된 각 인스턴스는 독립된 상태(state)를 유지합니다.
- 컴포넌트가 한 개의 루트 요소만을 가져야 하는 이유 : 리액트 컴포넌트는 기술적인 제약으로 인해 단일 루트 요소만 반환할 수 있습니다. 이는 JavaScript의 함수가 단 하나의 객체만 반환할 수 있는 문법적 특성과 React의 createElement 메서드의 동작 방식 때문입니다. 따라서, 여러 요소를 반환하려면 그 요소들을 하나의 루트 요소로 감싸야 합니다.
2-2. 컴포넌트 생명주기
- 마운트: 컴포넌트가 최초로 실행되어 DOM에 삽입되는 과정을 말하며, 이 과정이 완료되면 사용자는 브라우저에서 해당 컴포넌트의 UI를 볼 수 있게 됩니다.
- 업데이트 : 컴포넌트의 상태(state)나 속성(props)이 변경될 때 발생합니다. 상태나 속성의 변경이 감지되면, 리액트는 컴포넌트 함수를 다시 호출하여 새로운 JSX를 반환받습니다. 이 과정에서 새로운 상태와 속성을 기반으로 가상 DOM에 새로운 UI를 구성합니다. 그리고 실제 DOM과 비교하여 변경 내용을 적용합니다.
- 언마운트 : 컴포넌트가 DOM에서 제거되는 과정을 의미합니다. 함수형 컴포넌트의 경우 useEffect의 클린업 함수를 호출(제공된 경우)합니다.
2-3. 상태와 참조의 차이
- 렌더링 트리거 :
- State: 상태가 변경될 때마다 컴포넌트가 재실행되어 UI가 업데이트됩니다.
- Ref: 참조 값이 바뀌어도 컴포넌트는 재실행되지 않습니다. 컴포넌트가 재실행되어도 참조 객체 내의 .current 속성에 저장된 값은 변경되지 않습니다.
- 연결이 수립되는 시점:
- State: 컴포넌트가 재실행될 때마다 상태는 이전 값을 기반으로 새로운 상태를 계산합니다.
- Ref: useRef를 사용하여 참조를 생성하면, 이 참조는 초기에 .current 속성이 null 또는 생성 시 제공된 초기값을 가지게 됩니다. 그리고 컴포넌트 함수 실행 시에 컴포넌트 내에서 사용하고자 하는 객체나 DOM 요소에 대해 단 한 번 설정되며, 컴포넌트의 생명주기 동안 해당 연결이 유지됩니다.
- 활용 예시:
- State: 값이 변경되면 UI에 바로 반영되어야 하는 상황에 사용합니다.
- Ref: UI 업데이트와 무관한 값을 관리할 때 사용합니다.(타이머, 외부 라이브러리의 인스턴스)
2-4. 외부 변수보다 useRef를 사용하는 게 좋은 이유
- 리액트는 데이터가 위에서 아래로(부모에서 자식으로) 흐르는 단방향 데이터 흐름을 채택합니다. 외부 변수를 사용하면 이러한 패턴이 깨져, 컴포넌트 간의 데이터 흐름을 예측하기 어려워집니다.
- 외부 변수는 컴포넌트 인스턴스 간에 공유되어 예상치 못한 부작용이 발생할 수 있습니다.
- 반면, useRef는 각 컴포넌트 인스턴스 내에서 독립적으로 값을 유지하며, 리액트의 렌더링 사이클에 영향을 주지 않기 때문에 외부변수를 사용하는 것보다 useRef를 사용하는 것이 권장됩니다.
2-5. 브라우저 렌더링 과정 (리액트는 아니지만 알고가자)
- URL 해석: 사용자가 입력한 URL에서 프로토콜, 도메인 이름, 경로 등을 분석합니다.
- DNS 조회: 도메인 이름 시스템(DNS)을 사용하여 도메인 이름을 해당 서버의 IP 주소로 변환합니다.
- 서버 요청: 브라우저는 HTTP/HTTPS 프로토콜을 사용하여 요청을 서버에 전송합니다.
- 서버 응답: 서버는 요청을 처리한 후 HTML, CSS, JavaScript 파일 등을 포함한 응답을 브라우저로 전송합니다.
- HTML 파싱: 브라우저는 받은 HTML 문서를 파싱하여 DOM(Document Object Model) 트리를 구성합니다.
- CSS 파싱: CSS 파일을 파싱하여 CSSOM(CSS Object Model) 트리를 구성합니다.
- 렌더 트리 생성: DOM 트리와 CSSOM 트리를 결합하여 렌더 트리를 만듭니다.
- 레이아웃(Layout): 각 요소의 위치와 크기를 계산합니다.
- 페인트(Paint): 레이아웃 단계에서 계산된 정보를 바탕으로 요소를 화면에 그립니다.
- 합성(Composite): 여러 레이어를 합성하여 최종적으로 화면에 표시합니다.
2-6. 리액트 렌더링 과정
- 리액트 렌더링은 컴포넌트의 상태나 속성을 UI에 반영하는 과정을 의미합니다.
- 초기 렌더링: 애플리케이션의 초기 로딩 시, 리액트는 최상위 컴포넌트로부터 시작하여 render 함수를 호출합니다. 이 과정에서 반환된 JSX를 통해 리액트는 컴포넌트 트리의 구조를 파악하고, 해당 구조에 따라 가상 DOM 트리를 생성합니다. 초기 렌더링에서 생성된 가상 DOM 트리는 실제 DOM에 처음으로 반영되어 사용자에게 UI가 표시됩니다.
- State 또는 Props 변경(+부모 컴포넌트의 재렌더링): 리액트는 다시 render 함수를 호출하여 새로운 JSX를 받습니다.
- 가상 DOM과의 비교: 새로운 JSX로부터 생성된 가상 DOM과 이전 가상 DOM을 비교(diffing)합니다. 리액트는 이 비교 과정을 통해 실제 변경이 필요한 부분만을 파악합니다.
- 실제 DOM 업데이트: 변경이 필요한 부분만을 실제 DOM에 반영(패치)합니다. 이 과정은 가능한 한 최소한의 DOM 조작으로 이루어집니다.
2-7. 리액트 배칭(Batching)
- 배칭은 여러 상태 업데이트를 하나의 업데이트로 그룹화하여 처리하는 과정을 말합니다. 예를 들어, 하나의 함수 내에서 여러 개의 상태 업데이트가 발생하면, 리액트는 이를 배치하여 단 한 번의 렌더링만을 수행합니다. 이는 리액트가 상태 변경을 효율적으로 관리하고, 성능을 최적화하는 데 도움을 줍니다.
- 배칭으로 인해 상태 업데이트(setState)는 비동기적으로 처리되며, 이로 인해 상태를 업데이트한 직후 그 결과를 즉시 확인하려고 할 때 예상치 못한 결과를 얻을 수 있습니다. 이러한 문제는 useEffect 훅을 사용하여, 해당 상태를 의존성 배열에 포함시킴으로써 해결할 수 있습니다. 이 방식을 통해 상태가 변경될 때마다 useEffect 내의 로직이 실행되도록 하여, 상태 업데이트 후의 동작을 안정적으로 처리할 수 있습니다.
2-8. State 끌어올리기 주의점
- State 끌어올리기는 여러 컴포넌트가 같은 상태를 공유해야 할 때, 그 상태를 공통 부모 컴포넌트로 이동시키는 리액트의 패턴입니다. 하지만 상태를 끌어올리는 것이 모든 상황에서 최선은 아닙니다.
- 상태를 공통 부모로 끌어올리면, 상태가 변경될 때 해당 상태를 공유하는 모든 컴포넌트가 리렌더링됩니다. 이로 인해 불필요한 렌더링이 많아질 수 있습니다.
- state를 여러 계층을 거쳐 전달해야하기 때문에 로직의 복잡성이 증가합니다.
- 이를 해결하기 위해 컴포넌트를 분리하는 방법, 전역 상태 관리(Redux, Context API)를 이용하는 방법이 있습니다.
2-9. state 불변성 유지의 중요성, 깊은 복사
- 리액트는 객체의 참조 변경을 통해 상태 변화를 감지하기 때문에, 상태 불변성을 유지함으로써 리액트가 상태 변화를 정확하게 감지하도록 할 수 있습니다. 상태 불변을 위해서는 상태 객체를 업데이트할 때 얕은 복사나 깊은 복사를 활용할 수 있습니다.
- 얕은 복사는 객체의 바로 아래 단계의 속성만 새로운 객체로 복사하는 것을 의미합니다.
- 깊은 복사된 객체는 객체 안에 객체가 있을 경우에도 원본과의 참조가 완전히 끊어진 객체를 말합니다. ex) JSON.parse(JSON.stringify(object))
2-10. React Key Prop
1) Key Prop
- 리액트에서 key prop의 역할은 주로 목록(list)이나 배열에서 각 항목을 유일하게 식별하는 데 사용됩니다.
- 재렌더링 최적화 : 리액트는 key prop을 통해 각 컴포넌트가 어떤 항목에 대응되는지 식별합니다. 목록의 항목이 재정렬되거나 업데이트될 때, key를 사용함으로써 리액트는 어떤 항목이 변경되었는지, 추가되었는지, 제거되어야 하는지를 더 정확하게 판단할 수 있습니다.
- key prop을 직접 참조하려고 시도하면, 그 값은 항상 undefined가 됩니다. 이는 key가 컴포넌트의 내부 로직이나 렌더링 결과에 직접적으로 영향을 미치는 값으로 사용되지 않도록 설계되었기 때문입니다.
2) Key로 Index를 사용했을 때의 문제점
- 배열의 요소가 추가, 삭제, 또는 재정렬되면, 그에 따라 각 요소의 index 값도 변하게 됩니다. 이 경우, 리액트는 변경된 key 값을 기반으로 각 요소를 새로운 요소로 인식하고, 결과적으로 변경된 모든 요소를 불필요하게 재렌더링하게 됩니다. 이는 특히 목록이 크고 복잡할수록 성능 저하를 초래할 수 있습니다.
- key가 변경되면 리액트는 이를 완전히 다른 컴포넌트로 인식합니다. 따라서, 컴포넌트의 상태가 예상치 못한 방식으로 초기화될 수 있습니다. 예를 들어, 리스트의 첫 번째 항목을 삭제하고 index를 key로 사용하는 경우, 모든 후속 항목의 key가 변경되어, 각 컴포넌트의 상태가 재초기화될 수 있습니다.
3) 컴포넌트 초기화에 Key 사용하기
- 컴포넌트의 key prop을 변경하는 것은 리액트에게 해당 컴포넌트를 완전히 새로운 컴포넌트로 간주하게 만들어, 컴포넌트의 상태를 초기화하고, 생명주기 메서드를 다시 실행하도록 합니다. 이러한 특성은 특정 조건에서 컴포넌트를 "재시작"하거나 초기 상태로 "리셋"하고자 할 때 유용하게 사용될 수 있습니다.
3. 리액트 Hook
3-1. 리액트 훅의 탄생 배경
- 이전의 리액트는 컴포넌트 간의 상태 로직을 재사용하기 어려웠습니다. 고차 컴포넌트(HOC)나 렌더 프롭스 같은 패턴을 사용하여 이 문제를 해결했지만, 이는 코드의 복잡성을 증가시키고 추적을 어렵게 만들었습니다.
- this 키워드와 클래스 문법은 특히 리액트에서 개발자들에게 혼란을 주고, 코드의 재사용성과 구성을 어렵게 만들었습니다.
- 이러한 문제들에 대응하기 위해, 리액트 훅이 도입되었습니다. 훅을 통해 개발자들은 상태 관련 로직을 컴포넌트로부터 분리해 재사용할 수 있게 되었고, 이는 코드의 구조를 단순화하며, 유지보수와 재사용성을 향상시켰습니다.
3-2. Hook 사용 규칙
- 최상위(at the top level)에서만 Hook을 호출해야 합니다. 반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하지 마세요.
- 리액트 훅은 함수형 컴포넌트, 커스텀 훅 내부에서만 호출해야합니다.
4. 컴포넌트 패턴
4-1. 사용자 입력 컴포넌트
1) 제어 컴포넌트로 할 것인가, 비제어 컴포넌트로 할 것인가?
- 제어컴포넌트
- 폼의 값은 리액트 상태(state)로 관리되며, setState를 통해 업데이트됩니다. value 속성을 사용하여 상태와 연결됩니다.
- 입력 데이터의 실시간 검증이나 동적 변환에 적합합니다.
- 비제어 컴포넌트
- ref를 사용하여 폼 요소에 직접 접근하며 폼의 상태는 DOM에 의해 관리됩니다. 초기값 설정은 defaultValue를 사용합니다.
- 간단하고 선언적이지 않은 방식으로 DOM 요소를 직접 처리해야 할 때 유용합니다.
- onChange와 onInput는 React에서 거의 동일하게 동작하지만 onChange가 조금 더 넓은 범위의 입력타입 처리(체크박스,라디오버튼)가 가능하기 때문에 더 권장됩니다.
4-2. 모달 컴포넌트
1) 리액트 Portal
- UI의 특정 부분을 부모 컴포넌트의 DOM 계층 바깥으로 "이동"할 수 있게 해주는 기능입니다.
- 주로 모달, 팝업, 툴팁과 같이 페이지의 다른 부분에 시각적으로 격리하여 렌더링해야 하는 요소에 사용됩니다.
- 포탈을 사용하여 생성된 컴포넌트는 물리적으로는 DOM 트리의 다른 위치에 렌더링되지만, 이벤트 버블링에 있어서는 React 컴포넌트 트리의 위치를 기준으로 동작합니다. 즉, 포탈 내부에서 발생한 이벤트는 포탈을 생성한 React 컴포넌트 트리를 통해 상위로 전파될 수 있습니다.
2) dialog 태그의 동작 방식
- HTML의 dialog 요소는 웹 애플리케이션에서 대화창 혹은 모달을 구현할 때 사용됩니다. 사용자가 키보드의 ESC 키를 누르거나, 다른 방식으로 대화창을 닫을 수 있는 기능을 내장하고 있습니다.
- ESC 키를 눌러 dialog를 닫을 때 시각적으로는 대화창이 사라지지만, 대화창의 표시 여부를 결정하는 애플리케이션의 상태는 자동으로 업데이트되지 않습니다.
- 이 문제를 해결하기 위해, dialog에 내장된 onClose Prop에 모달 상태 제어 함수를 연결하여 모달이 닫히는 것을 감지하도록 해야 합니다.
3) forwardRef
- useRef훅을 사용한 ref는 state와 다르게 컴포넌트 간 전달이 불가능합니다.
- forwardRef를 사용하면 부모 컴포넌트로부터 자식 컴포넌트로 참조를 전달할 수 있습니다. 이는 특히 DOM 요소에 대한 참조나 자식 컴포넌트의 메서드에 접근해야 할 때 유용합니다.
4) useImperativeHandle
- useImperativeHandle은 리액트에서 부모 컴포넌트가 자식 컴포넌트의 인스턴스 값을 직접 조작할 수 있도록 허용하는 훅입니다.
- 이 훅을 사용함으로써 자식 컴포넌트는 외부에서 사용할 수 있는 메서드나 속성을 명시적으로 정의할 수 있으며, 이를 통해 부모 컴포넌트는 자식 컴포넌트의 내부 메서드나 속성에 접근하여 제어할 수 있게 됩니다. 이 방법은 특정 메서드나 속성을 외부로 노출시키고 싶을 때 효과적입니다.
5. useEffect
5-1. Side Effect
- 사이드 이펙트는 리액트 컴포넌트의 렌더링 과정에 직접적으로 영향을 미치지 않으면서, 필수적으로 수행되어야 하는 작업을 의미합니다. 이는 데이터 fetching, 구독 설정, 수동으로의 DOM 조작 등 컴포넌트의 렌더링 결과와는 독립적으로 실행되어야 하는 코드를 포함합니다.
5-2. useEffect가 필요하지 않은 상황
- 이벤트 핸들러 내 코드: 사용자의 액션(예: 버튼 클릭)에 의해 특정 작업이 실행되어야 할 경우, 해당 작업은 직접 이벤트 핸들러 내에 정의될 수 있습니다. 이 경우, 작업 실행은 사용자의 액션에 의해 명시적으로 제어되므로 useEffect를 사용할 필요가 없습니다.
- 동기적이고 한 번만 실행되는 초기화 코드: 컴포넌트가 마운트될 때 단 한 번만 실행되어야 하는 동기적 작업(예: localStorage에서 데이터 읽기)의 경우, useEffect 내에서 실행될 필요가 없습니다.
5-3. 클린업 함수가 호출되는 시점은?
- 의존성 배열의 값이 변경될 때
- 컴포넌트가 언마운트될 때
5-4. useEffect 의존성 배열에 함수를 추가할 때 생기는 문제
- 함수는 객체이므로 컴포넌트 랜더링시에 항상 새로 생성됩니다. 이때 함수를 useEffect 의존성 배열에 추가하면, 함수가 다시 생성될 때마다 useEffect가 불필요하게 다시 실행되는 무한 루프의 위험을 초래합니다.
- 이 문제는 의존성 배열에서 함수를 사용하지 않거나 useCallback을 사용하여 함수를 메모이제이션하여 해결할 수 있습니다.
6. 메모이제이션
6-1. useCallback과 useMemo, 그리고 memo
- useMemo는 값을, useCallback은 함수 자체, memo는 컴포넌트 렌더링 결과를 메모이제이션합니다.
- useMemo는 계산 비용이 큰 연산의 결과값을 기억하기 위해 사용합니다.
- useCallback은 여러 컴포넌트에서 공유되고 의존성이 변경되지 않는 함수를 기억하기 위해 사용합니다.
- 부모 컴포넌트가 재렌더링 될 때 memo()로 감싸진 컴포넌트는 props가 변경되지 않는 한 이전 렌더링 결과를 재사용합니다.
- 무분별한 메모이제이션은 메모리 사용량을 증가시키므로 신중하게 사용해야합니다.
7. 클래스 컴포넌트
7-1. 클래스 컴포넌트에서의 state
- 클래스 컴포넌트에서 state(반드시 state라는 이름을 사용)는 컴포넌트의 상태를 저장하는 데 사용되며, 반드시 객체 형태여야 합니다. 이는 리액트가 정한 규칙으로, 사용자가 변경할 수 없습니다.
- setState 메서드를 사용하여 상태를 비동기적으로 업데이트할 수 있으며, 리액트는 기존 상태와 새로운 상태를 병합(merge)합니다. 이로 인해 일부 상태만 업데이트하더라도 나머지 상태는 유지됩니다.
7-2. 메서드와 this 바인딩
- 클래스 컴포넌트에서 메서드를 호출하거나 인자를 전달할때 this가 실행 문맥에 따라 동적으로 결정되기 대문에 주의가 필요합니다.(화살표 함수는 함수가 정의된 시점에서의 외부 컨텍스트의 this를 캡처)
- 메서드가 클래스 인스턴스와 올바르게 바인딩되도록 화살표 함수를 사용하거나, 메서드를 명시적으로 인스턴스에 바인딩(bind())해야 합니다.
7-3. 함수 바인딩 고려사항 (리액트는 아니지만)
- 화살표 함수: 컴포넌트가 렌더링될 때마다 새로운 화살표 함수가 생성되기 때문에, 대규모 리스트를 렌더링하거나 자주 업데이트되는 컴포넌트에서는 불필요한 리렌더링을 유발할 수 있습니다. 이는 특히 성능에 민감한 애플리케이션에서 고려해야 할 사항입니다.
- bind 메서드: bind를 사용하여 함수를 생성하는 방식은 함수를 한 번만 생성하고 재사용할 수 있으므로, 이론적으로 성능상의 이점을 제공할 수 있습니다. 그러나 현대 JavaScript 엔진의 최적화 기술을 고려했을 때, 실제 애플리케이션에서 이러한 성능 차이는 대부분 미미합니다.
7-4. 오류 경계(에러 바운더리)
- 오류 경계(Error Boundary)는 React 애플리케이션에서 컴포넌트 트리 어딘가에서 발생한 JavaScript 에러를 처리하는 컴포넌트입니다. 클래스 컴포넌트로만 가능하고 componentDidCatch 메서드를 사용하여 에러를 처리합니다.
- 에러 경계를 사용하면 애플리케이션 전체가 에러로 인해 중단되는 것을 방지하고, 대신 에러 발생 시 사용자에게 안정성을 제공하거나 로그를 기록하고 개발자에게 알릴 수 있습니다.
7-5. 클래스 컴포넌트에서 Context
- 클래스 컴포넌트에서는 static contextType 속성을 사용하여 컴포넌트가 구독할 Context를 지정할 수 있습니다. 이 방법을 통해 컴포넌트 내의 어느 곳에서든지 this.context를 통해 해당 Context의 현재 값을 직접 접근할 수 있게 됩니다.
- 단, 한 클래스 컴포넌트에서는 하나의 Context만 이 방식으로 접근할 수 있습니다.
8. Context API
8-1. Context의 필요, 고려해야할 점.
- Context는 리액트 애플리케이션에서 전역적으로 데이터를 공유해야 할 때 사용합니다. 프롭 드릴링(Prop Drilling)을 피해 코드 가독성을 높이고 유지 보수에 용이합니다.
- 컴포넌트 재사용성: Context에 의존하는 컴포넌트는 해당 Context 없이는 재사용하기 어렵습니다. 컴포넌트의 범용성을 유지하고자 할 때는 이 점을 고려해야 합니다.
- 성능: Context는 컨슈머가 렌더링될 때마다 값의 변화를 감지하여 렌더링을 유발합니다. Context 값이 자주 변경되는 경우, 불필요한 렌더링이 발생할 수 있으므로 성능에 영향을 줄 수 있습니다.
- 간단한 상태 관리의 경우 useState나 useReducer만으로 충분할 수 있습니다. Context의 사용은 실제로 전역적인 데이터 공유가 필요한 경우에 한정하여 고려하는 것이 좋습니다.
8-2. Context Api 사용시 defaultValue 설정
- createContext에서는 컨텍스트를 사용하는 컴포넌트가 Provider로 감싸지지 않았을 때의 기본값을 설정합니다.
- 사실 실제 애플리케이션에서는 대부분의 컨텍스트 데이터가 Provider를 통해 명시적으로 제공되기 때문에, defaultValue가 활용되는 경우는 상대적으로 적습니다. 그러나, defaultValue를 설정함으로써 컴포넌트 개발 시 타입 힌트나 자동완성 등의 추가적인 개발 도구 지원을 받을 수 있는 장점이 있습니다.
8-3. useReducer의 장점, context와의 연계
- 가독성 및 유지보수성 향상: useReducer는 상태 업데이트 로직을 액션에 따라 분리하여 관리할 수 있습니다. 이는 복잡한 상태 로직을 더 가독성 있고 구조화된 방식으로 다룰 수 있게 해줍니다.
- 예측 가능한 상태 전이: 액션과 리듀서 함수를 통해 상태가 어떻게 변경될지 명확하게 정의할 수 있습니다. 이는 상태 변화의 예측 가능성을 높이고, 디버깅을 용이하게 합니다.
- Context API와의 연계: useReducer와 Context API를 함께 사용하면, 애플리케이션의 전역 상태 관리를 위한 강력한 패턴을 구성할 수 있습니다. useReducer로 생성된 상태와 디스패치 함수를 Context를 통해 애플리케이션의 다른 부분에 쉽게 공유할 수 있습니다. 이를 통해, 상태를 필요로 하는 모든 컴포넌트가 일관된 방식으로 상태를 업데이트하고 접근할 수 있게 됩니다.
8-4. context vs redux
- Context API : Redux의 복잡성과 보일러플레이트 코드를 줄일 수 있으며, 리액트 내장 기능만으로 상태 관리가 가능합니다. 그러나 Redux만큼의 세밀한 상태 관리와 디버깅 도구를 제공하지는 않습니다.
- Redux : Redux는 애플리케이션의 상태를 중앙 집중화하여 예측 가능한 방식으로 관리할 수 있게 해주고, 강력한 미들웨어 시스템과 개발자 도구를 통해 복잡한 상태 관리와 디버깅을 용이하게 합니다.
9. Redux
9-1. Redux의 필요성
- 리액트 컨텍스트의 복잡성 대응: 컨텍스트 API는 전역 상태 관리에 유용하지만, 앱의 규모가 커지고 상태 관리가 복잡해질수록 성능 문제와 관리의 어려움이 증가합니다. Redux는 이러한 복잡성을 효과적으로 관리할 수 있는 구조를 제공합니다.
- 성능과 고빈도 변경 처리: Redux는 중앙 집중화된 상태 관리와 함께, 변경 사항에 대해 세밀한 제어를 가능하게 하여 고빈도의 상태 변경이나 대규모 데이터를 효과적으로 처리할 수 있습니다.
9-2. Redux의 기본 원칙
- 중앙 집중화된 상태 관리: react-redux의 Provider 컴포넌트를 통해 앱 전체에 걸쳐 상태를 공유하고 접근할 수 있게 합니다.
- 불변성 유지: 리덕스로 작업할 때 중요한 것은 절대 기존의 state를 변형해서 안 됩니다. 대신 새로운 상태 객체를 생성하여 반환합니다. 이는 상태 변경의 추적과 성능 최적화에 중요합니다.
- 리듀서는 순수 함수 : 리듀서는 동일한 입력에 대해 항상 동일한 출력하고 외부의 상태를 변경하거나, API 호출과 같은 부작용을 일으키지 않아야 합니다. 이는 리덕스의 핵심 원칙을 유지하고, 상태 관리의 일관성을 보장합니다.
- Redux Toolkit : createSlice같은 api 제공하여 보일러플레이트 코드를 대폭 줄였습니다. Immer 패키지가 자동으로 기존 상태의 변경사항들을 추적하여 새로운 상태 객체를 반환하기 때문에 불변성 관리를 자동화합니다.
- createSlice 함수는 reducers 객체를 직접 반환하지 않습니다. 대신, 이 함수는 리듀서 로직을 내부적으로 처리하고, 최종적으로 단일 리듀서 함수를 생성하여 counterSlice.reducer라는 속성에 할당합니다.
- 리덕스의 createStore는 하나의 리듀서만을 받는 반면, RTK의 configureStore로 여러개의 리듀서를 결합(하나의 객체)하여 하나의 스토어를 생성합니다.
- createAsyncThunk를 사용하여, HTTP 요청과 같은 비동기 작업을 리덕스 패턴 내에서 쉽게 처리할 수 있습니다.
9-4.useSelector 주의점
// const { counter, showCounter } = useSelector((state) => state); (X)
const counter = useSelector((state) => state.counter) (O)
const showCounter = useSelector((state) => state.showCounter)(O)
- useSelector 사용시에 위의 전체 상태를 반환하는 것은 Redux 스토어의 어떤 부분이든 변경될 때마다 해당 컴포넌트가 불필요하게 리렌더링되게 만들 수 있기 때문에 최소한의 상태만 선택해야합니다.
- 다른 패턴: 객체를 반환하는 useSelector 사용 시, 객체는 매번 호출될 때마다 새로운 참조를 가지기 때문에, useSelector가 구독하는 것은 사실상 이 객체의 "새로운 참조"입니다. 따라서, 스토어의 어떤 부분이 변경되어도 이 객체는 매번 새로 생성되고, React는 이를 새로운 상태로 인식하여 컴포넌트를 불필요하게 리렌더링하게 됩니다.
const {counter, showCounter} = useSelector((state) => ({
counter : state.counter,
showCounter : state.showCounter,
}))
9-5. 비동기 코드, Side Effect 코드를 리듀서 함수에 직접 넣으면 안되는 이유
- 순수 함수의 원칙 위배: 리듀서에서 비동기 작업을 수행하면, 리듀서의 순수성이 깨집니다. 비동기 작업의 결과는 실행 시점에 따라 달라질 수 있으므로, 리듀서의 결과가 예측 불가능해집니다.
- 상태 관리의 복잡성 증가: 비동기 작업을 리듀서 내부에서 처리하면, 상태 관리 로직이 복잡해지고, 액션, 상태 업데이트 사이의 관계가 모호해질 수 있습니다.
- 해결 방법
- 컴포넌트 내에서 직접 처리하고 액션 디스패치 : 컴포넌트 내에서 비동기 코드를 작성하고, 비동기 작업이 완료되면 Redux 액션을 디스패치하여 상태를 업데이트하는 방식으로 해결할 수 있습니다.
- Redux Toolkit의 createAsyncThunk 사용: 비동기 작업을 위한 액션 크리에이터를 쉽게 생성할 수 있습니다. 이는 비동기 작업을 시작, 성공, 실패 시에 따른 액션을 자동으로 디스패치합니다.
9-6. Thunk
- Thunk는 프로그래밍에서 사용되는 용어로, 특정 작업을 나중에 실행할 수 있도록 감싸는 함수를 말합니다. 리덕스에서는 주로 비동기 작업을 처리하기 위해 사용됩니다. 리덕스 자체는 동기적인 액션만을 지원하기 때문에, 비동기 로직(예: API 호출)을 처리하려면 thunk와 같은 미들웨어가 필요합니다.
- 비동기 로직의 중앙 집중화: Thunk를 사용하면 컴포넌트에서 분산되어 있을 수 있는 비동기 로직을 액션 크리에이터 내부로 옮길 수 있습니다. 이를 통해 비동기 로직을 한 곳에서 관리할 수 있어 코드의 가독성과 유지보수성이 향상됩니다.
- 상태 업데이트 로직의 분리: 비동기 작업의 결과에 따라 상태를 업데이트하는 로직을 액션 크리에이터 내부에 캡슐화할 수 있습니다. 이는 애플리케이션의 상태 관리를 더욱 명확하게 만듭니다.
9-7. 고급 Redux 패턴
- 커스텀 액션 크리에이터와 Thunk 사용: 복잡한 비동기 로직이나 조건부 액션 디스패치를 위해 커스텀 액션 크리에이터를 정의하고, Thunk를 사용하여 처리할 수 있습니다.
- '린'한 컴포넌트 유지: 리덕스 로직을 액션 크리에이터와 리듀서에 집중시키고, 컴포넌트는 UI 표현에 집중하여, 관심사의 분리를 통해 유지보수성을 높입니다.
10. 리액트 라우터
10-1. 리액트 라우터의 필요성
- 기존 멀티 페이지 애플리케이션의 단점 : 매 페이지 마다 새로운 HTTP 요청을 전송하고 새로운 응답을 받는 과정에서 새로고침과 지연이 발생, 사용자 경험이 저하됩니다.
- 위의 이유로 등장한 SPA는 페이지 전환 없이 클라이언트 측 JavaScript를 사용해 사용자 인터페이스를 동적으로 변경합니다. 이 방법으로 사용자 경험은 개선되지만 웹의 핵심 이점 중 하나인 특정 리소스에 대한 링크 제공을 상실되는 문제가 생깁니다. 이를 해결하기 위해 클라이언트 측 라우팅을 도입하여 SPA에서도 다른 URL을 통해 다양한 '페이지'로 사용자를 안내합니다.
- 리액트 라우터 패키지를 사용해도 HTML 요청이 최초에 한 번만 실행되는 것은 같습니다. 하지만 리액트 라우터 패키지가 현재 사용 중인 URL을 추적해 알맞는 리액트 컴포넌트를 표시합니다.
10-2. 라우팅 경로
- 라우팅 경로 설정 시 "/"로 시작하면 절대경로, 안 붙이면 상대 경로로 설정이 됩니다.
- Link컴포넌트에 relative="path"를 사용하면 현재 url을 토대로 경로를 제어할 수 있습니다.
- NavLink 컴포넌트를 사용하면 현재 위치한 경로를 UI상으로 간단하게 표현할 수 있습니다. "/" 경로일 end 프로퍼티를 추가해서 "/"위치를 정확하게 알릴 수 있습니다.
10-3. 경로 우선 순위
- React Router는 경로의 구체성을 인식합니다. 예를 들어, /events/new와 같은 구체적인 경로는 /events/:eventId와 같은 동적 경로보다 우선 처리됩니다. 이는 개발자가 경로의 충돌에 대한 염려없이 라우팅을 구성할 수 있음을 의미합니다.
10-4. loader()
- loader 함수: React Router v6에서는 loader 함수를 통해 페이지 데이터 로딩을 간소화합니다.
- loader는 페이지 전환 시작 시 호출되며, 필요한 데이터를 프로미스를 통해 해결하고 컴포넌트에 전달하기 전까지 페이지 렌더링을 지연시킵니다.
- loader를 사용하는 방법은 데이터 사용 시 항상 존재를 보장하지만, 데이터가 준비되기 전까지 로딩 상태를 표시하지 않아 사용자 경험을 위해 추가적인 보완책이 필요합니다.
- 리액트 라우터는 loader를 호출할 때 다음과 같은 형태의 객체를 인수로 전달합니다. {request, params}
10-5. useNavigation (useNavigate와 다름)
- useNavigation 훅을 사용하면, 현재 네비게이션 객체를 얻을 수 있으며, navigation.state를 통해 활성 라우트 전환 상태를 확인할 수 있습니다. 이 상태("idle", "loading", "submitting")에 따라 사용자에게 적절한 UI를 제공할 수 있습니다.
10-6. 데이터 로딩과 액션 처리
- useLoaderData 훅을 통해 loader 함수에서 반환된 데이터를 직접 사용할 수 있으며, useRouteError를 통해 발생한 에러를 처리할 수 있습니다.
- json() 함수를 사용하여 loader 및 action 함수에서 쉽게 데이터를 포맷하고 반환할 수 있습니다.
- 다른 라우트의 loader를 가져올 땐 해당 라우트 객체에 id를 추가하고 useRouteLoaderData(id)를 통해 loader의 데이터를 가져올 수 있습니다.
10-7. 데이터 전송과 액션 처리
- 리액트 라우터의 Form 컴포넌트와 useSubmit 훅을 사용하면 간편하게 백엔드로 데이터를 전송하고 액션을 처리할 수 있습니다.
- Form 컴포넌트는 (e.preventDefault() 없이도) 브라우저에서 백엔드로의 요청을 중지하고 action()에 요청을 전달합니다.
- 리턴된 action 데이터는 useActionData훅을 통해 페이지 컴포넌트에서 사용할 수 있습니다.
10-8. useFatcher
- 페이지 전환 없이 백엔드와의 통신을 원할 때 useFetcher 훅을 사용합니다. 이를 통해 폼 제출이나 데이터 로딩 같은 작업을 페이지 리로드 없이 수행할 수 있습니다.
10-9. defer를 통한 데이터 지연 로딩
- defer를 사용하여 중요하지 않은 데이터의 로딩을 라우트 전환 후로 지연시킬 수 있습니다. 이를 통해 중요한 컨텐츠를 먼저 렌더링하고, 나머지는 필요에 따라 로딩하여 사용자 경험을 향상시킬 수 있습니다.
- defer를 사용하지 않는다면 loader가 모든 데이터를 불러온 뒤 라우트 전환이 일어나는데 defer를 사용하면 라우트 전환 이후 선택적으로 다른 UI를 보여줄 수 있습니다.
11. 리액트 인증
11-1. 인증 토큰 메커니즘
- 사용자는 자신의 로그인 정보를 서버에 전송합니다. 서버는 이 정보를 검증한 후 유효하다고 판단되면 고유한 인증 토큰을 생성하여 클라이언트(예: 리액트 애플리케이션)에 반환합니다. 이 토큰은 이후 보호된 리소스에 대한 모든 요청에 포함되어, 서버에서 요청이 유효한 사용자에 의해 이루어졌는지 검증하는 데 사용됩니다.
- 상태 비저장성(Statelessness): 이 접근 방식은 서버가 클라이언트의 상태를 유지할 필요가 없으므로, 서버의 부하를 줄이고 확장성을 높이는 데 유리합니다.
11-2. URL을 통한 데이터 전송
- 쿼리 문자열: URL의 ? 뒤에 오는 키-값 쌍으로, 검색 조건, 필터링, 정렬 등의 정보를 전달하는 데 사용됩니다. 예를 들어, ?search=javascript&sort=recent는 search와 sort라는 두 개의 키와 그에 해당하는 값들을 나타냅니다. 리액트에서는 useSearchParams 훅을 통해 이를 처리할 수 있습니다.
- 동적 매개변수(Dynamic Parameters): URL 경로의 일부를 변수로 사용하여, 리소스의 식별에 사용됩니다. 리액트 라우터에서는 useParams 훅을 사용하여 이러한 매개변수를 쉽게 처리할 수 있습니다. URL 경로의 일부로 포함되기 때문에, 웹 사이트의 구조를 사용자에게 명확하게 전달하고 RESTful API 디자인에 적합하며 리소스 지향적인 URL을 생성합니다.
- 동적 매개변수는 주로 리소스의 식별이 필요할 때, 쿼리 문자열은 다양한 옵션을 유연하게 전달할 필요가 있을 때 사용합니다.
11-3. 내장 URL생성자 함수
- action함수에선 useSearchParams를 사용할 수 없으므로 브라우저가 제공하는 내장 URL생성자 함수를 사용합니다.(new URL(request.url).searchParams)
11-4. 인증 토큰의 저장과 관리
- 토큰 저장: 인증 토큰은 보안을 위해 클라이언트 측에서 안전하게 저장되어야 합니다. 가장 간단하고 일반적인 방법은 브라우저의 로컬 스토리지를 사용하는 것입니다.
- 로그아웃 처리: 로그아웃 기능을 구현할 때, 로컬 스토리지에서 토큰을 삭제하고 사용자를 로그아웃 상태로 전환하는 것이 일반적입니다. 리액트에서는 로그아웃 라우트를 설정하고, 해당 라우트에 접근할 때 로컬 스토리지에서 토큰을 삭제하는 로직을 실행할 수 있습니다. (라우트에 꼭 컴포넌트가 매칭되어 있을 필욘 없다!)
11-5. 상태 관리와 라우트 보호
- 인증 토큰과 같은 전역 상태를 관리하기 위해 React Context를 사용할 수도 있지만, React Router 내의 기능을 활용하여 같은 목적을 달성할 수 있습니다. 이 방법은 추가적인 상태 관리 로직을 구현하지 않고도 루트 로더를 통해 토큰을 모든 라우트에서 사용할 수 있으며, 이를 통해 사용자의 로그인 상태에 따라 조건부 렌더링을 수행할 수 있습니다.
- root 라우트 loader에 token을 가져오는 로직을 구성하고 하위 라우트에서 useRouteLoaderData 훅을 통해 해당 로더 데이터를 참조합니다.
11-6. 라우트 보호
- 로그인 상태가 아니라면 해당 라우트에 접근할 수 없도록 설계
- 토큰의 유무를 확인하는 단순한 로더 활용 -> 토큰이 없이 리디렉션 -> 보호가 필요한 모든 라우트에 해당 로더를 추가. 만약 이미 다른 로더 함수가 있는 경우는 어떻게 해야할까?
11-7. 토큰 만료
- 서버는 토큰에 만료 시간을 설정할 수 있으며, 클라이언트는 이를 고려하여 토큰이 만료되었을 때 적절한 조치(예: 자동 로그아웃 또는 토큰 갱신)를 취해야 합니다. 만료 시간이 다가올 때 사용자를 로그아웃시키는 로직을 구현할 때는, 토큰의 만료 시간을 확인하고 현재 시간과 비교하여 처리합니다.
12. 지연로딩과 배포
12-1. 프로덕션 빌드 준비
- 프로젝트의 최적화와 정상 작동을 확인한 후, 프로덕션용 앱을 빌드하는 과정이 시작됩니다. 이는 프로젝트에 포함된 빌드 스크립트를 실행해, 애플리케이션을 프로덕션에 배포할 준비가 된 최적화된 번들로 변환하는 작업입니다. 최소화 및 최적화 과정을 거친 번들은 파일 크기가 줄어들어, 애플리케이션의 로딩 속도가 향상되고 이로 인해 사용자 경험이 개선됩니다.
12-2. Lazy Loading (지연 로딩)
- 컴포넌트, 라이브러리 또는 기타 코드 조각을 로드하는 기법으로, 애플리케이션의 초기 로딩 속도를 향상.
12-3. 지연 로딩 구현
- 기존 import 제거: 지연 로딩을 적용하려는 컴포넌트의 기존 import 문을 제거합니다. 이는 컴포넌트가 항상 즉시 로드되지 않도록 하기 위함입니다.
- React.lazy와 Suspense 사용: 지연 로딩을 위해 React.lazy를 사용하여 컴포넌트를 동적으로 가져옵니다. React.lazy 함수는 동적 import()를 인자로 받아, 해당 모듈이 필요할 때만 로드하도록 합니다. 로딩 중 상태를 관리하기 위해 Suspense 컴포넌트를 사용하며, fallback prop을 통해 로딩 중에 표시될 JSX를 정의합니다.
- 동적 import 사용: import() 함수를 호출하여 컴포넌트를 동적으로 가져옵니다. 이 함수는 프로미스를 반환하므로, 로딩이 완료될 때까지 기다린 후 컴포넌트를 렌더링할 수 있습니다.
- 라우터와 함께 사용: 리액트 라우터를 사용하는 경우, 특정 경로에 접근했을 때만 컴포넌트를 로드하도록 지연 로딩을 적용할 수 있습니다. 각 라우트에 해당하는 컴포넌트를 React.lazy로 불러오고, 라우트를 렌더링하는 곳에서 Suspense로 감싸 줍니다.
12-4. 배포
- 리액트 SPA 배포에는 정적 웹사이트 호스팅 서비스 : 파일을 저장하고, 필요할 때 사용자에게 전달하는 역할만 하며, 서버 측에서 추가적인 코드 실행은 필요하지 않습니다. 이는 호스팅 비용을 절감하고 설정을 단순화하는 이점을 제공합니다.
12-5. 싱글 페이지 애플리케이션
- 사용자가 웹사이트의 특정 경로(URL)에 직접 접근하려 할 때, 기본적으로 서버는 해당 경로에 맞는 파일을 찾으려 시도합니다. 하지만 SPA에서는 모든 경로에 대해 동일한 HTML 파일(index.html)을 반환하고, 페이지의 로딩과 라우팅은 클라이언트 측 자바스크립트가 담당합니다.
- 간단히 말해, SPA 설정은 웹사이트의 모든 경로 요청이 클라이언트 측 자바스크립트로 처리되도록 보장하여, 서버 측에서 별도의 라우팅 로직을 구현할 필요가 없게 만듭니다. 이를 통해 사용자는 웹사이트의 어떤 URL로 접근하더라도 정상적으로 페이지를 볼 수 있습니다.
- 배포 시 고려사항: Firebase와 같은 일부 호스팅 서비스는 SPA 배포를 쉽게 지원하지만, 다른 서비스를 사용할 경우 모든 요청을 index.html로 리디렉션하는 설정을 수동으로 해야 할 수도 있습니다.
12-6. Firebase 데이터 처리 시 주의사항
- Firebase에서는 "비어 있는" 배열이나 객체를 저장하려 할 때 해당 키 자체가 데이터베이스에 저장되지 않습니다. 이로 인해 데이터를 불러올 때 해당 키에 대해 undefined가 될 수 있습니다.(
Firebase Realtime Database를 비롯한 일부 NoSQL 데이터베이스에서는 비어 있는 배열이나 객체를 저장할 때 특정 키를 완전히 제거하는 경우가 있음.)
- 적절한 초기값 설정이나 예외 처리가 필요합니다.
13. Tanstack Query
13-1. Tanstack Query (React Query) 사용 이유
- HTTP 요청 관리: 프론트엔드와 백엔드 데이터의 동기화를 용이하게 하며, HTTP 요청의 복잡한 상태 관리와 로직을 간결화합니다.
- 고급 기능: 자동 데이터 재요청, 캐싱, 백그라운드 업데이트 등을 포함하여 데이터 페칭 관련 고급 기능을 제공합니다.
- 요청 관리 중심: HTTP 요청을 직접 전송하는 것이 아니라, 요청과 관련된 데이터 및 오류를 추적하고 관리하는 로직을 제공합니다.
13-2. tanstack-query 동작
- 데이터 재요청 및 동기화: 컴포넌트가 재렌더링 될 때 캐시된 데이터를 즉시 반환하고, 백그라운드에서 데이터의 최신 상태를 확인하여 필요 시 캐시를 업데이트합니다.
13-3. staleTime & gcTime
- staleTime: 데이터가 최신 상태로 간주되는 기간을 제어,설정한 기간 동안 추가적인 요청을 통해 데이터를 업데이트하지 않습니다. 데이터의 신선도를 관리.
- 데이터가 캐시에 보관되는 최대 기간을 제어하며, 이 기간이 지나면 사용되지 않는 데이터를 메모리에서 제거합니다. 불필요한 데이터를 제거하여 리소스 사용 최적화.
13-4. Fetch API의 Signal
- signal은 요청을 취소할 수 있는 기능을 제공합니다. 요청과 연결된 signal 객체를 fetch 함수에 옵션으로 전달함으로써 요청을 중단할 수 있는 기능을 제공합니다.
- 리액트 쿼리는 쿼리 함수에 기본적으로 데이터를 전달(쿼리 키, 신호에 대한 정보가 담긴 객체), 띠라서 쿼리 함수에 데이터를 전달하려면 객체 형태로 전달할 것.
- useQuery 구성 객체에 enabled 프로퍼티를 설정하여 쿼리 활성화 제어.
- isPending은 isLoading과는 다르게, 쿼리가 활성화되어 있으나 실제 데이터 요청이 시작되지 않은 초기 상태를 더 세밀하게 다룰 수 있게 해줍니다. 대부분은 isLoading만으로 충분합니다.(useMutation에는 isPending만 존재)
13-5. useMutation
- 데이터 변경 최적화: useMutation 훅은 데이터를 변경하는 쿼리(예: POST, PUT, DELETE 등)에 최적화되어 있습니다.
- 요청 실행 제어: mutate 함수를 통해 요청 실행 시점을 명시적으로 제어할 수 있습니다. (인자도 mutate함수에 전달.)
- 변형은 응답데이터를 캐시 처리하지 않기 때문에 key가 반드시 필요하지는 않습니다.
- onSuccess 프로퍼티에 성공했을 떄 동작할 수 있는 함수를 전달.
13-6. 쿼리 무효화
- queryClient.invalidateQueries(): 지정된 쿼리 키에 해당하는 캐시된 쿼리 데이터를 무효화하고, 데이터의 최신 상태를 다시 가져오도록 요청합니다.
- 쿼리 키를 인자로 넣어 무효화할 쿼리를 특정할 수 있음.
- refetchType: 'none' 프로퍼티를 추가 설정하면 invalidateQueries를 호출할 때 현재의 쿼리가 즉시 자동으로 트리거되지 않도록 한다.
13-7. 낙관적 업데이트
- 즉각적인 UI 반응: 서버 요청의 완료를 기다리지 않고, 사용자에게 즉각적인 피드백을 제공합니다.
- 향상된 사용자 경험: 데이터가 실제로 업데이트되기 전에 UI 변경을 미리 보여줌으로써, 앱이 더 반응적이고 빠르다고 느끼게 합니다.
- 롤백 가능성: 서버 요청이 실패할 경우, UI를 이전 상태로 쉽게 되돌릴 수 있습니다.
13-8. 낙관적 업데이트 구현
- onMutate는 mutate 함수가 호출될 때 즉시 실행됩니다. 이는 서버 응답을 기다리지 않고, 즉시 UI를 업데이트하기 위해 사용됩니다. 변경하려는 데이터를 리액트 쿼리의 캐시에 낙관적으로 업데이트하여, UI가 즉각 반응하도록 합니다.
- 먼저 queryClient.cancelQueries를 사용하여 관련 쿼리를 일시적으로 취소하고, 현재 진행 중인 모든 데이터 패칭을 중단합니다. 이는 현재의 낙관적 업데이트와 충돌을 방지합니다.
- queryClient.setQueryData를 통해 캐시된 데이터를 사용자가 입력한 새 데이터로 낙관적으로 업데이트합니다.
- onError에서는 서버 요청이 실패한 경우, onMutate에서 반환된 이전 상태(context.prevEvent)를 사용하여 데이터를 원래 상태로 롤백합니다.
- onSettled는 변형 작업이 성공하든 실패하든, 결국 완료될 때 호출됩니다. 이는 성공 또는 실패 후에 실행해야 할 공통 로직을 처리하는 데 사용됩니다. 예를 들어, 낙관적 업데이트 후에는 서버와의 데이터 동기화를 보장하기 위해 관련 쿼리를 무효화하고 새로운 데이터를 패칭하는 것이 좋습니다. 이를 통해 사용자가 보는 데이터가 서버의 최신 상태와 일치하도록 보장합니다.
13-9. 라우터와 함께 사용하기
- 리액트 라우터와 리액트 쿼리를 함께 사용하면, 애플리케이션의 라우팅과 데이터 관리를 효율적으로 처리할 수 있습니다. 특히, 리액트 라우터의 데이터 로딩(loaders) 및 데이터 변형(actions) 기능과 리액트 쿼리의 데이터 패칭 및 캐싱 기능을 결합하여, 애플리케이션의 성능을 최적화하고 사용자 경험을 향상시킬 수 있습니다.
- 로더(loader) 함수: 리액트 라우터를 사용하여 페이지나 컴포넌트가 렌더링되기 전에 필요한 데이터를 사전에 로드할 수 있습니다. loader 함수를 정의하여 리액트 쿼리의 fetchQuery를 사용, 필요한 데이터를 사전에 패치하고 캐시에 저장할 수 있습니다. 이를 통해 페이지가 사용자에게 보여지기 전에 필요한 모든 데이터를 준비할 수 있으며, 사용자는 로딩 시간 없이 즉시 데이터를 볼 수 있습니다.
- 액션(action) 함수: 사용자의 입력이나 양식 제출과 같은 이벤트에 의해 데이터 변형이 필요한 경우, 리액트 라우터의 action 함수를 사용하여 처리할 수 있습니다. action 함수 내에서 리액트 쿼리의 updateEvent와 같은 함수를 직접 호출하여 데이터를 업데이트하고, queryClient.invalidateQueries를 통해 관련 쿼리를 무효화함으로써 최신 데이터로 UI를 업데이트할 수 있습니다.
- 기존 useMutation 로직은 상태 관리와 사이드 이펙트 처리에 초점을 맞춘 복잡한 상호작용이 필요한 경우에 적합합니다. 사용자 경험을 최우선으로 할 때 유리합니다.
loader와 action을 추가한 로직은 데이터 패칭을 페이지 로딩과 밀접하게 결합시키고 싶거나, SSR과 같은 고급 라우팅 기능을 활용하고자 할 때 적합합니다.
13-10. useIsFetching
- 리액트 쿼리가 애플리케이션 어딘가에서 데이터를 가져오는지 확인할 수 있는 값을 얻을 수 있습니다.
14. framer motion
14-1. motion 컴포넌트
- Framer Motion을 사용하여 애니메이션을 적용할 HTML 태그를 motion 함수로 감싸서 사용합니다. 예를 들어, <motion.div>는 div 태그에 애니메이션을 적용합니다.
initial, animate, exit 프로퍼티: 이 세 가지 프로퍼티를 통해 컴포넌트의 애니메이션 시작 상태(initial), 종료 상태(animate), 그리고 컴포넌트가 사라질 때 적용되는 상태(exit)를 정의합니다.
14-2. AnimatePresence와 조건부 렌더링
- AnimatePresence는 컴포넌트가 조건부로 렌더링되어 화면에 등장하거나 사라질 때 애니메이션을 적용할 수 있게 합니다.
- AnimatePresence를 사용하면 컴포넌트가 가상 DOM에서 제거되기 전 exit 애니메이션을 실행할 수 있습니다.
14-3. dialog와 닫힘 동작(esc)
- dialog 태그와 같이 특정 키 입력(예: Esc)에 대한 기본 동작을 가지는 요소와 Framer Motion 애니메이션을 연계하는 것은 어려울 수 있습니다. 예를 들어, dialog의 Esc 키 기본 닫기 동작과 Framer Motion의 exit 애니메이션을 함께 사용하기 위해서는
keydown 이벤트 리스너를 사용하여 Esc 키 입력을 감지하고, 이에 대한 애니메이션 처리 로직을 명시적으로 구현하는 것이 좋습니다.
14-4. while
- initial, animate, exit 대신에 간편하게 애니메이션을 추가할 수 있는 while 속성.
14-5. variants
- variants는 애니메이션 정의를 재사용할 수 있게 해주는 기능입니다. 복잡한 애니메이션 시퀀스를 정의하고, 이를 여러 컴포넌트에 걸쳐 쉽게 공유할 수 있습니다.
- 컴포넌트에 variants 객체를 전달하고, initial, animate, exit 프로퍼티를 통해 해당 상태를 참조함으로써, 컴포넌트의 애니메이션을 제어할 수 있습니다. 이를 통해 코드의 반복을 줄이고, 애니메이션 로직을 더 효율적으로 관리할 수 있습니다.
- 배리언트는 컴포넌트 트리 내에서 애니메이션 상태를 트리거하는 데 사용될 수 있으며, 부모 컴포넌트에서 설정된 애니메이션 상태는 자동으로 자식 컴포넌트에 전달되어 활성화됩니다.
- 자식 컴포넌트에서 initial이나 animate, exit과 같은 애니메이션 속성 중 하나를 설정할 경우 배리언트의 이름을 사용할 수 없습니다. 그 대신, 해당 배리언트에 할당된 값을 복사해서 붙여 넣어 원하는 동작을 적용할 수 있습니다.
14-6. 스태거링
- 스태거링(staggering) 효과를 적용하여 리스트 항목이 차례대로 애니메이션되도록 만들 수 있습니다. 스태거링은 리스트 항목들이 동시에 나타나지 않고, 하나씩 순차적으로 애니메이션되어 나타나는 기법을 말합니다.
- variants의 visible 프로퍼티에는 transition 속성을 추가하여, staggerChildren 옵션을 통해 자식 요소들 간의 애니메이션 시작 시간에 시차를 줄 수 있습니다.
14-7. 명령적 방식 애니메이션
- scope와 animate 두 가지 요소를 포함하는 배열을 반환하는 useAnimate 훅을 사용하여 특정 요소에 대해 명령적으로 애니메이션을 시작할 수 있습니다.
- animate는 세가지 인수를 받습니다. 첫 번째 인수는 CSS 선택자, JSX 요소 또는 ref 객체입니다. 두 번째 인수는 애니메이션의 목표 상태를 정의하는 객체입니다. 세 번째 인수는 애니메이션의 옵션을 설정하는 객체입니다.
- scope ref를 사용하여 애니메이션의 범위를 특정 부분으로 제한할 수 있습니다.
- 특정 한 요소만을 타겟으로 하고 싶은 경우 animate 첫번째 인수에 scope.current로 타겟팅합니다.
14-8. layout
- motion 컴포넌트에 layout 속성을 추가하면 해당 컴포넌트의 레이아웃 변화에 자동으로 애니메이션이 적용됩니다.
- AnimatePresence와 함께 DOM이 사라질 때 동작을 설정하여 자연스러운 애니메이션 동작을 만들 수 있습니다.
- AnimatePresence에 mode="wait"를 추가하고 모션 요소에 key를 각자 추가하면 애니메이션이 순차적으로 나타나게 만들 수 있습니다.
14-9. layoutId
- 모션 요소에 layoutId를 추가하면 페이지의 다른 위치에 있는 같은 layoutId를 지닌 요소가 렌더링 될 때를 자동을 감지해서 부드러운 애니메이션을 적용해줍니다.
14-10. key 속성으로 애니메이션 트리거
- key 속성은 주로 리스트 렌더링 시 각 항목의 고유성을 관리하는 데 사용됩니다. 그러나 이와 별개로, key 속성을 변경함으로써 React가 컴포넌트를 새로운 것으로 인식하게 만들어, 해당 컴포넌트를 재생성하고 내부 상태를 초기화할 수 있습니다. 이 방법으로 애니메이션을 다시 트리거할 수 있습니다.
15. 리액트 Testing
15-1. 테스팅의 필요성
- 수동 테스팅의 한계: 복잡한 애플리케이션에서 모든 가능한 조합과 시나리오를 테스트하기 어렵습니다. 새로운 기능을 추가하거나 기존 기능을 변경할 때, 해당 부분만을 테스트하게 되며, 다른 부분에서 발생할 수 있는 문제를 놓칠 수 있습니다.
- 자동화된 테스팅: 애플리케이션의 코드를 자동으로 테스트하는 추가 코드를 작성함으로써, 모든 변경 사항에 대해 전체 애플리케이션을 항상 테스트할 수 있습니다. 이는 시간을 절약하고 모든 부분을 지속적으로 테스트할 수 있는 이점을 제공합니다. 자동화된 테스팅은 수동 테스팅을 대체하는 것이 아니라, 이를 보완합니다. 애플리케이션의 개별 빌딩 블록을 테스트하고, 이러한 블록들이 함께 잘 작동하는지 확인함으로써, 오류를 더 일찍 발견하고 더 나은 코드와 애플리케이션을 제공할 수 있습니다.
15-2. 테스트 케이스 작성
- render 함수: RTL에서 제공하는 render 함수를 사용하여 리액트 컴포넌트를 가상의 DOM에 렌더링합니다. 이를 통해 반환된 쿼리 함수들을 사용하여 테스트할 요소를 찾고, 상호작용을 시뮬레이션할 수 있습니다.
- userEvent: 사용자 이벤트를 시뮬레이션하는 함수로, 버튼 클릭, 입력 필드 변경 등 사용자의 액션을 모의할 수 있습니다.
- screen: render 함수에 의해 렌더링된 컴포넌트 내의 DOM 요소들에 접근할 수 있는 유틸리티입니다. screen.getByText(), screen.getByRole() 등 다양한 쿼리 함수를 제공하여 테스트 대상 요소를 쉽게 찾을 수 있습니다.
15-3. 테스트 스위트
- 테스트 스위트 : 특정 기능이나 애플리케이션의 한 컴포넌트에 속하는 모든 테스트를 그룹화하여 하나의 테스트 스위트로 구성합니다. 이렇게 하면 관련 테스트를 묶어서 관리할 수 있습니다.
- describe 함수는 두 개의 인자를 받으며, 첫 번째 인자는 테스트 스위트의 설명이고, 두 번째 인자는 테스트 스위트에 속한 개별 테스트를 포함하는 익명 함수입니다.
- 테스트 스위트의 구성: describe 블록 내에 test 함수를 사용하여 개별 테스트를 추가함으로써, 하나의 테스트 스위트 내에 여러 테스트를 조직할 수 있습니다. 이를 통해 관련된 테스트들을 논리적으로 그룹화하여 관리하는 것이 가능합니다.
- 테스트 상세요청: npm test -- --verbose
16. 리액트 + 타입스크립트
16-1. 타입스크립트?
- 타입스크립트는 자바스크립트의 슈퍼셋(superset), 자바스크립트를 기반으로한 프로그래밍 언어.(라이브러리x)
- 자바스크립트는 동적 타입언어, 즉 타입이 런타임에 동적으로 결정되는 반면 타입스크립트는 코드가 실행되기 전 컴파일 타임에 결정됩니다.
- 타입스크립트는 브라우저에서 실행되지 않습니다. 자바스크립트 형태로 컴파일되어야 브라우저에서 실행할 수 있습니다.
16-2. 타입 추론
- 타입 추론(Type Inference)이란, 개발자가 명시적으로 타입을 지정하지 않았을 경우, 타입스크립트 컴파일러가 코드 내의 값, 표현식, 반환값 등을 기반으로 해당 변수나 요소의 타입을 자동으로 결정하는 기능입니다.
16-3. void
- void 타입은 주로 함수의 반환 타입으로 사용되며, 함수가 명시적인 값을 반환하지 않을 때 사용됩니다.
- <주의> 함수의 반환타입을 undefined로 지정할 경우 함수는 명시적으로 return undefined 해야하는데 이는 일반적으로 쓰이지 않는 용법입니다.
- void 타입의 호환성 : 일반적으로, void를 반환하는 함수는 명시적인 값을 반환하지 않는다고 이해할 수 있습니다. 그러나 TypeScript는 void를 반환하는 함수가 실제로 값을 반환하더라도, 이를 에러로 간주하지 않습니다. 이는 void 반환 타입이 "이 함수의 반환 값을 사용하지 않겠다"는 의도를 나타내기 때문입니다.
16-4. 제네릭
- 타입스크립트의 제네릭(Generic)은 다양한 타입에 대해 재사용 가능한 컴포넌트(함수, 클래스, 인터페이스 등)를 생성할 수 있게 해주는 타입스크립트의 기능입니다. 제네릭을 사용함으로써, 하나의 타입 대신 타입을 변수처럼 사용할 수 있어, 코드의 유연성과 타입 안정성을 동시에 증가시킬 수 있습니다.
16-5. React.FC 사용
- React.FC (Function Component)는 함수형 컴포넌트가 React 컴포넌트임을 명시적으로 나타내고, 기본적으로 children prop을 포함합니다. React.FC를 사용하여 컴포넌트의 타입을 정의할 때 제네릭을 활용하여 컴포넌트가 받을 props의 타입을 지정할 수 있습니다.
- React.FC를 사용하지 않고 interface로 컴포넌트의 프로퍼티 타입을 직접 정의함으로써 더 유연한 타입 지정이 가능합니다. children 프로퍼티를 포함할 시 React.ReactNode 타입을 추가해야줘야하는 번거로움 있지만 유연한 타입 지정이 가능해 선호도가 높은 편입니다.
16-6. 클래스와 인터페이스
- 타입스크립트에서 클래스는 객체의 실제 구현체를 생성하며, 인스턴스화하여 사용할 수 있습니다. 클래스 내에서는 속성과 메소드를 정의할 수 있으며, 타입스크립트에서는 속성 타입도 명시할 수 있습니다. 클래스를 사용하여 구체적인 데이터 모델을 생성하고, 이를 통해 앱 내에서 사용할 데이터의 타입을 정확하게 지정할 수 있습니다.
- 인터페이스는 객체의 구조를 정의하는 데 사용되며, 구현체를 제공하지 않습니다. 대신 객체가 특정 구조를 준수해야 함을 명시하는 데 사용됩니다.
16-7. nullish coalescing, Non-null assertion operator
- ? : 레퍼런스(ref)와 같이 값이 할당되기 전에는 null일 수 있는 경우에, 해당 프로퍼티나 메소드에 접근하려 할 때 ?. 연산자(옵셔널 체이닝)를 사용하여, 해당 값이 null이나 undefined인 경우에는 더 이상 진행하지 않고 undefined를 반환하도록 합니다. 이를 통해 런타임 에러를 방지하고, 코드의 안정성을 높일 수 있습니다.
- ! : TypeScript에서만 사용할 수 있는 Non-null assertion operator는 개발자가 해당 값이 절대 null이나 undefined가 아님을 확신할 때 사용합니다. 이 연산자는 컴파일러에게 값의 타입이 null이나 undefined가 아니라고 알려주어, 타입 체크 오류를 방지합니다.
16-18. 함수 매개변수 타입
interface ITodosContext {
items: Todo[];
addTodo: (text: string) => string;
removeTodo: (id: string) => void;
}
export const TodosContext = React.createContext<ITodosContext>({
items: [],
addTodo: () => {},
removeTodo: () => {},
});
- React.createContext의 예에서 addTodo와 removeTodo 함수는 기본값으로 매개변수 없는 함수가 제공됩니다. 이는 이 함수들이 실제로 사용될 때 (즉, 컨텍스트의 소비자에 의해 호출될 때) 필요한 매개변수를 받도록 설계되었음에도 불구하고, 초기 컨텍스트 값 설정에서는 매개변수를 생략해도 타입 에러가 발생하지 않습니다.이는 TypeScript에서 함수 타입이 매개변수의 수에 대해 느슨한 검사를 수행하기 때문입니다. 함수가 매개변수를 덜 받아도 되는 상황에서는, 매개변수가 더 많은 함수 타입에 매개변수가 더 적은 함수를 할당할 수 있습니다. 이러한 유연성은 다양한 상황에서 함수를 보다 쉽게 재사용할 수 있게 합니다.
17. styled-components
17-1. 스타일드 컴포넌트에 Prop 전달시 주의사항
- React에서 styled-components를 사용할 때, boolean 타입의 props를 스타일 컴포넌트에 직접 전달하면, React는 HTML DOM 요소에 boolean 속성을 전달하는 것으로 간주하고 경고를 발생시킵니다.
- 이를 위해 prop 전달시 $접두사(일시적인 속성이라는 의미)를 사용하여 prop이 스타일링 용도로만 사용되고, 실제 DOM에는 전달되지 않도록 할 수 있습니다.
17-2. 조건부 스타일
- css 함수 사용 : 스타일드 컴포넌트에서 제공하는 css를 import
${(props) =>
props.done &&
css`
color: #ced4da;
`}
color: ${(props) => (props.$done ? '#ced4da' : '#495057')};
17-3. 선택자 사용 주의사항 (&)
- &는(ex : &:hover) 스타일드 컴포넌트 자체의 스타일을 명시적으로 적용하려는 경우 사용합니다.
- :만 사용할 경우 (ex: :hover) 컴포넌트 자체가 아니라 내부의 특정요소 등 예상치 못한 요소에 스타일이 적용될 수 있습니다. 따라서 &를 사용하는 것을 권장합니다.
18. 리덕스 미들웨어
18-1. 리덕스 미들웨어란
- 리덕스 미들웨어는 리덕스를 사용하는 애플리케이션에서 액션을 디스패치했을 때, 리듀서에서 이 액션이 처리되기 전에 실행되는 코드를 말합니다. 즉, 액션과 리듀서 사이의 중간자(middleware) 역할을 합니다. 미들웨어는 액션을 가로채서 액션 자체를 변경하거나, 액션이 리듀서에 도달하기 전에 추가적인 로직을 실행할 수 있습니다.
- 리덕스 미들웨어는 스토어의 dispatch와 getState 함수에 접근할 수 있는 함수를 반환하는 고차 함수입니다. 미들웨어는 store.dispatch 메소드를 통해 액션이 디스패치되면, 해당 액션을 가로채서 미들웨어 체인을 통과시키고, 각 미들웨어는 액션을 다음 미들웨어로 전달하거나, 액션을 수정하거나, 아예 다른 액션을 디스패치할 수 있습니다. 모든 미들웨어가 액션을 처리한 후, 액션은 마지막으로 리듀서에 전달되어 상태가 업데이트됩니다.
- API 요청과 같은 비동기 작업을 처리할 때 사용됩니다. 예를 들어, Redux Thunk나 Redux Saga 미들웨어를 사용하여 애플리케이션의 비동기 로직을 관리할 수 있습니다.