21 09 01 - 21 09 03
Velopert 벨로퍼트와 함께하는 모던 리액트 (https://react.vlpt.us/)를 보고 개인적인 복습을 위해 정리한 내용입니다. 원문을 보시는 것을 추천합니다. (1장 리액트 입문 부분)
Virtual DOM : 브라우저에 실제로 보여지는 DOM이 아니라 메모리 상에만 존재하는 가상의 DOM으로,
그냥 자바스크립트 객체이기 때문에 작동 속도가 실제 DOM 보다 훨씬 빠르다.
리액트는 상태(state)가 업데이트 되면 Virtual DOM에 먼저 렌더링한 후,
실제 DOM과 비교해 차이가 있는 부분만 부분적으로 패치한다.
컴포넌트 : 재사용 가능한 UI 조각.
리액트 컴포넌트를 작성할 땐 리액트를 import해야 한다.
import React from 'react';
ReactDOM.render()
: 브라우저에 있는 실제 DOM 내부에 리액트 컴포넌트를 렌더링해주는 메소드.
리액트 Fragment : <></>
, 브라우저 상에 실제 엘리먼트로 나타나지 않음. 리액트 컴포넌트는 반드시 하나의 태그로 감싸져 있어야 하는데, 단순히 감싸기 위한 목적으로 불필요한 div를 사용하는 대신 Fragment를 사용할 수 있다.
인라인 스타일은 객체 형태로 작성해야 하며, 스타일 속성 이름은 카멜케이스(background-color -> backgroundColor
)로 작성해야 한다.
const style = {
backgroundColor: 'black',
fontSize: 24
}
<div style={style}>{name}</div>
css class는 class
대신 className
으로 작성.
JSX 내부 주석은 {/* 이 안에 */}
작성. (중괄호로 감싸야 함)
태그 안에서는 //
주석 사용 가능. (<Hello //주석 />
)
defaultProps
: 컴포넌트에 props를 지정하지 않았을 때 기본적으로 사용할 값 설정. import React from 'react';
function Hello({ color, name }) {
return <div style={{ color }}>안녕하세요 {name}</div>
}
Hello.defaultProps = {
name: '이름없음'
}
props.children
: 하위 컴포넌트를 조회하고 싶을 땐 props.children
을 사용한다. return <div>{props.children}</div>;
주로 삼항 연산자 또는 &&
연산자를 사용해 처리한다.
JSX에서 null
, false
, undefined
를 렌더링하면 아무것도 나타나지 않는다.
props 값 할당을 생략하면 true
가 할당된다. (isSpecial === isSpeicial={true}
)
사용자 인터랙션에 따라 바뀌는 동적인 부분을 구현할 때 state를 사용한다.
원래 클래스형 컴포넌트에서만 상태를 사용할 수 있었지만, 리액트 16.8에서 Hooks이 도입되면서, 함수형 컴포넌트에서도 상태를 관리할 수 있게 되었다.
useState
Hook을 이용해 함수형 컴포넌트에서 state를 관리할 수 있다.
Setter 함수의 사용 :
- 새로운 state 값을 파라미터로 넣어주는 방식
setValue(value + 1);
setValue(prevValue => prevValue + 1);
이벤트 처리 : onClick={onIncrease()}
X, onClick={onIncrease}
OK
(전자는 렌더링되는 시점에서 함수가 호출되어 버림. )
여러 개의 input 태그 상태 관리 :
import React, { useState } from 'react';
function InputSample() {
const [inputs, setInputs] = useState({
name: '',
nickname: ''
});
const { name, nickname } = inputs; // 비구조화 할당을 통해 값 추출
const onChange = (e) => {
const { value, name } = e.target; // 우선 e.target 에서 name 과 value 를 추출
setInputs({
...inputs, // 기존의 input 객체를 복사한 뒤
[name]: value // name 키를 가진 값을 value 로 설정
});
};
const onReset = () => {
setInputs({
name: '',
nickname: '',
})
};
return (
<div>
<input name="name" placeholder="이름" onChange={onChange} value={name} />
<input name="nickname" placeholder="닉네임" onChange={onChange} value={nickname}/>
<button onClick={onReset}>초기화</button>
<div>
<b>값: </b>
{name} ({nickname})
</div>
</div>
);
}
export default InputSample;
e.target.name: e.target.value
)...
) 를 사용해 기존 객체를 복사해 사용한다. //XXX
inputs[name] = value;
//OOO
setInputs({
...inputs,
[name]: value
});
const nameInput = useRef(); //Ref 객체 생성
const onReset = () => {
setInputs({
name: '',
nickname: ''
});
nameInput.current.focus(); //Ref 객체의 current 값으로 접근
};
<input
name="name"
onChange={onChange}
value={name}
ref={nameInput} //ref 지정
/>
컴포넌트 안에서 접근할 수 있는 변수 관리 :
useRef
로 관리하는 변수는, 값이 바뀌어도 컴포넌트가 리렌더링 되지 않는다.
(함수형 컴포넌트 내부에서 useRef
를 사용하지 않고 일반 변수를 선언하면, 컴포넌트가 렌더링될 때마다 값이 초기된다. state를 사용하면, state 값이 설정될 때마다 컴포넌트가 리렌더링된다. )
리액트의 state는 setter를 호출하고 나서 렌더링이 된 이후에 업데이트된 상태를 조회할 수 있는 반면,
useRef
로 관리하는 변수는 설정 후 바로 조회할 수 있다.
useRef()
를 사용할 때 파라미터를 넣으면, 이 값이 Ref 객체 .current
의 기본값이 된다.
.current
를 통해 값을 수정하거나 조회할 수 있다.
const nextId = useRef(4);
const onCreate = () => {
nextId.current += 1;
};
배열을 사용해 컴포넌트를 렌더링하려면, map()
함수를 사용한다.
리액트에서 배열을 렌더링할 때는 key
props를 반드시 설정해야 한다.
key
는 고유한 값으로 설정해야하며 (주로 id
값으로 설정, 중복되는 key
가 있으면 업데이트가 제대로 이루어지지 않는다. ), 만약 key
로 설정할만한 값이 없다면 map()
함수의 두 번째 파라미터인 index
를 key
로 사용할 수 있다.
key
props가 필요한 이유 :
key
가 없다면 배열에 변경 사항이 생겨서 리렌더링을 할 때 비효율적인 방식으로 렌더링을 하게 된다. (ex : a, b, d, e 사이에 c를 추가하는 경우 b와 d 사이에 c를 추가하는 게 아니라
a, b, d->c, e->d, e 와 같은 방식으로 삽입된다. )
key
가 있으면 수정되지 않는 기존의 값은 그대로 두고 원하는 내용만 삽입하거나 삭제할 수 있다.
배열에 항목 추가/제거/수정하기
배열에 변화를 줄 때에도 객체와 마찬가지로 불변성을 지켜야 한다.
push
, splice
, sort
등 기존 배열을 수정하는 메소드는 사용하면 안 된다.
배열에 항목 추가하기
spread ...
연산자 사용 (기존 배열을 펼쳐서 복사해 새로운 배열에 담을 수 있다)
setUsers([...users, user]);
concat
함수 (기존의 배열을 수정하지 않고 새로운 배열 반환)
setUsers(users.concat(user));
배열의 항목 제거하기
filter
함수 (배열에서 특정 조건이 만족하는 원소들만 추출해 새로운 배열을 생성함)setUsers(users.filter(user => user.id !== id));
//id === user.id인 항목을 제거
배열의 항목 수정하기
- 배열을 업데이트할 때도 map()
함수를 사용할 수 있다.
const onToggle = id => {
setUsers(
users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
)
);
};
해당하는 id
의 유저 active 값을 반전시켜준다.
useEffect
를 사용해 컴포넌트 마운트/언마운트/업데이트(특정 props가 바뀔 때) 시 수행할 작업을 설정할 수 있다. deps 배열이 빈 배열인 경우
useEffect
첫 번째 파라미터는 콜백 함수, 두 번째 파라미터는 의존값 배열(deps
)를 넣는다.
deps
배열에 있는 항목의 값이 바뀔 때마다 콜백 함수가 호출된다.
deps
로 빈 배열을 넣으면, 컴포넌트가 마운트될 때만(처음 렌더링될 때) useEffect
에 등록한 함수가 호출된다.
useEffect
는 함수를 반환할 수 있다. 이 때 반환되는 함수는 cleanup
함수라고 부른다.
deps
가 비어있는 경우 컴포넌트가 언마운트될 때 cleanup
함수가 호출된다.
마운트/언마운트 :
props
로 받은 값을 컴포넌트의 로컬 상태로 설정deps 배열에 값이 있는 경우
deps
배열에 값을 넣으면, 해당 값이 바뀔 때에도 useEffect
의 콜백 함수가 호출되며,
값이 바뀌기 직전에 cleanup
함수가 호출된다.
useEffect
안에서 사용하는 상태나 props는 반드시 deps
에 넣어주어야 한다. 그렇지 않으면 useEffect
에 등록한 함수가 실행될 때 가장 최신 값을 참조할 것이라고 보장할 수 없다.
deps 배열을 생략한 경우 (=useEffect
의 두 번째 파라미터 생략)
컴포넌트가 리렌더링될 때마다 콜백 함수가 호출된다.
*리액트 컴포넌트는 기본적으로 부모 컴포넌트가 리렌더링되면 자식 컴포넌트는 바뀐 내용이 없더라도 리렌더링된다.
(실제 DOM에서는 바뀐 내용이 있는 컴포넌트만 업데이트되지만, Virtual DOM에서는 전부 렌더링된다. )
useEffect 완벽 가이드(https://rinae.dev/posts/a-complete-guide-to-useeffect-ko) 읽어보기
const count = countActiveUsers(users);
const count = useMemo(() => countActiveUsers(users), [users]);
첫 번째 파라미터 : 콜백 함수 (연산 함수),
두 번째 파라미터 : deps 배열
deps 배열 안의 내용이 바뀌면, 콜백 함수를 호출해 값을 연산해주고, 내용이 바뀌지 않았다면 이전에 연산한 값을 재사용하게 된다.
useMemo
는 특정 (연산의) 결과 값을 재사용할 때, useCallback
은 특정 함수를 재사용할 때 쓴다.
useCallback
은 useMemo
를 기반으로 만들어졌다. (useMemo
로 대체할 수도 있다. )
컴포넌트 내부에 작성한 함수들은 컴포넌트가 리렌더링될 때마다 새로 선언된다.
함수를 선언하는 데에 많은 비용이 드는 건 아니지만,
나중에 컴포넌트 최적화 작업을 할 때는 함수의 재사용이 필수이기 때문에 useCallback
을 사용해야 한다.
함수 안에서 사용되는 props나 상태는 반드시 deps 배열에 포함시켜야 한다.
함수 안에서 state를 업데이트할 때, 함수형 업데이트를 하면, setState에 등록하는 콜백함수의 파라미터에서 최신 state를 참조할 수 있어 deps에 state를 넣어주지 않아도 된다.
React DevTools > 설정 > Highlight Updates : 리렌더링되는 컴포넌트를 확인할 수 있다.
React.Memo : 컴포넌트의 props가 바뀌지 않았다면, 컴포넌트 리렌더링을 방지해 컴포넌트 성능 최적화를 해주는 함수.
(리렌더링이 필요한 상황에서만 리렌더링하도록 설정할 수 있다. )
사용 방법은,
컴포넌트를 export할 때 export default React.memo(component_name)
와 같이 감싸주기만 하면 된다.
React.memo의 두번째 파라미터로 콜백 함수(propsAreEqual
함수)를 사용해 특정 props만 비교하는 것도 가능하지만, 잘못 사용하면 오히려 의도치 않은 버그가 발생하기 쉽기 때문에 주의해야 한다.
export default React.memo(
UserList,
(prevProps, nextProps) => prevProps.users === nextProps.users
);
useMemo
, useCallback
, React,memo
는 컴포넌트의 성능을 실제로 개선할 수 있는(=실제로 불필요한 렌더링을 방지할 수 있는) 상황에서만 사용하는 것이 좋다. useReducer
를 통해서도 상태를 관리할 수 있다. useReducer
를 사용하면 컴포넌트에서 상태 업데이트 로직을 분리시킬 수 있다. //사용 방법
//const [state, dispatch] = useReducer(reducer, initialState);
function reducer(state, action) {
//state: 현재 상태, action: 업데이트를 위한 정보(주로 type 값을 가진 객체 형태)
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
function Counter() {
const [number, dispatch] = useReducer(reducer, 0);
const onIncrease = () => {
dispatch({ type: 'INCREMENT' });
};
const onDecrease = () => {
dispatch({ type: 'DECREMENT' });
};
return (
<div>
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
}
컴포넌트의 로직을 분리시켜 필요할 때 쉽게 재사용할 수 있다.
만드는 방법 : useState, useEffect, useReducer, useCallback 등 기존 Hooks 를 사용하여 원하는 기능을 구현하고, 컴포넌트에서 사용하고 싶은 값들을 반환한다.
-useInputs 커스텀 Hook 을 useReducer를 사용해서 구현하기 : https://gist.github.com/velopert/e0d5a027f60a7368b2bb6f9277e3f742
Context API를 사용해 프로젝트의 전역 값을 관리할 수 있다. 상태 뿐만 아니라 함수, 외부 라이브러리 인스턴스, DOM 등을 모두 전역으로 관리할 수 있다.
React.createContext()
함수를 사용해 새로운 Context를 생성한다.
파라미터로는 Context의 기본값을 설정할 수 있다.
Context 객체 안에는 Provider라는 컴포넌트가 들어있는데, 이 컴포넌트를 통해 다음과 같이 Context의 값을 정할 수 있다.
export const AuthContext = React.createContext(null);
<AuthContext.Provider value={dispatch}>
...
</AuthContext.Provider>
//dispatch를 Context 값으로 정해주어서 전역에서 dispatch를 사용할 수 있다.
Provider로 감싸진 컴포넌트라면 어디에서든지 Context 값을 조회해서 사용할 수 있다.
Context 사용하기 :
import React, { useContext } from 'react';
import { AuthContext } from './App';
const User = () => {
const dispatch = useContext(AuthContext);
return (
<div onClick={() => {
dispatch({type: "TOGGLE" , id: user.id})}} />
);
};
이렇게 useReducer
와 Context API를 같이 활용하면,
Context API를 사용해 dispatch
를 어디서든지 조회할 수 있어서 코드 구조가 훨씬 깔끔해진다.
리액트에서 배열이나 객체를 업데이트할 때 불변성을 지키려면, 기존 배열/객체를 수정하지 않고 새로운 배열/객체를 생성해줘야 한다.
대부분의 경우에서는 어렵지 않지만, 데이터의 구조가 복잡해지면 불변성을 지키면서 새로운 데이터를 생성해내는 코드가 조금 복잡해질 수 있다.
Immer를 사용하면, 상태를 업데이트할 때 불변성을 신경쓰지 않고 코드를 작성해도 Immer가 불변성 관리를 대신 해준다.
render()
메소드가 꼭 있어야 한다.this.props
로 접근해야 한다. import React, { Component } from 'react';
class Hello extends Component {
render() {
const { color, name, isSpecial } = this.props;
defaultProps
설정은 함수형 컴포넌트와 동일한 방식으로 할 수도 있고,static
키워드와 함께 선언할 수도 있다. class Hello extends Component {
static defaultProps = {
name: '이름없음'
};
...
}
Hello.defaultProps = {
name: '이름없음'
};
class Counter extends Component {
handleIncrease() {
console.log('increase');
}
handleDecrease() {
console.log('decrease');
}
//...
이렇게 클래스 내부에 종속된 함수를 메서드라고 부른다. 클래스에서 커스텀 메서드를 만들 때는 보통 이름을 handle..
로 짓는다.
this.setState
라는 함수를 사용한다.this
는 컴포넌트 인스턴스를 가리키는데, 커스텀 메서드 안에서 this
를 조회해보면undefined
가 나온다.constructor(props) {
super(props);
this.handleIncrease = this.handleIncrease.bind(this);
this.handleDecrease = this.handleDecrease.bind(this);
}
handleIncrease = () => {
console.log('increase');
console.log(this);
};
handleDecrease = () => {
console.log('decrease');
};
화살표 함수는 CRA로 만든 프로젝트에는 적용이 되어 있는 문법(class-properties 문법)이기 때문에 바로 사용할 수 있고,
그렇지 않은 경우에는 추가적인 설정이 필요하다.
constructor
내부에서 this.state
를 설정해주면 된다. constructor(props) {
super(props);
this.state = {
counter: 0
};
}
클래스형 컴포넌트의 state 는 무조건 객체형태여야 한다.
class-properties 문법이 적용되어 있다면(ex: CRA로 만든 프로젝트) constructor
를 사용하지 않고 다음과 같이 state를 설정할 수 있다.
class Counter extends Component {
state = {
counter: 0
};
setState
에서도 useState
와 마찬가지로 함수형 업데이트를 할 수 있다.setState
를 여러 번에 걸쳐 해야 하는 경우에 유용하다.setState
는 단순히 상태를 바꾸는 함수가 아니라, 상태를 바꿔달라고 요청하는 함수이다. 성능 상의 이유로 상태가 바로 업데이트되지 않고 비동기적으로 업데이트되기 때문이다.setState
의 두 번째 파라미터에 콜백함수를 넣어주거나, 함수형 업데이트를 사용해야 한다. LifeCycle Method : 컴포넌트가 브라우저 상에 나타나고, 업데이트되고, 사라질 때 호출되는 메서드들. (+컴포넌트에서 에러가 났을 때 호출되는 메서드도 있음)
라이프싸이클 메서드는 클래스형 컴포넌트에서만 사용할 수 있다.
마운트 :
props
로 받아온 값을 state
에 넣어주고 싶을 때 사용하는 메서드. 업데이트 :
componentDidUpdate
에서 받아와서 사용할 수 있다. (DOM에 변화가 반영되기 직전에 DOM 속성을 확인하고 싶을 때 사용)getSnapshotBeforeUpdate
에서 반환한 값을 조회할 수 있다. setTimeout
등을 clear하거나,function User({ user }) {
if (!user) {
return null;
}
//...
네트워크 요청을 통하여 나중에 데이터를 받아오게 되는 상황에서는
이렇게 데이터가 없으면 null 을 보여주거나, 아니면 <div>로딩중</div>
과 같은 결과물을 렌더링하면 된다.
props가 전달되지 않은 상황에서의 에러를 방지하기 위해 defaultProps
설정을 해줄 수도 있다.
Users.defaultProps = {
onToggle: () => {
console.warn('onToggle is missing!');
}
};
또는 실수로 props 전달을 깜빡했을 때 개발 단계에서 경고를 볼 수 있도록 PropTypes를 사용하거나 TypeScript, Flow를 사용해 관리할 수 있다.
componentDidCatch
메서드를 사용해 사전에 예외처리가 되지 않은 에러가 발생하는 경우를 핸들링할 수 있다. 그러나 componentDidCatch가 실제로 호출되는 일은 서비스에서 없어야 하는 게 맞다.
그럼에도 발견하지 못한 에러가 있다면, componentDidCatch
에서 error
와 info
값을 네트워크를 통해 다른 곳으로 전달해 확인할 수 있다.
이를 위해 따로 서버를 만드는 것은 번거롭기 때문에 Sentry라는 서비스를 프로젝트에 적용하면 실시간으로 에러 정보를 확인할 수 있다.
단, 배포 모드에서는 componentDidCatch
로 잡은 에러는 Sentry에 전달이 되지 않으므로 따로 에러 바운더리로 처리해야 한다.
componentDidCatch(error, info) {
console.log('에러가 발생했습니다.');
console.log({
error,
info
});
this.setState({
error: true
});
if (process.env.NODE_ENV === 'production') {
Sentry.captureException(error, { extra: info });
}
}
process.env.NODE_ENV
값을 조회해 현재 환경(개발 환경/배포 환경)을 확인할 수 있다.
Sentry 연동을 해두면 버그 관리에 매우 도움이 되므로 연동해두는 것을 추천!