주소록 만들기

1. 준비작업

$ 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

  • open-color: 매우 유용한 색상 관련 라이브러리입니다.
  • prop-types: 컴포넌트의 PropTypes 를 지정할때 필요합니다.
  • react-icons: 다양한 아이콘들을 SVG 형태로 불러와서 사용 할 수 있습니다.
    필요한 아이콘만 불러오기 때문에 용량 걱정이 적습니다.
    Material Design Icons, FontAwesome, Typicons, Github Octicons, Ionicons 의 모든 아이콘들을 골라서 사용 할 수 있습니다)

src/components 디렉토리 생성

2. 기본 컴포넌트 만들기

vscode-styled-components

VSCode 확장 프로그램 설치

 

(CSS 코드에 색상을 입혀줌)

Header 컴포넌트 만들기

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 렌더링

검정색 바, 잘 보여요?

open-color

디자인 초보도 오픈컬러가있으면 절반은 갑니다!

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 라는 익스텐션을 사용하면 쉽게 스니펫 생성 가능

Container 컴포넌트 만들기

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 추가

3. ViewSelector

컴포넌트 만들기

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 렌더링

아이콘 보여주기

  • react-icons/lib/md/star
  • react-icons/lib/md/people

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;

아이콘 불러와서, 사용하기

(...) 은 생략을 의미합니다

App 상태 정의 및 업데이트

즐겨찾기를 보여줄지, 리스트를 보여줄지?

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 설정

쉼,

4. FloatingButton

컴포넌트 만들기

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 렌더링

5. 모달 만들기

App 에서 모달 상태 정의 및 업데이트

1. state 수정

2. modalHandler 작성

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 를 이용하면 시작과 끝 뿐만 아니라 그 중간에서도 변화를 줄 수 있다!

@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>
        );
    }

6. ContactModal

완성하기

틀 만들기

유저이미지를 담을 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>
        );
    }

ContactModal 내부 버튼 만들기

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 값 설정

state 에 데이터 추가하기

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 사용하기

쉼,

7. 주소록 리스트 

렌더링하기

주소록 기본 값 설정

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
            }
        ]
    }

ContactList 컴포넌트 만들기

기본 틀 만들기

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

ContactItem 컴포넌트 만들기

기본 틀 만들기

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;

8. 주소록 수정 및 삭제

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

주소록 수정 / 삭제 함수 구현하기

App 컴포넌트의 modalHandler 수정

배열을 변경 할 땐, ... 와 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 렌더링하기

9. 검색 기능, 정렬기능

구현하기

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

10. 즐겨찾기 기능

튜토리얼을 따라하며

직접 해보세요!

contact-tutorial

By Minjun Kim

contact-tutorial

  • 2,116