ex) 순수한 컴포넌트
function Info({name}) {
return (
<ol>
<li>{name}는 프론트 엔드 개발자다.</li>
<li>{name}는 리액트를 좋아한다.</li>
<li>{name}는 아침마다 운동하는 걸 좋아한다.</li>
<li>{name}는 아아를 좋아한다.</li>
</ol>
)
}
export default function App() {
return (
<section>
<h1>프론트엔드 개발자 Zoey는?</h1>
<Info name="Zoey" />
</section>
)
}
ex) 순수하지 않은 컴포넌트
let guest = 0;
function Cup() {
guest = guest + 1;
return <h2> Tea cup for guest #{guest} </h2>;
}
export default function teaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
)
}
프로그램의 어떠한 상태를 변경한다.
여기서 guest = guest + 1; 의 상태가 원하는 결과 값으로 나오지 않는다.
Conputer science에서 함수가 결과값 이외에 다른 상태를 변경시킬 때 Side Effect가 있다고 한다.
React의 Side Effect가 많이 일어나는 건?
렌더링 자체로 발생하는 것
렌더링으로 인해 발생하는 Side Effect
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// 여기의 코드는 매 렌더링 후에 실행된다
}, []);
useEffect(() => {
// 여기의 코드는 첫 렌더링과 a의 값이 변화할 때 실행된다
}, [a]);
return <div />;
}
서버 연결 관련되 함수들은 아래처럼 cleanup 함수를 넣어줄 수 있다.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnection();
};
}, []);
return <div />;
}
Clean up 함수는?
다음과 같은 경우에 사용할 수 있다.
보통은 페이지 로드 하자마자 데이터를 보여줘야 하는 경우 useEffect를 써서 페이지 렌더링이 끝나자 마자 데이터를 불러올 수 있다.
이유는 다음과 같다.
1. Effects는 서버에서 실행되지 않는다.
2. Effect에서 직접 fetch하면 "네트워크 워터풀"이 만들어기 쉽다.
3. Effect에서 직접 fetch하는 것은 일반적으로 데이터를 미리 로드하거나 캐시하지 않는다.
생각해보기
function Form() {
const [firstName, setFirstName] = useState('강');
const [lastName, setLastName] = useState('송');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(lastName + firstName);
}, [firstName, lastName]);
...
}
=> 위 코드는 아래처럼 수정 가능하다.
function Form() {
const [firstName, setFirstName] = useState('강');
const [lastName, setLastName] = useState('송');
const fullName = lastName + firstName
}
useEffect를 피할수 있는 상황이면 피해서 코드 작성하기
Prop이 변경될 때 모든 state가 초기값으로 재설정 되어야 한다면?
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
=> prop값이 변경될때 comment의 상태값을 삭제하고 싶다면 위에 코드처럼이 아니라 아래처럼 변경가능하다.
export default function ProfilePage({ userId }) {
retrun (
<Profile
userId={userId}
key={userId}
/>
)
}
function Profile({ userId }) {
const [comment, setComment] = useState('')
}
Prop이 변경될 때 일부 state만 조정이 필요하다면?
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
=> preItems State를 만들어서 전상태값을 저장하고 아래 코드처럼 변경 가능하다.
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
const [preItems, setPreItems] = useState(items);
if(items !== preItems) {
setPreItems(items)
setSelection(null)
}
// ...
}
=> 위 코드가 가독성이 떨어진다 다음과 같이 변경 가능하다.
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectionId, setSelectionId] = useState(null);
const selection = items.find((item) => item.id === selectedId) ?? null;
// ...
}
Props나 Message와 같은 변경될 수 있는 값을 반응형 값이라고 하고 서버 URL과 같은 완전 정적인 값은 비반응형 값 이라고 한다.
const serverUrl = 'https://localhost:1234'; // 비반응형 값
function ChatRoom({ roomId }){
const [message, setMessage] = useState(''); // 반응형 값
//...
}
반응형 값은 렌더링 시 데이터 흐름에 참여한다. 이런 반응형 값을 이용해서 이벤트 핸들러와 Effect를 사용할 수 있다.
위 함수를 예로 들자면 sendMessage함수는 message가 변할때마다 이벤트가 일어나야 할까?
=> 답은 아니다! 사용자가 message를 보내고 싶을 때만 이벤트가 일어나야 할것이다.
일반적으로 위와 같은 로직은 비반응형 로직이라고 한다.
그리고 대체적으로 이런 비반응형 로직을 이벤트 핸들러로 다룬다.
function Example({ message = '' }) {
const [message2, setMessage2] = useState('');
const click = () => sendUserMessage('message1 or message2');
return (
<>
<button onClick={click}>메세지 보내기</button>
...
</>
);
}
다음과 같은 반응형 로직이 있을 경우
const connection = createConnection(serverUrl, roomId);
connection.connect();
아래와 같이 useEffect를 사용하여 처리한다.
function ChatRoom({serverUrl, roomId}) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, serverUrl]); // 이렇게!
return <div>연결!</div>
}
const serverUrl = 'http://api.example.com';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]); // 이제 serverUrl은 비반응형 값이기 때문에 의존성에 추가하지 않아도 된다!
return <div>연결!</div>
}
const serverUrl = 'http://api.example.com';
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNofication('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // 이 경우 theme이 의존성에 추가된다.
//...
}
const serverUrl = 'http://api.example.com';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNofication('Connected!', theme);
// 이렇게 반응형 로직을 비반응형 로직으로 동작하게 할 수 있다!
})
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect()
}, [roomId]); // 따라서 이제 의존성에 theme은 들어가지 않는다.
//...
}
function Example() {
const [isHovered, setIsHovered] = useState(false);
const { isMobile } = useMediaQuery();
useEffect(
() => {
// 최초 렌더링 시와 isHover가 변화할 때 들어갈 로직을 넣는다.
},
isMobile ? [] : [isHovered], // 삼항 연산자를 활용해 의존성을 이디어 환경에 따라 변경한다.
)
}
useEffect
useLayoutEffect
렌더링 -> 화면 그리기 -> useEffect로 인해 렌더링 -> 화면 그리기
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0)
// 아직 height 값이 0이기 때문에 height가 0인 상태로 화면을 그린다.
useEffect(() => {
const { height } = ref.current.getBoundingClientReact();
setTooltipHeight(height); // 실제 높이를 구한 후 렌더링을 하고 화면을 다시 그린다.
}, []);
//... 아래에 작성될 렌더링 로직에 tooltipHeight를 사용한다.
}
=> 이런 경우 렌더링될 때마다 화면의 깜빡거림이 발생한다.
렌더링 -> useLayoutEffect로 인해 렌더링 -> 화면 그리기
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0)
// 아직 height 값이 0이다. 하지만 사용자는 height가 0인 화면을 볼 수 없다.
useLayoutEffect(() => {
// useLayoutEffect를 사용했기 때문에 렌더링이 일어난 이후 화면을 그리지 않고 아래 작업을 수행한다.
const { height } = ref.current.getBoundingClientReact();
setTooltipHeight(height); // 실제 높이를 구한 후 렌더링을 하고 화면을 그린다.
}, []);
//... 아래에 작성될 렌더링 로직에 tooltipHeight를 사용한다.
}
Effect 로직이 길어지면 화면에 그려지는 시간이 늦어진다.
State 변화에 따라 브라우저가 화면을 그리는 것을 차단한다. => 사용자가 이를 알아차리지 못한다.
위와 같은 단점들 때문에 주의해서 써야한다.
let isInserted = new Set();
function useCSS(rule) {
useInsertionEffect(() => {
if(!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule
}
function Button() {
const className = useCSS('...')
return <div className={className} />;
}
useInsertionEffect를 사용하면 렌더링 하기도 전에 dom이 변이될때 같이 effect가 일어난다. 예시로 이렇게 버튼 컴포넌트를 만들 때 미리 돔을 주입해주는 형식으로 많이 쓰인다.