$ create-react-app contact
프로젝트 생성
프로젝트 초기화
src 내부의 필요없는 파일들 제거
- App.css
- logo.svg
- App.test.js
src/App.js
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<div>
</div>
);
}
}
export default App;
src/index.css
body {
margin: 0;
padding: 0;
background: #f1f3f5;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html {
box-sizing: border-box;
}
의존 모듈 설치
$ yarn add open-color prop-types react-icons react-onclickoutside react-transition-group@1.x shortid styled-components
src/components 디렉토리 생성
vscode-styled-components
VSCode 확장 프로그램 설치
(CSS 코드에 색상을 입혀줌)
src/components/Header.js
import React from 'react';
import styled from 'styled-components';
const Wrapper = styled.div`
height: 5rem;
background: black;
`;
const Header = () => (
<Wrapper>
주소록
</Wrapper>
);
export default Header;
src/App.js
import React, { Component } from 'react';
import Header from './components/Header';
class App extends Component {
render() {
return (
<div>
<Header/>
</div>
);
}
}
export default App;
App 에서 Header 렌더링
디자인 초보도 오픈컬러가있으면 절반은 갑니다!
src/components/Header.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
const Wrapper = styled.div`
/* 레이아웃 */
height: 4rem;
background: ${oc.teal[6]};
border-bottom: 1px solid ${oc.teal[8]};
`;
const Header = () => (
<Wrapper>
주소록
</Wrapper>
);
export default Header;
open-color 불러와서 사용하기
src/App.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
const Wrapper = styled.div`
/* 레이아웃 */
height: 4rem;
background: ${oc.teal[6]};
border-bottom: 1px solid ${oc.teal[8]};
/* 폰트 설정 */
color: white;
font-weight: 500;
font-size: 1.5rem;
/* 가운데로 정렬 */
display: flex;
align-items: center; /* 세로 정렬 */
justify-content: center; /* 가로 정렬 */
`;
const Header = () => (
<Wrapper>
주소록
</Wrapper>
);
export default Header;
폰트 설정 및 가운데 정렬
VS Code 상단 메뉴의 파일 (macOS 에선 Code) -> 기본설정 -> 사용자 코드조각 -> Javascript React
Text
{
"Styled Stateless Component": {
"prefix": "rssc",
"body": [
"import React from 'react';",
"import styled from 'styled-components';",
"import oc from 'open-color';",
"",
"const Wrapper = styled.div`",
"",
"`;",
"",
"const ${1:ComponentName} = () => (",
" <Wrapper>",
"",
" </Wrapper>",
");",
"",
"export default ${1:ComponentName};"
],
"description": "Create Styled Stateless Component"
}
}
rssc - 스타일된 stateless 컴포넌트
snippet-creator 라는 익스텐션을 사용하면 쉽게 스니펫 생성 가능
src/components/Container.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
const Wrapper = styled.div`
width: 700px;
margin: 0 auto; /* 가운데 정렬 */
padding: 1rem;
background: black; /* 테스트용 색상, 추후 지워짐 */
`;
const Container = ({children}) => (
<Wrapper>
{children}
</Wrapper>
);
export default Container;
Container 만들기
src/App.js
import React, { Component } from 'react';
import Header from './components/Header';
import Container from './components/Container';
class App extends Component {
render() {
return (
<div>
<Header/>
<Container></Container>
</div>
);
}
}
export default App;
App 에서 Container 렌더링
const Wrapper = styled.div`
width: 700px;
margin: 0 auto; /* 가운데 정렬 */
padding: 1rem;
background: black; /* 테스트용 색상, 추후 지워짐 */
/* 모바일 크기 */
@media (max-width: 768px) {
width: 100%;
}
`;
src/lib/style-utils.js
import { css } from 'styled-components';
export const media = {
mobile: (...args) => css`
@media (max-width: 768px) {
${ css(...args) }
}
`
};
미디어 쿼리 모듈화
src/components/Container.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import { media } from '../lib/style-utils';
const Wrapper = styled.div`
width: 700px;
margin: 0 auto; /* 가운데 정렬 */
padding: 1rem;
background: black; /* 테스트용 색상, 추후 지워짐 */
/* 모바일 크기 */
${media.mobile`
width: 100%;
`}
`;
const Container = ({children}) => (
<Wrapper>
{children}
</Wrapper>
);
export default Container;
모바일 대응하기
src/components/Container.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import { media } from '../lib/style-utils';
import PropTypes from 'prop-types';
const Wrapper = styled.div`
width: 700px;
margin: 0 auto; /* 가운데 정렬 */
padding: 1rem;
/* 모바일 크기 */
${media.mobile`
width: 100%;
`}
`;
// visible 이 false 면 null 반환
const Container = ({visible, children}) => visible ? (
<Wrapper>
{children}
</Wrapper>
) : null;
// PropTypes 설정
Container.propTypes = {
visible: PropTypes.bool
};
export default Container;
visible props 추가
src/components/ViewSelector.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
const Wrapper = styled.div`
height: 4rem;
background: white;
width: 100%;
/* 하단 핑크색 바 위치 설정을 위해 설정
bottom, left 값 설정할때 이 Wrapper 에 의존 */
position: relative;
display: flex;
`;
const StyledItem = styled.div`
/* 레이아웃 */
height: 100%;
/* 형제 엘리먼트들과 동일한 사이즈로 설정 */
flex: 1;
/* 가운데 정렬 */
display: flex;
align-items: center;
justify-content: center;
/* 색상 */
color: ${oc.gray[6]};
/* 기타 */
font-size: 1.5rem;
cursor: pointer;
/* 마우스가 위에 있을 때 */
&:hover {
background: ${oc.gray[0]};
}
`;
const Bar = styled.div`
/* 레이아웃 */
position: absolute;
bottom: 0px;
height: 3px;
width: 50%;
/* 색상 */
background: ${oc.pink[6]};
`;
// 추후 아이템 컴포넌트에 기능을 달아줄것이기에 컴포넌트 추가생성
const Item = ({children}) => (
<StyledItem>
{children}
</StyledItem>
);
const ViewSelector = () => (
<Wrapper>
<Item>즐겨찾기</Item>
<Item>리스트</Item>
<Bar/>
</Wrapper>
);
export default ViewSelector;
기본 디자인
src/App.js
import React, { Component } from 'react';
import Header from './components/Header';
import Container from './components/Container';
import ViewSelector from './components/ViewSelector';
class App extends Component {
render() {
return (
<div>
<Header/>
<ViewSelector/>
<Container>
</Container>
</div>
);
}
}
export default App;
App 에서 ViewSelector 렌더링
src/components/ViewSelector.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import StarIcon from 'react-icons/lib/md/star';
import PeopleIcon from 'react-icons/lib/md/people';
(...)
const ViewSelector = () => (
<Wrapper>
<Item><StarIcon/></Item>
<Item><PeopleIcon/></Item>
<Bar/>
</Wrapper>
);
export default ViewSelector;
아이콘 불러와서, 사용하기
(...) 은 생략을 의미합니다
즐겨찾기를 보여줄지, 리스트를 보여줄지?
src/App.js
import React, { Component } from 'react';
import Header from './components/Header';
import Container from './components/Container';
import ViewSelector from './components/ViewSelector';
class App extends Component {
/* --- #1 --- */
state = {
view: 'favorite'
}
// view 선택 메소드 정의
/* --- #2 --- */
handleSelectView = (view) => this.setState({view})
render() {
// 레퍼런스 준비
/* --- #3 --- */
const { handleSelectView } = this;
const { view } = this.state;
return (
<div>
<Header/>
{/* --- #4 --- */}
<ViewSelector onSelect={handleSelectView} selected={view}/>
{/* view 값에 따라 다른 컨테이너를 보여준다 */}
{/* --- #5 --- */}
<Container visible={view==='favorite'}>즐겨찾기</Container>
<Container visible={view==='list'}>리스트</Container>
</div>
);
}
}
export default App;
App 상태 정의 및 업데이트
src/components/ViewSelector.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import StarIcon from 'react-icons/lib/md/star';
import PeopleIcon from 'react-icons/lib/md/people';
/* --- #1 --- */
import PropTypes from 'prop-types';
const Wrapper = styled.div`
height: 4rem;
background: white;
width: 100%;
flex: 1;
/* 하단 핑크색 바 위치 설정을 위해 설정
bottom, left 값 설정할때 이 Wrapper 에 의존 */
position: relative;
display: flex;
`;
const StyledItem = styled.div`
/* 레이아웃 */
height: 100%;
/* 형제 엘리먼트들과 동일한 사이즈로 설정 */
flex: 1;
/* 가운데 정렬 */
display: flex;
align-items: center;
justify-content: center;
/* 색상 */
color: ${oc.gray[6]};
/* 기타 */
font-size: 1.5rem;
cursor: pointer;
/* 마우스가 위에 있을 때 */
&:hover {
background: ${oc.gray[0]};
}
`;
/* --- #2 --- */
StyledItem.propTypes = {
active: PropTypes.bool
}
const Bar = styled.div`
/* 레이아웃 */
position: absolute;
bottom: 0px;
height: 3px;
width: 50%;
/* 색상 */
background: ${oc.pink[6]};
`;
/* --- #3 --- */
Bar.propTypes = {
right: PropTypes.bool
}
/* --- #4 --- */
const Item = ({children, selected, name, onSelect}) => (
<StyledItem>
{children}
</StyledItem>
);
Item.propTypes = {
selected: PropTypes.string,
name: PropTypes.string,
onSelect: PropTypes.func
};
/* --- #5 --- */
const ViewSelector = ({selected, onSelect}) => (
<Wrapper>
<Item><StarIcon/></Item>
<Item><PeopleIcon/></Item>
<Bar/>
</Wrapper>
);
ViewSelector.propTypes = {
selected: PropTypes.string,
onSelect: PropTypes.func
}
export default ViewSelector;
ViewSelector 내부 props 설정하기
src/components/ViewSelector.js - StyledItem
const StyledItem = styled.div`
/* 레이아웃 */
height: 100%;
/* 형제 엘리먼트들과 동일한 사이즈로 설정 */
flex: 1;
/* 가운데 정렬 */
display: flex;
align-items: center;
justify-content: center;
/* 색상 */
/* active 값에 따라 다른 색상을 보여줌 */
color: ${ props => props.active ? oc.gray[9] : oc.gray[6] };
/* 기타 */
font-size: 1.5rem;
cursor: pointer;
/* 마우스가 위에 있을 때 */
&:hover {
background: ${oc.gray[0]};
}
`;
props 에 값에 따라 StyledItem 에 변화주기
src/components/ViewSelector.js - Bar
const Bar = styled.div`
/* 레이아웃 */
position: absolute;
bottom: 0px;
height: 3px;
width: 50%;
/* 색상 */
background: ${oc.pink[6]};
/* 애니메이션 */
transition: ease-in .25s;
/* right 값에 따라 우측으로 이동 */
transform: ${props => props.right ? 'translateX(100%)' : 'none'};
`;
props 에 값에 따라 Bar 에 변화주기
src/components/ViewSelector.js - Bar
const ViewSelector = ({selected, onSelect}) => (
<Wrapper>
<ItemContainer>
<Item
selected={selected}
name="favorite"
onSelect={onSelect}>
<StarIcon/>
</Item>
<Item
selected={selected}
name="list"
onSelect={onSelect}>
<PeopleIcon/>
</Item>
</ItemContainer>
<Bar right={selected==='list'}/>
</Wrapper>
);
ViewSelector 의 props 를 하위컴포넌트로 전달
src/components/ViewSelector - Item
const Item = ({children, selected, name, onSelect}) => (
<StyledItem onClick={() => onSelect(name)} active={selected===name}>
{children}
</StyledItem>
);
Item props 설정
src/components/FloatingButton.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import PropTypes from 'prop-types';
import AddIcon from 'react-icons/lib/md/add';
const Wrapper = styled.div`
/* 레이아웃 */
position: fixed;
bottom: 2rem;
right: 2rem;
width: 4rem;
height: 4rem;
/* 색상 */
background: white;
border: 3px solid ${oc.pink[6]};
color: ${oc.pink[6]};
/* 기타 */
border-radius: 2rem;
font-size: 2rem;
cursor: pointer;
/* 중앙정렬 */
display: flex;
align-items: center;
justify-content: center;
/* 애니메이션 */
transition: all .15s;
/* 마우스가 위에 있을 때 */
&:hover {
/* 위로 조금 움직이고 색바꿈 */
transform: translateY(-0.5rem);
color: white;
background: ${oc.pink[6]};
}
/* 클릭될때 */
&:active {
/* 색 좀 더 어둡게 */
background: ${oc.pink[7]};
}
`;
const FloatingButton = ({onClick}) => (
<Wrapper onClick={onClick}>
<AddIcon/>
</Wrapper>
);
FloatingButton.propTypes = {
onClick: PropTypes.func
}
export default FloatingButton;
FloatingButton 스타일링
src/App.js
import React, { Component } from 'react';
import Header from './components/Header';
import Container from './components/Container';
import ViewSelector from './components/ViewSelector';
import FloatingButton from './components/FloatingButton';
class App extends Component {
(...)
render() {
// 레퍼런스 준비
const { handleSelectView } = this;
const { view } = this.state;
return (
<div>
<Header/>
<ViewSelector onSelect={handleSelectView} selected={view}/>
{/* view 값에 따라 다른 컨테이너를 보여준다 */}
<Container visible={view==='favorite'}>즐겨찾기</Container>
<Container visible={view==='list'}>리스트</Container>
<FloatingButton/>
</div>
);
}
}
export default App;
App 에서 FloatingButton 렌더링
App 에서 모달 상태 정의 및 업데이트
src/App.js
import React, { Component } from 'react';
import Header from './components/Header';
import Container from './components/Container';
import ViewSelector from './components/ViewSelector';
import FloatingButton from './components/FloatingButton';
class App extends Component {
state = {
view: 'favorite',
modal: {
visible: false,
mode: null // create 혹은 modify
}
}
// view 선택 메소드 정의
handleSelectView = (view) => this.setState({view})
// 모달 관련 메소드들
modalHandler = {
show: (mode, payload) => {
this.setState({
modal: {
mode,
visible: true,
...payload // payload 안의 값을 풀어서 여기에 넣음
}
})
},
hide: () => {
this.setState({
modal: {
...this.state.modal, // 기존 값들을 복사해서 안에 넣음
visible: false
}
})
},
// 추후 구현될 메소드들
change: null,
action: {
create: null,
modify: null,
remove: null
}
}
render() {
(...)
}
}
export default App;
src/App.js - 상단
import oc from 'open-color';
function generateRandomColor() {
const colors = [
'gray',
'red',
'pink',
'grape',
'violet',
'indigo',
'blue',
'cyan',
'teal',
'green',
'lime',
'yellow',
'orange'
];
// 0 부터 12까지 랜덤 숫자
const random = Math.floor(Math.random() * 13);
return oc[colors[random]][6];
}
랜덤 색상 생성 함수 만들기
src/App.js
(...)
class App extends Component {
(...)
// FloatingButton 클릭
handleFloatingButtonClick = () => {
// 현재 view 가 list 가 아니면 list 로 설정
const { view } = this.state;
if(view !== 'list')
this.setState({view: 'list'});
// Contact 추가 모달 띄우기
this.modalHandler.show(
'create',
{
name: '',
phone: '',
color: generateRandomColor()
}
);
}
render() {
// 레퍼런스 준비
const {
handleSelectView,
handleFloatingButtonClick
} = this;
const { view } = this.state;
return (
<div>
<Header/>
<ViewSelector onSelect={handleSelectView} selected={view}/>
{/* view 값에 따라 다른 컨테이너를 보여준다 */}
<Container visible={view==='favorite'}>즐겨찾기</Container>
<Container visible={view==='list'}>리스트</Container>
<FloatingButton onClick={handleFloatingButtonClick}/>
</div>
);
}
}
export default App;
FloatingButton 이 클릭될때 실행 할 메소드 만들기
세개의 컴포넌트
- Modal.js : 모달 보여주기, 숨기기, 애니메이션 담당
- ContactModal.js : 모달 내용, 기능 담당
- Dimmed.js : 화면을 어둡게 함
Modal.js 작성
onClickoutside(Component)
컴포넌트를 감싼다.
handleClickOutside 를 호출함.
컴포넌트 밖에 클릭되면
내부에는 2개의 StyledComponent
1. Wrapper: 화면의 정 중앙에 컴포넌트 위치 시킴
2. ModalBox: 흰색 박스를 생성해줌
모달의 기본 너비: 400px
props 로 설정 가능;
모바일 사이즈에선 양옆에 1rem 여백
handleClickOutside 에서는
props 로 전달받은 onHide 호출
Esc 가 눌려졌을때도 모달종료
handleKeyUp 리스너를 body 에 적용
visible 값에 따라서 ModalBox 보여주거나 숨김
src/components/Modal.js
import React, { Component } from 'react';
import styled from 'styled-components';
import onClickOutside from 'react-onclickoutside';
import {media} from '../lib/style-utils';
import PropTypes from 'prop-types';
// 모달 위치 및 사이즈 설정
const Wrapper = styled.div`
/* 레이아웃 */
position: fixed;
/* 화면 가운대로 정렬 */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* 레이어 */
z-index: 10;
/* 너비 (기본값 400px) */
width: ${ props => props.width };
/* 모바일일땐 양옆 여백 1rem 에 꽉 채우기 */
${media.mobile`
width: calc(100% - 2rem);
`}
`;
Wrapper.propTypes = {
width: PropTypes.string
};
// 모달 틀
const ModalBox = styled.div`
background: white;
border: 1px solid rgba(0,0,0,0.3);
`
class Modal extends Component {
static propTypes = {
visible: PropTypes.bool,
onHide: PropTypes.func,
width: PropTypes.string
}
static defaultProps = {
width: '400px'
}
// 컴포넌트 외부를 클릭하면 실행되는 메소드
handleClickOutside = (e) => {
const { visible, onHide } = this.props;
if(!visible) return null; // 이미 visible 이 false 라면 아무것도 안함
onHide();
}
// Esc 키가 클릭되면 onHide 를 실행한다
handleKeyUp = (e) => {
const { onHide } = this.props
if (e.keyCode === 27) {
onHide();
}
}
componentDidUpdate(prevProps, prevState) {
// visible 값이 변할 때:
if(prevProps.visible !== this.props.visible) {
if(this.props.visible) {
// 방금 보여졌다면
// body 에 keyUp 이벤트 등록해서 Esc 키를 감지한다.
document.body.addEventListener('keyup', this.handleKeyPress);
} else {
// 방금 사라졌다면
document.body.removeEventListener('keyup', this.handleKeyPress);
}
}
}
render() {
// 레퍼런스 생성
const {visible, children, width} = this.props;
return (
<div>
<Wrapper width={width}>
{
/* visible 이 참일때만 ModalBox 보여줌 */
visible && (<ModalBox>{children}</ModalBox>)
}
</Wrapper>
</div>
);
}
}
// onClickoutside 라이브러리 적용
export default onClickOutside(Modal);
ContactModal 을 만들어서
Modal 이 잘 작동하는지 확인
src/components/ContactModal.js
import React, { Component } from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import Modal from './Modal';
import PropTypes from 'prop-types';
class ContactModal extends Component {
static propTypes = {
visible: PropTypes.bool,
// 모달의 모드
mode: PropTypes.oneOf(['create', 'modify']),
// 모달에 들어갈 데이터 값
name: PropTypes.string,
phone: PropTypes.string,
color: PropTypes.string,
onHide: PropTypes.func,
onAction: PropTypes.func, // 추가 혹은 수정
onRemove: PropTypes.func // 나중에 구현할 삭제
}
render() {
const {
visible,
onHide
} = this.props;
return (
<Modal visible={visible} onHide={onHide}>
하이
</Modal>
);
}
}
export default ContactModal;
src/App.js
(...)
import ContactModal from './components/ContactModal';
class App extends Component {
(...)
render() {
// 레퍼런스 준비
const {
handleSelectView,
handleFloatingButtonClick,
modalHandler
} = this;
const {
view,
modal
} = this.state;
return (
<div>
<Header/>
<ViewSelector onSelect={handleSelectView} selected={view}/>
{/* view 값에 따라 다른 컨테이너를 보여준다 */}
<Container visible={view==='favorite'}>즐겨찾기</Container>
<Container visible={view==='list'}>리스트</Container>
<ContactModal {...modal} onHide={modalHandler.hide}/>
<FloatingButton onClick={handleFloatingButtonClick}/>
</div>
);
}
}
export default App;
App 에서 ContactModal 렌더링
<ContactModal
color={modal.color}
mode={modal.mode}
name={modal.name}
phone={modal.phone}
visible={modal.visible}
onHide={modalHandler.hide}
/>
{ ... modal } 이 이렇게 변환됨
Dimmed.js 배경화면 어둡게하기
src/components/Dimmed.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import PropTypes from 'prop-types';
const Black = styled.div`
/* 레이아웃 - 화면 꽉 채움 */
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
/* 레이어 */
z-index: 5;
/* 색상 */
background: rgba(0,0,0,0.3);
`;
const Dimmed = ({visible}) => (
<div>
{visible && <Black/>}
</div>
);
Dimmed.propTypes = {
visible: PropTypes.bool
};
export default Dimmed;
src/App.js - 상단
import Dimmed from './components/Dimmed';
App 에서 Dimmed 렌더링
App 에서 Dimmed 렌더링
src/App.js - render
render() {
// 레퍼런스 준비
const {
handleSelectView,
handleFloatingButtonClick,
modalHandler
} = this;
const {
view,
modal
} = this.state;
return (
<div>
<Header/>
<ViewSelector onSelect={handleSelectView} selected={view}/>
{/* view 값에 따라 다른 컨테이너를 보여준다 */}
<Container visible={view==='favorite'}>즐겨찾기</Container>
<Container visible={view==='list'}>리스트</Container>
<ContactModal {...modal} onHide={modalHandler.hide}/>
<Dimmed visible={modal.visible}/>
<FloatingButton onClick={handleFloatingButtonClick}/>
</div>
);
}
}
export default App;
애니메이션 설정하기
.example-enter {
opacity: 0.01;
}
.example-enter.example-enter-active {
opacity: 1;
transition: opacity 500ms ease-in;
}
.example-leave {
opacity: 1;
}
.example-leave.example-leave-active {
opacity: 0.01;
transition: opacity 300ms ease-in;
}
<CSSTransitionGroup
transitionName="example"
transitionEnterTimeout={500}
transitionLeaveTimeout={300}>
{items}
</CSSTransitionGroup>
@keyframes slideDown {
0% {
opacity: 0;
transform: translateY(-100vh);
}
75% {
opacity: 1;
transform: translateY(25px);
}
100% {
transform: translateY(0px);
}
}
@keyframes slideUp {
0% {
transform: translateY(0px);
opacity: 1;
}
25% {
opacity: 1;
transform: translateY(25px);
}
100% {
opacity: 0;
transform: translateY(-100vh);
}
}
src/lib/utils.js
import { css, keyframes } from 'styled-components';
export const media = {
mobile: (...args) => css`
@media (max-width: 768px) {
${ css(...args) }
}
`
};
export const transitions = {
slideDown: keyframes`
0% {
opacity: 0;
transform: translateY(-100vh);
}
75% {
opacity: 1;
transform: translateY(25px);
}
100% {
transform: translateY(0px);
}
`,
slideUp: keyframes`
0% {
transform: translateY(0px);
opacity: 1;
}
25% {
opacity: 1;
transform: translateY(25px);
}
100% {
opacity: 0;
transform: translateY(-100vh);
}
`
}
keyframes 모듈화
Modal.js 에서 transition 불러오기
src/components/Modal.js
// 상단
import {media, transitions} from '../lib/style-utils';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
// Wrapper
const Wrapper = styled.div`
(...)
/* 애니메이션 */
.modal-enter {
animation: ${transitions.slideDown} .5s ease-in-out;
animation-fill-mode: forwards;
}
.modal-leave {
animation: ${transitions.slideUp} .5s ease-in-out;
animation-fill-mode: forwards;
}
`;
src/components/Modal.js
// render 함수
render() {
// 레퍼런스 생성
const {visible, children, width} = this.props;
return (
<div>
<Wrapper width={width}>
<CSSTransitionGroup
transitionName="modal"
transitionEnterTimeout={500}
transitionLeaveTimeout={500}>
{
/* visible 이 참일때만 ModalBox 보여줌 */
visible && (<ModalBox>{children}</ModalBox>)
}
</CSSTransitionGroup>
</Wrapper>
</div>
);
}
유저이미지를 담을 ThumbnailWrapper
인풋을 담을 Form
버튼을 담을 ButtonsWrapper
src/components/ContactModal.js
import React, { Component } from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import Modal from './Modal';
import PropTypes from 'prop-types';
const ThumbnailWrapper = styled.div`
/* 레이아웃 */
padding-top: 3rem;
padding-bottom: 3rem;
display: flex;
justify-content: center;
/* 색상 */
background: white;
`;
const Form = styled.div`
/* 레이아웃 */
padding: 1rem;
/* 색상 */
background: ${oc.gray[0]};
`;
const ButtonsWrapper = styled.div`
/* 레이아웃 */
display: flex;
`;
class ContactModal extends Component {
static propTypes = {
visible: PropTypes.bool,
// 모달의 모드
mode: PropTypes.oneOf(['create', 'modify']),
// 모달에 들어갈 데이터 값
name: PropTypes.string,
phone: PropTypes.string,
color: PropTypes.string,
onHide: PropTypes.func,
onAction: PropTypes.func, // 추가 혹은 수정
onRemove: PropTypes.func // 나중에 구현할 삭제
}
render() {
const {
visible,
onHide
} = this.props;
return (
<Modal visible={visible} onHide={onHide}>
<ThumbnailWrapper></ThumbnailWrapper>
<Form></Form>
<ButtonsWrapper>안녕하세요 버튼들</ButtonsWrapper>
</Modal>
);
}
}
export default ContactModal;
Thumbnail 컴포넌트 (유저이미지) 만들기
컴포넌트의 색상과 크기는 유동적,
각 유저마다 색상이 다름.
리스트에서 보여줄때와 모달에서 보여줄때 크기가 다름.
size, color 를 props 으로 받아와서 이에따라 반영됨
src/components/Thumbnail.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import Person from 'react-icons/lib/md/person';
import PropTypes from 'prop-types';
const Wrapper = styled.div`
/* 레이아웃 */
width: ${props => props.size };
height: ${props => props.size };
display: flex;
align-items: center;
justify-content: center;
/* 기타 */
border-radius: calc(${props => props.size} * 0.5); /* 동그라미가 되려면 이 값이 사이즈의 1/2 이상이어야 함 */
font-size: calc(${props => props.size} * 0.75);
/* 색상 */
background: ${props => props.color};
color: white;
`;
Wrapper.propTypes = {
size: PropTypes.string,
color: PropTypes.string
};
const Thumbnail = ({size, color}) => (
<Wrapper size={size} color={color}>
<Person/>
</Wrapper>
);
Thumbnail.propTypes = {
size: PropTypes.string,
color: PropTypes.string
};
Thumbnail.defaultProps = {
size: '4rem',
color: '#000'
};
export default Thumbnail;
모달에서 보여주기
1. Thumbnail 불러오기
2. mode, name, phone, color 레퍼런스 생성
3. Thumbnail size="8rem" color={color}
src/components/ContactModal.js
// 상단
import Thumbnail from './Thumbnail';
// render 함수
render() {
const {
visible,
mode,
name,
phone,
color,
onHide
} = this.props;
return (
<Modal visible={visible} onHide={onHide}>
<ThumbnailWrapper>
<Thumbnail size="8rem" color={color}/>
</ThumbnailWrapper>
<Form></Form>
<ButtonsWrapper>안녕하세요 버튼들</ButtonsWrapper>
</Modal>
);
}
Input 컴포넌트 만들기
styled.div 대신 styled.input !
src/components/Input.js
import React from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import PropTypes from 'prop-types';
const Input = styled.input`
/* 레이아웃 */
width: 100%;
padding: 0.5rem;
/* 색상 */
border: 1px solid ${oc.gray[2]};
/* 기타 */
font-size: 1.5rem;
line-height: 2rem;
transition: all .25s;
/* 입력중일때 */
&:focus {
outline: none;
border: 1px solid ${oc.pink[3]};
color: ${oc.pink[6]};
}
/* 컴포넌트 사이 간격 */
& + & {
margin-top: 1rem;
}
`;
Input.propTypes = {
name: PropTypes.string,
value: PropTypes.string,
placeholder: PropTypes.string,
onChange: PropTypes.func
};
export default Input;
모달에서 Input 불러오기
src/components/ContactModal.js
// 상단
import Input from './Input';
// render 함수
render() {
const {
visible,
mode,
name,
phone,
color,
onHide
} = this.props;
return (
<Modal visible={visible} onHide={onHide}>
<ThumbnailWrapper>
<Thumbnail size="8rem" color={color}/>
</ThumbnailWrapper>
<Form>
<Input
name="name"
placeholder="이름"
/>
<Input
name="phone"
placeholder="전화번호"
/>
</Form>
<ButtonsWrapper>안녕하세요 버튼들</ButtonsWrapper>
</Modal>
);
}
src/components/ContactModal.js - Button
const Button = styled.div`
/* 레이아웃 */
padding-top: 1rem;
padding-bottom: 1rem;
flex: 1;
display: inline-block;
/* 기타 */
cursor: pointer;
text-align: center;
font-weight: 500;
font-size: 1.2rem;
transition: all .3s;
/* 색상 */
color: white;
background: ${props => oc[props.color][7]};
/* 마우스가 위에 있을 때 */
&:hover {
background: ${props => oc[props.color][6]};
}
/* 클릭 될 때 */
&:active {
background: ${props => oc[props.color][8]};
}
`;
Button.propType = {
color: PropTypes.string
};
src/components/ContactModal.js - render
render() {
const {
visible,
mode,
name,
phone,
color,
onHide
} = this.props;
return (
<Modal visible={visible} onHide={onHide}>
<ThumbnailWrapper>
<Thumbnail size="8rem" color={color}/>
</ThumbnailWrapper>
<Form>
<Input
name="name"
placeholder="이름"
/>
<Input
name="phone"
placeholder="전화번호"
/>
</Form>
<ButtonsWrapper>
<Button color="teal">
{ mode === 'create' ? '추가' : '수정'}
</Button>
<Button color="gray">
취소
</Button>
</ButtonsWrapper>
</Modal>
);
}
Input 상태 연동하기
src/App.js - modalhandler - change
change: ({name, value}) => {
this.setState({
modal: {
...this.state.modal,
[name]: value // 인자로 전달받은 name 의 값을 value 로 설정
}
})
},
src/App.js - render - ContactModal
<ContactModal
{...modal}
onHide={modalHandler.hide}
onChange={modalHandler.change}
/>
onChange 프로퍼티로 전달
onChange 받아서 사용하기
src/components/ContactModal.js - handleChange
handleChange = (e) => {
const { onChange } = this.props;
onChange({
name: e.target.name,
value: e.target.value
});
}
handleChange 만들기
src/components/ContactModal.js - handleChange
render() {
const { handleChange } = this;
const {
visible,
mode,
name,
phone,
color,
onHide
} = this.props;
return (
<Modal visible={visible} onHide={onHide}>
<ThumbnailWrapper>
<Thumbnail size="8rem" color={color}/>
</ThumbnailWrapper>
<Form>
<Input
name="name"
placeholder="이름"
value={name}
onChange={handleChange}
/>
<Input
name="phone"
placeholder="전화번호"
value={phone}
onChange={handleChange}
/>
</Form>
<ButtonsWrapper>
<Button color="teal">
{ mode === 'create' ? '추가' : '수정'}
</Button>
<Button color="gray">취소</Button>
</ButtonsWrapper>
</Modal>
);
}
}
onChange 와 value 값 설정
src/App.js - state
state = {
view: 'favorite',
modal: {
visible: false,
mode: null // create 혹은 modify
},
contacts: []
}
App 컴포넌트 state 에 contact 빈 배열 생성
src/App.js - 상단
import shortid from 'shortid';
App 에서 shortid 불러오기
src/App.js - modalHandler - action
action: {
create: () => {
// 고유 ID 생성
const id = shortid.generate();
// 레퍼런스 생성
const { contacts, modal: { name, phone, color } } = this.state;
// 데이터 생성
const contact = {
id,
name,
phone,
color,
favorite: false // 즐겨찾기의 기본값은 false
};
this.setState({
// 기존 배열에있던것들을 집어넣고, contact 를 뒤에 추가한 새 배열로 설정
contacts: [...contacts, contact]
});
// 모달 닫기
this.modalHandler.hide();
},
modify: null,
remove: null
}
데이터 생성함수 만들기
src/App.js - render - ContactModal
<ContactModal
{...modal}
onHide={modalHandler.hide}
onChange={modalHandler.change}
onAction={modalHandler.action[modal.mode]}
/>
onAction 전달하기
src/components/ContactModal.js - render
render() {
const { handleChange } = this;
const {
visible,
mode,
name,
phone,
color,
onHide,
onAction
} = this.props;
return (
<Modal visible={visible} onHide={onHide}>
<ThumbnailWrapper>
<Thumbnail size="8rem" color={color}/>
</ThumbnailWrapper>
<Form>
<Input
name="name"
placeholder="이름"
value={name}
onChange={handleChange}
/>
<Input
name="phone"
placeholder="전화번호"
value={phone}
onChange={handleChange}
/>
</Form>
<ButtonsWrapper>
<Button color="teal"
onClick={onAction}>
{ mode === 'create' ? '추가' : '수정'}
</Button>
<Button
onClick={onHide}
color="gray">
취소
</Button>
</ButtonsWrapper>
</Modal>
);
}
onAction 사용하기
주소록 기본 값 설정
src/App.js - state
state = {
view: 'list', // 당분간 list 에서 작업하기 때문에 임시 기본값
modal: {
visible: false,
mode: null // create 혹은 modify
},
contacts: [
{
"id": "SyKw5cyAl",
"name": "김민준",
"phone": "010-0000-0000",
"color": "#40c057",
"favorite": true
},
{
"id": "r1s_9c10l",
"name": "아벳",
"phone": "010-0000-0001",
"color": "#12b886",
"favorite": true
},
{
"id": "BJcFqc10l",
"name": "베티",
"phone": "010-0000-0002",
"color": "#fd7e14",
"favorite": false
},
{
"id": "BJUcqqk0l",
"name": "찰리",
"phone": "010-0000-0003",
"color": "#15aabf",
"favorite": false
},
{
"id": "rJHoq91Cl",
"name": "데이비드",
"phone": "010-0000-0004",
"color": "#e64980",
"favorite": false
}
]
}
기본 틀 만들기
src/components/ContactList.js
import React, { Component } from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
class ContactList extends Component {
static propTypes = {
contacts: PropTypes.arrayOf(PropTypes.object),
search: PropTypes.string, // 검색 키워드
onToggleFavorite: PropTypes.func, // 즐겨찾기 토글
onOpenModify: PropTypes.func // 수정 모달 띄우기
}
render() {
const { contacts } = this.props
const contactList = contacts.map(
contact => <div key={contact.id}>{JSON.stringify(contact)}</div>
);
return (
<div>
{contactList}
</div>
);
}
}
export default ContactList;
src/App.js
// 상단
import ContactList from './components/ContactList';
App 에서 ContactList 렌더링
render() {
// 레퍼런스 준비
const {
handleSelectView,
handleFloatingButtonClick,
modalHandler
} = this;
const {
view,
modal,
contacts
} = this.state;
return (
<div>
<Header/>
<ViewSelector onSelect={handleSelectView} selected={view}/>
{/* view 값에 따라 다른 컨테이너를 보여준다 */}
<Container visible={view==='favorite'}>즐겨찾기</Container>
<Container visible={view==='list'}>
<ContactList contacts={contacts}/>
</Container>
<ContactModal
{...modal}
onHide={modalHandler.hide}
onChange={modalHandler.change}
onAction={modalHandler.action[modal.mode]}
/>
<Dimmed visible={modal.visible}/>
<FloatingButton onClick={handleFloatingButtonClick}/>
</div>
);
}
기본 틀 만들기
src/components/ContactItem.js
import React, { Component } from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import PropTypes from 'prop-types';
import Thumbnail from './Thumbnail';
const Wrapper = styled.div`
/* 레이아웃 */
padding: 1rem;
position: relative;
overflow: hidden;
display: flex;
/* 색상 */
background: ${oc.gray[0]};
border: 1px solid ${oc.gray[2]};
/* 애니메이션 */
transition: all .25s;
/* 사이 간격 */
& + & {
margin-top: 1rem;
}
.actions {
/* 레이아웃 */
position: absolute;
top: 0;
right: -3rem; /* 기본적으로는 숨겨있음 */
width: 3rem;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column; /* 세로로 나열 */
/* 색상 */
background: ${oc.gray[1]};
border-left: 1px solid ${oc.gray[2]};
opacity: 0; /* 기본적으론 투명함 */
/* 애니메이션 */
transition: all .4s;
}
/* 커서가 위에 있으면 */
&:hover {
border: 1px solid ${oc.gray[4]};
background: white;
/* actions 를 보여준다 */
.actions {
opacity: 1;
right: 0rem;
}
}
`
const Info = styled.div`
/* 레이아웃 */
margin-left: 1rem;
flex: 1;
display: flex;
justify-content: center;
flex-direction: column;
/* 나머지 공간 꽉 채워서 잘 나타나는지 테스트용 */
background: black;
`
class ContactItem extends Component {
static propTypes = {
contact: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
phone: PropTypes.string,
color: PropTypes.string,
favorite: PropTypes.bool
}),
onToggleFavorite: PropTypes.func,
onOpenModify: PropTypes.func
}
render() {
return (
<Wrapper>
<Thumbnail/>
<Info/>
<div className="actions">Hi</div>
</Wrapper>
);
}
}
export default ContactItem;
ContactItem 렌더링하기
src/components/ContactList.js
import React, { Component } from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import ContactItem from './ContactItem';
class ContactList extends Component {
static propTypes = {
contacts: PropTypes.arrayOf(PropTypes.object),
search: PropTypes.string, // 검색 키워드
onToggleFavorite: PropTypes.func, // 즐겨찾기 토글
onOpenModify: PropTypes.func // 수정 모달 띄우기
}
render() {
const { contacts } = this.props;
const contactList = contacts.map(
contact => (
<ContactItem
key={contact.id}
contact={contact}
/>
)
);
return (
<div>
{contactList}
</div>
);
}
}
export default ContactList;
src/components/ContactItem.js
(...)
const Info = styled.div`
/* 레이아웃 */
margin-left: 1rem;
flex: 1;
display: flex;
justify-content: center;
flex-direction: column;
`;
const Name = styled.div`
font-size: 1.25rem;
color: ${oc.gray[9]};
font-weight: 500;
`;
const Phone = styled.div`
color: ${oc.gray[6]}
margin-top: 0.25rem;
`;
class ContactItem extends Component {
static propTypes = {
contact: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
phone: PropTypes.string,
color: PropTypes.string,
favorite: PropTypes.bool
}),
onToggleFavorite: PropTypes.func,
onOpenModify: PropTypes.func
}
render() {
// 레퍼런스 준비
const {
contact: { name, phone, favorite, id, color }
} = this.props;
return (
<Wrapper>
<Thumbnail color={color}/>
<Info>
<Name>{name}</Name>
<Phone>{phone}</Phone>
</Info>
<div className="actions">Hi</div>
</Wrapper>
);
}
}
export default ContactItem;
ContactItem actions 버튼들 만들기
src/components/ContactItem.js
import StarIcon from 'react-icons/lib/md/star';
import EditIcon from 'react-icons/lib/md/edit';
아이콘 불러오기
src/components/ContactItem.js - CircleButton
const CircleButton = styled.div`
/* 레이아웃 */
height: 2rem;
width: 2rem;
display: flex;
align-items: center;
justify-content: center;
margin: 0.25rem;
/* 색상 */
background: white;
border: 1px solid ${oc.gray[4]};
color: ${oc.gray[4]};
/* 기타 */
border-radius: 1rem;
font-size: 1.15rem;
/* 마우스 커서가 위에 있을 때*/
&:hover {
border: 1px solid ${oc.gray[7]};
color: ${oc.gray[9]};
}
/* 즐겨찾기 - 노란색 */
${props => props.favorite && `
&:active {
border: 1px solid ${oc.yellow[6]};
color: ${oc.yellow[6]};
}
`}
`;
CircularButton 컴포넌트 스타일 생성
src/components/ContactItem.js - render
render() {
// 레퍼런스 준비
const {
contact: { name, phone, favorite, id, color }
} = this.props;
return (
<Wrapper>
<Thumbnail color={color}/>
<Info>
<Name>{name}</Name>
<Phone>{phone}</Phone>
</Info>
<div className="actions">
<CircleButton favorite>
<StarIcon/>
</CircleButton>
<CircleButton>
<EditIcon/>
</CircleButton>
</div>
</Wrapper>
);
}
CircularButton 컴포넌트 렌더링
App 컴포넌트에 itemHandler 만들기
- toggleFavorite
- openModify
src/App.js
itemHandler = {
toggleFavorite: null,
openModify: (id) => {
const { contacts } = this.state;
// id 로 index 조회
const index = contacts.findIndex(contact => contact.id === id);
const item = this.state.contacts[index];
this.modalHandler.show(
'modify',
{
...item,
index
}
);
}
}
ContactList 에 onOpenModify 전달
src/App.js
render() {
// 레퍼런스 준비
const {
handleSelectView,
handleFloatingButtonClick,
modalHandler,
itemHandler
} = this;
(...)
<ContactList
contacts={contacts}
onOpenModify={itemHandler.openModify}
/>
(...)
}
onOpenModify 를 ContactItem 으로 전달
src/components/ContactList.js
render() {
const { contacts, onOpenModify } = this.props;
const contactList = contacts.map(
contact => (
<ContactItem
key={contact.id}
contact={contact}
onOpenModify={onOpenModify}
/>
)
);
return (
<div>
{contactList}
</div>
);
}
onOpenModify 호출하기
src/components/ContactItem.js
render() {
// 레퍼런스 준비
const {
contact: { name, phone, favorite, id, color },
onOpenModify
} = this.props;
return (
<Wrapper>
<Thumbnail color={color}/>
<Info>
<Name>{name}</Name>
<Phone>{phone}</Phone>
</Info>
<div className="actions">
<CircleButton favorite>
<StarIcon/>
</CircleButton>
<CircleButton onClick={() => onOpenModify(id)}>
<EditIcon/>
</CircleButton>
</div>
</Wrapper>
);
}
배열을 변경 할 땐, ... 와 slice 를 사용
src/App.js - modalHandler
// 모달 관련 메소드들
modalHandler = {
(...)
action: {
create: () => {
(...)
},
modify: () => {
// 레퍼런스 준비
const {
modal: { name, phone, index },
contacts
} = this.state;
const item = contacts[index];
// 상태 변경
this.setState({
contacts: [
...contacts.slice(0, index), // 0 ~ index 전까지의 객체를 넣음
{
...item, // 기존의 아이템 값에
name, // name 과
phone // phone 을 덮어 씌움
},
...contacts.slice(index + 1, contacts.length) // 그 뒤에 객체들을 넣음
]
});
// 모달 닫기
this.modalHandler.hide();
},
remove: () => {
// 레퍼런스 준비
const {
modal: { index },
contacts
} = this.state;
// 상태 변경
this.setState({
contacts: [
...contacts.slice(0, index), // 0 ~ index 전까지의 객체를 넣음
...contacts.slice(index + 1, contacts.length) // 그 뒤에 객체들을 넣음
]
});
// 모달 닫기
this.modalHandler.hide();
}
}
}
ContactModal 에 onRemove 전달하기
src/components/ContactModal.js
<ContactModal
{...modal}
onHide={modalHandler.hide}
onChange={modalHandler.change}
onAction={modalHandler.action[modal.mode]}
onRemove={modalHandler.action.remove}
/>
삭제버튼 만들고
삭제함수 호출하기
src/components/ContactModal.js
import RemoveIcon from 'react-icons/lib/md/remove-circle';
삭제 아이콘 불러오기
src/components/ContactModal.js
const RemoveButton = styled.div`
/* 레이아웃 */
position: absolute;
right: 1rem;
top: 1rem;
/* 색상 */
color: ${oc.gray[6]};
/* 기타 */
cursor: pointer;
font-size: 2rem;
/* 마우스 커서 위에 있을 때 */
&:hover {
color: ${oc.red[6]};
}
/* 마우스 커서 클릭 시 */
&:active {
color: ${oc.red[7]}
}
${props => !props.visible && 'display: none;'}
`
RemoveButton.propTypes = {
visible: PropTypes.bool
};
RemoveButton 스타일링
src/components/ContactModal.js
render() {
const { handleChange } = this;
const {
visible,
mode,
name,
phone,
color,
onHide,
onAction,
onRemove
} = this.props;
return (
<Modal visible={visible} onHide={onHide}>
<ThumbnailWrapper>
<RemoveButton
visible={mode==='modify'}
onClick={onRemove}>
<RemoveIcon/>
</RemoveButton>
<Thumbnail size="8rem" color={color}/>
</ThumbnailWrapper>
(...)
RemoveButton 렌더링하기
src/App.js - 상단
import Input from './components/Input';
App에 Input컴포넌트 불러오기
src/App.js
state = {
view: 'list',
modal: {
visible: false,
mode: null // create 혹은 modify
},
contacts: [
(...)
],
search: ''
}
state 에 search 값 추가
src/App.js - state
// 검색창 수정
handleSearchChange = (e) => {
this.setState({
search: e.target.value
});
}
검색 수정 함수 만들기
src/App.js - render
render() {
// 레퍼런스 준비
const {
handleSelectView,
handleFloatingButtonClick,
modalHandler,
itemHandler,
handleSearchChange
} = this;
const {
view,
modal,
contacts,
search
} = this.state;
return (
<div>
<Header/>
<ViewSelector onSelect={handleSelectView} selected={view}/>
{/* view 값에 따라 다른 컨테이너를 보여준다 */}
<Container visible={view==='favorite'}>즐겨찾기</Container>
<Container visible={view==='list'}>
<Input
onChange={handleSearchChange}
value={search}
placeholder="검색"
/>
<ContactList
contacts={contacts}
onOpenModify={itemHandler.openModify}
search={search}
/>
(...)
Input 렌더링 및 상태 설정
src/components/ContactList.js
import React, { Component } from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import ContactItem from './ContactItem';
const Wrapper = styled.div`
margin-top: 1rem;
`;
class ContactList extends Component {
static propTypes = {
contacts: PropTypes.arrayOf(PropTypes.object),
search: PropTypes.string, // 검색 키워드
onToggleFavorite: PropTypes.func, // 즐겨찾기 토글
onOpenModify: PropTypes.func // 수정 모달 띄우기
}
render() {
const { contacts, onOpenModify } = this.props;
const contactList = contacts.map(
contact => (
<ContactItem
key={contact.id}
contact={contact}
onOpenModify={onOpenModify}
/>
)
);
return (
<Wrapper>
{contactList}
</Wrapper>
);
}
}
export default ContactList;
margin-top 설정한 Wrapper 생성
src/components/ContactList
const contactList = contacts.filter( // 키워드로 필터링
c => c.name.indexOf(search) !== -1
).sort( // 가나다순으로 정렬
(a,b) => {
if(a.name > b.name) return 1;
if (a.name < b.name) return -1;
return 0;
}
).map( // 컴포넌트로 매핑
contact => (
<ContactItem
key={contact.id}
contact={contact}
onOpenModify={onOpenModify}
/>
)
);