January 28, 2021
create-react-app
이라는 툴을 제공하여 직접 구축해야 하는 번거로움을 줄여주고 있다.$ npx create-react-app {PROJECT NAME}
package.json
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
start
HTTPS=true npm start
명령어 입력하면 된다.build
npx serve -s build
test
npm test
: 테스트 실행 명령어App.test.js
파일이 있는데 이 파일 명을 App.spec.js
로 변경해도 테스트가 잘 된다.__tests__
라는 폴더를 만들면 이 폴더 밑에 있는 모든 파일이 테스트 파일이 된다..test.js
라는 이름으로 관리하는 게 좋다고 생각하는데, 그 이유는 테스트 하려는 파일이랑 붙어 있으면 여러모로 관리하기가 편리하기 때문.eject
css
js
media
이미지 파일들이 있다. 이미지 파일은 한 가지 특이한 점이 있는데 이미지 파일의 크기에 따라서 동작이 조금 다르다.
padStart
를 사용하고 싶다고 할 때 오래된 브라우저에서는 지원을 하지 않을 수 있다.
브라우저별 지원 여부는 Can I use 에서 확인할 수 있다.import 'core-js/features/string/pad-start';
process.env.{변수 이름}
이런식으로 사용할 수 있다.CRA에서는 기본적으로 NODE_ENV
라는 환경 변수를 갖고 있다. process.env.NODE_ENV
development
test
production
환경 변수가 많아지면 .env 파일이라는 것으로 관리하는 것이 좋다.
root 경로에 .env.development 파일 생성
REACT_APP_API_URL=api.myapp.com
REACT_APP_TEMP1=temp_dev1
root 경로에 .env.production 파일 생성
REACT_APP_API_URL=prod.myapp.com
REACT_APP_TEMP1=temp_prod1
REACT_APP_
로 시작되어야 한다.import 할 때는 객체 형식으로 내보내기 때문에 객체 형식으로 받아서 클래스명을 속성 이름으로 입력해준다.
import Style from './Button.module.css';
<button className={`${Style.button} ${Style.big}`)>큰 버튼</button>
classnames
라는 모듈을 이용하면 더 간편하게 입력할 수 있다. (npm i classnames
로 설치)
<button className={cn(Style.Button, Style.big)}>버튼</button>
<button
className={cn(Style.Button, {
[Style.big]: isBig,
[Style.small]: !isBig,
})}
>
{isBig ? '큰 버튼' : '작은 버튼'}
</button>
npm i node-sass
로 모듈 설치)비교적 최근에 많이 사용하고 있는 방법
styled-components
를 사용하는 방법을 알아보자.모듈 설치
$ npm i styled-components
Example 1
import React from 'react';
import styled from 'styled-components';
const BoxCommon = styled.div`
height: 50px;
backgrouhnd-color: #aaaaaa;
`;
const BoxBig = styled(BoxCommon)`
width: 200px;
`
const BoxSmall = styled(BoxCommon)`
width: 100px;
`
export default function Box({ size }) {
if (size === 'big') {
return <BoxBig>큰 박스</BoxBig>;
} else {
return <BoxSmall>작은 박스</BoxSmall>;
}
}
height: 50px; backgrouhnd-color: #aaaaaa;
를 매개변수로 받아서 실행하는 함수가 있다고 생각하면 된다.BoxCommon
은 div
로 이루어진 컴포넌트를 생성한 것이고 이것을 확장해서 또 다른 컴포넌트인 BoxBig
과 BoxSmall
을 만들고 있다.Example 2
...
const BoxCommon = style.button`
width: ${props => (props.isBig ? 100 : 50)}px;
height: 30px;
background-color: yellow;
`;
export default function Box({ size }) {
const isBig = size === 'big';
const label = isBig ? '큰 버튼' : '작은 버튼';
return <BoxCommon isBig={isBig}>{label}</BoxCommon>;
}
isBig
이라는 속성값을 넣어주면 BoxCommon 컴포넌트에서 그 속성값을 받아서 동적으로 처리할 수 있다.멀티 페이지 애플리케이션(MPA)
SPA
위 조건을 만족시켜주는 브라우저 API
example
import React from 'react';
import { BrowserRouter, Router, Link } from 'react-router-dom';
...
export default function App() {
return (
<BrowserRouter>
<div>
<Link to="/">홈</Link>
<Link to="/photo">사진</Link>
<Link to="/rooms">방 소개</Link>
<Route exact path="/" component={Home} />
<Route path="/photo" component={Photo} />
<Route path="/rooms" component={Rooms} />
</div>
);
}
BrowserRouter
Link
to
에 해당하는 경로로 이동시켜준다Route
path
값에 따라서 어떤 Component
를 렌더링할지 결정을 해준다.Route 컴포넌트로 렌더링을 하면 해당 컴포넌트의 속성 값으로 match
라는 속성 값을 넣어준다. 이 match
안에는 url
이라는 속성이 있는데, 이 속성 값이 의미하는 것은 Rooms 컴포넌트가 렌더링될 당시에 매치됐던 그 url의 일부를 의미한다.
...
export default function Rooms({ match }) {
return (
<div>
...
<Link to={`${match.url}/blueRoom`}>파란 방으로 이동하기</Link>
<Link to={`${match.url}/greenRoom`}>초록 방으로 이동하기</Link>
...
</div>
);
}
리액트 컴포넌트에서는 UI 데이터를 속성값이나 상태값으로 관리를 해야 한다.
import React from 'react';
let color = 'red';
export default function App() {
function onClick() {
color = 'blue';
}
return (
<button style={{ backgroundColor: color }} onClick={onClick}>
좋아요
</button>
)
}
onClick
함수가 실행이 되어도 버튼 색이 파란색으로 변경되지 않는다.color
변수값을 변경은 했지만 리액트가 이 값이 변경됐다는 사실을 모르기 때문이다.import React from 'react';
export default function App() {
const [color, setColor] = useState('red');
function onClick() {
setColor('blue');
}
return (
<button style={{ backgroundColor: color }} onClick={onClick}>
좋아요
</button>
)
}
useState
함수를 호출하면 컴포넌트에 상태값을 추가할 수 있다.useState
의 매개변수인 red
는 상태값의 초기값을 의미한다.useState
는 배열을 반환한다. 배열의 첫 번째 아이템은 상태값이고 두 번째 아이템은 상태값 변경 함수이다.const [color, setColor] = useState('red');
여기서 배열 비구조화 문법이 사용되었다.setColor
를 호출해서 상태값인 color
가 변경되면 리액트는 자동으로 ui를 변경해준다.자식 컴포넌트
import React from 'react';
export default function Title(props) {
return <p>{props.title}</p>
}
// 위의 코드에서 객체의 비구조화 문법을 이용하면 아래와 같이 작성할 수 있다.
// export default function Title({ title }) {
// return <p>{title}</p>
// }
props
: 부모 컴포넌트로부터 전달 받은 속성값.props.{속성명}
을 입력하지 않고 바로 {속성명}
으로 사용할 수 있기 때문에 좀 더 간편하게 작성할 수 있다.부모 컴포넌트
import React, { useState } from 'react';
import Title from './Title';
export default function Counter() {
const [count, setCount] = useState(0);
function onClick() {
setCount(count + 1);
}
return (
<div>
<Title title={`현재 카운트: ${count}`} />
<button onClick={onClick}>증가</button>
</div>
);
}
title
이라는 속성값을 내려주고 있다.count
라는 상태값을 기반으로 title
값을 계산하고 있는데 count
값이 변경되면 Counter 컴포넌트는 다시 렌더링이 될거고 Title 컴포넌트도 다시 렌더링 된다. 이 때 새로 생성된 속성값을 받는다.속성값이 변경될 때만 이 컴포넌트가 다시 렌더링 되게 하려면 React.memo
를 사용할 수 있다.
import React from 'react';
function Title({ title }) {
return <p>{title}</p>
}
export default React.memo(Title);
불변 변수란? 변수의 값을 바꿀 수 없는 것
상태값을 불변 변수로 관리하지 않은 코드 Example
import React, { useState } from 'react';
import Title from './Title';
export default function Counter() {
const [count, setCount] = useState({ value: 0 });
function onClick() {
count.value += 1;
setCount(count);
}
return (
<div>
<Title title={`현재 카운트: ${count.value}`} />
<button onClick={onClick}>증가</button>
</div>
);
}
상태값을 불변 변수로 관리한 코드 Example
import React, { useState } from 'react';
import Title from './Title';
export default function Counter() {
const [count, setCount] = useState({ value: 0 });
function onClick() {
setCount({ ...count, value: count.value + 1 });
}
return (
<div>
<Title title={`현재 카운트: ${count.value}`} />
<button onClick={onClick}>증가</button>
</div>
);
}
컴포넌트에서 반환할 수 있는 값은 어떤 것들이 있는지 알아보자.
export default function App() {
return <div>안녕하세요</div>;
}
export default function App() {
return <Counter />;
}
export default function App() {
return '안녕';
}
export default function App() {
return [<p key={1}>world</p>, <p key={2}>hello</p>];
}
key
를 가지고 있어야 한다.key
는 렌더링을 효율적으로 하기 위해서 필요하다. 리액트가 이 값을 이용해서 가상돔에서의 연산을 효율적으로 할 수가 있다.export default function App() {
return (
<React.Fragment>
<p>Hello</p>
<p>World</p>
</React.Fragment>
);
}
key
역할을 하기 때문에 key
를 입력하지 않아도 오류가 나지 않는다.div
태그로 감싸 줬다. 이 경우에는 원치 않는데 이렇게 div
요소가 추가되는 단점이 있었다.<></>
export default function App() {
return (
<div>
{null}
{false}
{true}
</div>
);
}
export default function App() {
return (
<div>
{count.value > 0 && <Title title={`현재 카운트: ${count.value}`} />
</div>
);
}
예를 들어서 index.html이 아래와 같이 존재.
...
<body>
<div id="root"></div>
<div id="something"></div>
</body>
...
App.js
import React from 'react';
import ReactDOM from 'react-dom';
import Counter from './Counter';
export default function App() {
return (
<>
<p>안녕</p>
<Counter />
{ReactDOM.createPortal(
<div>
<p>hello</p>
<p>world</p>
</div>,
document.getElementById('something'),
)}
</>
);
}
something
요소 밑에 렌더링이 잘 된다.example 1
const element = {
<a key="key1" style={{ width: 100 }} href="https://google.com">
Click here
</a>
}
console.log(element);
// log 출력 결과
// {
// type: 'a',
// key: 'key1',
// ref: null,
// props: {
// href: 'https://google.com',
// style: {
// width: 100,
// },
// children: 'Click here',
// },
// ...
// };
example 2
function Title({ title, color }) {
return <p style={{ color }}>{title}</p>;
}
const element = <Title title="hello" color="blue" />;
console.log(element);
// log 출력 결과
// {
// type: Title,
// props: { title: 'hello', color: 'blue' },
// ...
// }
type
을 보면 컴포넌트 함수가 입력이 되어 있다. 이 함수를 이용해서 리액트는 렌더링을 위한 충분한 정보를 얻을 수가 있다.<p style={{ color }}>{title}</p>;
이러한 값을 얻어 갈 수가 있다.변경된 부분만 실제 돔에 반영 된다는 것을 한 번 확인해보자.
Example
import React, { useState, useEffect } from 'react';
export default function App() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
setTimeout(() => {
setSeconds(v => v + 1);
}, 1000);
});
return (
<div>
<h1>hello</h1>
<p>지금까지 {seconds}초가 지났습니다</p>
</div>
);
}
seconds
값이 1초마다 바뀌는데, p
태그 안의 {seconds}
부분만 변경된다.div
태그 key
값에 seconds
를 주게되면 div
돔 요소가 1초마다 삭제되고 다시 추가된다. key
를 변경하면 리액트는 이것을 다른 요소라고 판단을 해서 이전 것을 삭제하고 새로 만들어서 붙인다.돔 요소 말고 컴포넌트의 key
를 변경할 때는 어떻게 될까?
key
를 변경하게 되면 해당 컴포넌트는 삭제 되었다가 추가된다.useState
의 첫 번째 매개변수로 입력된 초기값이 상태값으로 할당이 된다. 즉, 초기화가 된다는 것이다. 따라서 1초에 한 번씩 0
이 새롭게 할당된다.key
를 변경하면 컴포넌트는 Unmount와 Mount를 반복한다.리액트에서 데이터 변경에 의한 화면 업데이트는 랜더 단계와 커밋 단계를 거친다.
하나의 화면을 표현하기 위해서 여러 개의 리액트 요소가 트리 구조로 구성이 된다.
<div>
<p>안녕하세요</p>
<div>
<p>이름: Jess2</p>
<p>나이: 29</p>
</div>
</div>
{
type: 'div',
props: {
children: [
{
type: 'p',
props: {
children: '안녕하세요',
},
},
{
type: 'div',
props: {
children: [
{
type: 'p',
props: {
children: '이름: Jess2',
},
},
{
type: 'p',
props: {
children: '나이: 29',
},
},
],
}
},
],
},
}
클래스형 컴포넌트보다 리액트 훅이 장점이 많고 리액트 팀에서도 훅에 집중을 하고 있다. 그래서 새로 리액트 프로그램을 작성한다면 리액트 훅으로 작성하는 것을 추천한다.
import React, { useState, useEffect } from 'react';
export default function App() {
const [count, setCount] = useState(0);
function onClick() {
setCount(count + 1);
setCount(count + 1);
}
console.log('render called');
return (
<div>
<h2>{count}</h2>
<button onClick={onClick}>증가</button>
</div>
);
}
useState
함수는 초기값을 넣어서 호출한다useState
함수는 배열을 반환하는데 첫번째 아이템은 상태값, 두번째 아이템은 상태값 변경 함수가 반환된다.onClick
함수가 호출되어도 setCount
는 두 번 작성하였지만 count
값은 1
씩만 증가하고 console.log('render called');
로그는 한 번만 출력된다.import React, { useState, useEffect } from 'react';
export default function App() {
const [count, setCount] = useState(0);
function onClick() {
setCount(v => v + 1);
setCount(v => v + 1);
}
console.log('render called');
return (
<div>
<h2>{count}</h2>
<button onClick={onClick}>증가</button>
</div>
);
}
onClick
이벤트 핸들러는 리액트 내부에서 관리되는 리액트 요소에 입력이 되어 있기 때문에 배치로 처리가 된다.부수 효과란 외부의 상태를 변경하는 것을 말한다.
useEffect Example
...
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `업데이트 횟수: ${count}`;
});
return <button onClick={() => setCount(count + 1)}>increase</button>;
}
Example - 사용자 정보를 렌더링 해주는 컴포넌트
import React, { useState, useEffect } from 'react';
export default function Profile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
getUserApi(userId).then(data => setUser(data));
}, [userId]);
return (
<div>
{!user && <p>사용자 정보를 가져오는 중...</p>}
{user && (
<>
<p>{`name is ${user.name}`}</p>
<p>{`age is ${user.age}`}</p>
</>
)}
</div>
);
}
const USER1 = { name: 'mike', age: 23 };
const USER2 = { name: 'jane', age: 31 };
function getUserApi(userId) {
return new Promise(result => {
setTimeout(() => {
result(userId % 2 ? USER1 : USER2);
}, 500);
});
}
userId
를 속성값으로 받아서 getUserApi
라는 api 함수를 호출해서 해당 유저의 정보를 가져온 다음에 user
상태값을 변경해주는 기능이다.userId
가 홀수, 짝수일 때일 때 각각 다른 값을 리턴해주도록 하는 함수를 작성함.userId
를 입력했기 때문에, userId
값이 변경될 때 부수효과 함수가 실행된다.getUserApi
는 외부에 있는 함수이기 때문에 입력하지 않아도 된다.userId
는 속성값이기 때문에 입력을 해줘야 한다.setUser
상태값 변경 함수는 조금 특별한데, 이 함수는 값이 변경되지 않는다는 것이 보장된다. 그래서 상태값 변경함수는 예외적으로 의존성 배열에 입력하지 않아도 된다.Example code
...
export default function WidthPrinter() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize); // 이벤트 리스너 등록
return () => {
window.removeEventListener('resize', onResize); // 이벤트 리스너 해제
};
}, []);
return <div>{`width is ${width}`}</div>;
}
use
로 시작하는 것이 좋다.userId
를 입력하면 user
객체를 가져올 수 있는 커스텀 훅useUser
를 사용하는 쪽에서는 userId
만 입력해주면 user
객체가 나오니까 굉장히 편리하고 직관적이다.userId
가 변경되면 훅 내부에서 자동으로 api를 호출해서 사용자 정보를 가져올거고, useUser
훅 내부 상태값이 변경되면 자동으로 이 컴포넌트도 같이 새로운 유저와 함께 렌더링이 될 것이다.user
데이터를 가져오는 것은 비동기이지만 마치 userId
를 넣으면 user
가 바로 나오는 것 처럼 (마치 동기 프로그래밍 방식 처럼) 간편하게 작성을 할 수가 있다.단, 매번 호출될 필요는 없으니까 useEffect
에 빈 배열을 넣어주어 초기에 한 번만 실행이 되도록 한다.
export default function useMounted() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted;
}
useBlockIfNotLogin()
)useBlockUnsavedChange(description)
)useEffect
를 실행을 하는데 로그인 유저인 경우에만 실행을 하고 싶을 때 (사용법은 useEffect
와 같은데 콜백을 호출하는 시점이 로그인 유저인 경우에만 호출을 해준다) (useEffectIfLoginUser(callback, deps)
)useLocalStorage(key, initalValue) ⇒ [value, setValue]
)아래의 두 가지 규칙을 지켜야 리액트가 각 훅의 상태를 제대로 기억할 수가 있다.
하나의 컴포넌트에서 훅을 호출하는 순서는 항상 같아야 한다.
훅은 함수형 컴포넌트 또는 커스텀 훅 안에서만 호출되어야 한다.
상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달할 때 속성값(Props)을 사용할 수 있다.
가까운 거리에 있는 몇 개의 컴포넌트로 전달할 때는 속성값으로도 충분하지만 많은 수의 하위컴포넌트로 전달할 때는 속성값을 내려주는 코드를 반복적으로 작성해야 한다.
특히 멀리 떨어져 있는 컴포넌트에 데이터를 전달할 때는 중간에 있는 컴포넌트들에도 데이터를 전달하는 코드를 작성해야 한다.
Context API 를 사용하면 좀 더 간편하게 코드를 작성할 수 있다.
import React, { createContext } from 'react';
const UserContext = createContext('unknown'); // 초기값을 넣어서 호출을 해주면 객체가 반환이 된다.
export default function App() {
return (
<div>
<UserContext.Provider value="mike">
<h1>메뉴</h1>
<Profile />
</UserContext.Provider>
</div>
)
}
function Profile() {
return (
<div>
<Greeting />
</div>
)
}
function Greeting() {
return (
<UserContext.Consumer>
{username => <p>{`${username}님 안녕하세요.`}</p>} {/* mike님 안녕하세요. */}
</UserContext.Consumer>
)
}
createContext
를 가져와야 한다.value
를 넣어주면 Consumer에서 그 값을 받아서 처리할 수 있다.unknown
이 사용된다.value
값이 변경되면 하위의 모든 Consumer 컴포넌트는 다시 렌더링이 된다.function Greeting() {
const username = useContext(UserContext);
return <p>{`${username}님 안녕하세요.`}</p>
}
import React, { createContext } from 'react';
const UserContext = createContext({ username: 'unknown', count: 0 });
const SetUserContext = createContext(() => {});
export default function App() {
const [user, setUser] = useState({ username: 'jessie', count: 0 });
return (
<div>
<SetUserContext.Provider value={setUser}>
<UserContext.Provider value={user}>
<Profile />
</UserContext.Provider>
</SetUSerContext.Provider>
</div>
)
}
function Greeting() {
const setUser = useContext(SetUserContext);
const { username, count } = useContext(UserContext);
return (
<>
<p>{`${username}님 안녕하세요.`}</p>
<p>{`카운트 : ${count}`}</p>
<button onClick={() => setUser({ username, count: count + 1 })}>
인사하기
</button>
</>
)
}
username
과 count
를 별도의 상태값으로 관리하고 Provider에 value={{ username, count }}
이런식으로 넣으면 이 컴포넌트가 렌더링이 될 때마다 매번 새로운 객체가 만들어지게 된다. 그래서 value
값이 변경되지 않아도 Consumer는 불필요하게 렌더링이 될 수 있다.username
과 count
속성을 가진 user
라는 객체를 상태값으로 관리하고 value
에는 value={user}
이런 식으로 하나의 객체로 넘기면 매번 새로운 객체가 만들어지지 않는다.리액트로 작업하다보면 실제 돔 요소에 직접 접근해야할 때가 있다.
예를 들어, 돔 요소에 포커스를 주거나 돔 요소 크기나 스크롤 위치를 알아야 하는 경우 등등..
ref
속성값을 이용하면 자식 요소에 직접 접근할 수 있다. 여기에서 자식 요소는 돔 요소나 컴포넌트일 수 있다.
export default function App() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<input type="text" ref={inputRef} />
)
}
ref
속성값은 아래와 같이 일반적인 컴포넌트에도 입력할 수 있다.<Box ref={inputRef} />
current
속성은 해당 클래스의 메서드를 호출할 수 있게 된다.useImperativeHandle
이라는 훅을 사용하면 함수형 컴포넌트에서도 마치 클래스형 컴포넌트의 멤버 변수나 메서드에 접근하는 것처럼 함수형 컴포넌트의 변수나 함수를 외부로 노출시킬 수 있다.주의: 별다른 처리를 하지 않았다면 함수형 컴포넌트에 ref
속성을 입력할 수 없고 아래와 같이 별도의 속성명을 사용하거나 forwardRef 를 사용해야 한다.
별도의 속성명 사용하기
export default function App() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<div>
<InputAndSave inputRef={inputRef} />
...
</div>
)
}
function InputAndSave({ inputRef }) {
return (
<div>
<input type="text" ref={inputRef} />
...
</div>
)
}
forwardRef 사용하기 : 두 번째 매개변수로 ref 속성값을 받을 수 있다.
export default function App() {
const buttonRef = useRef();
return (
<div>
<Button ref={buttonRef} />
...
</div>
)
}
const Button = React.forwardRef(function ({ onClick }, ref) {
return (
<button onClick={onClick} ref={ref}>저장</button>
)
})
export default function App() {
const [text, setText] = useState(INITIAL_TEXT);
const [showText, setShowText] = useState(true);
return (
<div>
{showText && (
<input
type="text"
ref={ref => ref && setText(INITIAL_TEXT)}
value={text}
onChange={e => setText(e.target.value)}
/>
)}
<button onClick={() => setShowText(!showText)}>보이기/가리기</button>
</div>
)
}
const INITIAL_TEXT = 'Hello world!';
ref
속성값에 함수를 입력할 수 있다.null
값이 넘어온다.setText(INITIAL_TEXT)
를 실행하여 text
를 초기값으로 설정한다.showText
를 보였다가 가렸다가를 반복하게 되는데, input
태그는 showText
가 true
일 때만 보여지게 되므로 버튼을 클릭하여 showText
가 true
가 되는 순간 setText(INITIAL_TEXT)
함수가 호출된다.setText
함수가 실행되고 컴포넌트가 다시 렌더링이 된다. 그런데 컴포넌트가 렌더링될 때마다 새로운 함수가 입력되면서 setText(INITIAL_TEXT)
가 실행되어 초기화되기 때문에 text
가 입력하는 텍스트로 제대로 업데이트 되지 않는 문제가 있다.export default function App() {
const [text, setText] = useState(INITIAL_TEXT);
const [showText, setShowText] = useState(true);
const setInitialText = useCallback((ref) => {
ref && setText(INITIAL_TEXT)
}, []);
return (
<div>
{showText && (
<input
type="text"
ref={setInitialText}
value={text}
onChange={e => setText(e.target.value)}
/>
)}
<button onClick={() => setShowText(!showText)}>보이기/가리기</button>
</div>
)
}
const INITIAL_TEXT = 'Hello world!';
setText
함수가 실행되고 컴포넌트가 다시 렌더링 되어도 setText(INITIAL_TEXT)
가 다시 실행되지 않는다. ref
가 새로 생성될 때 setText(INITIAL_TEXT)
가 실행된다.렌더링과 상관 없는 데이터를 저장할 때 useRef가 유용하게 사용될 수 있다.
export default function App() {
const timerIdRef = useRef(-1);
useEffect(() => {
timerIdRef.current = setTimeout(() => {}, 1000);
});
useEffect(() => {
if (timerIdRef.current >= 0) {
clearTimeout(timerIdRef.current);
}
});
}
export default function App() {
const [age, setAge] = useState(20);
const prevAgeRef = useRef(20);
useEffect(() => {
prevAgeRef.current = age;
}, [age]);
const prevAge = prevAgeRef.current;
const text = (age === prevAge) ? ('same') : (age > prevAge ? 'older' : 'younger');
return (
// ...
// 버튼을 누르면 setAge 함수를 호출하여 랜덤값으로 age를 변경한다.
);
}
age
값을 기억하기 위해서 prevAgeRef
를 사용한다.prevAge.current
는 age
가 변경되기 이전의 값을 기억하고 있게 되는 것이다.예시 과정
setAge
호출 -> age
가 초기값 20에서 28로 변경됨 -> 렌더링 -> 이 때 prevAgeRef.current
는 20임 -> 화면에는 age
로 28이 출력되고 prevAge
로 20이 출력됨 -> prevAgeRef.current
에 28을 저장
-> setAge
호출 -> age
가 19로 변경됨 -> 렌더링 -> 이 때 prevAgeRef.current
는 28임 -> 화면에는 age
로 19가 출력되고 prevAge
로 28이 출력됨 -> … 반복메모이제이션 기능이 있어, 계산량이 많은 함수의 반환값을 재활용하는 용도로 사용된다.
export default function App() {
const [v1, setV1] = useState(0);
const [v2, setV2] = useState(0);
const [v3, setV3] = useState(0);
const value = useMemo(() => {
runExpensiveJob(v1, v2); // 계산량이 많은 함수 호출
}, [v1, v2]);
return (
//...
);
}
v1
이나 v2
가 변경되면 runExpensiveJob
함수가 실행되지만 v3
가 변경되면 runExpensiveJob
함수가 실행되지 않는다.useMemo와 마찬가지로 메모이제이션 기능이 있으며, 함수 메모이제이션에 특화된 훅이다.
export default function App() {
const [name, setName] = useState('');
return (
<div>
<p>{`name is ${name}`}</p>
<UserEdit
onSave={() => saveToServer(name)}
setName={setName}
/>
</div>
);
}
const UserEdit = React.memo(function ({ onSave, setName }) {
//...
});
onSave={() => saveToServer(name)}
: 자식 컴포넌트에 함수를 입력해서 속성값으로 전달할 때는 App 컴포넌트가 렌더링될 때마다 새로운 함수가 생성되어 입력이 된다. 이러면 App 컴포넌트가 렌더링될 때마다
매번 onSave
속성값이 변경되기 때문에, 자식 컴포넌트인 UserEdit 컴포넌트 입장에서는 불필요하게 새로운 속성값으로 입력받고 불필요하게 다시 렌더링된다.export default function App() {
const [name, setName] = useState('');
const onSave = useCallback(() => {
saveToServer(name);
}, [name]);
return (
<div>
<p>{`name is ${name}`}</p>
<UserEdit
onSave={onSave}
setName={setName}
/>
</div>
);
}
const UserEdit = React.memo(function ({ onSave, setName }) {
//...
});
name
이 변경될 때만 새로운 함수가 생성된다.useState와 비슷하지만 여러 개의 상태값 관리할 때 사용하면 좋은 훅
export default function App() {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
return (
<div>
<p>{`name is ${state.name}`}</p>
<p>{`age is ${state.age}`}</p>
<input
type='text'
value={state.name}
onChange={e => dispatch({ type: 'setName', name: e.currentTarget.value})}
/>
<input
type='number'
value={state.age}
onChange={e => dispatch({ type: 'setAge', age: e.currentTarget.value})}
/>
</div>
);
}
function reducer(state, action) {
switch(action.type) {
case 'setName':
return { ...state, name: action.name };
case 'setAge':
return { ...state, age: action.age };
default:
return state;
}
}
action
을 보고 상태값을 어떻게 변경할지 판단한다.보통 상위 컴포넌트에서 다수의 상태값을 관리한다. 이 때 자식 컴포넌트에서 발생한 이벤트에서 상위 컴포넌트의 상태값을 변경해야 하는 경우가 많다. 이를 위해서 상위 컴포넌트에서 트리의 깊은 곳까지 이벤트 처리 함수를 전달하기도 하는데 이는 비효율적이다.
useReducer 훅과 Context API를 같이 이용하면 쉽게 전달할 수 있다.
export const ProfileDispatch = React.createContext(null);
export default function App() {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
return (
<div>
<p>{`name is ${state.name}`}</p>
<p>{`age is ${state.age}`}</p>
<ProfileDispatch.Provider value={dispatch}>
<SomeComponent />
</ProfileDispatch.Provider>
</div>
);
}
ProfileDispatch
라는 Context를 만들었다.value
로 useReducer의 dispatch
함수를 내려준다.dispatch
함수를 사용할 수 있다.