보통 3가지로 나뉨. 인라인 스타일, CSS(SCSS)파일 빼서 두는 스타일 그리고 CSS in JS(emotion)
또한 상태의존 스타일은 인라인 스타일로 하는게 좋음.
=> 그러면 서타일 싯트로 빼서 쓰는게 가장 적은 비율이겠구먼?
일단 제일 많이 사용하는 emotion을 설치해보자!
일단 npm i @emotion/react
로 설치해주고, 바벨 플러그인도 설치해준다. npm i -D @emotion/babel-plugin
이때 바벨 플러그인을 설정해주어야하는데, Creat-reat-app으로 만든 프로젝트는 이미 환경세팅이 다 되어있어서, 설정을 바꾸기 까다로움.
두가지방법이 존재함.
use strict
구문처럼 컴파일러에게 지시를 내린다. /** @jsxImportSource @emotion/react */
를 적어주면 된다.일일히 적는건 귀찮으니 크라꼬가 나아보인다!
참고로 크라꼬는 CreatReactAppConfigOverride의 줄임말임ㅋㅋ
//craco.config.js
module.exports = {
babel: {
presets: ["@emotion/babel-preset-css-prop"],
},
};
적어주면 잘 덮어쓴다. 플러그인은 설치하자.
이렇게하고
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "craco eject"
},
react-scripts
로 실행하던걸 크라꼬로 바꿔주면 끝!
emotion의 스타일 방식은 두가지지로 나뉨.
styeld-compontnt, css-prop
스타일드컴포넌트는 알고있으니, css-prop를 알아보자.
const SomeComponent = ({ children }) => (
<div css={{
backgroundColor: 'hotpink',
'&:hover': {
color: 'lightgreen'
}
}>
Some hotpink text.
{children}
</div>
);
이렇게 css
라는 이름의 prop으로 넘겨준다.
간단한걸? 참고로 문자열로 넘겨주는 방식도 있지만, 이렇게 객체타입으로 넘겨주는 걸 권장한다.
타입스크립트로 타입 체킹이 용이하대나? 일단 써봐야 알 것 같음.
함수의 연산된 값을 메모이제이션 하는 훅
컴포넌트는 함수로 구현됨. 그저 JSX를 반환하는 함수일뿐. 리액트 내부에서 컴포넌트 함수를 실행하여 렌더링함.
이때 내부에 구현된 함수들이나 변수들이 다시 실행되는 과정을 거치게 됨. => 리소스 낭비 발생.
특히 연산의 속도가 느린 컴포넌트라면 불쾌한 사용자경험 발생!
//App.js
const [content, setContent] = useState("버튼");
return (
<div>
<button onClick={() => setContent(content + ":")}>{content}</button>
<ShowSum n={400000000} />
</div>
);
//ShowSum.js
const ShowSum = ({ n }) => {
const sum = (n) => {
let temp = 0;
for (let i = 1; i <= n; i++) {
temp += i;
}
console.log(temp);
return temp;
};
const result = sum(n);
return <div>{result}</div>;
};
이렇게 큰 값이 들어오는 연산은 시간이 오래걸림.
이때 부모의 상태가 변경되었지만, 상태가 변경되지 않은 자식이 리렌더링 되는 사이드이펙트 발생!
이럴때 useMemo를 사용하여 최적화를 해준다.
const result = useMemo(() => sum(n), [n]);
첫번째 인자는 반환값, 두번째 인자는 의존값이다. 의존값이 같다면 메모된값을 내려줌.
이렇게 큰 연산은 드물것같은데...잘못 사용하면 오히려 낭비일수도 있겠다
부모에게서 받아온 props가 변경되었을때만 리-렌더링
비슷한 메모이제이션 기법이지만, 하는 기법이다. 위에서 부모 상태가 바뀌면 자식도 리렌더링이 일어난다했는데, 그걸 방지해줌.
export default React.memo(ShowSum);
이렇게해주면...
App.js에서 상태변화가 일어나도 ShowSum.js는 리렌더링 되지 않기에 연산이 이루어지지 않는다.
함수 자체를 메모이제이션 함
아니, 함수는 연산이 오래걸릴뿐인데 왜 함수 자체를 메모이제이션 하는걸까?
//App.js
const onSave = () => console.log("저장합니다");
const [val, setVal] = useState(0);
return (
<div>
<button onClick={() => setVal(val + 1)}>{val}</button>
<Editor onSave={onSave} />
</div>
);
//Editor.js
const Editor = ({ onSave }) => {
console.log("editor렌더링됨");
return <div onClick={onSave}>Editor</div>;
};
분명 함수를 그냥 내려줬을 뿐인데, 버튼을 누를시 자식 컴포넌트 리 렌더링이 일어난다. 이때 React.memo
를 사용해서 프롭스를 메모이제이션해도 역시나 리렌더링이 일어난다.
=>App.js가 리렌더링(함수 재실행)될때마다 onSave함수도 새로 생긴다. 함수는 일급 객체(참조형)이다. 따라서 이전에 있던 함수와 메모리 위치가 다르기때문에 다른 함수다...!! 이를 방지하기위하여 useCallback
을 사용하는 것이다.
참고로 같은 내용의 함수가 들어올때 리렌더링을 방지하려면React.memo
와 같이 사용해야 제 기능을 누릴수있다.
const Editor = ({ onSave }) => {
console.log("editor렌더링됨");
return <div onClick={onSave}>Editor</div>;
};
export default React.memo(Editor);
커스텀훅이라 왠지 거창하고 어려워보이지만, 기존 훅을 조합하거나 자주 사용되는 로직을 따로 빼서 사용하는 거다. 그냥 중복코드를 함수로 빼낸다고 생각하면 됨.
import { useCallback, useState } from "react";
const useToggle = (initialState = false) => {
const [state, setState] = useState(initialState);
const toggle = useCallback(() => setState((state) => !state), []);
return [state, toggle];
};
export default useToggle;
import { useCallback, useEffect, useRef, useState } from "react";
const useHover = () => {
const [state, setState] = useState(false);
const ref = useRef(null);
const handleMouseOver = useCallback(() => setState(true), []);
const handleMouseOut = useCallback(() => setState(false), []);
useEffect(() => {
const el = ref.current;
if (el) {
el.addEventListener("mouseover", handleMouseOver);
el.addEventListener("mouseout", handleMouseOut);
return () => {
el.removeEventListener("mouseover", handleMouseOver);
el.removeEventListener("mouseout", handleMouseOut);
};
}
}, [ref, handleMouseOut, handleMouseOver]);
return [ref, state];
};
export default useHover;
useCallback
을 잘 사용하는구나?
import { useCallback, useEffect, useRef, useState } from "react";
const useKeyPress = (targetKey) => {
const [keyPressed, setKeyPreseed] = useState(false);
const ref = useRef(null);
const handleKeyDown = useCallback(
({ key }) => {
if (key === targetKey) {
setKeyPreseed(true);
}
},
[targetKey]
);
const handleKeyUp = useCallback(
({ key }) => {
if (key === targetKey) {
setKeyPreseed(false);
}
},
[targetKey]
);
useEffect(() => {
const el = ref.current;
if (el) {
el.addEventListener("keydown", handleKeyDown);
el.addEventListener("keyup", handleKeyUp);
return () => {
el.removeEventListener("keydown", handleKeyDown);
el.removeEventListener("keyup", handleKeyUp);
};
}
}, [ref, handleKeyDown, handleKeyUp]);
return [ref, keyPressed];
};
export default useKeyPress;
이렇게 중복된 로직을 훅으로 따로 빼내면 보기 더 편해진다 ㅎㅎ
컴포넌트를 문서화 하고, 눈으로 바로 확인가능하고 상태별로 어떤 스타일을 줄지도 결정할수 있음!
npx storybook@latest init
로 설치하면...
src/stories
라는 폴더가 생성되고 그 안에 파일들이 잘 생성되있음ㅎㅎ
npm run storybook
으로 실행해보면...
이런 화면이 나온다(벨로그 이미지 업로드 오류로인하여 다음에 첨부예정)
컴포넌트를 하나 만들어서 스토리북에 넣어보자.
//Box.js (컴포넌트)
const Box = ({ width = 100, height = 100, backgroundColor = "red" }) => {
const style = {
width,
height,
backgroundColor,
};
return <div style={style}></div>;
};
export default Box;
//stories/Box.stories.js
import Box from "../components/Box";
export default {
title: "Example/Box",
component: Box,
//프롭스를 정해준다.
argTypes: {
width: { control: "number" },
height: { control: "number" },
backgroundColor: { control: "color" },
},
};
//박스의 형태들을 export로 내보내준다.
export const Primary = {
args: {
primary: true,
},
};
이렇게 하면 끝!
강의 버전과 현재 버전이 조금 달라서 쬐끔 이해하기 어려웠는데, 역시 공식문서를 보니 해결됐다.
지금까지 배운 내용을 토대로 로그인폼을 만들어본다. 스토리북도 사용하시는데, 시간이 좀 더 소요되지만 좋아보인다.
argTypes:{
onClick:{action:"onClick"}
}
useForm
커스텀훅import { useState } from "react";
const useForm = ({ initialValues, onSubmit, validate }) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isLoading, setIsLoading] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({ ...values, [name]: value });
};
const handleSubmit = async (e) => {
try {
e.preventDefault();
setIsLoading(true);
const newErrors = validate(values);
if (!Object.keys(newErrors).length) {
await onSubmit();
} else {
setErrors(newErrors);
}
} catch (e) {
console.log(e);
} finally {
setIsLoading(false);
}
};
return { values, errors, isLoading, handleChange, handleSubmit };
};
코드 전부를 첨부하기엔 좀 길어서...중요 부분만 뽑아옴!
바닐라 JS로 컴포넌트 => Vue => React순으로 배웠더니, React의 구조가 눈에 잘 들어온다.
살짝 Vue랑 헷갈리는 부분도 있긴한데 괜찮음ㅎㅎ 하다보면 명확해지겠지.
그리고 알고리즘 문제를 푸는게 굉장히 도움됐다. 커스텀 훅 같이 함수를 짤때 논리적으로 생각할수 있게됨...
뭐든 필요 없는 공부는 없다!