$ 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}
                />
            )
        );