1. styled-components
리액트와 같은 자바스크립트 프레임워크에서 CSS 작성을 도와주는 CSS-in-JS 라이브러리로, 자바스크립트 코드 안에서 스타일을 작성할 수 있게 해준다. 자바스크립트 코드 안에서 스타일을 작성하기 때문에 컴포넌트 단위로 스타일을 지정하여 모듈화된 코드 작성이 가능해져 유지보수와 확장성을 높이는 데 유리하다.
1.1. 특징
- CSS-in-JS: 자바스크립트 파일 안에서 직접 스타일링을 정의 -> 컴포넌트 자체적으로 스타일을 정의해 컴포넌트 간의 스타일이 격리되고 CSS 충돌을 방지할 수 있음
- Tagged Template Literals: 자바스크립트의 Tagged Template Literals 문법을 사용하여 CSS 를 정의하여 CSS 와 JavaScript 를 직접적으로 결합
import styled from 'styled-components';
const Button = styled.button`
background: blue;
color: white;
`;
- 동적 스타일링: 컴포넌트의 props 에 따라 스타일을 동적으로 변경할 수 있음
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
color: white;
`;
// 사용시
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
- CSS 코드 재사용: 스타일을 믹스하고 여러 컴포넌트에서 재사용할 수 있도록 해줌
const buttonStyles = css`
padding: 10px 20px;
border-radius: 5px;
`;
const Button = styled.button`
${buttonStyles}
background: blue;
color: white;
`;
const SecondaryButton = styled.button`
${buttonStyles}
background: gray;
color: white;
`;
- Theming 지원: ThemeProvider 를 통해 전역 스타일을 정의하는 테마 기능을 제공
import { ThemeProvider } from 'styled-components';
const theme = {
primaryColor: 'blue',
secondaryColor: 'gray',
};
const Button = styled.button`
background: ${props => props.theme.primaryColor};
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
`;
// ThemeProvider로 감싸기
<ThemeProvider theme={theme}>
<Button>Primary Button</Button>
</ThemeProvider>
※ 추가 - styled-components 의 props 타입으로는 type 별칭과 interface 만 사용가능!
스타일 컴포넌트에서 props 의 타입을 제네릭으로 정의할 때는 인터페이스나 타입 별칭을 사용하여야 한다.
기본적인 타입 (boolean, string, number) 로 타입만 직접 넣게 되면 에러가 난다.
interface ButtonProps {
primary?: boolean;
}
const Button = styled.button<ButtonProps>`
background: ${props => props.primary ? 'blue' : 'gray'};
color: white;
`;
// 사용시
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
2. React Context API
리액트의 Context API 는 컴포넌트 트리 전체에 걸쳐 데이터를 손쉽게 전달하고, 컴포넌트 간의 상위-하위 관계 없이 상태를 공유할 수 있도록 돕는 기능이다. 이를 통해 상태나 함수를 중간 컴포넌트에 props 로 일일이 전달하지 않고도 손쉽게 공유할 수 있다.
컴포넌트 구조가 깊어질수록 props drilling 이 (필요한 데이터를 하위 컴포넌트에 전달하기 위해 중간 컴포넌트를 하나하나 거쳐서 props 를 전달하는 과정) 발생할 수 있는데, Context API 를 사용하면 prop drilling 을 방지할 수 있다.
2.1. Context API 사용하기
- Context 생성
import React, { createContext } from 'react';
// 테마 컨텍스트에서 사용할 객체 형태를 정의
interface State {
themeName: ThemeName,
toggleTheme: () => void;
}
// 초기 상태 설정 및 컨텍스트 생성 -> 테마 컨텍스트에서 사용할 객체 형태를 정의
export const state: State = {
themeName: DEFAULT_THEME_NAME,
toggleTheme: () => {}
};
// Context 생성
export const ThemeContext = createContext<State>(state);
- provider 컴포넌트 만들기
createContext 로 만든 ThemeContext 는 Provider 를 제공한다.
Provider 는 하위 컴포넌트들에게 value 를 통해 값을 전달하고, 하위 컴포넌트에서는 전달된 값을 useContext 라는 훅을 이용해 사용할 수 있다. (Provider 가 감싸고 있는 하위 컴포넌트에서는 모두 context 에 접근가능)
export const BookStoreThemeProvider = ({children}: {children: ReactNode}) => {
const [themeName, setThemeName] = useState<ThemeName>(DEFAULT_THEME_NAME);
const toggleTheme = () => {
setThemeName(themeName === 'light' ? 'dark' : 'light');
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, themeName === 'light' ? 'dark' : 'light');
};
// ...
return (
<ThemeContext.Provider value={{themeName, toggleTheme}}>
<ThemeProvider theme={getTheme(themeName)}>
<GlobalStyle themeName={themeName} />
{children}
</ThemeProvider>
</ThemeContext.Provider>
);
}
- useContext 를 사용하여 Context 데이터 가져오기
ThemeContext 의 하위 컴포넌트읜 ThemeSwitcher 에서는 useContext 를 사용해서 Context 에 있는 데이터를 사용할 수 있다.
import React, { useContext } from 'react'
import { ThemeContext } from '../../context/ThemeContext';
const ThemeSwitcher = () => {
const { themeName, toggleTheme } = useContext(ThemeContext); // useContext 사용
return (
<div>
<button onClick={toggleTheme}>{themeName}</button>
</div>
)
}
export default ThemeSwitcher
3. 토글 버튼으로 테마 변경 구현하기 (light/dark)
만들려고 하는 기능: 테마스위처
1. 사용자는 토글 UI 를 통해 웹사이트의 색상 테마를 바꿀 수 있다.
2. 색상 테마는 전역상태로 존재
3. 사용자가 선택한 테마는 로컬스토리지에 저장
코드 (일부 코드 생략)
// App.tsx
import React from 'react';
import Home from './pages/Home';
import Layout from './components/layout/Layout';
import ThemeSwitcher from './components/header/ThemeSwitcher';
import { BookStoreThemeProvider } from './context/ThemeContext';
function App() {
return (
<BookStoreThemeProvider>
<ThemeSwitcher />
<Layout>
<Home />
</Layout>
</BookStoreThemeProvider>
);
}
export default App;
// ThemeContext.tsx
import React, { createContext, ReactNode, useEffect, useState } from 'react'
import { ThemeName } from '../style/theme';
import { ThemeProvider } from 'styled-components';
import { getTheme } from '../style/theme';
import { GlobalStyle } from '../style/global';
const DEFAULT_THEME_NAME = 'light';
const THEME_LOCAL_STORAGE_KEY = 'book_store_theme';
interface State {
themeName: ThemeName,
toggleTheme: () => void;
}
// 초기 상태 설정 및 컨텍스트 생성 -> 테마 컨텍스트에서 사용할 객체 형태를 정의
export const state: State = {
themeName: DEFAULT_THEME_NAME,
toggleTheme: () => {}
};
export const ThemeContext = createContext<State>(state);
// children 을 props 로 받아 모든 하위 컴포넌트가 테마를 사용할 수 있도록 설정
export const BookStoreThemeProvider = ({children}: {children: ReactNode}) => {
const [themeName, setThemeName] = useState<ThemeName>(DEFAULT_THEME_NAME);
const toggleTheme = () => { // 테마변경함수
setThemeName(themeName === 'light' ? 'dark' : 'light');
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, themeName === 'light' ? 'dark' : 'light');
};
// localstorage 에 초기값이 있다면 받아오고 없다면 디폴트 테마 적용
useEffect(() => {
const savedThemeName = localStorage.getItem(THEME_LOCAL_STORAGE_KEY) as ThemeName;
setThemeName(savedThemeName || DEFAULT_THEME_NAME);
}, []);
return (
<ThemeContext.Provider value={{themeName, toggleTheme}}>
<ThemeProvider theme={getTheme(themeName)}>
<GlobalStyle themeName={themeName} />
{children}
</ThemeProvider>
</ThemeContext.Provider>
);
}
import React, { useContext } from 'react'
import { ThemeContext } from '../../context/ThemeContext';
// 테마 스위처는 스스로 상태를 가지는 것이 아니라 useContext 에 의존해서 상태를 바꿈
const ThemeSwitcher = () => {
const { themeName, toggleTheme } = useContext(ThemeContext);
return (
<div>
<button onClick={toggleTheme}>{themeName}</button>
</div>
)
}
export default ThemeSwitcher
context 에는 themeName, toggleTheme 라는 값이 들어있고, ThemeContext.Provider 는 해당 값들을 자신의 children 컴포넌트들에게 넘겨준다. children 의 타입을 React.Node 로 지정했기 때문에 모든 리액트 컴포넌트들이 children 자리에 올 수 있다.
ThemeProvider 컴포넌트는 styled-components 에서 제공하는 컴포넌트로 애플리케이션 전체에서 테마를 관리하고 적용하는 데 사용하는 컴포넌트이다. ThemeContext 에서는 어떤 테마인지에 대한 데이터를 제공한다면, ThemeProvider 는 context 에서 가져온 테마 데이터에 해당하는 데이터를 가져와서 하위 컴포넌트에 테마를 제공하는 역할을 한다.
GlobalStyle 컴포넌트는 ThemeProvider 컴포넌트가 제공한 테마를 전역에서 사용할 수 있도록 해주는 역할을 하며, styled-components 에서 제공하는 createGlobalStyle 로 만들 수 있다. GlobalStyle 로 정의된 스타일은 하위 컴포넌트들에게 자동으로 상속되기 때문에 스타일이 특정 컴포넌트에 한정되지 않고 애플리케이션 전역에 걸쳐 스타일이 적용된다.
ThemeSwitcher 는 스스로 상태를 가지는 것이 아니라 useContext 를 사용해서 context 를 읽어온다. 읽어온 데이터를 통해서 테마를 바꾸어준다.
궁금한 점1. Context 에서 사용할 객체에서 상태 변경 함수는 왜 빈 함수?
-> 컴포넌트가 렌더링되면서 자동으로 toggleTheme 는 덮어씌워짐, 상태 변경 함수가 아직 정의되지 않았거나 Provider 로 해당 값이 전달되지 않았을 경우에도 Context 를 사용할 컴포넌트들이 정상적으로 동작할 수 있도록 일단 빈 함수라도 만들어 놓는 것.
궁금한 점2.
- 자동으로 덮어씌워 진다면 왜 굳이 컴포넌트 밖에서 Context 에서 사용할 객체를 만드는지?
- Context 에서 사용할 객체는 원래 컴포넌트 밖에서 만드는건지?
-> 일반적으로 Context 는 리액트의 최적화와 컴포넌트 재렌더링을 효율적으로 관리하기 위해 컴포넌트 바깥에서 정의하는 것이 권장됨.
Context 를 컴포넌트 안에서 정의하게 되면, 컴포넌트가 렌더링될 때마다 새로운 Context 객체가 생성됨 -> 리액트가 전과 동일한 Context 가 아닌 것으로 인식 -> 불필요하게 하위 컴포넌트들이 재렌더링될 수 있음
4. 타입 가드 (Type Guard)
타입 스크립트에서는 변수나 값의 타입을 좁혀서(더욱 구체적인 정보제공) 타입 정보를 제공하여 안정성을 높일 수 있다.
export type ThemeName = "light" | "dark";
type ColorKey = "primary" | "background" | "secondary" | "third"; // 컬러가 계속해서 늘어날 수 있기 때문에
interface Theme {
name: ThemeName,
color: {
[key in ColorKey]: string;
}
}
export const light: Theme = {
name: 'light',
color: {
primary: 'brown',
background: 'lightgray',
secondary: 'blue',
third: 'green'
},
};
(나는 자바스크립트, 파이썬 같은 언어만 써봐서 타입에 익숙하지 않은데, 생각보다 더 빡빡하게 타입을 좁혀놓는 것이 신기하면서 어려웠다. 확실히 타입을 좁혀놓으니 빡빡하긴 해도 실행 전에 오류를 잡기 쉬워서 안정성이 올라갈 수 밖에 없겠다고 느껴졌다. )
5. 자잘자잘 알게된 것
5.1. 리액트 컴포넌트의 children 을 전달하는 두 가지 방법
기본적인 방법으로 컴포넌트 안에 자식을 직접적으로 포함시키는 방법
<Layout><Home /></Layout>
children 을 props 로 전달하는 방법
<Layout children={<Home />} />
5.2. 객체의 속성 타입 선언
export const BookStoreThemeProvider = ({ children }: { children: ReactNode }) => {
// ...
}
{ children } : ReactNode 라고 쓰면 되는거 아닌가? 라고 생각했는데, children 은 객체의 속성이기 때문에 { children: ReactNode } 로 써야한다.
'TIL with Programmers' 카테고리의 다른 글
[TIL] 11/13 회원가입, useForm, http 클라이언트 생성, forwardRef (0) | 2024.11.13 |
---|---|
[TIL] 11/12 styled-components 모르는 것 정리 (0) | 2024.11.12 |
[TIL] 11/8 React.ts 살펴보기 (0) | 2024.11.08 |
[TIL] 11/7 프론트엔드 심화4 - React Beautiful Dnd, firebase 로그인 구현, 배포하기 (0) | 2024.11.07 |
[TIL] 11/6 프론트엔드 심화3 - 모르는 것 정리 (0) | 2024.11.06 |