2일차 (1.10~1.21)
10. useRef 로 특정 DOM 선택하기
ref
- HTML의 id처럼 리액트 프로젝트 내부에서 DOM에 이름을 다는 방법
- DOM을 꼭 직접적으로 건드려야 할 때 사용
- 함수형 컴포넌트에서
ref
를 사용 할 때에는useRef
라는 Hook 함수를 사용 - 특정 엘리먼트의 크기, 스크롤바 위치를 구하거나 포커스를 설정할 때 사용할 수 있다
리액트 컴포넌트에서 id사용을 권장하지 않는 이유
- id는 유일해야하는데, 컴포넌트를 재사용하게 되면 중복 id를 가진 dom이 여러 개 생기기 때문
- ref는 전역이 아닌 컴포넌트 내부에서만 작동
사용법
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;
12. useRef 로 컴포넌트 안의 변수 만들기
컴포넌트 로컬 변수로 사용해야할 때 useRef를 사용할 수 있음
로컬 변수는 렌더링과 상관없이 바뀔 수 있는 값
값이 바뀌어도 리렌더링 되지 않고, 리렌더링이 되더라도 최신 값을 유지한다
- 언제 사용?
setTimeout
,setInterval
을 통해서 만들어진id
- 외부 라이브러리를 사용하여 생성된 인스턴스
- scroll 위치
컴포넌트가 리렌더링 되어도 재선언되는게 아니라 기존에 있던 값을 유지한다
- 컴포넌트는 그 컴포넌트의 state나 props가 변경될 때마다 호출된다(re-rendering),
- 함수형 컴포넌트는 일반 자바스크립트 함수와 마찬가지로 호출될 때마다 함수 내부에 정의된 로컬 변수들을 초기화한다
- 예를 들어
const nextId = { current: 4 };
일 때 nextId.current는 함수가 호출될 때마다 4이다. - 반면 useRef로 만들어진 객체는 React가 만든 전역 저장소에 저장되기 때문에 함수를 재 호출하더라도 마지막으로 업데이트한 current 값이 유지된다
import React, { useRef } from 'react';
import UserList from './UserList';
function App() {
const users = [
{
id: 1,
username: 'velopert',
email: '[email protected]'
},
{
id: 2,
username: 'tester',
email: '[email protected]'
},
{
id: 3,
username: 'liz',
email: '[email protected]'
}
];
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로 넣은 값은 바로 확인할 수 있다
11. 배열 렌더링하기
- 자바스크립트 배열의 내장함수
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: '[email protected]' }, { id: 2, username: 'tester', email: '[email protected]' }, { id: 3, username: 'liz', email: '[email protected]' } ]; return ( <div> {users.map(user => ( <User user={user} key={user.id} /> ))} </div> ); } export default UserList;
배열 조작 - 이벤트 핸들러 함수에 setter함수 실행
13. 배열에 항목 추가하기
불변성을 지키면서 배열에 새 항목을 추가하는 방법
-
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; };
14. 배열에 항목 제거하기
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>
);
}
15. 배열 항목 수정하기
리스트 요소에 active값 추가
const [users, setUsers] = useState([
{
id: 1,
username: 'velopert',
email: '[email protected]',
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: '[email protected]', active: true }, { id: 2, username: 'tester', email: '[email protected]', active: false }, { id: 3, username: 'liz', email: '[email protected]', 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;
16. useEffect를 사용하여 마운트/언마운트/업데이트 시 할 작업 설정하기
- 리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook
- 렌더링이 되고난 직후마다 실행
- 두번째 인자(
deps
)에 따라 실행되는 조건이 달라짐
마운트(처음 렌더링) 될 때만 실행하고 싶을 때
→ 두번째 파라미터로 빈 배열을 넣어준다
useEffect(() => {
console.log('마운트 될 때만 실행');
}, []);
마운트 시에 주로 하는 작업들
- 외부 API 요청 (REST API 등)
- 전역 이벤트 설정
props
로 받은 값을 컴포넌트의 로컬 상태로 설정- 라이브러리 사용 (D3, Video.js 등...)
- setInterval 을 통한 반복작업 혹은 setTimeout 을 통한 작업 예약
특정 값이 업데이트 될 때만 실행하고 싶을 때 (+ 처음 렌더링 될 때)
→ 두번 째 파라미터에 검사하고 싶은 값(state or props)을 넣어준다
useEffect(() => {
console.log(name);
}, [name]);
useEffect
안에서 사용하는 상태나, props 가 있다면useEffect
의 deps
에 넣어주는 게 규칙이다
뒷정리하기
- useEffect에서 함수를 반환 할 수 있는데 이를
cleanup
함수라고 부른다 - deps가 비어있는 경우에는 컴포넌트가 사라지기 직전,
- 비어있지 않은 경우에는 업데이트되기 직전(+언마운트시에도)에 어떠한 작업을 수행하고 싶다면
cleanup
함수를 반환해주어야 한다
useEffect(() => {
return () => {
console.log('컴포넌트가 화면에서 사라짐');
};
}, []);
useEffect(() => {
console.log('user 값이 설정됨');
console.log(user);
return () => {
console.log('user 가 바뀌기 전..');
console.log(user);
};
}, [user]);
언마운트 시에 하는 작업들
- setInterval, setTimeout 을 사용하여 등록한 작업들 clear 하기 (clearInterval, clearTimeout)
- 등록한 전역 이벤트 제거
- 라이브러리 인스턴스 제거
deps 파라미터를 생략하기
deps
파라미터를 생략한다면, 컴포넌트가 리렌더링 될 때마다 호출된다
useEffect(() => {
console.log(user);
});
17. useMemo 를 사용하여 연산한 값 재사용하기
- 성능 최적화를 위하여 연산된 값을
useMemo
라는 Hook 을 사용하여 결과값을 재사용할 수 있다 - 렌더링하는 과정에서 특정 값이 바뀌었을 때만 연산을 실행하고,
- 원하는 값이 바뀌지 않았다면 이전에 연산했던 결과를 다시 사용한다
- 함수를 실행 할 때 useMemo함수의 인자로 넣어서 실행한다
useMemo를 사용하면 의존 상태가 변할 때만 연산되게 할 수 있다
(다른 state나 props변화, 부모 렌더링 경우를 제외할 수 있다)
// 첫번째 인자로 연산할 함수를 실행하는 콜백함수, 두번째 인자로 의존하는 상태를 배열 안에 넣어준다
const count = useMemo(() => countActiveUsers(users), [users]);
- 예제 코드 상태가 inputs와 users가 있는데 countActiveUsers함수는 users상태에만 의존한다 inputs가 바뀔 때 리렌더링되어 다시 연산되는 것을 방지하기 위해 useMemo사용
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: '[email protected]', active: true }, { id: 2, username: 'tester', email: '[email protected]', active: false }, { id: 3, username: 'liz', email: '[email protected]', 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;
18. useCallback 을 사용하여 함수 재사용하기
useMemo와 비슷, 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용함
- 컴포넌트 안에 선언된 함수들은 리렌더링 될 때마다 새로 만들어진다
- 한 번 만든 함수를 필요할 때만 새로 만들고 재사용하기 위해 useCallback을 사용한다
- 함수 안에서 사용하는 상태 혹은 props 가 있다면 꼭,
deps
배열안에 포함시켜야 된다 - 최신 state를 참조하기 위해 - 주로 자식에게 전해주는 이벤트 핸들러 함수에 쓰인다
사용법
const onChange = useCallback(
e => {
const { name, value } = e.target;
setInputs({
...inputs,
[name]: value
});
},
[inputs]
);
컴포넌트 안에 함수가 선언되어 있다면 컴포넌트가 재실행 될 때마다 선언된 함수의 메모리가 바뀌기 때문에 내부 함수를 props으로 전달할 시 해당 props를 받은 자식도 렌더링된다
→선언문이 다시 실행되는 건 메모리가 바뀌는 것
→자식에게 props으로 넘긴 함수가 매번 다른 메모리를 가지게된다
→props이 바뀌기 때문에 자식 컴포넌트 리렌더링
useCallback으로 감싸주면 함수 재선언을 막아서 바뀐것만 렌더링된다
19. React.memo 를 사용한 컴포넌트 리렌더링 방지
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
는 컴포넌트의 성능을 실제로 개선할수있는 상황에서만 해야 오히려 자원낭비를 안한다
20. useReducer 를 사용하여 상태 업데이트 로직 분리하기
useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트(함수)해 주고 싶을 때 사용하는 Hook
reducer함수는 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수이다.
function reducer(state, action) {
// 새로운 상태를 만드는 로직
// const nextState = ...
return nextState; // 곧 컴포넌트가 지닐 새로운 상태
}
action은 업데이트를 위한 정보를 가지고 있다
// 카운터에 1을 더하는 액션
{
type: 'INCREMENT'
}
// input 값을 바꾸는 액션
{
type: 'CHANGE_INPUT',
key: 'email',
value: '[email protected]'
}
// 새 할 일을 등록하는 액션
{
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;
}
}
21. 커스텀 Hooks 만들기
useState
, useEffect
, useReducer
, useCallback
등 Hooks 를 사용하여 원하는 기능을 구현해주고, 컴포넌트에서 사용하고 싶은 값들을 반환한다
- 반환 값은 상태와 상태를 바꾸는 로직이 담긴 함수(setState에 원하는 로직을 추가한)이다
- setState를 내가 원하는 방식(변할 값, 변하는 조건)으로 정의해서 반환해 사용하는 것
- 값을 바꿔주는 setState를 기본으로 어떻게 값을 바꿔줄지에 대한 로직을 더 추가하는 것
선언
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: ''
});
Author And Source
이 문제에 관하여(2일차 (1.10~1.21)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@yooon26/2일차-1.101.21저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)