- useEffect의 dependency array에 대해서 설명해주세요.
- 리액트의 내부 작동 원리를 재조정 (Reconciliation) 개념과 함께 설명하세요.
- CSR에 대해서 설명하세요.
- React의 state와 props는 각각 무엇인지 설명해주세요.
- React 컴포넌트의 key 속성에 대해 설명하세요.
- React를 사용하는 이유에 대해 설명하세요.
- React의 생명주기에 대해 설명하세요.
- useEffect와 useLayoutEffect의 차이에 대해 설명해주세요.
- 제어컴포넌트와 비제어 컴포넌트의 차이점에 대해 설명해주세요.
- 한 화면에 input이 100개 이상이 있을 때, 이를 어떻게 효율적으로 관리할 수 있을까요?
- useMemo와 useCallback에 대해 설명해주세요.
- Context API에 대해 설명해주세요.
- props drilling은 무엇이고, 어떻게 해결할 수 있나요?
- 클래스 컴포넌트와 함수형 컴포넌트의 차이에 대해 설명해주세요.
- state를 직접 변경하지 않고 setState를 사용하는 이유에 대해서 설명하세요.
- React 애플리케이션의 성능을 어떻게 최적화할 수 있습니까?
- JSX란 무엇인지 설명해주세요.
- 브라우저에서 어떻게 JSX 파일이 실행되는지 설명해주세요.
- Redux에 대해 설명하세요.
- 리액트 hooks는 무엇이고, 장점은 무엇인가요?
- 리액트의 상태에는 어떤 것들이 있나요?
The Effect Hook lets you perform side effects in function components
데이터 패칭, DOM을 직접적으로 변경하는 것 등은 리액트에서 모두 side effect 입니다. 때문에 원활한 렌더링을 위하여 이를 분리시켜줘야 하는데 그럴 때 사용할 수 있는 것이 useEffect 입니다. useEffect은 외부 세계와 상호 작용하면서 해당 컴포넌트의 렌더링이나 성능에는 영향을 미치지 않도록 만들어줍니다. 즉, 컴포넌트 내에서 side effect를 실행할 수 있도록 만들어 줍니다.
useEffect(callback, dependencyArray)
useEffect의 첫번째 인자는 콜백 함수를 받아 데이터 패칭과 같은 실행할 코드를 작성해줍니다. 두번째 인자로는 배열을 받는데 이를 dependency array라고 합니다.
useEffect은 브라우저 렌더링이 모두 끝난 후, 즉 첫 렌더링이 일어난 후 실행되게 됩니다. 이때 dependency array(종속 배열)에 어떤 값을 주냐에 따라 실행의 빈도가 달라지게 됩니다. 빈 배열을 놓을 경우에는 첫 렌더링 이후 한 번만 실행되지만, 빈 배열이 아닌 state나 변수를 넣을 경우, 값이 변경될 때마다 이를 감지하여 useEffect이 실행되게 됩니다. 만약 두번째 인자에 아무 것도 설정하지 않을 경우, proprs가 변경될 때, state가 변경될 때마다 실행되기 때문에 무한 렌더링으로 이어지기 때문에 지양해야 합니다.
👆 맨 위로 올라가기
리액트는 선언형(declarative)입니다. 이는 복잡한 과정을 추상화하여 결과에만 집중할 수 있게 만들어주며, 이 말은 곧 리액트 내부에서 어떤 일이 일어나고 있는지 명확히 눈에 보이지 않는다는 말이기도 합니다. 배열 고차 함수인 filter, map, reduce를 사용할 때를 생각해 보면 더 잘 와닿을 거라 생각합니다. 이러한 리액트의 특징은 Virtual DOM 덕분에 가능한 것인데 리액트는 실제 DOM에 직접 업데이트하지 않고 실제 DOM 객체를 복사하여 변경된 요소만 업데이트합니다. 이 때 복사된 DOM을 Virtual DOM(VDOM)이라 합니다.
내부 동작을 다시 정리해보자면 상태가 변경될 때마다 새로운 가상 돔 트리를 만들고, diffing 알고리즘을 사용하여 실제 DOM 트리를 업데이트 합니다. 이때 실제 돔과 가상 돔이 일치하는지 안 하는지 비교하는 과정을 reconciliation(재조정)이라 합니다.
👆 맨 위로 올라가기
서버로부터 HTML 파일을 받아 브라우저 화면에 표시하고 그리는 일련의 작업을 렌더링이라 합니다!
모바일 시대가 도래하면서 이에 맞는 최적화된 서비스가 필요해졌고, 이때 등장한 것이 SPA입니다.
SPA(Single Page Application)란?
최초 한 번 페이지 전체를 로딩한 이후부터는 데이터만 변경하여 사용하는 단일 페이지로 구성된 웹 애플리케이션이다.
SPA에서는 화면 구성에 필요한 모든 HTML을 클라이언트가 갖고 있고 서버 측에는 필요한 데이터를 요청하고 JSON으로 받기 때문에 기존의 어플리케이션에 비해 화면을 구성하는 속도가 빠르다.
그러나 전통적인 웹 렌더링 방식인 SSR은 SPA를 구현하기엔 한계가 있었습니다. 그 중 가장 큰 이유는 요청 시마다 서버로부터 자원을 받아 파싱(해석)하고 화면에 렌더링했기 때문입니다. 즉, 요청 시마다 새로고침이 되는 문제가 있었습니다. 이에 클라이언트 측에서 자바스크립트를 수행시킬 수 있는 Client Side Rendering 즉, CSR이 등장하게 되었고, 영어 의미 그대로 렌더링이 클라이언트에서 발생하는 것을 말합니다.
이러한 CSR의 방식은 브라우저가 완전히 새로운 문서를 요청하거나 재평가할 필요가 없기 때문에 더 빠른 사용자 경험을 가능하게 합니다.
✔️ 페이지 간 로드 시간이 빠르다.
✔️ 그러나 초기 업로드 때 모든 자원들을 읽기 때문에 시간이 다소 느리다.
✔️ 이는 SEO에 친화적이지 않게 만든다. 이는 크롤러가 해당 페이지에 처음 방문했을 때는 빈 페이지이기 때문이다.
CSR 방식을 기반으로 SPA를 지원하는 것으로는 React, Angular, Vue가 있습니다.
Server Side Rendering은 전통적인 웹 어플리케이션 렌더링 방식으로 사용자가 새 페이지나 새 데이터에 접근할 때, 페이지에 대한 요청을 보내면 서버에서 이와 일치하는 응답(새로운 URL)을 사용자에게 제공해줍니다.
이는 새로운 전체 페이지를 렌더링한다는 의미이기도 합니다. 때문에 SSR은 페이지를 이동할 때마다 콘텐츠를 브라우저에 표시하기까지 시간이 걸립니다. 그러나 SSR은 검색 엔진 최적화(SEO)가 가능하다는 점에서 큰 메리트가 있습니다.
✔️ 검색 엔진 최적화(SEO)에 친화적이다.
✔️ 브라우저가 HTML만을 표시하기 때문에 초기 로딩 속도가 빠르다. (렌더링 후 자바스크립트 다운)
✔️ 그러나 서버에 요청할 때마다 매번 새로고침이 발생한다.
정보가 지속적으로 변경되고 사용자 요구를 충족하기 위해 빠른 업데이트를 수행하기 위해 크고 동적인 테이터 처리가 필요한 동적 웹 앱에 사용한다. 즉, SEO보다 사용자 경험을 우선시 하는 사이트에 추천한다. ex. 소셜 네트워크, 온라인 메신저 등
검색 엔진 결과 페이지에 높은 순위를 얻어야 하는 경우에 좋은 선택이다. ex. 온라인 마켓플레이스, 직관적인 사용자 인터페이스를 갖춘 애플리케이션 등
둘 다 장단점이 있기에 SSR과 CSR의 특징을 잘 파악하여 애플리케이션에 알맞는 방식을 결정해야 합니다.
cf. client-side routing
👆 맨 위로 올라가기
React에서 state와 props는 둘 다 컴포넌트에서 데이터
를 다루는데 사용되지만, 목적과 사용 방법에서 차이점이 존재합니다.
state는 컴포넌트 내부에서 관리되는 변경 가능한 데이터입니다. setState()
메소드 또는 클래스형 컴포넌트에서는 this.state
객체를 사용해 상태를 변경할 수 있습니다.
state가 변경되면 render() 메소드를 트리거해 UI가 업데이트 됩니다. 따라서 컴포넌트에서 데 state를 사용해 데이터의 상태를 관리하기 위해 사용됩니다.
props는 ‘properties’의 준말로, 부모 컴포넌트에서 자식 컴포넌트로 전달되는 읽기 전용 데이터를 말합니다. 함수의 매개변수 또는 클래스형 컴포넌트의 this.props
객체를 통해 사용할 수 있습니다.
props는 읽기 전용 데이터이기 때문에 변경되지 않으며,부모 컴포넌트에서 props를 이용해 자식 자식 컴포넌트에 데이터를 전달할 때 사용합니다.
state와 마찬가지로 props도 변경되면 render() 메소드를 트리거해 UI가 업데이트 됩니다.
React에서 state와 props는 둘 다 컴포넌트에서 데이터를 다루는데 사용되며 목적과 사용 방법에서 차이점이 존재합니다.
state는 함수 내에서 선언된 변수
처럼 컴포넌트 내부에서 변경 가능한 데이터를 관리하고,
props는 함수의 매개변수
처럼 외부 컴포넌트(부모 컴포넌트)에서 상속 받은 데이터로, 변경이 불가능한 읽기 전용 데이터를 전달하는데 사용된다는 차이가 있습니다.
incrementCount() {
this.setState({count : this.state.count + 1 });
}
handleSomething() {
this.incrementCount();
this.incrementCount();
this.incrementCount();
}
React에서 map
등을 사용해 컴포넌트를 반복적으로 생성할 때, 'Warning: Each child in a list should have a unique "key" prop.’
와 같은 경고를 마주치게 됩니다.
React 컴포넌트에서 왜 key 속성이 필요한지 React 렌더링 원리에 대해 알아보도록 하겠습니다.
리액트는 컴포넌트의 상태나 속성(props)이 변할 때마다 render()
함수를 호출합니다.
이때 render()
함수는 새로운 리액트 요소 트리를 반환하고 이를 기존의 요소 트리와 비교해 새로운 변경점에 대해서만 제렌더링을 수행합니다.
여기서 기존 요소들 뒤에 새로운 내용을 추가할 경우 새로운 부분에 대해서만 재렌더링 되지만, 기존 요소 앞에 새로운 내용을 추가할 경우 모든 부분이 재렌더링됩니다. 바로 이런 상황을 최적화하기 위해 key
속성을 사용합니다.
각 요소에 key
속성을 부여하면 더 이상 모든 요소를 렌더링하지 않고, 추가된 부분만 제렌더링해 효율적인 렌더링을 실현할 수 있습니다.
key
값은 변하지 않는 고유한 값을 설정하는 것이 권장됩니다. 다만 이때 인덱스를 key
값으로 사용할 경우 문제가 발생할 수 있습니다.
예를 들어 순서가 변할 수 있는 상황에서 key
값으로 고유한 값이 아닌 인덱스를 사용하면 key
를 사용하지 않은 때와 같이 모든 형제 요소가 재렌더링됩니다. 이는 성능 저하와 예상치 못한 문제들을 발생시킬 수 있습니다,
또한 Math.random()
으로 생성된 값과 같이 변하는 값을 key로 사용하면 컴포넌트와 인스턴스, DOM 노드를 블필요하게 재생성하게 되어 성능이 저하되고 자식 컴포넌트의 state가 유실될 수 있습니다.
따라서 해당 데이터가 갖는 id값이나 별도 고유 id 라이브러리 등을 사용해 언제나 요소의 key
값이 고유하도록 보장해야 합니다.
👆 맨 위로 올라가기
react는 component와 Virtual DOM 때문에 사용합니다.
component는 재사용이 가능한 각각의 독립적 모듈이라고 할 수 있습니다. react는 component를 조립하여 프로그램을 만듭니다. 그래서 재사용에 용이하며 재사용하기 때문에 코드의 불필요한 반복이 줄어 생산성이 좋아지며 재사용한 컴포넌트에 에러가 발생했을때 해당 컴포넌트의 에러만 해결하면 되기 때문에 유지보수에도 용이합니다.
기존에는 상태나 페이지가 바뀌면 전체를 렌더링했지만 react는 Virtual DOM을 이용하여 상태나 페이지가 바뀌었을 경우 바뀐 부분만 렌더링하게되어 불필요한 렌더링이 줄어듭니다.
react를 사용하는 이유는 재사용이 가능하고 그로인해 생산성과 유지보수가 용이하고 불필요한 렌더링을 줄일수 있기 때문입니다.
👆 맨 위로 올라가기
react의 생명주기는 mount(생성) -> 업데이트 -> unmount(제거) 순으로 구성되어 있습니다. 각각의 생명주기에 특별한 메서드를 선언하여 코드를 작동할 수 있습니다.
constructor(props) {
super(props);
}
- 컴포넌트의 생성자 메서드이며 컴포넌트가 만들어지면 가장 먼저 실행되는 메서드입니다.
static getDerivedStateFromProps(nextProps, prevState) {
console.log("getDerivedStateFromProps");
if (nextProps.color !== prevState.color) {
return { color: nextProps.color };
}
return null;
}
- props로 받아온 값을 state에 넣어주고 싶을 경우 사용합니다.
- 다른 생명주기 메서드와 다르게 static 키워드가 필요합니다.
- this로 조회할 수 없습니다.
- 특정 객체를 반환하면 객체 안의 내용들이 컴포넌트의 state로 설정됩니다. (null일 경우 아무일도 발생하지 않음)
- 컴포넌트를 렌더링하는 메서드입니다.
- 컴포넌트의 첫번째 렌더링이 끝나면 호출되는 메서드입니다.
- 주로 DOM을 사용해야하는 외부 라이브러리를 연동하거나 데이터를 요청하기 위해 ajax 요청을 하거나 DOM의 속성을 읽거나 직접 변경하는 작업을 진행합니다.
- 위의 다뤘던 내용과 같습니다.
- 컴포넌트의 state와 props가 바뀌었을때도 호출됩니다.
shouldComponentUpdate(nextProps, nextState) {
return true;
}
- 컴포넌트가 리렌더링의 여부를 결정하는 메서드입니다.
- 주로 최적화 할 때 사용하는 메서드입니다.
- 반환 값이 true이면 리렌더링을 하며, false일 경우 리렌더링을 하지 않습니다.
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log("getSnapshotBeforeUpdate");
if (prevProps.color !== this.props.color) {
return this.myRef.style.color;
}
return null;
}
- 컴포넌트에서 변화가 일어나기 직전의 DOM상태를 가져와서 특정 값을 반환하면 그 값을 componentDidUpdate에서 사용할 수 있습니다.
componentDidUpdate(prevProps, prevState, snapshot) {
console.log("componentDidUpdate", prevProps, prevState);
if (snapshot) {
console.log("업데이트 되기 직전 색상: ", snapshot);
}
}
- 리렌더링과 업데이트를 마치고 난 뒤에 호출되는 메서드입니다.
- getSnapshotBeforeUpdate에서 반환한 값을 3번째 인자로 받을 수 있습니다.
- 업데이트 전의 props와 이후의 props를 비교하는데 사용할 수 있습니다.
- DOM에 직접 등록했었던 이벤트를 제거합니다.
- setTimeout을 사용했다면 clearTimeout으로 제거합니다.
💡 필수 용어
- Render : DOM Tree를 구성하기 위해 각 엘리먼트의 스타일 속성을 계산하는 과정
- Paint : 실제 스크린에 Layout을 표시하고 업데이트하는 과정
useEffect은 컴포넌트들이 render와 paint된 후에 실행
되며, 비동기적입니다.
paint된 후에 실행되기 때문에, useEffect 내부에 DOM에 영향을 주는 코드가 있을 경우 화면 깜빡임 현상이 발생할 수 있습니다.
useEffect의 Life cycle
useLayoutEffect은 컴포넌트들이 render된 후에 실행되며, 그 후 paint
됩니다. 즉 동기적
으로 실행됩니다.
paint가 되기 전에 실행되기 때문에 DOM을 조작하는 코드가 존재하더라도 사용자는 깜빡임을 경험하지 않습니다.
useLayoutEffect의 Life cycle
useLayoutEffect은 동기적으로 실행되어 내부 코드가 모두 실행된 후 painting 작업을 거칩니다. 따라서 로직이 복잡할 경우 사용자가 레이아웃을 보는데까지 시간이 오래 걸린다는 단점이 있어 보통의 경우 useEffect의 사용이 권장됩니다. 대표적으로 데이터 fetch
, event handler
, state reset
의 경우 useEffect을 많이 사용합니다.
화면이 깜빡 거리는 상황일 때 유용합니다.
예를 들어 아래와 같이 state가 있고, 조건에 따라 첫 painting 시 다르게 렌더링 되어야 할때, useEffect은 처음에 0이 보여지고 re-rendering 되면서 화면 깜빡임 현상이 발생하지만, useLayoutEffect은 코드가 모두 실행된 후 painting 되기 때문에 화면 깜빡임 현상이 발생하지 않습니다.
const Test = (): JSX.Element => {
const [value, setValue] = useState(0);
useLayoutEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
console.log('render', value);
return <button onClick={() => setValue(0)}>value: {value}</button>;
};
React에 의해 값이 제어되는 컴포넌트를 제어 컴포넌트라고 합니다. 보통 form
이나 input
등의 입력 요소 태그를 다룰 때, 요소에 입력되는 값을 state로 관리하거나 DOM API를 통해서 관리할 수 있습니다. 이때 state로 DOM 요소의 값을 다루는 컴포넌트가 바로 제어 컴포넌트입니다.
아래 코드는 input
의 값이 바뀔 때마다 ChangeName 함수를 통해 state의 값을 업데이트해주는 제어 컴포넌트입니다.
import React, { useState } from 'React';
function Control() {
const [name, setName] = useState(null);
const ChangeName = (e) => {
setName(e.current.value);
};
return <input onChange={ChangeName} value={name} />;
}
제어 컴포넌트는 사용자의 입력을 기반으로 state를 관리하고 update합니다. 이처럼 제어 컴포넌트는 React에 의해 값이 제어되는 입력, 폼 요소에서 사용됩니다.
제어 컴포넌트는 입력할 때마다 렌더링 하기 때문에, 불필요하게 리렌더링되거나 API를 호출할 수 있습니다. 즉 사용자가 입력하는 모든 데이터가 동기화됩니다. 이 문제를 해결하기 위해서 쓰로틀링(Throttling)과 디바운싱(Debouncing)을 활용할 수 있습니다.
- 쓰로틀링(Throttling) : 마지막 함수가 호출된 후 일정시간이 지나기 전에 다시 호출되지 않도록 하는 것
- 디바운싱(Debouncing) : 연이어 호출되는 함수들 중 마지막(혹은 맨 처음) 함수만 호출하도록 하는 것
- 유효성 검사
- 실시간으로 필드 검사를 해야하는 경우
- 조건에 따라 버튼의 활성화 여부가 바뀌는 경우
React에 의해 값이 제어되지 않은 컴포넌트를 비제어 컴포넌트라고 합니다. 제어 컴포넌트에서 폼 데이터는 React 컴포넌트에서 다뤄지는 반면, 비제어 컴포넌트는 DOM 자체에서 폼 데이터가 다뤄집니다.
따라서 모든 state 업데이트에 대한 이벤트 핸들러를 작성하는 대신 ref를 사용해 DOM에서 폼 값을 가져올 수 있습니다.
아래 코드는 ref를 통해 input 값에 접근 할 수 있고, 이벤트 핸들러를 통해 ref에 저장된 요소의 값을 가져와 활용하는 비제어 컴포넌트입니다.
import React, { useRef } from 'React';
function UnControl() {
const nameRef = useRef(null);
return <input ref={nameRef} />;
}
비제어 컴포넌트는 state로 값을 관리하지 않기 때문에 값이 바뀔 때마다 리렌더링, API 호출을 하지 않아 성능상 이점이 있습니다. 즉 비제어 컴포넌트는 사용자가 직접 트리거 하지 전까지는 리렌더링을 발생시키지 않고 값을 동기화 시키지도 않습니다. 대표적 예로 submit 버튼을 클릭하면 함수 내에서 ref를 통해 form 내 value들에 접근합니다.
일반적으로 모든 form 요소의 동기화가 필요하지 않고, form 요소가 증가할수록 면 모든 컴포넌트에 쓰로틀링이나 디바운싱을 걸기엔 어려움이 있습니다. 따라서 만약 값이 트리거 된 이후에 값이 갱신되어도 된다면, 비제어 컴포넌트를 통해 불필요한 렌더링을 방지하고 성능 향상을 할 수 있습니다.
대표적으로 비제어 컴포넌트를 사용해 렌더링을 최적화하는 라이브러리로 react-hook-form
이 있습니다.
제어 컴포넌트 | 비제어 컴포넌트 | |
---|---|---|
1. 동기화 | 항상 동기화(제어 컴포넌트의 값은 항상 최신값을 유지) | 동기화 X |
2. 폼 데이터 | React 컴포넌트 | DOM 자체 |
✔️ React 공식 문서에 따르면 대부분의 경우 폼 구현 시 제어 컴포넌트를 사용하는 것을 권장
기능 | 제어 컴포넌트 | 비제어 컴포넌트 |
---|---|---|
일회성 정보 검색 (ex. 제출) | ✅ | ✅ |
제출 시 값 검증 | ✅ | ✅ |
실시간으로 필드값의 유효성 검사 | ✅ | ❌ |
조건부로 제출 버튼 비활성화(disabled) | ✅ | ❌ |
실시간으로 입력 형식 적용하기(ex. 숫자만 가능하게 등) | ✅ | ❌ |
동적 입력 | ✅ | ❌ |
- 즉각적, 실시간으로 값에 대한 피드백이 필요하다 → 제어 컴포넌트
- 즉각적인 피드백이 불필요하고 제출시에만 값이 필요하다 혹은 불필요한 렌더링과 값 동기화가 싫다 → 비제어 컴포넌트
React-hook-form과 같은 비제어 컴포넌트를 통해 input 요소를 효율적으로 관리할 수 있습니다. 그 이유는 비제어 컴포넌트의 비동기화 때문입니다.
제어 컴포넌트의 경우 사용자의 입력으로 input의 값이 변경될 때마다 state가 동기화되고, 이에 따라 불필요한 리렌더링이 발생합니다. 반면 비제어 컴포넌트는 제출 버튼을 누르는 시점에만 해당 input 요소의 값에 접근합니다. 따라서 예시와 같이 한 화면에 input 요소가 많을 때, React-hook-form과 같은 비제어 컴포넌트를 사용하면 불필요한 리렌더링을 줄이고, 자원 낭비를 막을 수 있습니다.
👆 맨 위로 올라가기
memoization 기법을 사용하여 동일한 값을 반환하는 함수가 있다면 첫 렌더링 때 계산된 값을 메모리에 캐시해둔 다음 필요할 때마다 캐시된 값을 꺼내 재사용할 수 있도록 만들어준다. 주로 컴포넌트 내부에서 대량의 연산과 같은 무거운 일을 수행할 때 불필요하게 반복적으로 호출되는 것을 막아주기 위해 사용한다.
import { useMemo } from 'react';
function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
// ...
}
즉, 특정 값을 재사용하고자 할 때 사용한다. 렌더링될 때마다 종속성을 비교해 값이 동일한 경우에는 이전 렌더링의 값을 그대로 재활용할 수 있게 되며, 종속성이 변경되었다면 해당 함수를 실행하여 새 값을 반환한다.
useMemo와 마찬가지로 memoization 기법을 사용한다. 그러나 다른 점이면 이름에서도 유추할 수 있듯이, 값이 아닌 함수를 캐시한다. 매번 렌더링 될 때마다 이전 렌더링과 현재 렌더링의 종속성을 비교하여, 변경이 없다면 이전에 캐시된 동일한 함수를 반환하지만 변경이 있다면 useCallback에서 전달한 함수를 반환한다.
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
}
react 16.3 버전부터 정식적으로 context api (opens new window)를 지원하여 일반적으로 부모와 자식간 props를 날려 state를 변화시키는 것과는 달리 context api는 컴포넌트 간 간격이 없습니다. 즉, 컴포넌트를 건너띄고 다른 컴포넌트에서 state, function을 사용할 수 있습니다. 또한 redux의 어려운 개념보다 context api는 Provider, Consumer, createContext 개념만 알면 적용가능합니다.
const DarkModeContext = createContext(defaultValue);
context 객체를 만듭니다. 컴포넌트가 이 context를 가지려면 해당 컴포넌트 상위에 provider로 부터 context를 정의한 변수 DarkModeContext를 감싸면 됩니다. defaultValue는 트리 안에 적절한 provider를 찾지 못했을 때 쓰이는 값입니다. createContext 를 실행하면 Provider와 Consumer을 담고 있는 컨텍스트 객체가 생성됩니다.
<DarkModeContext.Provider value={this.state}>
<subComponent1 />
<subComponent2 />
</DarkModeContext.Provider>
provider는 정의한 context를 하위 컴포넌트에게 전달하는 역할을 합니다. 전달하는 변수는 꼭 value를 사용해야 합니다.
function Button() {
return (
<ThemeContext.Consumer>
{theme => (
<button className={theme} />
)}
</ThemeContext.Consumer>
);
}
context 변화를 구독하는 컴포넌트입니다. Provider에 담긴 state를 필요한 컴포넌트에서 접근할 수 있게 만드는 역할을 합니다. 하지만 이 방법을 사용하지 않고 useContext를 사용하여 컨텍스트를 읽을 수 있다. useContext 사용을 더 권장합니다.
import { useContext } from 'react';
function MyComponent() {
const theme = useContext(DarkModeContext);
// ...
컨텍스트를 읽고 구독할 수 있는 리액트 훅입니다. useContext에 createContext로 만든 컨텍스트를 전달하면 해당 객체에 접근하여 값을 반환합니다.
Context API는 리액트에 내장된 기능으로 Props를 사용하지 않아도 특정 값이 필요한 컴포넌트끼리 쉽게 값을 공유할 수 있게 해 줍니다. 주로 프로젝트에서 전역 상태를 관리할 때 많이 사용합니다.
props drilling은 데이터를 부모 컴포넌트에서 props로 하위 컴포넌트에게 전달할 때 발생합니다. 부모 컴포넌트부터 props가 필요한 하위 컴포넌트까지 해당 props를 사용하지 않고 전달만 하는 컴포넌트가 있을 경우 props drilling이라고 합니다.
- props의 전달이 3 ~ 5개 정도면 문제가 되지 않지만 10개와 같이 더 많은 컴포넌트를 거치게 된다면 props의 추적이 어렵게 됩니다.
- 컴포넌트를 세분화하지 않고 state를 가장 가까운 부모 컴포넌트와 공유함으로써 해결할 수 있습니다.
- 멀리 떨어진 컴포넌트와 state를 공유해야할 경우 Redux와 같은 상태관리 라이브러리를 사용하면 해결할 수 있습니다.
클래스 컴포넌트는 내부 상태를 유지하는데 필요한 컴포넌트를 생성하거나 생명주기 메소드를 활용하기 위해 사용했습니다. 그러나 Hooks의 도입 이전의 함수형 컴포넌트는 상태값을 가질 수 없고 생명주기 메소드도 활용하지 못했습니다. 하지만 Hooks가 도입되면서 함수형 컴포넌트에서도 상태값과 생명주기 메소드를 사용할 수 있게 되었습니다.
import React, { Component } from 'react';
class App extends Component {
render() {
const name = 'react';
return <div className='react'>{name}</div>;
}
}
export default App;
- class 키워드를 사용합니다.
- Component로 상속 받아야 합니다.
- render() 메소드가 필요합니다.
import React from 'react';
import './App.css';
function App() {
const name = 'react';
return <div className='react'>{name}</div>;
}
export default App;
- return으로 화면을 구성합니다.
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: 'data',
};
}
}
- constructor 안에서 this.state 초기 값 설정이 가능합니다.
class App extends Component {
this.state = {
data : "data",
}
}
- constructor 없어도 state 초기 값 설정이 가능합니다.
this.state = {
data: 'data',
};
- 클래스형 컴포넌트의 state는 객체형태입니다.
onClick = {()=>this.setState({data : "newData"})}
- this.setState 함수로 state의 값을 변경할 수 있습니다.
const [data, setData] = useState('data');
- useState 함수로 state를 사용합니다.
- useState의 배열 중 첫번째 원소는 현재 state, 두번째 원소는 상태를 바꾸는 함수입니다.
import React, { Component } from 'react';
class App extends Component {
render() {
const { name } = this.props;
return <div className='react'>{name}</div>;
}
}
export default App;
- this.props를 통해 값을 불러올 수 있습니다.
import React from 'react';
import './App.css';
function App({ name }) {
return <div className='react'>{name}</div>;
}
export default App;
- this.props를 통하지 않고 바로 값을 불러올 수 있습니다.
handleChange = e => {
this.setState({
data : e.target.value
})
}
...
<input
onChange = {this.handleChange}
/>
- 요소를 적용하기 위해서는 this를 사용해야 합니다.
const handleChange = e => {
setData(e.target.value)
}
...
<input
onChange = {handleChange}
/>
- 요소를 적용하기 위해 this를 필요로하지 않습니다.
- state(상태)는 불변성을 유지해야 하기 때문에 state를 직접 변경하지 않고 setState를 사용합니다. 또한 컴포넌트는 현재의 this.state와 setState를 비교해서 업데이트가 필요한 경우에만 render 함수를 호출하는데, state를 직접 수정하게 되면 리액트가 render 함수를 호출하지 않아 상태 변경이 일어나도 렌더링이 일어나지 않을 수 있습니다. 상태 변경을 추적하고 변경에 따라 구성요소를 다시 렌더링하려면 setState를 사용해야 합니다.
-
메모이제이션을 사용하여 불필요한 재렌더링을 방지하고 렌더링 성능을 향상시킵니다.
- 메모이제이션(memoization)이란 프로그래밍을 할 때 반복되는 결과를 메모리에 저장함으로써 이후 같은 결과가 사용될 때 저장한 값을 이용해 빠르게 실행하는 코딩 기법을 말합니다.
-
React.Component 클래스 대신 React.PureComponent 클래스를 사용하여 소품 또는 상태가 변경될 때 다시 렌더링할 필요가 없는 구성 요소의 성능을 최적화합니다.
-
React.Component
- shouldComponentUpdate를 따로 설정해주지 않은 경우, 항상 true를 반환하며 상태를 변경하는 setState가 실행되면 state와 props의 변경 여부를 신경쓰지 않고 컴포넌트를 업데이트 시킵니다.
-
React.PureComponent
- shouldComponentUpdate가 내장되어 있는 컴포넌트 입니다. props랑 state를 얕은 비교를 통해 비교한 뒤 변경된 것이 있을때는 true를 return 해서 리렌더링 하고, 변경된 것이 없을때는 false를 리턴합니다.
-
-
shouldComponentUpdate 수명 주기 메서드를 사용하여 구성 요소가 다시 렌더링되는 시기를 제어하고 불필요한 업데이트를 방지합니다.
shouldComponentUpdate() {
if(아니 그래서 너 진짜 바뀌었니?) {
그렇다면 render() 하렴;
} else {
그렇다면 render 하지마렴;
}
}
- React.Suspense 및 React.Lazy 구성 요소를 사용하여 구성 요소를 지연 로드하고 초기 로드 성능을 개선합니다.
- 불필요한 컴포넌트가 로드되지 않도록 할 수 있기 때문에 초기 렌더링 지연시간을 어느정도 줄일 수 있습니다.
- 컴포넌트가 로드되지 않았기 때문에 로드하는 시간이 필요할 수 있습니다. 이때 Suspense로 로딩 화면을 보여줄 수 있습니다.
- lazy 컴포넌트는 반드시 Suspense 컴포넌트 하위에서 렌더링 되어야 합니다.
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
- 불필요한 래퍼 요소를 피하고 DOM 노드 수를 줄이려면 React.Fragment 구성 요소를 사용하세요.
- React의 return문 안에 하나의 최상위 태그가 존재해야 합니다. 따라서 쓸데없는 태그가 들어갈 수 있는데 이때 쓸데없는 태그 대신 사용할 수 있습니다.
- 또는 <> </>을 사용할 수 있습니다.
function Glossary(props) {
return (
<React.Fragment key={item.id}>
<div>{item.term}</div>
<div>{item.description}</div>
</React.Fragment>
);
}
- React.Profiler 구성 요소를 사용하여 성능 병목 현상을 식별하고 렌더링 성능을 최적화하십시오.
- React 애플리케이션이 렌더링하는 빈도와 렌더링 “비용”을 측정합니다. Profiler의 목적은 메모이제이션 같은 성능 최적화 방법을 활용할 수 있는 애플리케이션의 느린 부분들을 식별해내는 것입니다.
- Profiler는 React 트리 내에 어디에나 추가될 수 있으며 트리의 특정 부분의 렌더링 비용을 계산해줍니다.
render(
<App>
<Profiler id="Navigation" onRender={callback}>
<Navigation {...props} />
</Profiler>
<Main {...props} />
</App>
);
-
React.StrictMode 구성 요소를 사용하여 애플리케이션에서 잠재적인 런타임 오류 및 성능 문제를 식별합니다.
- 안전하지 않은 생명주기를 사용하는 컴포넌트 발견
- 레거시 문자열 ref 사용에 대한 경고
- 권장되지 않는 findDOMNode 사용에 대한 경고
- 예상치 못한 부작용 검사
- 레거시 context API 검사
- Ensuring reusable state
-
Webpack과 같은 프로덕션 수준 빌드 도구를 사용하여 애플리케이션 코드를 최적화하고 전반적인 성능을 향상시킵니다.
-
서버 측 렌더링을 사용하여 초기 로딩 성능을 개선하고 검색 엔진 및 소셜 미디어 플랫폼의 사용자 경험을 개선하십시오.
JavaScript XML(eXtensible Markup Language)의 약어로, JavaScript에 XML을 추가한 확장된 문법입니다.
- 리액트 사용시 JSX를 이용해 HTML과 JavaScript를 모두 포함하고 있는
컴포넌트
를 생성합니다.
- 보기 쉽고 익숙하다.
- JSX는 HTML 코드와 비슷하기 때문에 일반 자바스크립트만 사용한 코드보다 더 익숙하고 간결하며 가독성이 좋습니다.
- 높은 활용도
- JSX에서는 div, span과 같은 HTML 태그를 사용할 수 있으며, 개발자가 만든 컴포넌트도 JSX 안에서 작성할 수 있습니다.
- 요소가 하나 이상이라면, 반드시 부모 요소로 감싸는 형태여야 합니다.
Virtual DOM에서 컴포넌트 변화를 감지할 때 효율적으로 비교할 수 있도록 컴포넌트 내부는 하나의 DOM Tree 구조로 이루어져야 한다는 규칙이 있기 때문입니다.
function App() {
return (
<div>
<div>Hello!</div>
<div>World!</div>
</div>
)
}
- 자바스크립트 표현식
JSX 안에서도 자바스크립트 표현식을 사용할 수 있습니다. JSX 내부에서 코드를 { }
로 감싸주면 됩니다.
import React from 'react';
// 자바스크립트 표현
function App(){
const name = 'react';
return(
<>
<h1>Hello! {name}</h1>
<h2>Is it working well?</h2>
<>
)
}
export default App;
- 삼항 연산자(조건 연산자) 사용
if문
과 for 루프
는 JavaScript 표현식이 아니기 때문에 JSX 내부의 자바스크립트 표현식에서 사용할 수 없습니다. 대신 JSX 밖에서 if 문을 사용하거나 { }
안에서 삼항 연산자(조건부 연산자)를 사용할 수 있습니다.
//1. 외부에서 사용
function App() {
let desc = '';
const loginYn = 'Y';
if(loginYn === 'Y') {
desc = <div>GodDaeHee 입니다.</div>;
} else {
desc = <div>비회원 입니다.</div>;
}
return (
<>
{desc}
</>
);
}
//2. 삼항 연산자 사용
function App() {
const loginYn = 'Y';
return (
<>
<div>
{loginYn === 'Y' ? (
<div>GodDaeHee 입니다.</div>
) : (
<div>비회원 입니다.</div>
)}
</div>
</>
);
}
CamelCase
표기법으로 작성
JSX는 HTML보단 JavaScript에 가깝기 때문에 React DOM은 HTML 어트리뷰트 이름 대신 CamelCase
프로퍼티 명명 규칙을 사용합니다.
또한 JSX 내부에서 스타일을 설정할 때는 String 형식이 아닌 Key가 카멜 표기법으로 작성된 객체 형태로 넣어주어야 합니다.
/*
background-color → backgroundColor
font-size → fontSize
*/
function App() {
const style = {
backgroundColor: 'green',
fontSize: '12px'
}
return (
<div style={style}>Hello, GodDaeHee!</div>
);
}
- JSX에서 자바스립트 문법을 쓰려면
{ }
를 써야합니다.
- class 대신 className
일반 HTML에서 CSS 클래스를 사용할 땐 class 속성을 사용하지만, JSX에서는 className을 사용합니다.
브라우저는 JSX를 이해하지 못하기 때문에 JSX 코드를 JavaScript 코드로 변환하는 과정이 필요합니다. JSX를 JS로 변환하기 위해 바벨(Babel)과 같은 컴파일러를 사용합니다.
바벨(Babel)
: 대표적인 트렌스파일러로, 특정 언어로 작성된 코드를 비슷한 다른 언어로 변환시키는 역할을 수행합니다.
바벨은 JSX를 JavaScript로 변환하는데 사용되는 가장 일반적인 도구로, 바벨을 사용하면 JSX를 JavaScript 코드로 변환하고 이를 브라우저에서 실행할 수 있도록 컴파일합니다.
//JSX
function App() {
return (
<div>
Hello <b>react</b>
</div>
);
}
//JavaScript로 변환되었을 때의 모습
function App() {
return React.createElement("div", null, "Hello ", React.createElement("b", null, "react"));}
이렇게 바벨을 사용하여 JavaScript로 변환하면, React에서 작성한 UI 구성 요소를 일반적인 JavaScript 코드로 컴파일하여 브라우저에서 실행할 수 있습니다.
Redux는 React를 위한 Third-Party 상태관리 라이브러리로 Context API가 개발되기 이전부터 존재했습니다. Redux는 Store라는 상태 컨테이너를 기반으로 전체 어플리케이션의 상태를 저장합니다.
1️⃣ 리덕스의 구조
- 상태가 변경되어야 하는 이벤트가 발생하면, 변경될 상태에 대한 정보가 담긴 Action 객체가 생성된다.
- 이 Action 객체는 Dispatch 함수의 인자로 전달된다.
- Dispatch 함수는 Action 객체를 Reducer 함수로 전달해준다.
- Reducer 함수는 Action 객체의 값을 확인하고, 그 값에 따라 전역 상태 저장소 Store의 상태를 변경한다.
- 상태가 변경되면, React는 화면을 다시 렌더링 합니다.
2️⃣ 리덕스의 세 가지 규칙
- Store는 무조건 하나만 존재한다.
- Redux의 state는 읽기 전용이다.
(dispatch를 통해서만 state의 변경이 가능합니다.) - Reducer는 순수 함수로만 구성한다.
(순수함수는 동일한 인자가 들어갈 때 항상 같은 값이 나와야 합니다. 또한 함수에서 외부의 변수 값을 변경하거나 함수 내로 들어온 인자 값을 변화하게 만드는 일 역시 일어나서는 안 됩니다.)
3️⃣ 리덕스의 장점
- 컴포넌트의 구조가 평평해지고 데이터 전달을 위한 복잡한 계층 구조가 필요하지 않게 됩니다.
- 애플리케이션에서 Action과 Reducer 등을 각각 모아 분리함으로써 개발자들이 비즈니스 로직에 대해 파악하기 더 쉬워집니다.
- props와 state를 이용해 상태에 대해 받아오지 않아도 되기 때문에, 무분별한 렌더링이 일어나는 것을 방지할 수 있습니다.
- 상태 업데이트를 위한 함수들로 인해 컴포넌트가 필요 이상으로 커지는 것을 막을 수 있습니다.
cf. How to use Redux Hooks in a React Native App
Hooks는 클래스 기반 컴포넌트의 장점(예를 들어 내부 상태와 생명주기 메소드)을 함수형 컴포넌트로 가져오려는 리액트의 시도에서 시작되었습니다.
- 클래스 기반 컴포넌트, lifecyle hooks, this의 필요성이 사라졌다.
- 공통 기능을 커스텀 hook으로 만들어서 로직을 재사용하기 쉬워졌다.
(컴포넌트 자체에서 로직을 분리할 수 있어서 읽기 쉽고 테스트하기 쉬운 코드를 작성할 수 있습니다.)
이렇듯 Hooks의 등장으로 더이상 클래스형 컴포넌트를 사용하지 않아도 더 쉽고 빠르게 상태 관리를 할 수 있게 되었습니다.
리액트의 상태는 크게 범위
와 역할
, 제어 여부
로 나눌 수 있습니다.
💡 상태(State)란?
리액트에서
상태(State)
란 컴포넌트 내부에서 관리되는 동적인 값으로, 애플리케이션의 렌더링에 영향을 미치는 자바스크립트 객체를 말합니다. State는 해당 State를 기반으로 동작되는 모든 컴포넌트의 상위 컴포넌트 내에 위치하는 것이 좋습니다.
가장 작은 개념의 지역 상태부터 컴포넌트 간 상태, 전역 상태로 나눌 수 있습니다.
지역 상태(Local State)
특정 컴포넌트 안에서만 관리되는 상태로, 다른 컴포넌트들과 데이터를 공유하지 않는다는 특성이 있습니다.
Form 데이터들은 대부분 지역 상태에 속하는데, input
, selectbox
등이 대표적으로 지역 상태를 다룹니다.
컴포넌트 간 상태(Cross Component State)
여러 컴포넌트에서 관리되는 상태로, 다수의 컴포넌트에서 쓰이거나 영향을 미치는 상태를 의미합니다. 대표적으로 프로젝트 시 곳곳에서 사용되는 모달이 있습니다. 보통 상위 컴포넌트에서 하위 컴포넌트로 props를 전달하는 props Drilling 방식을 사용합니다.
전역 상태(Global State)
프로젝트 전체에 영향을 끼치는 상태로, 대표적으로 유저의 로그인 상태 등이 있습니다. 마찬가지로 props Drilling 방식을 사용해 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달합니다.
어플리케이션의 상호작용적인 부분을 컨트롤하는 UI 상태
, 서버로부터 데이터를 가져와 캐싱 해놓는 서버 캐시 상태
, Form의 로딩, Submitting, disabled, validation 등의 데이터를 다루는 Form 상태
, 브라우저에 의해서 관리되고 새로고침해도 변함 없는 URL 상태
등이 있습니다.
서버 상태(Server State)
서버로부터 불러온 데이터를 말합니다. 클라이언트가 제어 혹은 소유할 수 없기 때문에 서버로부터 특정 시점의 데이터를 가져와 저장해 사용합니다. 즉 비동기적인 상태를 갖습니다.
클라이언트 상태(Client State)
언어, UI 테마, 폼 입력, 사이드 바 상태 등 클라이언트가 제어 혹은 소유하는 데이터를 말합니다. 때문에 동기적인 상태를 가지며, 클라이언트 상태는 다시 범위 측면 혹은 역할 측면으로 구분할 수 있습니다.
👆 맨 위로 올라가기