들어가며
리액트 컴포넌트 스타일링 방법, 최적화를 위한 방법, 커스텀 훅 만드는 방법, 스토리북에 대해 공부했다. 뭔가 요즘 공부를 조금 소홀히 했던 것 같다. 다시 마음 잡고 열심히 공부할 필요가 있다.
컴포넌트 스타일링
컴포넌트에 스타일을 적용하는 방법은 크게 3가지가 있다.
- 스타일 시트를 사용해 적용
- 인라인 스타일 적용
- CSS-in-JS
스타일 시트를 적용하는 방법으로 가장 쉽게 스타일을 적용할 수 있다. CSS 파일을 작성하고 기존에 사용하던 방식으로 스타일을 정의하면 된다. 규모가 작은 프로젝트에서는 충분히 사용할 수 있지만 규모가 커질 경우 문제가 생길 수 있다. CSS 파일의 적용 범위는 전역이기 때문에 다른 컴포넌트에서 같은 이름을 사용할 경우 의도치 않게 원하지 않는 스타일이 적용될 수 있다. 이를 해결하기 위해 CSS Module 기능을 사용할 수 있다. 기존 스타일 시트의 .css 확장자를 .module.css 로 변경하면 사용할 수 있다. 사각형 컴포넌트에 스타일을 적용한다고 하자.
1 .Box {2 width: 100px;3 height: 100px;4 background-color: black;5 }
1 // Box.js2 import styles from './Box.module.css'3 const Box = () => {4 return (5 <div className={styles.Box}></div>6 )7 }89 export default Box;
module.css 파일에 스타일을 정의하고, 이 파일을 불러와서 사용하면 되는데 이 때는 styles 객체 내부의 값을 참조해야 한다. 개인적인 생각으로는 이 방법으로 거의 스타일을 적용하지 않는 것 같다. 전역에 정의해야 할 스타일이 있다면 그 때 사용하면 좋을 것 같다.
두 번째로 인라인 스타일을 사용할 수 있다. 리액트 컴포넌트 내에서 style 속성을 사용하면 된다.
1 const Box = () => {2 return (3 <div style={{ width: 100, height: 100, backgroundColor: 'black'}}></div>4 )5 }
style 속성에 자바스크립트 객체 리터럴을 전달하면 된다. 이 때 주의할 점은 CSS 속성명과 다르게 카멜 케이스로 작성하고, 숫자가 아닌 값이라면 문자열로 전달해주어야 한다. 지금까지 과제를 하면서 인라인 스타일로 작성을 했던 경우가 많았다. 코드 리뷰 과정에서 인라인 스타일은 지양하라는 피드백이 있었다. 보통의 경우 사용하지 말고, 동적으로 스타일을 변화시키거나, 스타일을 덮어써야 할 때? 사용하면 괜찮을 것 같다.
마지막으로 CSS-in-JS 방법이 있다. 이 방법은 자바스크립트 코드에서 CSS를 작성하는 방식인데 2014년 페이스북 개발자 Christopher Chedeau aka Vjeux 가 처음으로 소개했다. 자세한 내용은 웹 컴포넌트 스타일링 관리 : CSS-in-JS vs CSS-in-CSS 를 참고했다. CSS-in-JS 의 대표적인 라이브러리로 styled-component 와 emotion 이 있는데 강의에서는 emotion을 사용했다. 두 라이브러리의 차이는 이 블로그 를 참고했다.
emotion의 경우 CRA로 프로젝트를 구성했을 때 매번 아래와 같이 pragma 선언이 필요하다.
1 /** @jsx */2 import { css } from '@emotion/react'
pragma를 선언하지 않고 사용하기 위해서는 바벨 설정이 추가로 필요한데, CRA에서는 사용자가 직접 바벨 설정을 할 수 없다. eject 명령을 사용해서 숨겨진 모든 설정을 커스터마이징 할 수 있는데 한번 eject 명령을 수행하면 다시 원래 상태로 돌아갈 수 없다.
이 때 사용할 수 있는 게 @craco/craco 라이브러리이다. Craco 는 Create React App Configuration Override의 약자로 eject 명령 없이 세부 설정을 커스터마이징 할 수 있다.
1 npm i @craco/craco
craco를 설치하고 기존에 package.json 에 scripts 부분을 craco 로 실행하도록 변경한다.
1 "scripts": {2 "start": "craco start",3 "build": "craco build",4 "test": "craco test",5 // ...6 },
다음으로 emotion을 사용하기 위해 패키지를 설치한다.
1 npm -i @emotion/react @emotion/styled2 npm i -D @emotion/babel-preset-css-prop
프로젝트 최상위 경로에 craco.config.js 파일을 생성하고 해당 파일에 아래와 같이 작성한다.
1 // craco.config.js2 module.exports = {3 babel: {4 presets: ['@emotion/babel-preset-css-prop'],5 },6 }
이 코드를 통해 바벨 설정을 할 수 있다.
Memoization
메모이제이션은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다.
리액트 컴포넌트는 다음의 몇 가지 상황에서 리렌더링된다.
- 자신의 상태가 변경될 때
- 부모 컴포넌트로부터 받는 prop이 변경될 때
- 부모 컴포넌트의 상태가 변경될 때
자신의 상태가 변경될 때, 부모 컴포넌트로부터 받는 prop이 변경될 때는 값이 변경됐기 때문에 자식 컴포넌트가 리렌더링 되는게 맞는 것 같다. 그런데 세 번째의 경우 자식 컴포넌트는 변경되는 게 하나도 없는데 리렌더링이 된다.
컴포넌트의 경우 JSX를 반환하는 함수이기 때문에 리렌더링이 되면 함수 내부의 변수나 함수들이 다시 선언되거나 실행된다. 그렇기 때문에 불필요한 연산이 수행될 수 있고, 성능 저하로 이어질 수 있다. 이를 해결하기 위해 useMemo, React.memo, useCallback 등의 방법을 사용한다.
useMemo
값의 메모이제이션을 위해 사용한다.
1 const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo의 첫 번째 인자로는 결과값을 반환할 함수, 두 번째 인자로는 어떤 값에 의존할 지를 전달한다. 위 코드의 경우 a, b의 값이 변경되면 computeExpensiveValue 함수의 계산 결과를 memoizedValue에 저장한다. 함수의 이름에서도 알 수 있듯이 계산에 많은 비용이 소모되는 작업의 최적화에 사용된다. 부모 컴포넌트의 상태가 변경된 경우 a, b의 값은 변경되지 않았기 때문에 기존에 계산한 결과를 그대로 사용함으로써 불필요한 계산을 줄일 수 있다.
React.memo
컴포넌트가 동일한 props로 동일한 결과를 렌더링한다면, React.memo를 사용해 렌더링 결과를 메모이징 할 수 있다.
1 const MyComponent = React.memo(function MyComponent(props){2 ...3 })
만약 부모 컴포넌트로부터 함수를 props로 받으면 같은 함수인데도 리렌더링이 발생할 수 있다. React.memo는 props 가 갖는 복잡한 객체에 대하여 얕은 비교만을 수행하기 때문이다.
useCallback
React.memo 에서 살펴 본 것처럼 같은 함수라도 다시 선언될 경우 다른 함수로 인식하기 때문에 불필요한 렌더링이 발생한다. useCallback은 함수가 다시 정의되는 것을 막기 위해 사용한다.
1 const memoizedCallback = useCallback(2 () => {3 doSomething(a, b);4 },5 [a, b],6 );
사용법은 useMemo와 비슷하다. 첫 번째 인자로는 콜백 함수를 전달하고, 두 번째 인자로는 어떤 값에 의존할 지 의존성 값을 넘겨준다. 위의 코드에서 a, b의 값이 변경되지 않으면 첫 번째 인자의 함수는 재정의되지 않는다.
참고! useCallback(fn, deps) 는 useMemo(() => fn, deps) 와 같다.
커스텀 훅
리액트에서 기본적으로 제공하는 훅 외에 사용자가 직접 원하는 대로 훅을 만들어 사용할 수 있다.
1 // useToggle.js2 import React from 'react';34 const useToggle = (initialState = false) => {5 const [state, setState] = React.useState(initialState);6 const toggle = React.useCallback(() => setState(state => !state), []);78 return [state, toggle];9 }1011 export default useToggle;
위와 같이 토글 상태를 확인할 수 있는 훅을 만든다고 가정하자. 이렇게 만든 훅은 토글이 필요한 모든 곳에서 재사용 할 수 있다.
1 import useToggle from './useToggle';23 const Button = () => {4 const [state, toggle] = useToggle(false);56 return (7 <button onClick={toggle}>{state ? "True" : "False"}</button>8 )9 }
이렇게 사용하면 토글 상태를 쉽게 관리할 수 있다. 사용자가 직접 정의한 훅은 use로 시작하는 이름을 가져야 한다. 커스텀 훅을 정의할 때 어떤 인수를 받아야 하고 어떤 값을 반환해야 하는지는 따로 정해져 있지 않다.
정리
강의 길이가 짧은 대신 공부하는 데에는 더 많은 시간이 들어가는 것 같다. 원래 useMemo, useCallback, React.memo에 대해 대충만 알고 있었는데, 오늘 강의를 통해 어떤 상황에서 어떻게 사용해야 하는지 좀 더 명확해진 것 같다.
지금까지 필요한 정보를 찾을 때, 다른 사람이 작성한 블로그 글을 많이 참고했다. 오늘 들었던 생각인데 너무 잘 차려진 정보만 골라 먹은 게 아닌가 걱정이 된다. 정제된 자료를 읽고 내가 고민하는 시간이 줄어든 것 같다. 좋은 것 같은데 뭔가 불안한 느낌... 어느 정도는 공식 문서를 읽고 내 스스로 자료를 정리하는 시간도 필요할 것 같다는 생각이다.
참고자료
https://velog.io/@sorious77/React-CSS-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0
https://react.vlpt.us/styling/02-css-module.html
https://www.samsungsds.com/kr/insights/web_component.html
https://ideveloper2.dev/blog/2019-05-05--thinking-about-emotion-js-vs-styled-component/
https://velog.io/@codenmh0822/CRA-Emotion-%ED%8C%A8%ED%82%A4%EC%A7%80-%EC%84%A4%EC%A0%95
https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98
https://ko.reactjs.org/docs/getting-started.html