1-2. 기본 유저 인터페이스 만들기
프로젝트의 구조잡기가 끝났으니, 이번에는 기본 유저 인터페이스를 구성해보겠습니다.
PageTemplate, Header, Footer 컴포넌트 만들기
ListPage 와 PostPage 는 모두 동일한 레이아웃을 가지고 있습니다.

위 페이지들은, PageTemplate 컴포넌트로 감싸져있습니다:

이 컴포넌트에는, 상단엔 Header 가 있고, 하단에는 Footer 컴포넌트가 위치합니다.
컴포넌트 만들기
우리의 첫 유저인터페이스 컴포넌트를 만들어보겠습니다. 우리가 컴포넌트를 만들때에는, 종류별로 디렉토리를 나눠서 만들게 되며, Sass 와 CSS 모듈을 사용하므로, 각 컴포넌트마다 디렉토리를 하나씩 만들게 됩니다.
우선, components 디렉토리에 common 디렉토리를 만드세요. common 디렉토리는, 두개 이상의 페이지에서 사용되는 컴포넌트들을 넣습니다.
그리고, common 내부에 PageTemplate 라는 디렉토리를 만들고 다음 파일들을 생성하세요:
src/components/common/PageTemplate/PageTemplate.scss
.page-template {
}
src/components/common/PageTemplate/PageTemplate.js
import React from 'react';
import styles from './PageTemplate.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
const PageTemplate = () => (
<div className={cx('page-template')}>
PageTemplate
</div>
);
export default PageTemplate;
src/components/common/PageTemplate/index.js
export { default } from './PageTemplate';
앞으로 컴포넌트를 만들때 마다 이렇게 3개의 파일을 생성하게 될 텐데요, 편의를 위해서 컴포넌트의 경우 에디터의 스니펫 기능을 사용하여 자동생성하게끔 해도 되고, 더 유용한 방식은 VS Code 의 generate-react-component 를 사용하는 것 입니다.

이 확장 프로그램은, 파일 탐색기에서 우클릭을 하여 Generate new component 를 했을 때, 자동으로 원하는 설정으로 리액트 컴포넌트를 위한 디렉토리와 파일들을 생성해주는 확장 프로그램입니다.

generate-react-component 를 설치하고, 에디터를 재시작하세요. 이 확장 프로그램의 기본 리액트 컴포넌트 템플릿은 Sass 를 사용하지 않고 .css 파일을 사용한 CSS 모듈을 사용합니다. 따라서, 이 템플릿을 커스터마이징 해주어야합니다.
여러분의 편의를 위하여 템플릿은 이미 구성되어 깃헙 저장소에 올려놓았습니다. 템플릿을 직접 만들고 싶다면 generate-react-component 의 세부정보를 확인하세요. 다음 명령어를 통하여 템플릿을 다운로드 받으세요.
$ git clone https://github.com/vlpt-playground/react-sass-component-template.git
$ cd react-sass-component-template
$ pwd
pwd 를 입력하면 현재 경로의 절대경로가 나타날 것입니다. (예: /Users/react/react-sass-component-template)
이 경로를 복사하여 VS Code 의 환경설정에 넣어주어야 합니다.

설정 창을 열고, 다음과 같이 generate-react-component.componentTemplatePath 값을 템플릿이 위치한 절대경로를 입력하세요.

이제, 템플릿을 사용한 컴포넌트 생성을 자동화 할 수 있습니다. 에디터의 탐색기 부분에서 common 디렉토리를 우클릭 한 다음에 Generate New Component 를 누르세요.

클릭하고나면, 컴포넌트의 이름을 묻습니다. 이번엔 Header 컴포넌트를 만들어볼것이니, Header 를 입력하고 엔터를 누르세요. 그 다음엔, 클래스 형태로 만들것인지, 함수형으로 만들것인지 묻습니다. n 을 입력하여 함수형태로 작성하도록 설정하세요.

그러면 이렇게, 쉽게 컴포넌트 관련 파일들을 만들 수 있게 됩니다.
동일한 방식으로, common 디렉토리 내부에 Footer 컴포넌트도 만드세요.
글로벌 스타일 및 스타일 유틸 설정
우선 프로젝트의 폰트 및 전역적으로 사용되는 스타일을 지정해주겠습니다. styles 디렉토리에 base.scss 를 생성하세요.
src/styles/base.scss
/* body, 타이포그래피 등의 기본 스타일 설정 */
@import url("//fonts.googleapis.com/earlyaccess/notosanskr.css");
@import url("//cdn.jsdelivr.net/gh/velopert/font-d2coding@1.2.1/d2coding.css");
body {
margin: 0;
box-sizing: border-box;
font-family: "Noto Sans KR", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
// box-sizing 일괄 설정
* {
box-sizing: inherit;
}
// 링크 스타일 밑줄 및 색상 무효화
a {
text-decoration: inherit;
color: inherit;
}
이 프로젝트에서는 두가지 폰트를 사용합니다. 포스트 제목, 내용 등의 부분에서 주로 사용 할 폰트 Noto Sans KR 과, 나중에 마크다운 에디터를 만들 때 코드 작 성 부분에서 사용 할 D2 Coding 입니다.
그리고, 사이즈 설정을 용이하게 하기 위하여 box-sizing 값을 border-box 로 설정하였으며, 링크의 경우 자동으로 생성되는 밑줄과 파란색 색상을 비활성화 하였습니다. 스타일을 다 작성하셨으면 src/index.js 파일에서 이 스타일을 불러와서 적용하세요.
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Root from './Root';
import registerServiceWorker from './registerServiceWorker';
import 'styles/base.scss';
ReactDOM.render(<Root />, document.getElementById('root'));
registerServiceWorker();
이제 컴포넌트를 스타일링 할 때 사용 할 스타일 유틸을 설정하겠습니다. 우리가 적용 할 것은, 색상을 쉽게 선정 할 수 있게 해주는 open-color, 반응형 디자인을 쉽게 할 수 있게 해주는 include-media, 그리고 그림자를 간편하게 설정 할 수 있는 material-shadow 믹스인을 작성하겠습니다.
open-color 와 include-media 는 yarn 을 통하여 설치 할 수있습니다.
$ yarn add open-color include-media
설치를 하고 나서, styles/lib/_all.scss 파일에서 다음과 같이 스타일 파일들을 불러오세요.
src/styles/lib/_all.scss
@import '~open-color/open-color';
@import '~include-media/dist/include-media';
그 다음엔, 그림자를 쉽게 설정 할 수있는 material-shadow 믹스인을 styles/lib/_mixins.scss 에 작성하세요.
src/styles/lib/_mixins.scss
// source: https://codepen.io/dbox/pen/RawBEW
@mixin material-shadow($z-depth: 1, $strength: 1, $color: black) {
@if $z-depth == 1 {
box-shadow: 0 1px 3px rgba($color, $strength * 0.14), 0 1px 2px rgba($color, $strength * 0.24);
}
@if $z-depth == 2 {
box-shadow: 0 3px 6px rgba($color, $strength * 0.16), 0 3px 6px rgba($color, $strength * 0.23);
}
@if $z-depth == 3 {
box-shadow: 0 10px 20px rgba($color, $strength * 0.19), 0 6px 6px rgba($color, $strength * 0.23);
}
@if $z-depth == 4 {
box-shadow: 0 15px 30px rgba($color, $strength * 0.25), 0 10px 10px rgba($color, $strength * 0.22);
}
@if $z-depth == 5{
box-shadow: 0 20px 40px rgba($color, $strength * 0.30), 0 15px 12px rgba($color, $strength * 0.22);
}
@if ($z-depth < 1) or ($z-depth > 5) {
@warn "$z-depth must be between 1 and 5";
}
}
이 믹스인을 작성 한 다음엔 이 파일을 styles/lib/_all.scss 에서 불러오세요.
src/styles/lib/_all.scss
@import '~open-color/open-color';
@import '~include-media/dist/include-media';
@import 'mixins';
마지막으로, styles/utils.scss 파일을 생성하여 방금 작성한것들을 불러오세요. 추가적으로, 반응형 디자인을 하게 될 때 참조 할 구간을 변수로 저장해두겠습니다.
src/styles/utils.scss
@import 'lib/all';
$breakpoints: (small: 320px, medium: 768px, large: 1024px, wide: 1400px);
반응형 디자인을 할 때에는, 큰 화면 일때, 중간 사이즈의 화면일때, 그리고 모바일 사이즈의 화면일 때에 따라서 다른 스타일을 주게 됩니다.

그리고, 우리가 설치한 open-color 라이브러리를 사용하면, 색상을 간편하게 불러와서 사용 할 수 있습니다.
색상 변수의 형태는 $oc-색상-밝기 로 되어있습니다. 예를들어서, GRAY 9 의 경우엔 $oc-gray-7 의 형식으로 작성합니다.
색상 팔레트는 https://yeun.github.io/open-color/ 에서 확인 하실 수 있습니다.
예제 코드:
.my-class {
background: $oc-gray-1;
color: $oc-indigo-9;
}
앞으로 우리의 프로젝트는, 파란색 계열로 디자인을 하게 됩니다. 색상을 선정 할 때, 꼭 파란색이 아니어도, 여러분이 좋아하는 색상을 선택하여 사용을 하셔도 됩니다.
우리가 유틸에 적용한 코드들을 사용하려면, 각 컴포넌트 디렉토리에 들어있는 스타일 파일의 상단에 다음 코드를 삽입해주어야 합니다:
@import 'utils';
Header 컴포넌트 만들기

헤더 컴포넌트에서는, 좌측엔 로고가 있고, 우측엔 버튼이 있습니다. 포스트 목록을 볼 때에는, 우측에 새 포스트 버튼이 있고, 포스트를 보고 있을땐, 수정과 삭제 버튼이 추가적으로 보입니다.
이 요구에 따라, 다음과 같이 헤더 컴포넌트를 작성해보세요
src/components/common/Header/Header.js
import React from 'react';
import styles from './Header.scss';
import classNames from 'classnames/bind';
import { Link } from 'react-router-dom';
const cx = classNames.bind(styles);
const Header = () => (
<header className={cx('header')}>
<div className={cx('header-content')}>
<div className={cx('brand')}>
<Link to="/">reactblog</Link>
</div>
<div className={cx('right')}>
{/* 조건에 따라 버튼 렌더링 */}
오른쪽
</div>
</div>
</header>
);
export default Header;
그 다음엔, Header.scss 를 작성하세요.
src/components/common/Header/Header.scss
@import 'utils';
.header {
background: $oc-blue-6;
.header-content {
height: 5rem;
width: 1400px;
margin: 0 auto; // 중앙 정렬
padding-left: 3rem;
padding-right: 3rem;
// 내부 아이템 세로 중앙 정렬
display: flex;
align-items: center;
// 반응형 레이아웃
@include media("<wide") {
width: 100%;
}
@include media("<medium") {
padding-left: 1rem;
padding-right: 1rem;
}
.brand {
// 로고
color: white;
font-size: 1.5rem;
font-weight: 600;
}
.right {
// 우측 내용
margin-left: auto;
}
}
}
이제 Header 컴포넌트의 기본 스타일을 모두 작성하였습니다. 이제 이 컴포넌트를 PageTemplate 에서 렌더링 해주고, ListPage 에서 PageTemplate 을 렌더링 해주겠습니다.
src/components/PageTemplate/PageTemplate.js
import React from 'react';
import styles from './PageTemplate.scss';
import classNames from 'classnames/bind';
import Header from 'components/common/Header';
const cx = classNames.bind(styles);
const PageTemplate = () => (
<div className={cx('page-template')}>
<Header/>
</div>
);
export default PageTemplate;
src/pages/ListPage.js
import React from 'react';
import PageTemplate from 'components/common/PageTemplate';
const ListPage = () => {
return (
<PageTemplate>
List
</PageTemplate>
);
};
export default ListPage;
코드를 저장하고, 브라우저를 띄워서 헤더 컴포넌트가 잘 보여지는지 확인하세요.

헤더 컴포넌트가 위와같이 보여졌나요?
Footer 컴포넌트 만들기
이번엔 페이지의 하단에 위치 할 푸터 컴포넌트를 보여주겠습니다.

우리가 만들 블로그 프로젝트에서는, 초반에는 별도의 인증 작업 없이 포스트 작성, 삭제, 수정을 가능케 하겠지만, 나중엔 간단한 비밀번호 인증을 구현하게 됩니다.
이 컴포넌트에서는, 단순히 로고를 보여주고, 로그인되지 않았을때는 관리자 로그인 버튼을, 그리고 로그인 중일때는 로그아웃 버튼을 보여줍니다.
Footer 컴포넌트를 다음과 같이 작성하세요:
src/components/common/Footer/Footer.js
import React from 'react';
import styles from './Footer.scss';
import classNames from 'classnames/bind';
import { Link } from 'react-router-dom';
const cx = classNames.bind(styles);
const Footer = () => (
<footer className={cx('footer')}>
<Link to="/" className={cx('brand')}>reactblog</Link>
<div className={cx('admin-login')}>관리자 로그인</div>
</footer>
);
export default Footer;
스타일링도 하겠습니다.
src/components/common/Footer/Footer.scss
@import 'utils';
.footer {
background: $oc-gray-7;
height: 10rem;
// 내부 내용 중앙 정렬
display: flex;
align-items: center;
justify-content: center;
flex-direction: column; // 위에서 아래로
.brand {
// 로고
color: white;
font-size: 2rem;
font-weight: 600;
}
.admin-login {
// 로그인버튼
margin-top: 0.5rem;
font-weight: 600;
font-size: 0.8rem;
color: rgba(255,255,255,0.8);
cursor: pointer; // 손가락 모양 커서
&:hover { // 마우스 호버시 불투명도 없애기
color: white;
}
}
}
이제, 이 컴포넌트를 PageTemplate 에서 렌더링하세요.
src/components/common/PageTemplate/PageTemplate.js
import React from 'react';
import styles from './PageTemplate.scss';
import classNames from 'classnames/bind';
import Header from 'components/common/Header';
import Footer from 'components/common/Footer';
const cx = classNames.bind(styles);
const PageTemplate = () => (
<div className={cx('page-template')}>
<Header/>
<Footer/>
</div>
);
export default PageTemplate;
Footer 를 Header 하단에 렌더링 해주었습니다. 그럼, 다음과 같이 헤더와 푸터가 붙어있는 상태로 보여지게 됩니다.

이 둘이 붙어있으니까 조금 이상하지요? 푸터는 언제나 페이지의 하단에 위치하게끔, PageTemplate 을 수정하겠습니다
PageTemplate 페이지 중간영역 설정하기
PageTemplate 에서, 헤더와 푸터사이의 중간영역의 배경색을 회색으로 지정하고, 푸터가 언제나 페이지의 하단에 위치하도록 중간영역의 최소 높이를 지정해주겠습니다. 헤더와 푸터의 크기를 합치면 15rem 이므로, 페이지의 높이에서 15rem 을 뺀 수치를 min-height 로 지정하세요.
그리고, 그 중간영역에는 컴포넌트의 children 이 보여지도록 설정하세요.
src/components/common/PageTemplate/PageTemplate.js
import React from 'react';
import styles from './PageTemplate.scss';
import classNames from 'classnames/bind';
import Header from 'components/common/Header';
import Footer from 'components/common/Footer';
const cx = classNames.bind(styles);
const PageTemplate = ({children}) => (
<div className={cx('page-template')}>
<Header/>
<main>
{children}
</main>
<Footer/>
</div>
);
export default PageTemplate;
src/components/common/PageTemplate/PageTemplate.scss
@import 'utils';
.page-template {
main {
background: $oc-gray-1;
min-height: calc(100vh - 15rem);
}
}
이렇게 코드를 작성하고 나면, 중간 영역에 회색 배경을 가진 블록이 나타납니다.

버튼 만들기
프로젝트에서 사용되는 버튼 컴포넌트를 만들겠습니다. 이 컴포넌트는, to 값이 props 로 전달 되었을땐 Link 컴포넌트를 사용하고, to 값이 없을 때에는 div 태그를 사용합니다. 여러 종류의 스타일을 가진 버튼 컴포넌트를 만들기 위해서, theme props 를 받아와서 이에 따라 다른 스타일을 설정하겠습니다. Button 컴포넌트를 Generate New Component 를 통하여 common 디렉토리에 생성하고, 다음 코드들을 작성하세요.
src/components/common/Button/Button.js
import React from 'react';
import styles from './Button.scss';
import classNames from 'classnames/bind';
import { Link } from 'react-router-dom';
const cx = classNames.bind(styles);
// 전달받은 className, onClick 등의 값들이 rest 안에 들어있습니다.
// JSX 에서 ... 을 사용하면 내부에 있는 값들을 props 로 넣어줍니다.
const Div = ({children, ...rest}) => <div {...rest}>{children}</div>
const Button = ({
children, to, onClick, disabled, theme = 'default',
}) => {
// to 값이 존재하면 Link 를 사용하고, 그렇지 않으면 div 를 사용합니다.
// 비활성화 되어있는 버튼인 경우에도 div 가 사용됩니다.
const Element = (to && !disabled) ? Link : Div;
// 비활성화되면 onClick 은 실행되지 않습니다
// disabled 값이 true 가 되면 className 에 disabled 가 추가됩니다.
return (
<Element
to={to}
className={cx('button', theme, { disabled })}
onClick={disabled ? () => null : onClick}>
{children}
</Element>
)
}
export default Button;
버튼에 옵션들이 꽤 많지요? 그 대신에, 이 버튼 컴포넌트 하나 가지고, 프로젝트 상에서 나타나는 대부분의 버튼 부분에 사용 할 수 있습니다. 비슷한 기능을 가진 컴포넌트를 여러개를 만드는 것 보다는, 재사용 가능한 컴포넌트를 만드는것이 더 좋겠지요?
이제 스타일도 작성해보겠습니다. 버튼의 테마는 총 3가지가 있습니다: default, outline, gray. 그리고, 버튼은 비활성화 되기도 합니다. 이 때에는 disabled 클래스가 적용됩니다.
src/components/common/Button/Button.scss
@import 'utils';
.button {
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
font-weight: 600;
font-size: 0.9rem;
color: white;
cursor: pointer;
user-select: none; // 드래그 방지
display: inline-flex;
// default: 파란색 버튼
&.default {
background: $oc-blue-6;
&:hover {
background: $oc-blue-5;
}
&:active {
background: $oc-blue-6;
}
}
// gray: 회색 버튼
&.gray {
background: $oc-gray-7;
&:hover {
background: $oc-gray-6;
}
&:active {
background: $oc-gray-7;
}
}
// outline: 흰색 테두리 버튼
&.outline {
border: 2px solid white;
border-radius: 2px;
&:hover {
background: white;
color: $oc-blue-6;
}
&:active {
background: rgba(255,255,255, 0.85);
border: 2px solid rgba(255,255,255, 0.85);
}
}
&:hover {
@include material-shadow(2, 0.5); // 마우스 호버시 그림자 생성
}
// 비활성화된 버튼
&.disabled {
background: $oc-gray-4;
color: $oc-gray-6;
cursor: default;
&:hover, &:active {
box-shadow: none;
background: $oc-gray-4;
}
}
// 버튼 두개 이상이 함께 있다면, 중간 여백
& + & {
margin-left: 0.5rem;
}
}
스타일을 다 작성하였다면, 이 컴포넌트를 Header 컴포넌트의 오른쪽 부분에 렌더링하세요. 버튼의 테마는 outline 으로 설정하세요.
src/components/common/Header/Header.js
import React from 'react';
import styles from './Header.scss';
import classNames from 'classnames/bind';
import { Link } from 'react-router-dom';
import Button from 'components/common/Button';
const cx = classNames.bind(styles);
const Header = () => (
<header className={cx('header')}>
<div className={cx('header-content')}>
<div className={cx('brand')}>
<Link to="/">reactblog</Link>
</div>
<div className={cx('right')}>
<Button theme="outline" to="/editor">새 포스트</Button>
</div>
</div>
</header>
);
export default Header;

버튼이 잘 렌더링 되었나요?