본문 바로가기
공부/프로그래밍

[react, next.js, redux] material-ui 를 이용해 다크테마 적용하기(Layout.js 적용)

by demonic_ 2021. 4. 1.
반응형

다크모드는 유튜브를 보면 알 수 있는데 본래 흰바탕의 화면이 검은 바탕의 화면으로 변경되는 것을 말한다.

아래 사진을 보면 단번에 이해될 것이다.

 

일반 유튜브

다크모드 유튜브

 

material-ui 에서는 theme를 이용해 이를 조절할 수 있는데, 이번에 이걸 적용하는 방법에 대해 알아볼 것이다.

 

우선 테마를 등록해야 한다. 다음처럼 ThemeManager.tsx 파일을 만들어 생성하도록 하자.

(여기서는 타입스크립트를 사용했기 때문에 tsx 확장자를 사용한 것이다. js, jsx 로 만들어도 상관없다)

 

여기서는 3개만 생성했다. 원하는 만큼 만들어 등록하는 것도 추천. 테마에 활용할 수 있는 컬러종류는 아래 링크 참조. red, pink, purple indego, teal 등 여러가지 있다.

https://material-ui.com/customization/color/

 

Color - Material-UI

Convey meaning through color. Out of the box you get access to all colors in the Material Design spec.

material-ui.com

import {createMuiTheme} from "@material-ui/core";
import {amber, blue, green, indigo, red} from "@material-ui/core/colors";


/**
 * 테마변경 매니저
 */
const defaultTheme = createMuiTheme({
    palette: {
        primary: indigo,
    },
});
const darkTheme = createMuiTheme({
    palette: {
        type: 'dark',  // type 지정이 중요 
        primary: {
            main: '#2D3032'   // 제공된 색상이 없어 직접 설정
        }
    },
});
const amberTheme = createMuiTheme({
    palette: {
        primary: amber
    },
});


// 테마종류
export enum themes {
    default
    , dark
    , amber
}


function changeTheme(theme:themes) {
    switch (theme as themes) {
        case themes.amber:
            return amberTheme;
        case themes.dark:
            return darkTheme
        default:
            return defaultTheme
    }

    return defaultTheme;
}

export {changeTheme}

 

createMuiTheme로 생성한 코드를 보면 type: 'dark'라는 부분이 있는데 여기서 type이 위에서 본 밝기, 어둡게 모드를 지정하는 것이다. 아무것도 설정하지 않으면 기본 light 버전으로 된다.

 

enum으로 쓴 이유는 typesafe 를 위해 사용한 것이다.

changeTheme를 이용해서 코드에 맞는 테마를 리턴하도록 되었다. 잘못된 코드가 들어오면 기본코드를 리턴하도록 한다.

 

이번엔 Redux에 등록해보도록 하자.

내 경우 로딩바와 같은 레벨로 보기 때문에 GlobalReducer라는 곳에 만들었다.

 

import {ActionType, createAction, createReducer} from 'typesafe-actions'
import { themes } from '../function/comm/ThemeManager'

export interface IGlobalReducer {
    loading: boolean
    theme: themes
}

const initialState: IGlobalReducer = {
    loading: false,
    theme: themes.default
}

export const LOADING_ON = "global/LOADING_ON"
export const LOADING_OFF = "global/LOADING_OFF"
export const THEME_CHANGE = "global/THEME_CHANGE"

export const loadingOn = createAction(LOADING_ON)();
export const loadingOff = createAction(LOADING_OFF)();
export const themeChange = createAction(THEME_CHANGE)<themes>();

export const actions = {loadingOn, loadingOff, themeChange}
type GlobalReducerAction = ActionType<typeof actions>

// 리듀서 추가
const reducer = createReducer<IGlobalReducer, GlobalReducerAction>(initialState, {
    [LOADING_ON]: (state) => ({
        ...state
        , loading: true
    })
    , [LOADING_OFF]: (state) => ({
        ...state
        , loading: false
    })
    , [THEME_CHANGE]: (state, action) => {
        const payload = action.payload;
        return ({
            ...state
            , theme: payload
        })
    }
})

export default reducer

 

dispatch에 THEME_CHANGE 를 이용해 현재 테마를 저장할 수 있도록 했다.

 

그럼 이제 호출하는 부분을 만들어 보자

import {Box, FormControlLabel, Radio, RadioGroup} from "@material-ui/core";
import {themes} from "../../function/comm/ThemeManager";
import React, {useState} from "react";
import {THEME_CHANGE} from "../../reducers/GlobalReducer";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "../../reducers";
import styled from "styled-components";


const InputArea = styled(Box)`
    padding: 5px 10px;
`


const ChangeThemeTest = () => {
    const dispatch = useDispatch();

    const {theme} = useSelector((state:RootState) => state.globalReducer);

    const [choiceTheme, setChoiceTheme] = useState<themes>(theme);

    // 태마변경
    const changeThemeHandler = (value:string) => {
        // 넘어오는게 string 이므로 int로 파싱
        let themeIdx = parseInt(value);
        setChoiceTheme(themeIdx)

        dispatch({
            type: THEME_CHANGE
            , payload: themeIdx
        })
    }

    return (
        <>
            <InputArea>
                테마설정
                <RadioGroup
                    row
                    aria-label="choiceTheme"
                    name="choiceTheme"
                    value={choiceTheme}
                    onChange={(e) => changeThemeHandler(e.target.value)}>
                    <FormControlLabel
                        value={themes.default}
                        control={<Radio />}
                        label={"기본"} />
                    <FormControlLabel
                        value={themes.dark}
                        control={<Radio />}
                        label={"dark"} />
                    <FormControlLabel
                        value={themes.amber}
                        control={<Radio />}
                        label={"amber"} />
                </RadioGroup>
            </InputArea>
        </>
    )
}

export default ChangeThemeTest

 

라디오박스로 설정할 수 있도록 했고, 화면으로 보면 다음과 같다.

스샷 위쪽의 AppBar는 Layout 에서 설정한 것이며 여기서는 신경쓰지 않아도 된다. 다만 테마가 변경될때마다 색상변화를 가장 직관적으로 알 수 있어 배치해놨다.

이제 교체할때마다 Redux 값이 변경되는 것을 확인한다.(관련코드는 아래 추가했으니 밑에 참고)

 

그럼 이제 모든화면에 적용될 수 있도록 설정해보자.

 

ThemeProvider를 이용해 설정해야 한다. 그러기 위해선 최상단에 등록하는 것이 좋다. 내겐 두가지 선택권이 있었는데 _app.tsx 와 Layout.tsx 파일이었다. 여기선 _app.tsx 파일에 설정했다. 그 이유는 아래 설명하겠다.

 

아래는 app.tsx 파일

...
    // material-ui 테마설정
    const {theme} = useSelector((state:RootState) => state.globalReducer);

    return (
        <>
            <ThemeProvider theme={changeTheme(theme)}>
                <CssBaseline />
                    <Layout>
                        <GlobalStyle />
                        <Component {...pageProps} />
                    </Layout>
            </ThemeProvider>
        </>
    )
...

 

아래는 Layout.tsx 파일

 

다크모드의 경우 배경화면을 모두 다크색으로 채워줘야 하는 작업이 필요한데, 그 설정은 직접 넣어줘야했다. 다음과 같은 설정이다.

참고로 type이 기본과 다크모드가 다른데, theme.palette.background에 들어가는 값이 다음과 같다.

기본: {paper: "#fff", default: "#fafafa"}

다크모드: {paper: "#424242", default: "#303030"}

...
  // 테마설정
  const useStyles = makeStyles((theme) => ({
      root: {
        height: "100%",
        background: theme.palette.background.default,
      }
  }))
  const classes = useStyles();

...
  return (
      <div>
          {/* 테마설정에서 classes.root 설정을 여기에, */}
          <div className={classes.root}>
              <TopBar />
              {children}
          </div>
      </div>
  )

 

useStyles 에 커스텀한 스타일을 만들고 그것을 div className에 삽입했다.

 

처음에는 ThemeProvider와 useStyles 설정을 같은곳에 넣었는데 useStyles에 설정한 배경색 변경이 작동하지 않았다. 아마 렌더링을 새로하면서 스타일을 새로 적용 => 배경색 변경이 일어나야 하는데 그부분이 안되는거 같았다. 그래서 두개의 설정을 각각 분리하여 설정했다. 참고로 _app.tsx 는 최상단, Layout은 다음으로 감싸는 포멧이다.

 

이제 이것이 작동하는 것을 구현하자.

changeTheme.tsx 라는 컴포넌트를 만들어 라디오버튼을 만들어 각 테마를 연결하도록 했다.

import {Box, FormControlLabel, Radio, RadioGroup} from "@material-ui/core";
import {themes} from "../../function/comm/ThemeManager";
import React, {useState} from "react";
import {THEME_CHANGE} from "../../reducers/GlobalReducer";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "../../reducers";
import styled from "styled-components";


const InputArea = styled(Box)`
    padding: 5px 10px;
`


const ChangeTheme = () => {
    const dispatch = useDispatch();

    const {theme} = useSelector((state:RootState) => state.globalReducer);

    const [choiceTheme, setChoiceTheme] = useState<themes>(theme);

    // 태마변경
    const changeThemeHandler = (value:string) => {
        // 넘어오는게 string 이므로 int로 파싱(enum 대비)
        let themeIdx = parseInt(value);
        setChoiceTheme(themeIdx)

        dispatch({
            type: THEME_CHANGE
            , payload: themeIdx
        })
    }

    return (
        <>
            <InputArea>
                테마설정
                <RadioGroup
                    row
                    aria-label="choiceTheme"
                    name="choiceTheme"
                    value={choiceTheme}
                    onChange={(e) => changeThemeHandler(e.target.value)}>
                    <FormControlLabel
                        value={themes.default}
                        control={<Radio />}
                        label={"기본"} />
                    <FormControlLabel
                        value={themes.dark}
                        control={<Radio />}
                        label={"dark"} />
                    <FormControlLabel
                        value={themes.amber}
                        control={<Radio />}
                        label={"amber"} />
                </RadioGroup>
            </InputArea>
        </>
    )
}

export default ChangeThemeTest

 

dispatch를 이용해 store값을 변경했고, 변경된 값은 _app.tsx 에서 설정된 ThemeProvider에 즉각 반영되도록 했다.

 

이제 실행되는 것을 아래 스크린샷에서 확인해보자.

 

기본 테마

dark 테마

amber 테마

 

 

 

끝.

반응형

댓글