Context?

Context는 리액트에서 데이터를 전역적으로 관리해야할 때 사용한다. 자주 쓰이는 예로는 현재 로그인한 유저, 테마 등이 있다. 앞서 언급한 대로 Context는전역(Global)값이고, 여러 단계로 nesting된 컴포넌트에 데이터를 전달하는 것이 목적이기 때문에 props를 통해 전달할 필요가 없다. 그런데 이때 Context를 사용하게 되면 재사용이 어려워지기 때문에 꼭 필요할 때만 써야한다. 다음 예시를 보자.

 

<Page user={user} avatarSize={avatarSize} />

// ...

<PageLayout user={user} avatarSize={avatarSize} />

// ...

<NavigationBar user={user} avatarSize={avatarSize} />

// ...

<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

 

여러 컴포넌트에 의해 전달되고 있는 데이터인 useravatarSize를 사용하는 컴포넌트는 실제로 Avatar뿐이다. 매우 번거로운 과정인 것을 볼 수 있다. 또한 Avatar에서 필요한 props가 하나 더 늘어날 경우, 모든 props를 전달하는 컴포넌트에 하나씩 더 추가를 해야하는 상황이 생긴다. 이럴 때는 아래와 같이 Context를 사용하지 않고 Avatar컴포넌트 자체를 넘겨주는 방식으로 해결할 수 있다. 더 자세한 내용은 공식 문서를 참고한다.

 

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

<Page user={user} avatarSize={avatarSize} />

// ...

<PageLayout userLink={...} />

// ...

<NavigationBar userLink={...} />

// ...

{props.userLink}

 

API

React.CreateContext

const MyContext = React.createContext(defaultValue);

 

위와 같이 Context 객체를 생성한다. defaultValue는 컴포넌트가 밑에서 설명할 Provider가 제공하는 값을 읽지 못했을 때 가져올 기본 값을 지정한다.

Context.Provider

<MyContext.Provider value={ 공유할 데이터 } />

 

Providervalue라는 props를 받아서 이 값을 컴포넌트들에게 전달하는 역할을 한다. 또한 Context의 값이 바뀔 경우 useContext를 통해 Context값을 읽는 컴포넌트들에 변화를 알려주는 역할도 하고 있다. 따라서 Providervalue값이 바뀔 경우 useContext를 사용한 모든 컴포넌트들이 리렌더링 된다.

 

DarkMode

먼저 theme.ts를 만들고 아래와 같이 작성한다. 그리고 테마를 타입으로서 사용할 수 있게 Theme라는 이름으로 선언해준다.

 

// theme.ts

export const lightTheme = {
  body: '#fcfcfc',
  text: '#363537',
  toggleBackground: '#fcfcfc',
  mainColor: '#e6328d',
  navBar: '#fcfcfc',
};

export const darkTheme = {
  body: '#252424',
  text: '#fcfcfc',
  toggleBackground: '#3b3b3b',
  mainColor: '#fcfcfc',
  navBar: '#303030',
};

export type Theme = typeof lightTheme;

 

 

global-styles.ts파일을 작성한다. App.tsx에서 GlobalStyle에 테마가 변경될 때마다 전달을 해주고, GlobalStyle은 변경된 테마에 맞는 색을 적용하는 역할을 한다.

 

// global-styles.ts

import { createGlobalStyle } from 'styled-components';
import reset from 'styled-reset';

interface ThemeInterface {
  theme: {
    body: string;
    text: string;
    toggleBackground: string;
    mainColor: string;
    navBar: string;
  };
}

export const GlobalStyle = createGlobalStyle<ThemeInterface>`
    ${reset}
    * {
        box-sizing: border-box;
    }
    body {
        font-family: 'NanumSquare', sans-serif;
        background: ${({ theme }) => theme.body};
        color: ${({ theme }) => theme.text};
        transition: all 0.5s ease-in-out;
    }
    button {
        background: none;
        cursor: pointer;
        border: none;
        outline: none;
        transition: all 0.5s ease-in-out;
    }
    ol, ul, li {
        list-style: none;
    }
    a {
        text-decoration: none;
        cursor: pointer;
    }
    img {
        width: 100%;
        height: 100%;
    }
`;

 

이제 App.tsx에서 Context객체인 ThemeContext를 만들어주자.

 

interface ContextProps {
  theme: Theme;
  toggleTheme: () => void;
}

// ThemeContext 객체 생성
export const ThemeContext = createContext<ContextProps>({
  theme: lightTheme,		// 테마(라이트, 다크)
  toggleTheme: () => {		// 테마 변경하는 함수
    return null;
  },
});

 

위와 같이 ThemeContext라는 객체를 생성한다. 

 

// App.tsx

import React, { createContext } from 'react';
import Router from './Router';

import { GlobalStyle } from './global-styles';
import { lightTheme, darkTheme, Theme } from './theme';
import { useDarkMode } from './hooks/useDarkMode';

interface ContextProps {
  theme: Theme;
  toggleTheme: () => void;
}

export const ThemeContext = createContext<ContextProps>({
  theme: lightTheme,
  toggleTheme: () => {
    return null;
  },
});

export default function App() {
  const { theme, toggleTheme } = useDarkMode();

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <>
        <GlobalStyle theme={theme === lightTheme ? lightTheme : darkTheme} />
        <Router />
      </>
    </ThemeContext.Provider>
  );
}

 

ThemeContext.Provider에서 valuethemetoggleTheme을 전달하는 것을 볼 수 있다. 또한 GlobalStyle에 현재 테마를 전달한다. 다음으로는 토글 버튼인 DarkModeToggle.tsx를 작성할 것이다.

 

// DarkModeToggle.tsx

import React, { ReactElement, useContext } from 'react';
import styled from 'styled-components';
import { ThemeContext } from '../App';
import { lightTheme, Theme } from '../theme';

interface ToggleProps {
  theme: Theme;
}

const ToggleButton = styled('button')<ToggleProps>`
  position: fixed;
  width: 115px;
  height: 45px;
  right: 1.5rem;
  bottom: 1.5rem;
  border-radius: 30px;
  cursor: pointer;
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
  align-items: center;
  background: ${({ theme }) => theme.toggleBackground};
  color: ${({ theme }) => theme.text};
  box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2);
  z-index: 10000;

  &:hover {
    filter: brightness(
      ${({ theme }) => (theme === lightTheme ? '0.9' : '1.13')}
    );
  }
`;

const Emoji = styled.figure`
  width: 33px;
  height: 33px;
  border-radius: 100%;
  font-size: 1.5rem;
  display: flex;
  justify-content: center;
  align-items: center;
`;

const ModeContent = styled.p`
  font-size: 0.8rem;
  margin-left: 5px;
`;

export default function DarkModeToggle(): ReactElement {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <ToggleButton onClick={toggleTheme} theme={theme}>
      {theme === lightTheme ? (
        <>
          <Emoji>
            <span role="img" aria-label="darkMoon">
              🌚
            </span>
          </Emoji>
          <ModeContent>다크 모드</ModeContent>
        </>
      ) : (
        <>
          <Emoji>
            <span role="img" aria-label="lightSun">
              🌞
            </span>
          </Emoji>
          <ModeContent>라이트 모드</ModeContent>
        </>
      )}
    </ToggleButton>
  );
}

 

아까 App.tsx에서 Provider가 전달한 themetoggleThemeuseContext를 통해 구독한다. 이때 useContext안에는 Context객체 그대로가 들어가야 한다. 다른 컴포넌트에서 Context를 구독하고 싶을 때도 똑같이 useContext를 통해 간단하게 작성할 수 있다.

 

참고로 TypeScriptReact의 BoilerPlate를 만들면서 포스팅하게 된거라 코드를 그대로 복붙해서 작성을 했다🧑‍🔧 그래서 DarkModeToggle이 놈은 Router를 통해 다른 컴포넌트에서 렌더링이 되는 구조라 여기서는 파악이 되지 않는다. 결과를 바로 보고싶으신 분들은 App.tsx에서 <Router />를 따로 만든 컴포넌트로 바꾸거나 <DarkModeToggle />로 바꾸면 될 것이다. 

 

const { theme, toggleTheme } = useContext(ThemeContext);

 

마지막으로 hooks라는 폴더를 하나 만들고 그 안에 useDarkMode.tsx를 작성하자.

 

// useDarkMode.tsx

import { useEffect, useState } from 'react';
import { lightTheme, darkTheme, Theme } from '../theme';

export const useDarkMode = () => {
  const [theme, setTheme] = useState<Theme>(lightTheme);

  const setMode = (mode: Theme) => {
    mode === lightTheme
      ? window.localStorage.setItem('theme', 'light')
      : window.localStorage.setItem('theme', 'dark');
    setTheme(mode);
  };

  const toggleTheme = () => {
    theme === lightTheme ? setMode(darkTheme) : setMode(lightTheme);
  };

  useEffect(() => {
    const localTheme = window.localStorage.getItem('theme');
    if (localTheme !== null) {
      if (localTheme === 'dark') {
        setTheme(darkTheme);
      } else {
        setTheme(lightTheme);
      }
    }
  }, []);

  return { theme, toggleTheme };
};

 

먼저 마운트가 되면 localStorage에서 theme에 대한 값을 확인하여 localTheme에 담고 theme이라는 상태에 Theme타입으로 저장한다. toggleTheme는 테마를 변경시켜주는 함수이다. 바꿀 때마다 localStorage의 값도 갱신시켜준다. 그리고 현재 테마인 theme와 변경하는 함수 toggleTheme를 리턴한다. 이 리턴한 값은 위에 App.tsx에서 useDarkMode를 통해 사용하는 것을 볼 수 있다. 

 

 

전체 코드는 아래 링크에서 확인할 수 있다🌚

참고


생강강

,