Context API 를 사용하면, 프로젝트 안에서 전역적으로 사용 할 수 있는 값을 관리 할 수 있다.
이 값은 함수일 수도, 상태일 수도, 어떤 외부 라이브러리 인스턴스일수도, DOM 일 수도 있다.
// App.jsx (데이터를 넣는 곳)
export const UserDispatch = React.createContext(null); // 초기값을 넣어 새로운 context를 생성
// App.jsx (데이터를 넣는 곳)
<UserDispatch.Provider value={dispatch}> // value에 전달할 값을 넣어준다
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} />
<div>활성사용자 수 : {count}</div>
</UserDispatch.Provider>
// UserList.jsx에서 User컴포넌트(데이터를 받는 곳)
import React, { useContext } from 'react'; // 훅과
import { UserDispatch } from './App'; // 만든 Context를 import
// 컴포넌트 안에서 정의
const dispatch = useContext(UserDispatch); // value로 전달한 값이 할당된다
onClick={() => {
dispatch({ type: 'TOGGLE_USER', id: user.id });
}}
리액트에서 배열이나 객체를 업데이트 해야 할 때에는 직접 수정 하면 안되고
불변성을 지켜주면서 업데이트를 해주어야 한다
// 객체 - 전개 연산자 사용
const object = {
a: 1,
b: 2
};
const nextObject = {
...object,
b: 3
};
// 배열 - concat, filter, map사용
const todos = [
{
id: 1,
text: '할 일 #1',
done: true
},
{
id: 2
text: '할 일 #2',
done: false
}
];
const inserted = todos.concat({
id: 3,
text: '할 일 #3',
done: false
});
const filtered = todos.filter(todo => todo.id !== 2);
const toggled = todos.map(
todo => todo.id === 2
? {
...todo,
done: !todo.done,
}
: todo
);
불변성을 지키는 코드는 코드의 구조가 좀 복잡해지면 코드를 봤을 때 한 눈에 들어오질 않게된다.
Immer 를 사용하면 우리가 상태를 업데이트 할 때, 불변성을 신경쓰지 않으면서 업데이트를 해주면 Immer 가 불변성 관리를 대신 해준다.
사용법
$ yarn add immer // immer설치
import produce from 'immer'; //produce(함수)란 이름으로 불러온다
const state = {
number: 1,
dontChangeMe: 2
};
// 첫번째 파라미터 - 수정하고 싶은 상태
// 두번째 파라미터 - 어떻게 업데이트하고 싶을지 정의하는 함수
const nextState = produce(state, draft => {
draft.number += 1; // 불변성에 대해서 신경쓰지 않고 그냥 업데이트 해주면된다
});
console.log(nextState);
// { number: 2, dontChangeMe: 2 }
// immer를 사용한다면 push splice등의 함수를 사용해도 불변성을 지킬 수 있다
function reducer(state, action) {
switch (action.type) {
case 'CREATE_USER':
return produce(state, draft => {
draft.users.push(action.user);
});
case 'TOGGLE_USER':
return produce(state, draft => {
const user = draft.users.find(user => user.id === action.id);
user.active = !user.active;
});
case 'REMOVE_USER':
return produce(state, draft => {
const index = draft.users.findIndex(user => user.id === action.id);
draft.users.splice(index, 1);
});
default:
return state;
}
}
*수정할 상태가 객체에 깊은 곳에 있지않고 간결하다면 immer보다 concat
과 filter
를 사용하는것이 더 코드가 짧고 편하다. = 상황에 따라 잘 선택하여 사용해야 함
produce
함수에 두개의 파라미터를 넣게 된다면, 첫번째 파라미터에 넣은 상태의 불변성을 유지한 새로운 상태를 반환한다. 하지만
첫번째 파라미터를 생략하고 바로 업데이트 함수를 넣어주게 된다면, 반환 값은 새로운 상태가 아닌
상태를 업데이트 해주는 함수가 된다.
// 함수로 따로 빼고 후에 인자를 넣어서 실행하는 방식으로 사용 가능
const updater = produce(draft => {
draft.done = !draft.done;
});
const nextTodo = updater(todo);
결국 produce
가 반환하는 것이 업데이트 함수가 되기 때문에 useState
의 업데이트 함수 빙식을 사용 할 떄 다음과 같이 구현할 수 있게 된다
const [todo, setTodo] = useState({
text: 'Hello',
done: false
});
const onClick = useCallback(() => {
setTodo(
produce(draft => {
draft.done = !draft.done;
})
);
}, []);
render()
메서드가 꼭 있어야 하고 이 안에서 JSX를 반환한다props
를 조회 해야 할 때에는 this.props
를 조회한다static
키워드로 컴포넌트 안에 선언해줄 수 있다import React, { Component } from 'react';
class Hello extends Component {
static defaultProps = {
name: '이름없음'
};
render() {
const { color, name, isSpecial } = this.props;
return (
<div style={{ color }}>
{isSpecial && <b>*</b>}
안녕하세요 {name}
</div>
);
}
}
export default Hello;
기능을 구현할 땐 커스텀 메서드를 선언한다(클래스 내부에 종속된 함수를 "메서드" 라고한다)
화살표 함수로 구현해야 this binding이슈를 피할 수 있다
import React, { Component } from 'react';
class Counter extends Component {
handleIncrease = () => {
console.log('increase');
console.log(this);
};
handleDecrease = () => {
console.log('decrease');
};
render() {
return (
<div>
<h1>0</h1>
<button onClick={this.handleIncrease}>+1</button>
<button onClick={this.handleDecrease}>-1</button>
</div>
);
}
}
export default Counter;
상태를 선언할 때에는 state
라는 것을 사용한다
constructor
내부에서 this.state
를 설정해준다state
는 무조건 객체 형태여야 한다render
메서드에서 state
를 조회하려면 this.state
를 조회하면 된다this.setState
함수 사용 - 상태 값이 참조형일 경우 불변성을 유지하며 업데이트해야함import React, { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
};
}
// class-properties 문법이 적용되어 있는 경우(CRA)
state = {
counter: 0
};
handleIncrease = () => {
this.setState({
counter: this.state.counter + 1
});
};
handleDecrease = () => {
this.setState({
counter: this.state.counter - 1
});
};
render() {
return (
<div>
<h1>{this.state.counter}</h1>
<button onClick={this.handleIncrease}>+1</button>
<button onClick={this.handleDecrease}>-1</button>
</div>
);
}
}
export default Counter;
*함수형 업데이트를 하면 상태 변화가 바로바로 적용된다
보통 한 함수에서 setState
를 여러번에 걸쳐서 해야 되는 경우에 사용하면 유용하다
// 연속 사용 시 업데이트 바로 안됨
handleIncrease = () => {
this.setState({
counter: this.state.counter + 1
});
this.setState({
counter: this.state.counter + 1
});
};
// 업데이트 바로바로 됨
handleIncrease = () => {
this.setState(state => ({
counter: state.counter + 1
}));
this.setState(state => ({
counter: state.counter + 1
}));
};
마운트
props
로 받아온 것을 state
에 넣어주고 싶을 때 사용업데이트
언마운트
생명주기 메서드 중 하나로 리액트 애플리케이션에서 발생하는 에러를 처리하는 메서드
// early return 해주기
// 데이터가 없으면 null 을 보여주거나,
// <div>로딩중</div>과 같은 결과물을 렌더링하면 된다
if (!user) {
return null;
}
// 디폴트 값을 설정해주기
Users.defaultProps = {
onToggle: () => {
console.warn('onToggle is missing!');
}
};
사전에 예외처리를 하지 않은 에러가 발생 했을 때 사용자에게 에러가 발생했다고 알려주는 화면을 보여주기
ErrorBoundary 컴포넌트 만들기
첫번째 파라미터는 에러의 내용, 두번째 파라미터에서는 에러가 발생한 위치를 알려준다
import React, { Component } from 'react';
class ErrorBoundary extends Component {
state = {
error: false
};
componentDidCatch(error, info) {
console.log('에러가 발생했습니다.');
console.log({
error,
info
});
this.setState({
error: true // 현재 컴포넌트 상태 error 를 true로 설정
});
}
render() {
if (this.state.error) {
return <h1>에러 발생!</h1>; // error 값이 true 라면 에러가 발생했다는 문구를 렌더링
}
return this.props.children; // false라면 children을 렌더링하도록 처리
}
}
export default ErrorBoundary;
렌더링하려면 컴포넌트에 감싸서 사용
import React from 'react';
import User from './User';
import ErrorBoundary from './ErrorBoundary';
function App() {
const user = {
id: 1,
username: 'velopert'
};
return (
<ErrorBoundary>
<User />
</ErrorBoundary>
);
}
export default App;
${}안에서 화살표 함수를 사용해 props를 인자로 받고 원하는 값을 return 해줄 수 있다
import React from 'react';
import styled from 'styled-components';
const Circle = styled.div`
width: 5rem;
height: 5rem;
background: ${props => props.color || 'black'};
border-radius: 50%;
`;
function App() {
return <Circle color="blue" />;
}
export default App;
속성 값이 아닌, 여러줄의 코드를 조건부로 반환하고 싶다면 css를 불러와 사용할 수 있다
import React from 'react';
import styled, { css } from 'styled-components';
const Circle = styled.div`
width: 5rem;
height: 5rem;
background: ${props => props.color || 'black'};
border-radius: 50%;
${props =>
props.huge &&
css`
width: 10rem;
height: 10rem;
`}
`;
function App() {
return <Circle color="red" huge />;
}
export default App;
$ yarn add polished
import { darken, lighten } from 'polished';
&:hover {
background: ${lighten(0.1, '#228be6')};
}
ThemeProvider 라는 기능을 사용하여 styled-components 로 만드는 모든 컴포넌트에서 조회하여
사용 할 수 있는 전역적인 값을 설정할 수 있다
App.js - 값 넘겨주기
import React from 'react';
import styled, { ThemeProvider } from 'styled-components'; // ThemeProvider 받아오기
import Button from './components/Button';
const AppBlock = styled.div`
width: 512px;
margin: 0 auto;
margin-top: 4rem;
border: 1px solid black;
padding: 1rem;
`;
function App() {
return (
<ThemeProvider // 컴포넌트 가장 바깥에 감싸준다
theme={{ //theme속성에 원하는 값을 객체로 넘겨준다
palette: {
blue: '#228be6',
gray: '#495057',
pink: '#f06595'
}
}}
>
<AppBlock>
<Button>BUTTON</Button>
</AppBlock>
</ThemeProvider>
);
}
export default App;
Buttin.js - 값 props으로 받아서 사용하기
/* 색상 */
${props => {
const selected = props.theme.palette.blue;
return css`
background: ${selected};
&:hover {
background: ${lighten(0.1, selected)};
}
&:active {
background: ${darken(0.1, selected)};
}
`;
}}
Button 컴포넌트가 color
props 를 를 통하여 받아오게 될 색상을 사용하도록 할 시
/* 색상 */
`${({ theme, color }) => { // props객체를 구조분해할당
const selected = theme.palette[color];
return css`
background: ${selected};
&:hover {
background: ${lighten(0.1, selected)};
}
&:active {
background: ${darken(0.1, selected)};
}
`;
}}`
function Button({ children, color, ...rest }) {
return <StyledButton color={color} {...rest}>{children}</StyledButton>;
}
Button.defaultProps = {
color: 'blue'
};
유지보수를 할 때 더 편해질 수 있다
import React from 'react';
import styled, { css } from 'styled-components';
import { darken, lighten } from 'polished';
const colorStyles = css`
${({ theme, color }) => {
const selected = theme.palette[color];
return css`
background: ${selected};
&:hover {
background: ${lighten(0.1, selected)};
}
&:active {
background: ${darken(0.1, selected)};
}
`;
}}
`;
const sizeStyles = css`
${props =>
props.size === 'large' &&
css`
height: 3rem;
font-size: 1.25rem;
`}
${props =>
props.size === 'medium' &&
css`
height: 2.25rem;
font-size: 1rem;
`}
${props =>
props.size === 'small' &&
css`
height: 1.75rem;
font-size: 0.875rem;
`}
`;
const StyledButton = styled.button`
/* 공통 스타일 */
display: inline-flex;
outline: none;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
cursor: pointer;
padding-left: 1rem;
padding-right: 1rem;
/* 크기 */
${sizeStyles}
/* 색상 */
${colorStyles}
/* 기타 */
& + & {
margin-left: 1rem;
}
`;
function Button({ children, color, size, ...rest }) {
return (
<StyledButton color={color} size={size} {...rest}>
{children}
</StyledButton>
);
}
Button.defaultProps = {
color: 'blue'
};
export default Button;
const ShortMarginButton = styled(Button)`
& + & {
margin-left: 0.5rem;
}
`;