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에서 기본 제공해주고 있다.
메인 페이지 구성
구성하고자 하는 메인 페이지의 레이아웃은 다음과 같다.
이러면 잡다한 설명을 넣지 않고도 충분히 직관적으로 구성할 수 있을 것 같다.
메인 페이지에 더 보여줘야 할 정보가 있다면 파일 추가하기도 용이하고.
// 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를 해야 하는 함수형 컴포넌트에 적용하려고 보면
적용이 안 된다
정확히는 위에서 정의한 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,
}
...
잘 작동한다.
이제 이 레이아웃과 이전에 만든 스타일을 사용해 페이지를 구성하면 된다.
이 페이지 구성은 다음 포스트에서 다시 삽질해보도록 하자.