3장: 컴포넌트 CSS 스타일링
3-1. 리액트 컴포넌트의 CSS 스타일링
부트스트랩 사용하기
부트스트랩(bootstrap) : CSS 프레임워크
public 폴더의 index.css에 부트스트랩 cdn 붙여넣어 사용
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
부트스트랩 컴포넌트 예시
리액트 함수형 컴포넌트로 사용하기 위해 공식문서의 form에서 for->htmlFor, class->className, input 태그 닫기의 수정이 필요하다.
export default function Bootstrap() {
return (
<form>
<div className="form-group">
<label htmlFor="exampleInputEmail1">Email address</label>
<input type="email" className="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email" />
<small id="emailHelp" className="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
<div className="form-group">
<label htmlFor="exampleInputPassword1">Password</label>
<input type="password" className="form-control" id="exampleInputPassword1" placeholder="Password" />
</div>
<div className="form-check">
<input type="checkbox" className="form-check-input" id="exampleCheck1" />
<label className="form-check-label" htmlFor="exampleCheck1">Check me out</label>
</div>
<button type="submit" className="btn btn-primary">Submit</button>
</form>
)
}
material icon 사용하기
@import 방식은 다른 사이트에 호스팅된 외부 CSS 파일을 가져오므로 네트워크 속도에 영향을 받는다는 문제 있음
Node.js 패키지 방식으로 material icon 사용 가능
npm i @fontsource/material-icons
material icon을 적용한 Icon 컴포넌트 만들기
import { CSSProperties, DetailedHTMLProps, FC, HTMLAttributes } from "react";
type ReactSpanProps = DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
export type IconProps = ReactSpanProps & {
name: string
};
export const Icon: FC<IconProps> = ({name, className: _className, ...props}) => {
const className = ['material-icons', _className].join(' ');
return <span {...props} className={className}>{name}</span>
}
아래와 같이 사용
import { Icon } from "../components";
export default function UsingIcon() {
return <div>
<h3>UsingIcon</h3>
<Icon name="home" style={{color: 'blue'}} />
</div>
}
name이 icon의 명칭(스네이크케이스로 작성)
style은 props로 들어가서 적용된다.
3-2. 테일윈드CSS 리액트 프로젝트 만들기
테일윈드 CSS
2017년 11월에 '유틸리티 최우선'을 기치로 만든 CSS 프레임워크
//tailwind css 설치
npm i -D postcss autoprefixer tailwindcss
autoprefixer: 대표적인 PostCSS 플러그인. 벤더 접두사 문제를 후처리 과정에서 자동으로 해결함. 사용하려면 postcss도 같이 설치해야 함
//구성 파일 생성
npx tailwindcss init -p
postcss가 테일윈드css를 플러그인으로 동작시키려면 postcss.config.js 파일에 테일윈드 css를 등록해야 함
또한 테일윈드css는 별도의 자신만의 구성 파일이 필요함
-> 위 명령어로 postcss.config.js, tailwind.config.js 파일 생성
//daisyui 패키지 설치
npm i -D daisyui
daisyui: 무료로 제공하는 컴포넌트가 가장 많은 테일윈드CSS의 플러그인
//@tailwindcss/line-clamp 플러그인 설치
npm i -D @tailwindcss/line-clamp
@tailwindcss/ 접두사가 붙은 패키지는 테일윈드CSS 제작사가 직접 만들어 제공하는 것으로, 테일윈드css 기본에는 없는 기능을 추가로 사용할 수 있게 함
line-clamp: 여러 줄의 텍스트를 지정한 줄 수로 잘라서 표시해주는 플러그인
테일윈드 구성 파일 수정하기
//tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
safelist: [{pattern: /^line-clamp-(\d+)$/}],
plugins: [require('@tailwindcss/line-clamp'), require('daisyui')],
}
테일윈드CSS 기능 반영하기
//src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
개발서버를 열어서 제대로 설치되었음을 확인한다 (껐다 켜야 되더라)
테일윈드CSS 색상 클래스
이름 규칙
무채색: 접두사-색상명/불투명도 (e.g. bg-black/70)
유채색: 접두사-색상명-채도/불투명도 (e.g. border-sy-200)
불투명도는 20~100 사이, 채도는 50,100,200,... 등 10개 번호로 세분
테일윈드CSS 텍스트 설정
1) 크기
1rem: 기본 글꼴 기준 'M' 문자의 높이
태그와 무관하게 글자 크기 조절
text-xs, text-sm, text-base(기준. size 1rem, line-height 1.5rem), text-lg, text-xl, text-2xl ... 사용. rem 단위로 사용중인 웹 브라우저 관점에서 크기 설정
2) 두께
font- 접두사에 thin / light / normal / medium / bold / black 사용
(font-weight값: 100 / 300 / 400 / 500 / 700 / 900)
3) 기울임
italic / non-italic
4) 줄바꿈 문자
white space : 줄바꿈 문자 \n. 어떻게 해석할지 CSS에서 설정 가능
whitespace-접두사에 normal / nowrap / pre / pre-line / pre-wrap 사용
5) 정렬
css text-align 속성. 테일윈드에서는 text-로 글자크기와 같은 접두사를 사용
text- 접두사에 left / center / right / justify 사용
6) 텍스트 표시 줄 수
line-clamp 플러그인에서 제공.
line-clamp-3 : 텍스트가 아무리 길어도 최대 3줄로 출력, 텍스트 맨 뒤에 생략 문자열(...)을 붙임
테일윈드CSS 텍스트 컴포넌트 구현
클래스명을 좀 더 쉬운 형태로 사용하게 해주는 유틸리티 함수 작성
//src/components/textUtil.ts
export const makeClassName = (setting: string, _className?: string, numberOfLines?: number) =>
[setting, numberOfLines ? `line-clamp-${numberOfLines}` : '',
_className].join(' ');
제목에 사용할 Title 컴포넌트
font-bold text-5xl... 처럼 Title에서 사용할 기본 스타일을 지정하고, 컴포넌트 사용시 className으로 상황에 맞는 스타일을 추가해줄 수 있다.
//src/components/Texts.tsx
import type { DetailedHTMLProps, FC, HTMLAttributes } from "react";
import { makeClassName } from "./textUtil";
type TextProps = DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
export type TitleProps = TextProps & {
numberOfLines?: number
}
export const Title: FC<TitleProps> = ({
className: _className,
numberOfLines,
...props
}) => {
const className = makeClassName(
'font-bold text-5xl text-center whitespace-pre-line',
_className,
numberOfLines,
)
return <p {...props} className={className}/>
}
나머지 컴포넌트도 스타일만 다르고 같은 방식으로 구현
export type SubtitleProps = TitleProps & {}
export const Subtitle: FC<SubtitleProps> = ({
className: _className,
numberOfLines,
...props
}) => {
const className = makeClassName(
'font-semibold text-3xl text-center whitespace-pre-line',
_className,
numberOfLines,
)
return <p {...props} className={className} />
};
export type SummaryProps = SubtitleProps & {}
export const Summary: FC<SummaryProps> = ({
className: _className,
numberOfLines,
...props
}) => {
const className=makeClassName(
'text-sm whitespace-pre-line',
_className,
numberOfLines,
)
return <p {...props} className={className} />
}
export type ParagraphProps = SummaryProps & {}
export const Paragraph: FC<ParagraphProps> = ({
className: _className,
numberOfLines,
...props
}) => {
const className=makeClassName(
'font-normal text-base whitespace-pre-line',
_className,
numberOfLines,
)
return <p {...props} className={className} />
}
잘 적용된다.
3-3. CSS 상자 모델 이해하기
상자 모델이란?
HTML태그가 웹 브라우저 화면에 모두 상자 모양을 ㅗ보이는 것을 ㅗ델링한 것
컨테이너와 콘텐츠, box-sizing 스타일 속성
CSS에서 부모 요소를 컨테이너, 자식 요소를 콘텐츠라고 표현함
box-sizing: content-box(default) | padding-box | border-box | inherit
테일윈드 CSS에서 box-sizing: box- 접두사에 border/ content 사용
캐스케이딩
자식요소의 스타일 속성값을 설정하지 않으면 부모요소에 설정한 속성값이 내려와 적용됨
어떤 부모요소에도 명시되어 있지 않으면 최상단 부모인 <html> 요소의 값이 적용됨
뷰포트
웹 페이지에서 사용자가 볼 수 있는 영역
뷰포트 관점에서 단위 : 넓이(vm) / 높이(vh). 1~100까지 값을 가지며 퍼센트 개념임
테일윈드 CSS에서 뷰포트 크기: w-screen / h-screen
ㄴwidth / height를 100vw/vh로 설정
+) w-full / h-full로 퍼센트 단위 사용 가능 (100%)
테일윈드 CSS의 길이 관련 클래스
width는 w-, height는 h- 뒤에 숫자(분수 형태도 가능. 이건 퍼센트로 적용)
숫자 단위는 기본적으로 rem
숫자의 기준은 4 (w-4는 1rem, h-4도 1rem) -> 1씩 증가나 감소할 때 0.25rem 단위로 변화
padding
부모 요소와 콘텐츠 간의 간격 (내부여백)
테일윈드에서 접두사 p- 에 숫자로 사용 (이것도 숫자기준은 4. p-4: padding 1rem)
방향은 px- py- / pt- pb- pl- pr- 로 지정가능
margin
html 요소와 인접한 요소 간의 간격을 결정하는 슽아리 속성 (외부여백)
테일윈드에서 m-숫자로 표현 (mx- my- mt- ml- mb- mr- 로 방향 지정)
background-image
url(이미지URL) 형식으로 사용
img 요소의 특정 높이 고정 어려움 문제 때문에 img요소 대신 사용
background-size
테일윈드에서 bg- auto/cover(기본)/contain 사용
border
테일윈드에서
1) 테두리 굵기
border- t- / r- / b- / l- (방향 안 쓰면 그냥 border-width) (숫자 안 쓰면 1px)
2) 테두리 스타일
border- solid / dashed / dotted / double / none
3) border-radius 스타일
rounded : 0.25rem
rounded- full : 9999px
rounded- sm / md / lg / xl / 2xl : 0.125rem부터 1rem까지 점점 커짐
rounded- t- / b- / tl- / tr- / bl- / br- 로 모서리 지정 가능
display
테일윈드 클래스
hidden (=display:none;)
block / inline-bloc / inline / flex
visibility
visible / invisible
display: none인 요소의 크기는 화면에 반영되지 않지만, visibility: hidden은 화면에 보이지 않아도 요소의 크기가 반영됨
position
absolute : 포지션값을 가진 가장 가까운 부모 기준
relative : 자기 자신을 기준
z-index
요소의 쌓임 맥락
z- 접두사에 0, 10, ... 50의 숫자를 부여. (or auto)
+) Overlay 컴포넌트 (모달 대화 상자에서 클릭 불가한 바깥 부분) 만들기
화면을 꽉 채우는 div 요소의 position을 absolute로 주고 z-index를 높게 설정 -> 모달 바깥을 클릭할 수 없게 함
//Overlay component
import { FC } from "react";
import { Div, ReactDivProps } from "./Div";
export type OverlayProps = ReactDivProps & {
opacityClass?: string
}
export const Overlay: FC<OverlayProps> = ({
className: _className,
opacityClass,
children,
...props
}) => {
const className = [
_className,
'absolute z-50 w-screen h-screen',
opacityClass ?? 'bg-black/70',
'flex items-center justify-center'
].join(' ');
return(
<Div {...props} className={className} top="0" left="0">{children}</Div>
)
}
3-4. 플렉스 레이아웃 이해하기
플렉스박스 레이아웃 : display: flex 속성을 가진 컨테이너 안에 콘텐츠 아이템을 배치한 것
flex-direction
flex- 접두사 + row(기본값) / row-reverse / column / column-reverse
overflow
overflow- + auto / hidden / visible / scroll
flex-wrap
wrap: 콘텐츠를 더이상 배치할 공간이 없으면 자동으로 다음 줄에 배치
flex-wrap / flex-wrap-reverse / flex-nowrap
min-width max-width
부모 컨테이너의 크기에 대응하는 콘텐츠의 최소 / 최대 크기 설정
min-w- / max- / min-h- / max-h- + 숫자
justify-content
플렉스 컨테이너의 콘텐츠 요소들을 주축 기준으로 조정 (가로정렬이면 수평, ...)
justify- + start / end / center / between / around / evenly
between : space-between. 컨테이너 양끝을 기준으로 요소 간 같은 거리를 두어 배치
around: space-around. 요소 간 같은 거리를 두어 배치. between과 다르게 양끝에 요소가 배치되지 않음. 양끝에도 간격
align-items
플렉스 컨테이너의 콘텐츠 요소들을 주축에 수직인 축 기준으로 조정
items- + start / end / center / baseline / stretch
stretch: 컨테이너만큼 늘려서 배치. 콘텐츠 아이템의 높이가 명시되지 않아야 함
3-5. daisyui CSS 컴포넌트 이해하기
CSS 컴포넌트 : html 요소의 스타일링을 쉽게 하기 위해 CSS 프레임워크(or 라이브러리)가 제공하는 CSS 클래스
색상 테마
주 색상: 웹페이지에서 가장 많이 사용되는 색상
보조 색상: 두 번째로 많이 사용되는 색상
daisyui의 btn btn-primary 컴포넌트 구현
import { ButtonHTMLAttributes, DetailedHTMLProps, FC } from "react";
export type ReactButtonProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>
export type ButtonProps = ReactButtonProps & {}
export const Button: FC<ButtonProps> = ({className: _className, ...buttonProps}) => {
const className = ['btn', _className].join(' ')
return <button {...buttonProps} className={className}/>
}
위 코드는 className btn을 생략할 수 있도록 편의성을 보강했다.
버튼 크기 설정
btn- lg / md / sm / xs
4장: 함수 컴포넌트와 리액트 훅
4-1. 처음 만나는 리액트 훅
리액트 훅
useMemo, useState... 등 use 접두사가 이름에 들어가는 일련의 함수. 함수 컴포넌트에서만 사용 가능
클래스 컴포넌트 구현의 복잡함과 모호함을 극복하기 위해 만들어짐. 함수 컴포넌트에서 다양한 기능을 구현할 수 있게 함
매개변수 개수 | 훅 함수 |
1개 | useState, useRef, useImperativeHandle, useContext |
2개(의존성 목록 사용) | useMemo, useCallback, useReducer, useEffect, useLayoutEffect |
* 의존성 목록에 있는 아이템 중 하나라도 변화가 있으면 콜백 함수를 새로고침함
+) setInterfval API
const id = setInterval(콜백함수, 갱신주기)
id값을 반환함. 갱신 주기마다 콜백함수를 호출. 더이상 호출하지 않으려면 clearInterval(id) 사용 (메모리누수 방지)
useEffect
useEffect(콜백함수, 의존성목록)
의존성목록 조건 중 어느 하나라도 충족되면 그때마다 콜백함수 다시 실행
컴포넌트 생성 시 한 번만 실행하려면 의존성 목록에 빈 배열 [] 할당
useState
const [현재값, setter] = useState(초깃값)
변수와 해당 변수값을 세팅하는 함수로 구성
세터 호출하여 변수값을 변경
useState와 useEffect로 현재 시간을 실시간으로 출력하는 시계 페이지 만들기
import { useEffect, useState } from 'react';
import './App.css';
import Clock from './pages/Clock';
function App() {
const [today, setToday] = useState(new Date());
useEffect(() => {
const duration=1000;
const id = setInterval(() => {
setToday(new Date());
}, duration)
return () => clearInterval(id);
}, []);
return(
<Clock today={today} />
);
}
export default App;
setInterval에서 1초(1000밀리초) 간격으로 setToday를 호출하므로 1초마다 새 date를 갱신하게 된다.
setToday()는 today 값이 변경되면 컴포넌트를 자동으로 다시 렌더링함
커스텀 훅
여러 훅 함수를 조합한 새로운 훅 함수. use- 접두어를 붙여서 만듦
위에서 만든 시계를 커스텀 훅으로 만들어보자
//useInterval.ts
import { useEffect } from "react"
export const useInterval = (callback: () => void, duration: number=1000) => {
useEffect(() => {
const id = setInterval(callback, duration);
return () => clearInterval(id);
}, [callback, duration]);
}
//useClock.ts
import { useState } from "react"
import { useInterval } from "./useInterval";
export const useClock = () => {
const [today, setToday] = useState(new Date());
useInterval(()=>setToday(new Date()));
return today;
}
//App.tsx
import './App.css';
import Clock from './pages/Clock';
import { useClock } from './hooks';
function App() {
const today = useClock();
return(
<Clock today={today} />
);
}
export default App;
리액트 훅 함수의 특징
1) 같은 리액트 훅을 여러번 호출 가능
2) 함수 몸통이 아닌 몸통 안 복합 실행문의 { } 안에서 호출할 수 없음
지역 변수 블록 안에서 쓰지 말라는 뜻이다 (if, for문 등에서도 마찬가지)
3) 비동기 함수를 콜백 함수로 사용할 수 없음 (콜백에 async 쓰지 말라는 뜻)
4-2. useMemo와 useCallback 훅 이해하기
상태(state) : 변수의 유효 범위와 무관하게 계속 유지하는 값
불변 상태(immutable state) : 한 번 설정되면 값을 변경할 수 없는 읽기 전용 상태
가변 상태(mutable state) : 아무 때나 값을 변경할 수 있고 계속 유지함
캐시(cache) : 데이터나 값을 미리 복사해놓는 임시 저장소. 원본 데이터에 접근하는 시간이 오래 걸리거나 값을 다시 계산하는 시간을 절약하고 싶을 때 주로 사용. 데이터를 캐시하면 리렌더링 시 속도가 빠름.
의존성(dependancy) : 캐시를 갱신하게 하는 요소. 의존성 목록 중 하나라도 충족되면 캐시된 값을 자동으로 갱신하고 해당 컴포넌트를 다시 렌더링해 변경사항을 반영함.
useMemo
데이터를 캐시하는 용도로 제공하는 훅. 불변 상태를 캐시함
메모이제이션 : 과거에 계산한 값을 반복해서 사용할 때 과거의 계산값을 캐시해 두는 코드 최적화 기법
const 캐시된데이터 = useMemo(콜백함수, [의존성1, 의존성2, ...])
의존성에 변화가 생길 떄마다 콜백 함수를 자도으로 호출하여 의존성을 반영.
useCallback
콜백 함수를 캐시하는 용도로 제공하는 훅
리렌더링때마다 콜백함수가 재생성되는 비효율성을 해결
const 캐시된콜백함수 = useCallback(원본콜백함수, 의존성목록)
+) 고차함수
다른 함수를 반환하는 함수
함수의 타입 불일치를 해결하기 위해 사용.
//고차함수로 구현한 콜백함수 예시
const onClick = useCallback((name: string) => () => alert(`${name} clicked`), []);
4-3. useState 훅 이해하기
useState
가변 상태를 캐시함
useState훅이 반환한 setter 함수는 리액트 프레임워크가 컴포넌트 내부의 상태변화를 쉽게 감지할 수 있게 해줌. (=세터 함수가 호출되면 컴포넌트 상태가 변화했다 판단하고 해당 컴포넌트를 리렌더링)
import { ChangeEvent, useState } from "react";
export default function InputTest() {
const [value, setValue] = useState<string>('');
const [checked, setChecked] = useState<boolean>(false);
const onChangeValue = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
}
const onChangeChecked = (e: ChangeEvent<HTMLInputElement>) => {
setChecked(e.target.checked);
}
return (
<section className="mt-4">
<h2 className="text-3xl font-bold text-center">InputTest</h2>
<div className="flex items-center justify-center p-4 mt-4">
<input type="text" value={value} onChange={onChangeValue} className="bg-sky-100" />
<input type="checkbox" checked={checked} onChange={onChangeChecked} />
</div>
</section>
)
}
세터함수를 이용해 적절한 함수를 만들어 인풋 텍스트 밸류값을 변경하거나 체크 상태를 변경할 수 있다.
FormData 클래스
자바스크립트 엔진이 기본으로 제공하는 클래스로서 사용자가 입력한 데이터들을 웹서버에 전송할 목적으로 사용
import { ChangeEvent, FormEvent, useCallback, useState } from "react";
export default function BasicForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const onSubmit = useCallback((e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData();
formData.append('name', name);
formData.append('email', email);
alert(JSON.stringify(Object.fromEntries(formData), null, 2))
}, [name, email])
const onChangeName = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setName(notUsed => e.target.value)
}, [])
const onChangeEmail = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setEmail(notUsed => e.target.value)
}, [])
return (
<section className="mt-4">
<h2 className="text-3xl font-bold text-center">BasicForm</h2>
<div className="flex justify-center mt-4">
<form onSubmit={onSubmit}>
<div className="form-control">
<label className="label" htmlFor="name">
<span className="label-text">Username</span>
</label>
<input value={email} onChange={onChangeEmail}
id="email" type="email" placeholder="enter your email"
className="input-primary" />
</div>
<div className="flex justify-center mt-4">
<input type="submit" value="SUBMIT"
className="w-1/2 btn btn-sm btn-primary" />
<input type="button" value="CANCEL" className="w-1/2 ml-4 btn btn-sm" />
</div>
</form>
</div>
</section>
)
}
쓰면서 느낀건데 부트스트랩같은 기본 제공 스타일이 편하긴 편하다
근데 어차피 디자인은 자체제작인데 쓸 일이 있나 싶기도 하고..
이런거 할 때는 편한듯
객체타입 값에서 useState
위 코드처럼 name, email을 각각 상태값으로 만들 수도 있지만 객체의 속성 형태로도 구현할 수 있다
type FormType = {
name: string
email: string
}
const [form, setForm] = useState<FormType>({name: '', email: ''});
이때 setForm처럼 객체 타입을 복사하는 경우 그냥 변수 값 줘서 복사하면 얕은 복사가 되기에 리액트가 컴포넌트를 리렌더링하지 않는다. Object.assign()이나 전개연산자를 사용해 깊은 복사를 해줘야 알아들음 (배열 타입도 마찬가지)
import { ChangeEvent, FormEvent, useCallback, useState } from "react";
type FormType = {
name: string
email: string
}
export default function BasicForm() {
const [form, setForm] = useState<FormType>({name: '', email: ''});
const onSubmit = useCallback((e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
alert(JSON.stringify(form));
}, [form])
const onChangeName = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setForm(state => ({...state, name: e.target.value}))
}, [])
const onChangeEmail = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setForm(state => ({...state, email: e.target.value}))
}, [])
return (
<section className="mt-4">
<h2 className="text-3xl font-bold text-center">BasicForm</h2>
<div className="flex justify-center mt-4">
<form onSubmit={onSubmit}>
<div className="form-control">
<label className="label" htmlFor="name">
<span className="label-text">Username</span>
</label>
<input value={form.name} onChange={onChangeName}
id="name" type="text" placeholder="enter your name"
className="input-primary" />
<input value={form.email} onChange={onChangeEmail}
id="email" type="email" placeholder="enter your email"
className="input-primary" />
</div>
<div className="flex justify-center mt-4">
<input type="submit" value="SUBMIT"
className="w-1/2 btn btn-sm btn-primary" />
<input type="button" value="CANCEL" className="w-1/2 ml-4 btn btn-sm" />
</div>
</form>
</div>
</section>
)
}
4-4. useEffect와 useLayoutEffect 훅 이해하기
컴포넌트의 생명 주기
생명 주기: 리액트 프레임워크가 컴포넌트를 생성하고 렌더링하다가 어떤 시점이 되면 소멸하는 과정
마운트 : 컴포넌트가 가상DOM 객체 형태로 생성되어 어떤 시점에 물리 DOM 트리의 멤버 객체가 되는 과정에서 처음 렌더링되는 시점
언마운트 : 컴포넌트가 물리DOM 객체로 있다가 소멸하는 것
useEffect
useEffect(콜백함수, 의존성목록)
useLayoutEffet(콜백함수, 의존성목록)
useLayoutEffect는 동기로 실행됨 (=콜백 함수가 끝날 때까지 프레임워크가 기다림)
useEffect는 비동기로 실행됨 (=콜백 함수의 종료를 기다리지 않음)
얘를 이용해서 이벤트리스너를 부착하고 이어서 remove메서드까지 호출해주는 커스텀훅을 만들수있다..
//src/hooks/useEventListener.ts
import { useEffect } from "react"
export const useEventListener = (
target: EventTarget | null,
type: string,
callback: EventListenerOrEventListenerObject | null,
) => {
useEffect(() => {
if(target && callback){
target.addEventListener(type, callback);
return () => target.removeEventListener(type, callback);
}
}, [target, type, callback])
}
그리고 이걸 이용해서 윈도우 사이즈를 리사이징하는 커스텀훅도 만들수있다
import { useEffect, useState } from "react"
import { useEventListener } from "./useEventListener";
export const useWindowResize = () => {
const [widthHeight, setWidthHeight] = useState([0,0]);
useEffect(() => {
setWidthHeight(notUsed => [window.innerWidth, window.innerHeight])
}, [])
useEventListener(window, 'resize', () => {
setWidthHeight(notUsed => [window.innerWidth, window.innerHeight])
})
return widthHeight;
}
fetch() 함수, Promise 클래스
fetch() : HTTP 메서드(GET, POST, PUT...)를 쉽게 사용하게 해주는 API. blob(), json(), text()와 같은 메서드가 있는 Response 타입 객체를 Promise 방식으로 얻을 수 있게 해줌
Promise 클래스: 비동기 콜백 함수를 쉽게 구현하려고 만든 것. then(), catch(), finally() 메서드 제공
then() : 모든 게 정상일 때 설정된 콜백 함수를 호출
catch() : 오류가 발생할 때 Error 타입의 값을 콜백함수의 입력 매개변수로 전달해 호출
finally() : then이나 catch의 콜백함수가 호출된 다음 항상 자신에 설정된 콜백함수를 호출
fecth로 얻은 데이터에서 일부만 가져오려면 가져올 데이터 타입을 선언해준다
4-5. useRef와 useImperativeHandle 훅 이해하기
ref 속성
물리DOM 객체의 참조. 초기에는 null이었다가 마운트되는 시점에서 물리DOM 객체의 값이 됨
리액트 요소가 물리DOM 상태일 때만 호출할 수 있는 메서드에서 ref속성값을 사용해 메서드를 호출할 수 있음
Ref<T> (T: DOM타입) 타입을 가짐
useRef
import { useCallback, useRef } from "react"
export default function ClickTest() {
const inputRef = useRef<HTMLInputElement>(null);
const onClick = useCallback(() => inputRef.current?.click(), []);
return (
<section className="mt-4">
<h2 className="text-2xl font-bold text-center">ClickTest</h2>
<div className="flex justify-center mt-4">
<button className="mr-4 btn btn-primary" onClick={onClick}>
Click me
</button>
<input ref={inputRef} className="hidden" type="file" accept="image/*" />
</div>
</section>
)
}
input요소는 hidden으로 보이지 않게 해두고, 버튼을 누르면 input요소의 ref에 click()메서드가 호출되기에 실제로 누른 건 버튼이지만 input 요소를 누른 것과 같은 동작을 하게 된다
input요소는 꾸미기가 어려우니 이런 방식을 쓰는 듯하다... 근데 그냥 htmlFor을 쓰면 되지 않나 싶기도
FileReader 클래스
File 타입 객체를 읽을수 있도록 제공되는 클래스
readAsDataUrl() 메서드로 File 타입 객체를 읽어서 문자열로 된 이미지를 제공함. (base64 인코딩)
이를 이용해 여러 이미지 파일을 Promise 객체로 가져오는 함수를 아래와 같이 만들 수 있다.
export const imageFileReaderP = (file: Blob) =>
new Promise<string>((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e: ProgressEvent<FileReader>) => {
const result = e.target?.result
if(result && typeof result === 'string') resolve (result);
else reject(new Error(`imageFileReaderP : can't read image file`));
}
fileReader.readAsDataURL(file)
})
눌러서 이미지를 업로드하거나 직접 이미지를 드래그해서 띄울 수 있는 페이지
import { ChangeEvent, useCallback, useMemo, useRef, useState, DragEvent } from "react"
import { imageFileReaderP } from "../utils/imageFileReaderP";
export default function FileDrop() {
const [imageUrls, setImageUrls] = useState<string[]>([]);
const [error, setError] = useState<Error|null>(null);
const [loading, setLoading] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null);
const onDivClick = useCallback(() => inputRef.current?.click(), []);
const makeImageUrls = useCallback((files: File[]) => { //사용자가 선택한 이미지 파일들을 imageUrls에 계속 추가
const promises = Array.from(files).map(imageFileReaderP)
Promise.all(promises)
.then(urls => setImageUrls(imageUrls => [...urls, ...imageUrls]))
.catch(setError)
.finally(() => setLoading(!loading))
}, [setLoading])
const onInputChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setError(null);
const files = e.target.files;
files && makeImageUrls(Array.from(files))
}, [makeImageUrls])
const onDivDragOver = (e: DragEvent) => e.preventDefault();
const onDivDrop = useCallback((e:DragEvent) => {
e.preventDefault();
setError(null);
const files = e.dataTransfer?.files
files && makeImageUrls(Array.from(files))
}, [makeImageUrls])
const images = useMemo(() => {
imageUrls.map((url, index) => (
<img key={index} src={url} className="m-2 bg-transparent bg-center bg-no-repeat bg-contain" />
))
}, [imageUrls])
return (
<section className="mt-4">
<h2 className="text-4xl font-bold text-center">FileDrop</h2>
<div className="mt-4">
{loading && (
<div>
<div className="flex items-center justify-center">
<button className="btn btn-circle loading"></button>
</div>
</div>
)}
<div onDragOver={onDivDragOver} onDrop={onDivDrop} onClick={onDivClick} className="flex justify-center pt-5 pb-5 bg-gray-200">
<p className="text-3xl font-bold">drop image or click me</p>
</div>
<input ref={inputRef} onChange={onInputChange} multiple className="hidden"
type="file" accept="image/*" />
</div>
</section>
)
}
forwardRef
부모 컴포넌트에서 생성한 ref를 자식 컴포넌트로 전달해주는 역할
근데 얘네가 왜케 유독 생소한가 했더니 리액트에서는 ref를 이용한 프로그래밍을 지양한다고 한다. 리액트의 지향성과 맞지 않는다고...
4-6. useContext 훅 이해하기
컨텍스트
속성 전달의 번거러움을 해소하기 위해 구현한 매커니즘
부모 컴포넌트에서 createContext로 공유할 정보를 설정하면 자식 컴포넌트에서 useContext로 공유 정보를 취득할 수 있다. 컨텍스트 기능을 사용하는 리액트와 리액트 네이티브 코드는 항상 이름에 Provider가 있는 컴포넌트와 use컨텍스트_이름() 형태의 커스텀 훅을 사용한다.
createContext
type ContextType = {
//공유할 데이터 속성
}
const defaultContextValue: ContextType = {
//공유할 데이터 속성 초깃값
}
const SomeContext = createContext<ContextType>(defaultContextValue)
컨텍스트 객체가 제공하는 Provider 컴포넌트
createContext 함수호출로 생성된 컨텍스트 객체는 Provider, Consumer 컴포넌트를 제공
Provider : 컨텍스트의 기능을 제공할 컴포넌트
Consumer : Provider가 제공한 기능을 사용하고 싶은 클래스 컴포넌트 (함수 컴포넌트에서는 대신 useContext 훅을 사용)
interface ProviderProps<T> {
value: T;
children?: ReactNode;
}
Provider 컴포넌트는 value와 children 속성이 있는 ProviderProps 속성을 제공함
타입변수 T는 createContext<T> 와 같아야 하고, value에 설정한 값이 Provider가 제공하는 기능이 됨
+) 모든 컨텍스트 제공자는 가장 최상위 컴포넌트로 동작해야 함
useContext
컨텍스트 객체가 제공하는 Provider 컴포넌트의 value 속성값을 얻을 수 있게 하는 목적으로 사용
useContext는 항상 컨텍스트 제공자의 value 속성값을 반환함
창 크기에 따라 테일윈드css의 breakpoint(중단점)을 알려주는 페이지를 작성하자
//App.tsx
import './App.css';
import { ResponsiveProvider } from './contexts/ResponsiveContext';
import ResponsiveContext from './pages/ResponsiveContext';
function App() {
return(
<ResponsiveProvider>
<div>
<ResponsiveContext />
</div>
</ResponsiveProvider>
)
}
export default App;
//src/contexts/ResponsiveContext.tsx
import { FC, PropsWithChildren, createContext, useContext } from "react"
import { useWindowResize } from "../hooks";
type ContextType = {
breakpoint: string
}
const defaultContextValue: ContextType = {
breakpoint: ''
}
export const ResponsiveContext = createContext<ContextType>(defaultContextValue);
type ResponsiveProviderProps = {};
export const ResponsiveProvider: FC<PropsWithChildren<ResponsiveProviderProps>> = ({
children,
...props
}) => {
const [width] = useWindowResize();
const breakpoint = width < 640 ? 'sm' :
width < 768 ?'md' :
width < 1024? 'lg' :
width < 1280? 'xl' : '2xl';
const value = {
breakpoint //breakpoint : breakpoint
}
return <ResponsiveContext.Provider value={value} children={children} />
}
export const useResponsive = () => {
const {breakpoint} = useContext(ResponsiveContext);
return breakpoint;
}
//src/pages/ResponsiveContext.tsx
import { useResponsive } from "../contexts/ResponsiveContext"
export default function ResponsiveContext() {
const breakpoint = useResponsive();
return (
<section className="mt-4">
<h2 className="text-3xl font-bold text-center">ResponsiveContextTest</h2>
<div className="mt-4">
<h3 className="text-2xl font-bold text-center">breakpoint: {breakpoint}</h3>
</div>
</section>
)
}
창 크기를 탐지해서 breakpoint를 표시한다.