immer: 불변성 객체로 인한 리렌더링 이슈 해결
패스워트 컴포넌트 내 useRef 통한 input 태그 타입 변경을 렌더링 없이 적용 한 후 유효성 검증 상태들을 객체로 단일화 하였더니 아래와 같은 문제 발생
import { useRef, useState } from 'react'
import '@/App.css'
function UsernameInput() {
return (
<div>
Username : <input />
</div>
)
}
function PasswordInput() {
const [valid, setValid] = useState({
maximum: false,
minimum: false,
required: false,
})
const reference = useRef(null)
function changeMode(e) {
if (reference.current.type === 'password') {
reference.current.type = 'text'
e.currentTarget.innerText = '🔒 감추기'
} else if (reference.current.type === 'text') {
reference.current.type = 'password'
e.currentTarget.innerText = '🔓 보이기'
}
}
console.log('- 매번 입력할때마다 rerender 발생. 상태를 객체로 단일화하자 useRef 사용 이유를 잃음')
return (
<div>
Password :{' '}
<input
type='password'
ref={reference}
onChange={(e) => {
const input = e.currentTarget.value
setValid({
maximum: input.length <= 10,
minimum: input.length > 5,
required: input.length > 0,
})
}}
/>
<button onClick={changeMode}>🔓 보이기</button>
{valid.maximum || <div style={{ color: 'red' }}>비밀번호는 10글자를 넘을 수 없습니다.</div>}
{valid.minimum || <div style={{ color: 'red' }}>비밀번호는 5글자를 넘어야합니다.</div>}
{valid.required || <div style={{ color: 'red' }}>비밀번호를 입력해주세요.</div>}
</div>
)
}
function App() {
function registration() {}
return (
<section style={{ textAlign: 'start', width: 400 }}>
<UsernameInput />
<PasswordInput />
<button onClick={registration}>회원가입 완료</button>
</section>
)
}
export default App
valid라는 상태를 만들어서 유효성 검증항목을 객체로 감싸서 상태를 변경하고 있다.
[문제]
input값이 바뀔때마다 새로운 객체를 생성해서 setValid해주므로 input값 변경할 때마다 리렌더링 발생
- ref 사용한 의미가 없다.
[해결]
immer를 사용해서 불변성 객체로 인한 리렌더링 이슈를 해결할 수 있다.
import { useRef, useState } from 'react'
import '@/App.css'
import { produce } from 'immer'
function UsernameInput() {
return (
<div>
Username : <input />
</div>
)
}
function PasswordInput() {
const [valid, setValid] = useState({
maximum: false,
minimum: false,
required: false,
})
const reference = useRef(null)
function changeMode(e) {
if (reference.current.type === 'password') {
reference.current.type = 'text'
e.currentTarget.innerText = '🔒 감추기'
} else if (reference.current.type === 'text') {
reference.current.type = 'password'
e.currentTarget.innerText = '🔓 보이기'
}
}
console.log('- 매번 입력해도 rerender 미발생. 객체 프로퍼티 단위 불변성을 유지하며 리렌더 방지')
return (
<div>
Password :{' '}
<input
type='password'
ref={reference}
onChange={(e) => {
const input = e.currentTarget.value
const changed = produce(valid, (draft) => {
if (valid.maximum !== input.length <= 10) draft.maximum = input.length <= 10
if (valid.minimum !== input.length > 5) draft.minimum = input.length > 5
if (valid.required !== input.length > 0) draft.required = input.length > 0
})
setValid(changed)
}}
/>
<button onClick={changeMode}>🔓 보이기</button>
{valid.maximum || <div style={{ color: 'red' }}>비밀번호는 10글자를 넘을 수 없습니다.</div>}
{valid.minimum || <div style={{ color: 'red' }}>비밀번호는 5글자를 넘어야합니다.</div>}
{valid.required || <div style={{ color: 'red' }}>비밀번호를 입력해주세요.</div>}
</div>
)
}
function App() {
function registration() {}
return (
<section style={{ textAlign: 'start', width: 400 }}>
<UsernameInput />
<PasswordInput />
<button onClick={registration}>회원가입 완료</button>
</section>
)
}
export default App
immer 동작 과정
리액트 공식 문서에서 immer의 작동을 아래와 같이 설명하고 있다.
Immer가 제공하는 draft는 Proxy라고 하는 아주 특별한 객체 타입으로, 당신이 하는 일을 “기록” 합니다. 객체를 원하는 만큼 자유롭게 변경할 수 있는 이유죠!
Immer는 내부적으로 draft의 어느 부분이 변경되었는지 알아내어, 변경사항을 포함한 완전히 새로운 객체를 생성합니다.
위 설명에 따르면 아래 코드를 적용했을 때 객체의 프로퍼티 값이 바뀔때만 리렌더링 되는 이유를 알 것이다.
const changed = produce(valid, (draft) => {
if (valid.maximum !== input.length <= 10) draft.maximum = input.length <= 10
if (valid.minimum !== input.length > 5) draft.minimum = input.length > 5
if (valid.required !== input.length > 0) draft.required = input.length > 0
})
console.log(changed)를 해보면 아래 사진과 같이 객체가 반환되는 걸 볼 수 있다.(maximum, required가 true로 변경된 경우이다)
draft는 valid라는 state를 base로 삼고 있고, draft는 valid라는 객체가 변경되면 변경 사항을 포함해 완전히 새로운 객체를 생성하게 된다.
baseState(valid)가 변경되는 조건이 if문으로 작성되어 있고, 해당 조건이 참일 경우에 draft라는 프록시 객체가 이전 객체와 비교하여 변경 되었다면 해당 프로퍼티 값을 변경해서 새로운 draft객체를 생성하여 반환한다. 따라서 새로운 객체가 반화되었으므로 valid의 상태가 변경되면서 리렌더링 된다? -> 질문 필요
immer를 사용하기 위해서는
프로젝트 폴더로 이동 한 후 터미널에 아래 명령어 입력
npm install immer
package.json에 immer가 생겼다면 immer를 사용할 수 있다.
immer는 `produce` 함수를 제공한다. 구조는 아래와 같다 (immper가 제공하는 produce함수에 대한 자세한 설명)
produce(baseState, recipe: (draftState) => void): nextState
- `baseState` :`produce`함수에 변경 불가능한 state를 넘겨 받는 인자.
- `recipe`: `baseState`를 변경해야하는 방식은 포착
- `draft`: `recipe`의 첫번째 인자로, `baseState`를 안전하게 변경하는 프록시이다.
- `producer`: `(baseState, ...arguments) => resultState` 형식인 함수이다.
`recipe` 의 첫번째 인자인 `draft`에 이름을 붙일 필요는 없다. 본인이 원하는 변수 이름으로 설정할 수 있음.
`draft` 라는 변수 이름을 사용하는 것은 draft객체에 대해서는 변경을 해도 괜찮다고 명시하는 것 뿐이다.
produce는 `baseState`와 `draf`에 전달되는 모든 돌연변이를 수행하는 데 사용할 수 있는 `recipe`를 제공한다.
`baseState`를 손상시키지 않고, `nextState`에 적용된 모든 변경사항이 반영된다.
immer를 사용하기 전에는 매번 새로운 객체가 생성되서 주소값이 바뀌니까 state가 계속 변했는데, immer를 사용하면 새로운 객체를 생성하지 않으면서 프로퍼티 값만 변경할 수 있다.
`recipe` 내부에서는 draf객체에 대해서 필드 할당, delete 연산, 배열 변형, Map 및 Set 연산(예 push: pop, splice, set, sort, remove, 등) 을 포함하여 모든 표준 JavaScript API를 객체에서 사용할 수 있습니다.
`recipe` 함수는 void를 반환한다는 점을 명시해라. 하지만 `draft` 객체를 새로운 객체로 대체할 경우 새로운 `draft` 객체를 반환할 수 있다. 자세한 내용은 새 데이터 반환 을 참조하세요
'ASAC 웹 풀스택' 카테고리의 다른 글
Java 기본 문법 및 JVM 구성(2) - 자바 개발 환경 설정(Intellij, Gradle, Lombok) (2) | 2024.09.25 |
---|---|
Java 기본 문법 및 JVM 구성(1) - Java동작 원리 (0) | 2024.09.25 |
React 의 특장점, 렌더 라이프사이클 및 Hook(6) - Ref (0) | 2024.09.22 |
React 의 특장점, 렌더 라이프사이클 및 Hook(4) - State (1) | 2024.09.22 |
React 의 특장점, 렌더 라이프사이클 및 Hook(5) - Props (0) | 2024.09.22 |