드림코딩 React편을 보면서 심화 부분에 있는 부분을 정리한 글이다.
만일에 마우스 움직임에 따라 dom요소가 따라서 움직이도록 구현 해보고 싶은 경우를 예로 들어보자
import React, { useState } from 'react';
import './AppXY.css';
export default function AppXY() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMove = (e) => {
setPosition(prev => ({ x: e.clientX, y: e.clientY }));
}
return (
<div className='container' onPointerMove={handleMove}>
<div className='pointer' style={{transform: `translate(${position.x}px, ${position.y}px)`}} />
</div>
);
}
위 코드를 통해 구현이 가능하고 x와 y좌표를 따로 state로 선언하지 않고 구현한걸 볼 수 있다.
서로 연관된 상태값 예를들어 상품의 이름 가격 상세설명등의 상태값을 따로 선언해서 관리하기 보다는 객체로 만들어서 한번에 관리하는게 좋다.
동시에 변경을 해야 하는 상태값의 경우 하나의 이벤트 핸들러에 여러번 setState를 호출하기 보다는 한번에 모든 상태값이 업데이트 되도록 하는게 좋다.
Reducer hook은 상태값이 객체안에 객체같은 중첩된 객체처럼 복잡한 구조로 되어있을때 이를 업데이트하는 로직을 따로 작성하고 이를 재사용하기 위해서 사용한다.
const initialPerson = {
name: '엘리',
title: '개발자',
mentors: [
{
name: '밥',
title: '시니어개발자',
},
{
name: '제임스',
title: '시니어개발자',
},
],
}
예를들어 위와 같이 객체안에 배열이 있는경우 이를 업데이트하기 위해서는 따로 깊은 복사를 해서 상태의 불변성을 지켜줘야한다. 이 때 필요한 로직을 컴포넌트 안에 작성하는건 일관성을 해치고 이런 로직을 한번 작성해놓고 다른 컴포넌트에서도 이런 구조의 상태값을 업데이트할 때 재사용 하기위해서는 Reducer를 사용해주면 된다.
export default function personReducer(person, action) {
switch (action.type) {
case 'updated': {
const { prev, current } = action;
return {
...person,
mentors: person.mentors.map((mentor) => {
if (mentor.name === prev) {
return { ...mentor, name: current };
}
return mentor;
}),
};
}
case 'added': {
const { name, title } = action;
return {
...person,
mentors: [...person.mentors, { name, title }],
};
}
case 'deleted': {
return {
...person,
mentors: person.mentors.filter((mentor) => mentor.name !== action.name),
};
}
default: {
throw Error(`알수없는 액션 타입이다: ${action.type}`);
}
}
}
위 코드처럼 외부에 js 파일을 따로 만들어서 action 값에 따라서 내부 상태값을 업데이트하는 로직을 작성한다.
export default function AppMentor() {
const [person, dispatch] = useReducer(personReducer, initialPerson);
const handleUpdate = () => {
const prev = prompt(`누구의 이름을 바꾸고 싶은가요?`);
const current = prompt(`이름을 무엇으로 바꾸고 싶은가요?`);
dispatch({ type: 'updated', prev, current });
};
사용하는 코드부분이다. 일부만 가져온건데 useReducer hook를 통해서 외부에서 선언해준 함수와 초기값을 매개변수로 넣어주고 이를 업데이트 하기위해서는 dispatch 함수를 통해서 업데이트 해주면 된다.
useReducer를 사용할때 객체의 가지수의 깊이가 깊은 경우에는 로직을 짜기 어렵다. 이 문제를 해결한 라이브러리가 Immer이다.
export default function AppMentorsImmer() {
const [person, updatePerson] = useImmer(initialPerson);
const handleUpdate = () => {
const prev = prompt(`누구의 이름을 바꾸고 싶은가요?`);
const current = prompt(`이름을 무엇으로 바꾸고 싶은가요?`);
updatePerson(person => {
const mentor = person.mentors.find(m => m.name === prev);
mentor.name = current;
})
};
updatePerson을 통해 직관적으로 업데이트 해주는게 가능하다.
useReducer는 React에 내장됝 hook이지만 내가 추가적으로 로직을 작성해줘야 한다는 단점이 있다. 그리고 이 Immer 라이브러리는 내 프로젝트에 추가적인 라이브러리를 설치해야 한다는 단점이 있지만 직관적으로 사용하기 편하다.
무조건 이 Immer 라이브러리를 사용하기 보단 우리 프로젝트에서 복잡한 객체를 상태값으로 사용하는지 유무에 따라서 적절하게 사용해주면 될꺼같다.
form 태그 내부에 있는 input 태그는 값이 입력될때 마다 UI상에 입력되는 것이 보인다.
이는 React의 컨셉인 상태값이 변경되어야 UI가 바뀐다는 컨셉을 지키지 않는 통제되지 않는 컴포넌트로 볼 수 있다.
export default function AppForm() {
const [form, setFrom] = useState({ name: '', email: '' });
const handleSubmit = (e) => {
e.preventDefault();
console.log(form);
};
const handleChange = (e) => {
const { name, value } = e.target;
setFrom({ ...form, [name]: value });
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor='name'>이름:</label>
<input
type='text'
id='name'
name='name'
value={form.name}
onChange={handleChange}
/>
<label htmlFor='email'>이메일:</label>
<input
type='email'
id='email'
name='email'
value={form.email}
onChange={handleChange}
/>
<button>Submit</button>
</form>
);
}
이를 위에 코드처럼 onChange 이벤트를 통해서 상태값으로 input 태그에 데이터가 보이도록 작성하는게 좋다.
만일 재사용을 하기위한 컴포넌트를 만드는데 만든 컴포넌트를 여러군데에서 사용할 때 어디에서는 추가적인 태그를 추가한 컴포넌트를 사용하고 싶은 상황이 있다고 생각해보자
이 때 props의 여부에 따라서 보여지는 태그까지는 작성을 할 수 있지만 컴포넌트를 사용하는 개발자가 임의로 새로운 태그를 추가하고 싶은 상황에는 이 방법이 사용이 불가능하다. 이 때 활용하는 방법이 wrap 컴포넌트이다.
export default function AppWrap() {
return (
<div>
<Navbar>
<Avatar
image='https://images.unsplash.com/photo-1527980965255-d3b416303d12?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1160&q=80'
name='Bob'
size={200}
/>
<p>안녕하세요!</p>
</Navbar>
<Navbar>
<p>안녕하세요!</p>
</Navbar>
</div>
);
}
평소에 우리가 작성하는 컴포넌트는 닫는 태그없이 한 줄로 <Navbar />
형태로 작성했지만 위에 코드에서는 닫는 태그까지 입력해서 넘겨주고 싶은 데이터를 다르게 작성한 모습니다.
function Navbar({ children }) {
return <header style={{ backgroundColor: 'yellow' }}>{children}</header>;
}
그리고 해당 컴포넌트가 정의된 부분은 children으로 한번에 받아와서 사용하도록 작성해주면 된다.
내가 원하는 곳에서 수정해서 새로운 자유롭게 작성하는게 가능하다.
만일 동등한 위치의 컴포넌트들이 하나의 상태값을 공유해야 한다면 보통은 부모 컴포넌트를 만들어서 두 자식 컴포넌트에게 props를 전달해서 상태값을 공유하도록 할 수 있다.
하지만 컴포넌트 트리가 복잡하고 제일 말단의 자식 컴포넌트에서 해당 상태값이 필요한경우 prop drilling을 통해 상태값을 여러번 전달해줘야 한다. 이 작업은 매우 비효율적이기 때문에 이 문제를 해결하기 위해서 나온게 Context API이다.
즉 전반적으로 모든 컴포넌트들이 공통적으로 필요한 state를 자식 컴포넌트가 활용하기 좋게 전달하는 방법이 Context API이다.
`예를들어 사용자들이 선택한 언어값, UI테마(다크모드), 로그인 정보등이 있다.
하지만 상태값이 바뀌면 모든 컴포넌트들이 리랜더링이 되기 때문에 빈번하게 업데이트되는 상태값은 지정해주지 않는게 좋다.
context api의 단점인 모든 자식 컴포넌트가 리랜더링 되는 문제를 해결하기 위한 테크닉이라고 한다.
컴포넌트 트리중간지점에 context api를 사용해서 지정 시점부터 그 아래 자식 컴포넌트만 업데이트 되도록 만드는 방법이다.
그리고 context api를 여러군데에서 사용할 수 있다.
내 데이터가 어디까지 공유되어야 하는지에 따라서 context api를 위치시키는게 중요하다.
React는 상태값의 변경으로 인해서 컴포넌트의 재호출이 발생한다. 이 때 컴포넌트 내부에 함수들도 새로운 함수 참조값이 할당이 되면서 자식 컴포넌트로 전달하는 함수 prop이나 데이터 prop들이 새로운 값으로 전달이되고 이는 자식 컴포넌트도 리랜더링이 되는 결과를 보여준다.
이런 특징 때문에 자주 리랜더링이 발생하면 어플리케이션의 성능에 매우 악영향을 끼칠꺼 같지만 실제로는 Virtual DOM으로 읺애서 실제 DOM요소는 이전과 비교해서 실제로 달라진 부분만 새로 그리기 때문에 성능에는 그렇게 차이는 없다.
하지만 복잡한 로직이 처리되는 함수같은 경우는 리랜더링이 될때 성능에 안좋은 영향을 끼칠수 있기 때문에 이는 다른 방법으로 처리하는게 좋다.
세가지 hooks는 리랜더링으로 인해 여러번 호출되는 문제를 막기위해 나온 hooks들이다.
하나의 컴포넌트내에서 특정 함수의 로직을 처음에만 수행하게 하거나 특정 dependancy가 변경 되었을 때만 수행되도록 해줄 수 있다.
보통 React에서 전달되는 props는 항상 새로운 객체의 형태로 만들어진다. 하지만 memo hook을 이용하면 props로 전달되는 값의 변경이 실제로 일어나야지만 리랜더링이 되도록 해줄 수 있다.
const Button = memo(({ text, onClick }) => {
console.log('Button', text, 're-rendering 😜');
const result = useMemo(() => calculateSomething(), []);
return (
<button
onClick={onClick}
style={{
backgroundColor: 'black',
color: 'white',
borderRadius: '20px',
margin: '0.4rem',
}}
>
{`${text} ${result}`}
</button>
);
});
참고로 props로 전달해주는 함수는 useCallback으로 처리해줘야한다.
우리가 자주 사용하던 hooks들 즉 함수를 정의해서 재사용하기 위해서 임의로 만들어서 사용하는걸 의미한다.
import { useState, useEffect } from "react";
export default function useProducts({ salsesOnly }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [products, setProducts] = useState([]);
useEffect(() => {
setLoading(true);
setError(undefined);
fetch(`data/${salsesOnly ? 'sale_' : ''}products.json`)
.then((res) => res.json())
.then((data) => {
console.log('🔥뜨끈한 데이터를 네트워크에서 받아옴');
setLoading(prev => {
if (prev) {
return !prev;
}
})
setProducts(data);
})
.catch(error => setError('에러가 발생했음!'))
.finally(() => setLoading(false));
return () => {
console.log('🧹 깨끗하게 청소하는 일들을 합니다.');
};
}, [salsesOnly]);
return [loading, error, products];
}
평소에 컴포넌트 만드는거처럼 내부에서 state를 선언하고 useEffect등의 hook을 사용할 수 있다.
기본 컴포넌트와 다른점은 return값이 jsx의 UI요소가 아닌 반환해주고 싶은 데이터를 넣는다는 점이다.
Custom Hooks는 값을 재사용하기 위해서 사용하는게 아닌 내부 로직의 재사용을 위해서 사용한다.
함수형 컴포넌트는 한번 호출되면 내부의 함수가 모두 호출이 되어버리기 때문에 따로 hook들을 사용해야 하지만 클래스 컴포넌트는 render 함수에 있는 부분만 재호출이 되고 내부 맴버 함수들은 변경이 되지 않는다는 차이가 있다.