FrontEnd/React,Redux

PART7:Simply build App with Redux - Read, Update / Redux Third Action

미스터황탄 2016. 9. 29. 02:11

Read,Update,Delete Here's the Plan

지난 포스팅에 Create 를 구현 했다.

Create 포스팅이 길었던 이유는 PART4 이후로 두번째로 구현한

Redux action 이기 때문에 조금더 상세히 설명하고자 했기 때문인것 같다.

이번 포스팅에서는 Read와 Update 를 구현 하고자 한다.

구현 흐름은 앞선 포스팅인 Create 와 비슷하기 때문에 설명이 다소 불친절 하다면

조금 친절한 Create 부분과 비교해서 보면 도움이 될것 같다.

구현에 앞서 CURD 중 남은 RUD 구현계획에 대해서 정리 해보자.

내가 포스팅 하고 있는 Redux App 에는 회원이란 개념이 존재 하지 않기 때문에

특정인이 특정글에 대한 권한을 갖지 않는다. 그렇기 때문에 내 계획은 이렇다.

  • Read(읽기)를 통해 특정 게시글을 읽게 되면 author,title,content 영역이 비활성화된다.

  • Read(읽기) 상태가 되면 Title이 Content 로 바뀌고 하단부에 버튼이 Modify로 바뀐다.

  • Modify 를 클릭하면 Update(수정) 으로 상태가 변경 된다.

  • Update(수정) 상태는 Title 이 Update 로 바뀌고 버튼이 Update 로 바뀐다.

  • Delete(삭제) 는 각 게시글 라인의 오른쪽 끝에 Delete 버튼을 추가해서 바로 삭제가 가능하게 한다.

남은 RUD 에 대한 구현 계획은 이러하고 이러한 각각에 Action 에 대해서 처리가

잘되었다는 알림을 띄우고 싶다.

Bootstrap 에 Modal을 활용 할수도 있지만 개발 환경 설정시 설치 했던 toastr 모듈을 활용해 보고자 한다.

toastr 를 활용해서 글이 작성되면 알림이 뜨는 기능을 추가 해볼것이다.

메니페스트 파일(package.json)에서 의존성"toastr": "^2.1.2" 이 없다면 설치 하면된다.

Toastr

Demo 는 여기서 확인 한다.

먼저 css 를 로드 해야 하는데 src 폴더 하위에 있는 index.js 파일을 열고 다음 코드를 추가 한다.

import '../node_modules/toastr/build/toastr.min.css';

다음 코드가 작동되지 않는다면 PART3 로더 부분을 설정 해야 한다.

그리고 ManageBoard.js 에 가서 toastr 을 import 하고 사용 하면 된다.

import toastr from 'toastr';

callWrite 에서 this.context.router.pushredirect함수로 분리하고

오른쪽 하단부에 성공 메세지를 출력 한다.

  callWrite(event) {
        event.preventDefault();

        this.props.actions.writeBoard(this.state.contents)
        .then(() => this.redirect())
        .catch(error => {
            console.log(error);
        })
    }

    redirect() {
        toastr.options.positionClass = 'toast-bottom-right';
        toastr.success('content saved');
        this.context.router.push('/about');
    }

오른쪽 하단부에 알림이 뜨는 모습을 확인할수 있다.

 

Read & Update

Read 와 Update 본격적으로 구현해보자.

Read 는 게시글 상에서 title 을 클릭했을때 컨텐츠를 보여주는 기능 이다.

페이지 로드시 모든 더미 데이터를 redux store가 가지고 있기 때문에

우리는 이 값을 통해 smart component 로부터 dumb component 를 수정해 나가면 될것이다.

폼을 상황에 맞게 재사용하기 위한 작업부터 진행 한다.

첫번째.사용자가 입력한 값을 입력받는 영역을 생각해보자.

폼은 WriteUpdate 외엔 사용하지 않음으로 수정이 불가한 영역을 만들어 준다.

바꿔 말하면 Read 인 경우에만 폼을 비활성화 시켜주면 된다.

두번째.하단부에 위치해 있는 버튼을 생각 해보자.

상황별로 버튼이 하는 역할은 바뀌어야 한다.

button-value description
Save 리스트 상에서 write 를 눌렀을때 호출 글을 저장한다.
Modify 수정 불가능한 Content 를 수정 가능한 상태로 변경 한다.
Update 수정이 완료되면 해당글을 업데이트하고 리스트로 돌아간다.

폼에 대해서 고려할 상황 두가지를 생각해 봤다. 이제 생각 대로 구현에 들어가면 된다.

최초 게시판에 게시글을 불러올때 라우터의 path 는 /about 을 가르키고 있다.

title을 클릭해서 내용을 확인 할때 해당 게시글에 id값으로 내용을 식별할것이다.

라우터에 id 파라미터를 받아와야 함으로 라우터를 추가해줘야 한다.

routers.js

        <Route path="/" component={Header}>
            <IndexRoute component={Main} />
            <Route path="main" component={Main} />
            <Route path="about" component={About} />
            <Route path="about/:id" component={ManageBoard} /> //추가
            <Route path="write" component={ManageBoard} />
        </Route>

이제 title이 id 값을 가지고 넘어갈수 있도록 Link 태그로 감싼다.

BoardList.js 에 title을 수정 한다.

BoardList.js

<tbody>
    {contents.map(content =>
        <tr key={content.id}>
            <td>{content.id}</td>
            <td>
              <Link to={'/about/'+content.id}>{content.title}</Link>
            </td>
            <td>{content.author}</td>
            <td>{content.date}</td>
        </tr>
            )}
</tbody>

title을 클릭하면 해당 게시글에 아이디 값을 가지고 ManageBoard 컴포넌트를

호출한다.

ManageBoard 는 smart component 이다. 이젠 모두 아시리라 믿는다.

아직 상황별 로직 구현이 되어있지 않기때문에 게시글을 클릭해도

아무것도 없는 Write 폼이 출력 된다.

ManageBoard.js을 수정해보자.

지난 포스팅까지는 단순히 게시글을 호출해 오는것에 불과 했기 때문에

mapStateToProps 에서 props로 contents 만 반환한것을 기억할것이다.

mapStateToProps 를 수정할 계획이다..

mapStateToProps 에서는 라우터에 params가 있는지 식별해서 없는경우

Write Form 있는경우에는 Content Form 이에 맞는 formTitleinitContent

props 로 변환하는 내용을 구현 했다.

function mapStateToProps(state, ownProps) {
    let initContent = {},
          formTitle = '',
            paramId = ownProps.params.id;

    if (paramId) {
        formTitle   = 'Content';
        initContent = state.contents.filter((content) => content.id === paramId)[0];
    }
    else {
        formTitle   = 'Write';
        initContent = {
                id: '',
                title:'',
                content: '',
                author:'',
                date:''
        }
    }

    return {
        contents : initContent,
        formTitle : formTitle,
    }
}

formTitle 이란 props 는 class 내에서 setState 가 필요하기 때문에

생성자의 this.state 에 객체 프로퍼티를 추가하고

당연히 prop이 추가 되었기때문에 class 외부에 propTypes 도 정의해줘야 한다.

/*constructor 내부*/
this.state = {
            contents: Object.assign({}, props.contents),
            formTitle: props.formTitle
        }

/*class 외부*/        
ManageBoard.propTypes = {
    contents: PropTypes.object.isRequired,
    actions: PropTypes.object.isRequired,
    formTitle: PropTypes.string.isRequired,
}

다음은 버튼 상황별로 함수들을 만들고 render 부분에 로직을 추가 한다.

/*클래스 내부 정의*/
callModify() {...}
callUpdate() {...}
render() {
            let formTitle     = this.state.formTitle,
                contentResult = this.props.contents,
                changeState   = this.updateChangeState.bind(this),
                onClickTypes  = '',
                buttonName    = '',
                isActivate    = false;

                 switch (formTitle) {
                    case 'Content':
                        buttonName   = 'Modify';
                        onClickTypes = this.callModify.bind(this);
                        isActivate   = true;
                    break;

                    case 'Update' :
                        buttonName   = 'Update';
                        onClickTypes = this.callUpdate.bind(this);
                    break;

                    case 'Write' :
                        buttonName   = 'Save';
                        onClickTypes = this.callWrite.bind(this);
                    break;
                    default : buttonName = 'unknown'; break;
                 }

        return (
            <BoardForm
                isActivate  = {isActivate}
                readContent = {contentResult}
                formTitle   = {formTitle}
                buttonName  = {buttonName}
                onChange    = {changeState}
                onSave      = {onClickTypes} />
            );
    }

smart component 에서 내려주는 작업이 끝났으니 dumb component 인

BoardForm.jsFormInput.js 를 수정 한다.

BoardForm.js

const BoardForm = ({onChange, onSave, formTitle, readContent, isActivate, buttonName}) => {
    return (
        <form>
        <h1>{formTitle}</h1>
            <FormInput name="author"
                       label="Author"
                       onChange={onChange}
                       defaultValue={readContent.author}
                       disabled={isActivate} />

            <FormInput name="title"
                       label="Title"
                       onChange={onChange}
                       defaultValue={readContent.title}
                       disabled={isActivate} />

            <div className="form-group">
                 <label htmlFor="content">Content</label>
                 <textarea name="content"
                             id="content"
                      className="form-control"
                       onChange={onChange}
                   defaultValue={readContent.content}
                   disabled={isActivate}></textarea>
            </div>
            <input type="button"
                   value={buttonName}
                   onClick={onSave}
                   className="btn btn-success"/>
        </form>
        );
}

BoardForm.propTypes = {
    onChange:    PropTypes.func.isRequired,
    onSave:      PropTypes.func.isRequired,
    formTitle:   PropTypes.string.isRequired,
    readContent: PropTypes.object.isRequired,
    isActivate:  PropTypes.bool.isRequired,
    buttonName:  PropTypes.string.isRequired
}

FormInput.js

const FormInput = ({name, label, onChange, defaultValue, disabled}) => {
    return (
        <div className="form-group">
            <label htmlFor={name}>{label}</label>
            <input type="text"
                   name={name}
                   className="form-control"
                   onChange={onChange}
                   defaultValue={defaultValue}
                   disabled={disabled} />
        </div>
        );
}

FormInput.propTypes = {
     name :        PropTypes.string,
     label:        PropTypes.string,
     onChange:     PropTypes.func.isRequired,
     defaultValue: PropTypes.string,
     disabled:     PropTypes.bool.isRequired
};

여기까지 구현했다면 이제 정상적으로 Content 를 확인할수 있을것이다.

 

다시 smart component 로 가보자.

class 내부에 정의한 callModifycallUpdate 를 구현한다.

먼저 callModify 는 Content 로 넘어가면서 비활성화 되었던 폼에 입력 영역을

활성화 시켜주고 Title을 변경하는 역할을 하기 때문에 setState 만 해주면 된다.

callModify() {
        this.setState({formTitle:'Update'});
    }

callUpdatecallWrite 에서 사용한 구현 방식을 그대로 재사용 하면 된다.

여기서 Redux 의 3번째 action 이 추가 된다.

여기 까지 왔다면 해당파일 어느 경로에 어떠한 구문을 추가 해야되는지

상세하게 설명할 필요가 없을것 같다.

파일명과 추가된 내용은 다음과 같다.

actionTypes.js

export const UPDATE_BOARD = 'UPDATE_BOARD';

boardActions.js

import
{
    LOAD_BOARD_CLEAR,
    CALL_WRITE,
    UPDATE_BOARD
} from './actionTypes';
export function updateBoard(content) {
     return (dispatch) => {
        return boardApi.updateContent(content)
        .then(content => dispatch({type : UPDATE_BOARD, content }))
        .catch(error => {
            throw(error);
        });
    }
}

boardApi.js

    static updateContent(content) {
        content = Object.assign({}, content);
        return new Promise((resolve, reject) => {
            let targetIndex = contents.findIndex(a => a.id === content.id);

            content.date =
            new Date()
            .toLocaleDateString()
            .replace(/(\s*)/g,"")
            .split('.')
            .slice(0,3)
            .join('/');

            resolve(content);
        });
    }

boardReducer.js

  case UPDATE_BOARD :
            return [
            ...state.filter(content => content.id !== action.content.id),
            Object.assign({},action.content)
            ].sort((a,b) => a.id > b.id);

이제 updateBoard action에 대한 흐름이 완성 되었다.

callUpdate 를 마무리할 시간이다.

  callUpdate(event) {
        event.preventDefault();

          this.props.actions.updateBoard(this.state.contents)
        .then(() => this.redirect('updated'))
        .catch(error => {
            console.log(error);
        })
    }

코드를 보면 this.redirect 에 문자열을 넘기고 있는것을 볼수 있는데

앞서 toastr 를 소개한적이 있다. 여기 사용하는 메세지 출력을 달리하기 위함인데

callWrite 에 정의된 redirect 에는 saved 라는 문자열을 넘기고

redirect 함수를 다음과 같이 수정해줬다.

 redirect(value) {
        toastr.options.positionClass = 'toast-bottom-right';
        toastr.success(`content ${value}`);
        this.context.router.push('/about');
    }

수정이 잘되는지 확인해보자.

첫번째 글에 작성자를 Jacob Hwang 으로 바꿔 보겠다.

 

수정이 잘되는것을 확인했다.

 

여기까지 Read, Update 를 마무리한다.

다음 포스팅에서는 Delete 를 마지막으로 CRUD 구현이 완료 되고

Modify -> Update 로 전환 되는 부분에 약간에 듀레이션을 추가 할것이다.

그리고 크롬 확장 프로그램인 redux devTools 을 소개 한다.

약간에 사설을 이야기 하자면..

당초 계획은 PART4 에 끝나는걸 목표로 했는데 생각보다 길어지고 있다.

이 포스팅을 보는 분들이 이해를 돕고자 자세히 써내려간 이유도 있지만

내가 안다고 혹은 알았다고 생각한 개념들을 구체화 하다보니 모르는 부분도

상당히 많았다는것을 느끼고 있다. 그것을 풀어서 정리하다 보니 길어지는것같다.

좋은 현상이라고 생각한다.

뻘글에서 part12 까지를 예상했지만 part9 mobX 를 마지막으로 끝날것같다.

필자 본인이 아쉬우면 게시글을 드래그앤 드랍으로 정렬하는 것까지 는 할것 같은데

그건 part9 를 포스팅 해보고 생각할 문제니까 생각은 그때로 접어두고..

오늘은 여기까지.