ref
를 사용 할 때에는 useRef
라는 Hook 함수를 사용리액트 컴포넌트에서 id사용을 권장하지 않는 이유
사용법
import React, { useState, useRef } from 'react'; // useRef훅을 가져온다
const nameInput = useRef(); // useRef를 실행해 Ref객체를 만든다
<input
name="name"
placeholder="이름"
onChange={onChange}
value={name}
ref={nameInput} // 접근하려는 DOM에 Ref객체를 ref값으로 설정해준다
/>
const onReset = () => {
setInputs({
name: '',
nickname: ''
});
nameInput.current.focus(); // Ref객체 안의 current값은 등록한 엘리먼트(input)를 가리킴
};
import React, { useState, useRef } from 'react';
function InputSample() {
const [inputs, setInputs] = useState({
name: '',
nickname: ''
});
const nameInput = useRef();
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: ''
});
nameInput.current.focus();
};
return (
<div>
<input
name="name"
placeholder="이름"
onChange={onChange}
value={name}
ref={nameInput}
/>
<input
name="nickname"
placeholder="닉네임"
onChange={onChange}
value={nickname}
/>
<button onClick={onReset}>초기화</button>
<div>
<b>값: </b>
{name} ({nickname})
</div>
</div>
);
}
export default InputSample;
컴포넌트 로컬 변수로 사용해야할 때 useRef를 사용할 수 있음
로컬 변수는 렌더링과 상관없이 바뀔 수 있는 값
값이 바뀌어도 리렌더링 되지 않고, 리렌더링이 되더라도 최신 값을 유지한다
setTimeout
, setInterval
을 통해서 만들어진 id
컴포넌트가 리렌더링 되어도 재선언되는게 아니라 기존에 있던 값을 유지한다
const nextId = { current: 4 };
일 때 nextId.current는 함수가 호출될 때마다 4이다.import React, { useRef } from 'react';
import UserList from './UserList';
function App() {
const users = [
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com'
},
{
id: 2,
username: 'tester',
email: 'tester@example.com'
},
{
id: 3,
username: 'liz',
email: 'liz@example.com'
}
];
const nextId = useRef(4);
const onCreate = () => {
// 나중에 구현 할 배열에 항목 추가하는 로직
// ...
nextId.current += 1;
};
return <UserList users={users} />;
}
export default App;
useRef
로 관리하고 있는 변수는 설정 후 바로 조회 할 수 있습니다. 댓글 답변 값이 변경된 시점 후부터 리렌더링 전까지 state 에 접근하여 값 변경이 가능합니다. 즉, setState를 통해 값을 변경하면 리렌더링이 일어나 변경한 값을 화면에서 확인할 수 있지만 리렌더링이 일어나기 전의 시점까지는 값이 바뀌더라도 화면에서 그 값을 바로 확인할 수 없는 것입니다 의문점 state는 값이 바뀌어도 리렌더링 후에 화면에서 확인할 수 있고 useRef는 렌더링 상관없이 바로 화면에서 확인 가능하다는 건가? 정답 setState한 후에 바로 console.log에서 업데이트 된 state값을 확인 할 수 없지만 useRef로 넣은 값은 바로 확인할 수 있다 map()
을 사용map()
- 파라미터로 전달된 함수를 사용해 배열 내 각 요소를 원하는 규칙에 따라 변환 후 그 결과로 새로운 배열을 생성한다key
라는 props 를 설정해야한다return (
<div>
{users.map(user => (
<User user={user} key={user.id} />
))}
</div>
);
key 의 존재유무에 따른 업데이트 방식
key
: 리액트에서 key는 컴포넌트 배열을 렌더링 했을 때 어떤 원소에 변동이 있었는지 알아내려고 사용.
key
설정을 하지 않게된다면 기본적으로 배열의 index
값을 key
로 사용하게되지만
고유 원소에 key
가 있어야만 배열이 업데이트 될 때 효율적으로 렌더링 될 수 있다
key
설정을 하지 않을 때 리스트를 추가, 변경하게되면 모든 요소들에 영향을 끼치지만
key
설정을 하면 수정되지 않는 기존의 값은 그대로 두고 원하는 곳에 내용을 삽입하거나 삭제할 수 있다.
전체 코드
import React from 'react';
function User({ user }) {
return (
<div>
<b>{user.username}</b> <span>({user.email})</span>
</div>
);
}
function UserList() {
const users = [
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com'
},
{
id: 2,
username: 'tester',
email: 'tester@example.com'
},
{
id: 3,
username: 'liz',
email: 'liz@example.com'
}
];
return (
<div>
{users.map(user => (
<User user={user} key={user.id} />
))}
</div>
);
}
export default UserList;
배열 조작 - 이벤트 핸들러 함수에 setter함수 실행
불변성을 지키면서 배열에 새 항목을 추가하는 방법
spread 연산자 사용
const nextId = useRef(4);
const onCreate = () => {
const user = {
id: nextId.current,
username,
email
};
setUsers([...users, user]); // 기존 배열 + 새로운 객체 추가한 새로운 배열 생성
setInputs({ // input 비우기
username: '',
email: ''
});
nextId.current += 1; // 리스트 id 1증가
};
concat 함수 사용
const nextId = useRef(4);
const onCreate = () => {
const user = {
id: nextId.current,
username,
email
};
setUsers(users.concat(user)); // 기존의 배열을 수정하지 않고, 새로운 원소가 추가된 새로운 배열을 만들어줌
setInputs({
username: '',
email: ''
});
nextId.current += 1;
};
filter 배열 내장 함수 사용
배열에서 특정 조건이 만족하는 원소들만 추출하여 새로운 배열을 만들어주어 불변성을 지킬 수 있다
const onRemove = id => { // 삭제하려는 요소의 id가 필요하다
// user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
// = user.id 가 id 인 것을 제거함
setUsers(users.filter(user => user.id !== id));
};
function User({ user, onRemove }) {
return (
<div>
<b>{user.username}</b> <span>({user.email})</span>
<button onClick={() => onRemove(user.id)}>삭제</button>
</div>
);
}
리스트 요소에 active값 추가
const [users, setUsers] = useState([
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com',
active: true
},
조건부 렌더링
리스트를 생성할 때 추가한 active값에 따라 다르게 렌더링할 수 있다
function User({ user, onRemove }) {
return (
<div>
<b
style={{
cursor: 'pointer',
color: user.active ? 'green' : 'black'
}}
>
{user.username}
</b>
<span>({user.email})</span>
<button onClick={() => onRemove(user.id)}>삭제</button>
</div>
);
}
toggle
클릭 된 요소의 id를 파라미터로 이벤트 핸들러 함수에 전달한다
전체 리스트를 돌면서 들어온 id를 비교해서 같을 시, 그 요소의 active값을 반대로 바꿔준다
⇒ 토글된 요소를 찾아 수정한 전체 리스트를 전부 다 다시 렌더링하는 것 (불변성 지키기)
const onToggle = id => {
setUsers(
users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
)
);
};
function User({ user, onRemove, onToggle }) {
return (
<div>
<b
style={{
cursor: 'pointer',
color: user.active ? 'green' : 'black'
}}
onClick={() => onToggle(user.id)}
>
{user.username}
</b>
<span>({user.email})</span>
<button onClick={() => onRemove(user.id)}>삭제</button>
</div>
);
}
import React, { useRef, useState } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';
function App() {
const [inputs, setInputs] = useState({
username: '',
email: ''
});
const { username, email } = inputs;
const onChange = e => {
const { name, value } = e.target;
setInputs({
...inputs,
[name]: value
});
};
const [users, setUsers] = useState([
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com',
active: true
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: false
},
{
id: 3,
username: 'liz',
email: 'liz@example.com',
active: false
}
]);
const nextId = useRef(4);
const onCreate = () => {
const user = {
id: nextId.current,
username,
email
};
setUsers(users.concat(user));
setInputs({
username: '',
email: ''
});
nextId.current += 1;
};
const onRemove = id => {
// user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
// = user.id 가 id 인 것을 제거함
setUsers(users.filter(user => user.id !== id));
};
const onToggle = id => {
setUsers(
users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
)
);
};
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} onRemove={onRemove} onToggle={onToggle} />
</>
);
}
export default App;
import React from 'react';
function User({ user, onRemove, onToggle }) {
return (
<div>
<b
style={{
cursor: 'pointer',
color: user.active ? 'green' : 'black'
}}
onClick={() => onToggle(user.id)}
>
{user.username}
</b>
<span>({user.email})</span>
<button onClick={() => onRemove(user.id)}>삭제</button>
</div>
);
}
function UserList({ users, onRemove, onToggle }) {
return (
<div>
{users.map(user => (
<User
user={user}
key={user.id}
onRemove={onRemove}
onToggle={onToggle}
/>
))}
</div>
);
}
export default UserList;
deps
)에 따라 실행되는 조건이 달라짐→ 두번째 파라미터로 빈 배열을 넣어준다
useEffect(() => {
console.log('마운트 될 때만 실행');
}, []);
마운트 시에 주로 하는 작업들
props
로 받은 값을 컴포넌트의 로컬 상태로 설정→ 두번 째 파라미터에 검사하고 싶은 값(state or props)을 넣어준다
useEffect(() => {
console.log(name);
}, [name]);
useEffect
안에서 사용하는 상태나, props 가 있다면useEffect
의 deps
에 넣어주는 게 규칙이다
cleanup
함수라고 부른다cleanup
함수를 반환해주어야 한다useEffect(() => {
return () => {
console.log('컴포넌트가 화면에서 사라짐');
};
}, []);
useEffect(() => {
console.log('user 값이 설정됨');
console.log(user);
return () => {
console.log('user 가 바뀌기 전..');
console.log(user);
};
}, [user]);
언마운트 시에 하는 작업들
deps
파라미터를 생략한다면, 컴포넌트가 리렌더링 될 때마다 호출된다
useEffect(() => {
console.log(user);
});
useMemo
라는 Hook 을 사용하여 결과값을 재사용할 수 있다useMemo를 사용하면 의존 상태가 변할 때만 연산되게 할 수 있다
(다른 state나 props변화, 부모 렌더링 경우를 제외할 수 있다)
// 첫번째 인자로 연산할 함수를 실행하는 콜백함수, 두번째 인자로 의존하는 상태를 배열 안에 넣어준다
const count = useMemo(() => countActiveUsers(users), [users]);
import React, { useRef, useState, useMemo } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
function App() {
const [inputs, setInputs] = useState({
username: '',
email: ''
});
const { username, email } = inputs;
const onChange = e => {
const { name, value } = e.target;
setInputs({
...inputs,
[name]: value
});
};
const [users, setUsers] = useState([
{
id: 1,
username: 'velopert',
email: 'public.velopert@gmail.com',
active: true
},
{
id: 2,
username: 'tester',
email: 'tester@example.com',
active: false
},
{
id: 3,
username: 'liz',
email: 'liz@example.com',
active: false
}
]);
const nextId = useRef(4);
const onCreate = () => {
const user = {
id: nextId.current,
username,
email
};
setUsers(users.concat(user));
setInputs({
username: '',
email: ''
});
nextId.current += 1;
};
const onRemove = id => {
// user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
// = user.id 가 id 인 것을 제거함
setUsers(users.filter(user => user.id !== id));
};
const onToggle = id => {
setUsers(
users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
)
);
};
const count = useMemo(() => countActiveUsers(users), [users]);
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} onRemove={onRemove} onToggle={onToggle} />
<div>활성사용자 수 : {count}</div>
</>
);
}
export default App;
useMemo와 비슷, 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용함
deps
배열안에 포함시켜야 된다 - 최신 state를 참조하기 위해사용법
const onChange = useCallback(
e => {
const { name, value } = e.target;
setInputs({
...inputs,
[name]: value
});
},
[inputs]
);
컴포넌트 안에 함수가 선언되어 있다면 컴포넌트가 재실행 될 때마다 선언된 함수의 메모리가 바뀌기 때문에 내부 함수를 props으로 전달할 시 해당 props를 받은 자식도 렌더링된다
→선언문이 다시 실행되는 건 메모리가 바뀌는 것
→자식에게 props으로 넘긴 함수가 매번 다른 메모리를 가지게된다
→props이 바뀌기 때문에 자식 컴포넌트 리렌더링
useCallback으로 감싸주면 함수 재선언을 막아서 바뀐것만 렌더링된다
Props변화 제외, 부모가 바뀔 시 자식까지 렌더링되지 않도록 하는 훅
(컴포넌트 렌더링 최적화 작업을 해주어야만 useMemo, useCallback의 최적화가 유효해짐)
함수 컴포넌트는 자신의 상태가 변경될 때 리렌더링된다.
부모 컴포넌트로 부터 받는 prop이 변경될 때 리렌더링된다.
*3. 부모 컴포넌트의 상태가 변경되면 (props을 전달해주지 않아도)리렌더링된다. (=불합리함)
3번의 경우 React.memo를 써서 막을 수 있음
사용법
// 내보낼 때 감싸면 된다
export default React.memo(CreateUser);
같은 deps를 여러 useCallback에서 사용할 경우 다른 함수들까지 영향받게 되고(재선언) 그 함수를 props으로 받는 자식 컴포넌트까지 불필요한 리렌더링이 발생하게 된다.
ex) userList에서 toggle을 하게되면 users가 변하게 되고, users를 의존하는 onCreate까지 재선언되고 onCreate를 props으로 받는 Input컴포넌트까지 리렌더링된다(userList랑 관련이 없는데도)
이를 방지하는 방법은 deps를 제거하고 state를 함수형으로 업데이트를 하는 것이다.
setUsers
에 등록하는 콜백함수의 파라미터에서 최신 users
를 참조 할 수 있기 때문에 deps
에 users
를 넣지 않아도 된다
→ state를 의존하지 않아도 함수가 실행되는 그때 그때마다 최신의 state값을 받아와서 사용할 수 있기 때문
= 함수가 처음 한 번만 선되고 그 뒤로는 쭉 재사용된다~~
// 이전
const onToggle = useCallback(
id => {
setUsers(
users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
)
);
},
[users]
);
// 이후 함수형 업데이트
const onToggle = useCallback(id => {
setUsers(users => // 이 부분만 추가해주면 된다
users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
)
);
}, []);
useCallback
, useMemo
, React.memo
는 컴포넌트의 성능을 실제로 개선할수있는 상황에서만 해야 오히려 자원낭비를 안한다
useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트(함수)해 주고 싶을 때 사용하는 Hook
reducer함수는 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수이다.
function reducer(state, action) {
// 새로운 상태를 만드는 로직
// const nextState = ...
return nextState; // 곧 컴포넌트가 지닐 새로운 상태
}
action은 업데이트를 위한 정보를 가지고 있다
// 카운터에 1을 더하는 액션
{
type: 'INCREMENT'
}
// input 값을 바꾸는 액션
{
type: 'CHANGE_INPUT',
key: 'email',
value: 'tester@react.com'
}
// 새 할 일을 등록하는 액션
{
type: 'ADD_TODO',
todo: {
id: 1,
text: 'useReducer 배우기',
done: false,
}
}
import React, { useReducer } from 'react';
function reducer(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>
);
}
export default Counter;
새로운 값을 return할 때에는 state객체 자체를 새로 만들어서 반환해야하기 때문에(불변성) 전개 연산자를 사용한다
function reducer(state, action) {
switch (action.type) {
case 'CHANGE_INPUT':
return {
...state,
inputs: {
...state.inputs,
[action.name]: action.value
}
};
default:
return state;
}
}
useState
, useEffect
, useReducer
, useCallback
등 Hooks 를 사용하여 원하는 기능을 구현해주고, 컴포넌트에서 사용하고 싶은 값들을 반환한다
선언
import { useState, useCallback } from 'react';
function useInputs(initialForm) {
const [form, setForm] = useState(initialForm);
const onChange = useCallback(e => {
const { name, value } = e.target;
setForm(form => ({ ...form, [name]: value }));
}, []);
const reset = useCallback(() => setForm(initialForm), [initialForm]);
return [form, onChange, reset]; // 상태와, 상태를 바꾸는 함수들 반환
}
export default useInputs;
사용
// 초기값을 넣어주고, 사용할 상태와 함수들을 꺼낸다
const [{ username, email }, onChange, reset] = useInputs({
username: '',
email: ''
});