2. 전체적인 레이아웃 설계하기

2. 전체적인 레이아웃 설계하기

이왕이면 누구나 사용하고 싶게!

모든 개발이 그렇지만, 웹은 사용자가 직접 체감하는 부분이 많다보니

컴포넌트 하나하나를 구성하는데도 많은 고민이 든다.

사람들이 첫 대면을 하는 메인 페이지 또한 어떻게 구성해야 직관적이면서 사용하기 편할 지 계속 고민했다.



들어가기에 앞서

Material-UI의 컴포넌트를 적극 사용했다. CSS 만질 일이 없더라구

따라서 거의 모든 컴포넌트는 JSX 또는 TSX를 사용했다.



어떤 방식으로 구성하지?

그림처럼 메인 페이지에서 스크롤하면서 정보를 얻는 방법이 제일 좋을 것 같았다.

특히 갤럭시 프리미엄 폰이 출시될 때 찾아봤던 소개 페이지가 굉장히 기억에 남았기 때문.

스크롤하면서 여러 사진들이 바뀌는 것도 그렇고, 정보도 눈에 확 들어와 인상적이었다.

자동 채점 서비스이긴 하지만, 학생들이 딱딱한 느낌이 들지 않고 채점하는데 부담되지 않도록 구성하는데 초점을 맞췄다.



삽질 시작⛏

베이스가 없다면 예제 코드를 그대로 따라 쓰면서 어떻게 나오는지 확인해 보는 것이 정말 좋은 것 같다

Material-UI에서는 이미 만들어진 템플릿을 몇 가지 무료로 공개하고 있어, 문법 파악이나 작동 방식을 파악하는데 많은 도움이 됐다.

// file: 'theme.ts'
import { green, grey, red } from '@material-ui/core/colors'
import { createMuiTheme, responsiveFontSizes } from '@material-ui/core/styles'

const rawTheme = createMuiTheme({
    palette: {
        primary: {
            light: '#69696a',
            main: '#28282a',
            dark: '#1e1e1f',
        },
        secondary: {
            light: '#f5f7ff',
            main: '#4a6eff',
            dark: '#282c40',
        },
        warning: {
            main: '#ffc071',
            dark: '#ffb25e',
        },
        error: {
            light: red[50],
            main: red[500],
            dark: red[500],
        },
        success: {
            light: green[50],
            main: green[500],
            dark: green[500],
        },
    },

    typography: {
        fontFamily: "'Sen', 'ELAND_Choice_M', sans-serif",
        fontSize: 14,
        fontWeightLight: 300,
        fontWeightRegular: 400,
        fontWeightMedium: 700,
    },
});

처음 가장 기본이 되는 Raw Theme를 정의했다.

예제 코드에서 테마를 오버라이드 해주고, 웹에서 주로 사용하고 싶은 폰트를 지정해줬다.



// file: 'theme.ts'

//(cont'd)

const theme = {
    ...rawTheme,
    palette: {
        ...rawTheme.palette,
        background: {
            ...rawTheme.palette.background,
            default: rawTheme.palette.common.white,
            placeholder: grey[200],
        },
    },

    typography: {
        ...rawTheme.typography,
        fontHeader,
        h1: {
            ...rawTheme.typography.h1,
            ...fontHeader,
            letterSpacing: 0,
            fontSize: 60,
        },
        h2: {
            ...rawTheme.typography.h2,
            ...fontHeader,
            fontSize: 52,
        },
        h3: {
            ...rawTheme.typography.h3,
            ...fontHeader,
            fontSize: 42,
        },
        h4: {
            ...rawTheme.typography.h4,
            ...fontHeader,
            fontSize: 36,
        },
        h5: {
            ...rawTheme.typography.h5,
            fontSize: 20,
            fontFamily: 'JSDongkang-Regular',
            fontWeight: rawTheme.typography.fontWeightLight,
        },
        h6: {
            ...rawTheme.typography.h6,
            ...fontHeader,
            fontSize: 18,
        },
        subtitle1: {
            ...rawTheme.typography.subtitle1,
            fontSize: 18,
        },
        body1: {
            ...rawTheme.typography.body1,
            fontSize: 14,
        },
    },
};

export default responsiveFontSizes(theme);

그 다음, rawTheme를 그대로 사용하되, 좀 더 세부적으로 디자인을 지정했다.


처음에 저 ...을 보고 굉장히 당황했는데, spread operator, 즉 전개 구문이다. (JS es6에서 추가된 문법이라고 한다)

배열의 값을 받아와 뿌려놓거나, 저 위처럼 배열을 확장해 여러 일을 할 수도 있다고.


궁금해서 ...rawTheme.typography...을 지워 봤더니 오류가 난다.

createMuiTheme가 반환해주는 Theme는 key-value 쌍의 배열이라 바로 .으로 접근할 수 없는 것 같다.


Material-UI에서는 반응형 웹을 위해 폰트 크기를 변경해주는 함수도 제공하고 있는데,

export 문장에서 responsiveFontSizes()가 그것이다.

theme에 이 함수를 감싸서 export하면, 어디에서 사용되든 페이지의 폰트 크기가 창 크기에 따라 변경되는 것을 확인할 수 있다.


다음은 지정된 테마를 입혀주는 ThemeProvider를 적용시킬 차례다.


// file: 'root.js'
import theme from './theme';  // 위에서 작성한 theme 컴포넌트

export default function root(Component) {
    function WithRoot(props) {
        return (
            <ThemeProvider theme={theme}>
                <CssBaseline />
                <Component {...props} />
            </ThemeProvider>
        );
    }

    return WithRoot;
}

처음엔 모든 컴포넌트를 TS로 작성하려고 했으나, 타입을 여러 가지 시도해봐도 도저히 맞지 않는 것이 있어

눈물을 머금고 JS로 돌린 첫 컴포넌트가 되겠다🥺


WithRoot으로 export하는데, import한 곳에서 컴포넌트를 argument로 주면

해당 컴포넌트에 테마를 적용시킨다.

spread operator를 사용한 이유도 WithRoot으로 감싸진 컴포넌트가 여러 자식들을 가질 수 있기 때문이다.


<CssBaseline />은 어느 브라우저를 통해 접속해도 동일한 디자인을 출력할 수 있도록 정규화하는 컴포넌트로,

Material-UI에서 기본 제공해주고 있다.



메인 페이지 구성

구성하고자 하는 메인 페이지의 레이아웃은 다음과 같다.


Main Layout


이러면 잡다한 설명을 넣지 않고도 충분히 직관적으로 구성할 수 있을 것 같다.

메인 페이지에 더 보여줘야 할 정보가 있다면 파일 추가하기도 용이하고.



// file: 'SectionMain.tsx'
const styles = (theme: Theme) => ({
    background: {
        backgroundImage: `url(${backgroundImage})`,
        backgroundColor: '#5d5447',
        backgroundPosition: 'center',
    },
    button: {
        minWidth: 125,
    },
    h5: {
        marginBottom: theme.spacing(4),
        marginTop: theme.spacing(4),
        [theme.breakpoints.up('sm')]: {
            marginTop: theme.spacing(10),
        },
    },
    
    ...
    
});    

메인 페이지 중 첫 번째 섹션의 스타일을 부분적으로 오버라이드 해주고,

export를 해야 하는 함수형 컴포넌트에 적용하려고 보면



적용이 안 된다



Sincerity 대체 왜…?




정확히는 위에서 정의한 styles를 사용할 방법이 없었다.

함수형 컴포넌트에 props로 넘겨주는 방법이 있었는데, 그 타입을 알 턱이 있나…



여기서 다시 스타일을 재지정하는 것은 앞서 theme, root를 만들어 준 의미가 없을 것 같아

여러 군데를 검색해보고 시도해봤는데, 답은 인터페이스 정의였다!

styles를 받는 스타일 컴포넌트를 Material-UI에서 제공해주므로, 이를 상속받는 인터페이스를 정의한 후,

이 인터페이스를 props의 타입으로 선언해주면 깔끔하게 돌아간다.



// file: 'SectionMain.tsx'
interface Props extends WithStyles<typeof styles> {}

...

function SectionMain(props: Props) {
    const { classes } = props;
    
    ...
    
}


스타일도 잘 들어갔으니 이제 컴포넌트를 이용해 화면을 그리는 단계다.

처음 화면은 진짜 아무것도 없는 순백의 화면인데, 여기에 레이아웃을 지정해야 뭔가가 들어갈 듯 싶었다.



// file: 'SectionLayout.tsx'

const styles = (theme: Theme) => createStyles ({
    root: {
        color: theme.palette.common.white,
        position: 'relative',
        display: 'flex',
        alignItems: 'center',
        [theme.breakpoints.up('sm')]: {
            height: '80vh',
            minHeight: 500,
            maxHeight: 1300,
        },
    },
    
    ...
    
}


스타일을 지정해주고,

// file: 'SectionLayout.tsx'

interface Props extends WithStyles<typeof styles> {}

function SectionLayout(props: Props) {
    const { backgroundClassName, children, classes } = props;

    return (
        <section className={classes.root}>
            <Container className={classes.container}>
                {children}
    
    ...
    
  );
}

넣어주면, 또 에러로 난리가 난다.

이번엔 뭐가 문제일까, 살펴봤는데

{children}의 타입에 대한 정보가 없었다.


레이아웃을 사용하는 컴포넌트가 props의 자식들이 될 것이고,

이것들은 React.ReactNode 타입일 것이다.

(ReactNode는 ReactChild, ReactFragment 등등을 포함하는 상위 클래스인 듯 하다)

따라서 인터페이스를 조금 변경하면,


// file: 'SectionLayout.tsx'

interface Props extends WithStyles<typeof styles> {
    children?: React.ReactNode, 
}

...


잘 작동한다.

이제 이 레이아웃과 이전에 만든 스타일을 사용해 페이지를 구성하면 된다.

이 페이지 구성은 다음 포스트에서 다시 삽질해보도록 하자.