기본적으로 useEffect는 두가지 인자를 가지며 첫번째는 실행하고자 하는 함수고 두번째는 의존성 배열이다.
const [value, setValue] = useState("");
useEffect(() => {
console.log(`hello useEffect : ${value}`);
return () => {
console.log("나 사라져요");
};
}, [value]);
위의 예시에서는 콘솔에 로그를 찍는 함수를 첫번째 인자로 전달했고, 의존성 배열에 value를 넣음으로서 value가 변경될 때마다 콘솔에 로그가 찍히게 하고 있다.
의존성 배열을 아예 전달하지 않은 경우 : 렌더링 될 때마다 실행된다.
의존성 배열을 빈 배열로 전달한 경우 : 속한 컴포넌트가 처음 렌더링 될 때 한번만 실행된다.
의존성 배열에 뭔가를 넣어준 경우 : 의존성 배열에 들어간 것의 값이 변경될 때마다 실행된다.
useEffect 에서 return 다음에 오는 코드를 클린업이라고 부른며 이는 컴포넌트가 화면에서 사라질때 실행된다. 위 예시에서 '나 사라져요'라고 콘솔에 찍히는 것이 클린업 코드의 작용이다.
const ref = useRef("초기값");
console.log(ref); //{current: '초기값'}
ref.current = "변경 ㅋ";
console.log(ref); //{current: '변경 ㅋ'}
위 예시에서는 ref.current로 값을 바꾸는 모습을 확인할 수 있다.
이 두가지 특징 때문에 보통 두가지 용도로 사용된다
저장공간
A. state랑 비슷하게 쓸 수 있다. state는 변화가 일어나면 다시 렌더링이 일어나고 내부 변수들은 초기화 된다. ref값의 변동은 렌더링을 유발하지 않는다. 따라서 내부 변수들이 초기화 되지 않는다. 컴포넌트가 100번 렌더링 되어도 ref 에 저장한 값은 유지된다.
B. state는 리렌더링이 꼭 필요한 곳에 쓰고 ref는 리렌더링을 발생시키고 싶지 않은 값을 저장할 때 사용한다.
DOM 요소 접근
import React, { useEffect, useRef } from "react";
const App = () => {
const idRef = useRef("");
//화면이 렌더링 될때 뭘 하고 싶다 : useEffct
useEffect(() => {
idRef.current.focus();
}, []);
return (
<>
<div>
아이디 : <input type="text" ref={idRef} />
</div>
<div>
비밀번호 : <input type="password" />
</div>
</>
);
};
export default App;
화면이 뜨자마자 id 입력창에 커서를 갖다놓고 싶은 상황. 화면에 렌더링될 때 한번 실행하고 싶기 때문에 useEffect를 썼고, 리액트로 생성한 DOM 요소에 접근을 해야하기 때문에 useRef를 썼다.
useRef 사용은 간단. const로 변수 이름 정해주고, 지목할 요소에 ref 속성을 달아서 위 예시처럼 해당 변수를 포인팅 하면 된다.
createContext : context 생성
Consumer : context 변화 감지
Provider : context 전달(to 하위 컴포넌트)
import { createContext } from "react";
export const FamilyContext = createContext(null);
위와 같은 방법으로 사용할 context 이름을 정하고 생성한다. 위 예시에서 초기값은 null로 설정해둔 상태.
그리고 이렇게 만든 FamilyContext(이름이 꼭 이거여야 하는건 아님) 로 해당 정보를 사용하는 가장 윗단계 컴포넌트(처음 그 정보를 자식에게 props로 보낸 컴포넌트)를 감싼다! 그리고 최초의 정보 발생 컴포넌트에는 .Provider 도 붙여주고 value로 객체를 전달한다.
import { FamilyContext } from "../context/FamilyContext";
function GrandFather() {
const houseName = "스파르타";
const pocketMoney = 10000;
return (
<FamilyContext.Provider
value={{
houseName, //객체의 key와 value가 같아서 생략
pocketMoney,
}}
>
<Father />
</FamilyContext.Provider>
);
}
이게 예시. 중간단계 컴포넌트에서는 이제 아무것도 안해도 된다. props를 받지도 않고 props를 주지도 않는다. 컨텍스트의 정보를 이용할 일자체가 없으므로 관련된 코드를 전혀 적지 않아도 된다.
마지막으로 컨텍스트를 사용할 컴포넌트에서는 어떤 모습일지 보자
import { FamilyContext } from "../context/FamilyContext";
const style = {
color: "red",
fontWeight: "900",
};
function Child() {
const data = useContext(FamilyContext);
console.log(data);
return (
<div>
나는 이 집안의 막내에요 <br />
할아버지가 우리 집이름은 <span style={style}>{data.houseName}</span>이라고
하셨어요.
<br />
게다가 용돈도 <span style={style}>{data.pocketMoney}</span>원 만큼이나
주셨어요
</div>
);
}
컴포넌트 내에서 컨텍스트 정보를 변수에 할당하면서 useContext(컨텍스트이름).
역시 props를 사용하는 것이 아니기 때문에 그냥 변수에 저장된 자료 써먹듯이 하면 된다.
위 예시에서는 변수명을 data로 하여 data.houseName 같이 닷 노테이션으로 정보에 접근하고 있다. 애초에 Provider에서 value로 제공한 것이 객체라는 것을 잊지 말자.
주의! useContext 사용할때 Provider 에서 제공한 value 가 달라진다면 useContext를 사용하고 있는 모든 컴포넌트가 리렌더링 된다(엄청 비효율적). 이런 문제점 이 있기 때문에 자주 바뀌지 않는 전역값을 관리하는데 유용하다.
리액트에서 부모 컴포넌트가 리렌더링 된 경우 모든 자식 컴포넌트도 함께 리렌더링 됨. 당연히 자식 컴포넌트에서 전혀 바뀐게 없는데 리렌더링하는 것은 비효율적. 이런 경우 비효율적인 리렌더링을 막기위해서 쓰는게 React.memo
React.memo를 이용해서 컴포넌트를 ‘메모리’에 저정해두고 필요할 때 갖다 쓸 수 있다. 이걸 캐싱 이라고도 한다. 이렇게 하면 props가 변경이 되지 않는한 컴포넌트는 리렌더링 되지 않는다.
지극히 간단하다. 컴포넌트를 export 했던 부분으로 가서 React.memo로 감싸주면 된다.
export default React.memo(Box1);
React.memo는 컴포넌트를 메모이제이션 했다면 useCallback은 인자로 들어오는 함수 자체를 메모이제이션한다.
부모 컴포넌트 내에서 만들어진 함수를 자식 컴포넌트가 props로 받아 사용하는 상황을 생각해보자. 자식 컴포넌트에 useMemo 를 사용했더라도 부모 컴포넌트가 리렌더링 될때 props로 받는 함수도 재생성되고 이는 자식 컴포넌트 입장에서 props가 바뀌는 것이나 다름없기 때문에 자식 컴포넌트도 꼼짝없이 리렌더링 된다
이럴때 쓰는 것이 useCallback
useMemo와 비슷하게 대상 함수를 useCallback()으로 감싸주기만 하면 된다.
//count 초기화 함수
const initCount = useCallback(() => {
setCount(0);
}, []);
감싼 모습. 의존성 배열도 있어야한다.
useCallback은 함수의 스냅샷을 그대로 저장해 두기 때문에 계속 바뀌는 state를 다루기에 적절하지 않을 수도 있다. 이럴땐 해당 state 를 의존성 배열에 넣어서 state의 변동에 따라 함수 스냅샷을 다시 찍도록 할 수 있다.
//AS-IS
const value = heavyWork();
//TO-BE
const value = useMemo(() => heavyWork(), []);
useMemo로 감싸주기만 하면 되며 의존성 배열을 가진다.
앞서 toDo List 만들기에서 unique id 를 생성하는 방법을 고민한 적이 있었다. 그에 대한 간단한 해결책을 또 하나 알게 되었다. 바로 uuid 를 사용하는것.
yarn add react-uuid
로 모듈을 설치하고 uuid가 필요한 곳에서 uuid()
로 함수를 실행하기만 하면 된다.
const submitHndlr = (e) => {
e.preventDefault();
const newToDo = { id: uuid(), title, content, isDone: false };
setTodo([...toDos, newToDo]);
setTitle("");
setContent("");
};
위는 신규 toDo 를 추가하는 기능을 하는 함수에서 uuid를 사용하는 예시이다.
당연한 얘기지만 import uuid from "react-uuid";
로 먼저 import 해야 쓸수 있다.
위에서 소개한 uuid 와 마찬가지로 서드파티 패키지다.
yarn add styled-components
const StBox = styled.div`
width: 100px;
height: 100px;
border: 1px solid ${(props) => props.borderColor};
margin: 20px;
`;
function App() {
return (
<>
<StBox borderColor="red">박스</StBox>;
<StBox borderColor="blue">박스</StBox>;
<StBox borderColor="green">박스</StBox>;
</>
);
}
위 예시가 기본적인 사용방법이다. 컴포넌트를 생성하는 것이기 때문에 당연히 props도 쓸수 있고 borderColor 같은걸 props로 전달해서 동적으로 색상을 원하는대로 바꿀수 있다. 조금만 능숙해지면 훨씬 간단하게 코드를 쓸 수 있고 당연히 map같은것도 돌릴 수 있다 아래 예시처럼.
const StContainer = styled.div`
display: flex;
`;
const StBox = styled.div`
width: 100px;
height: 100px;
border: 1px solid ${(props) => props.borderColor};
margin: 20px;
`;
//박스 색
const boxList = ["red", "blue", "green"];
//색을 넣으면 이름을 반환
const getBoxName = (color) => {
switch (color) {
case "red":
return "빨간박스";
case "green":
return "초록박스";
case "blue":
return "파란박스";
default:
return "검정박스";
}
};
function App() {
return (
<StContainer>
{/* <StBox borderColor="red">박스</StBox>; */}
{boxList.map((item) => {
return <StBox borderColor={item}>{getBoxName(item)}</StBox>;
})}
</StContainer>
);
}
GlobalStyle.jsx를 src 폴더 안에 만들어주고 App.js 에서 임포트 하고 모든 컴포넌트 최상단 위치에 넣어주면 된다.
import { createGlobalStyle } from "styled-components";
const GlobalStyle = createGlobalStyle`
// 이곳에 글로벌로 적용시키고 싶은 스타일 작성
`;
export default GlobalStyle;