React(생활코딩)_9일차_CRUD_Update 구현

Lina Hongbi Ko·2023년 9월 6일
0

React_생활코딩

목록 보기
10/23

Create 구현 다음, 이제는 Update 구현을 해보자!
이 Part가 가장 어렵다고 하니, 더 읽어보고 더 많이 연습해야겠다.

1. Update 구현

Update 기능 = 'Read기능 + Create기능' 이다.

업데이트를 하려면 새로운 값을 입력받아야 하므로 입력폼이 있어야 하고 (Create), 기존 콘텐츠를 수정해야 하므로 콘텐츠를 불러와야 한다. (Read)

이 책에서는 그래서 폼 기능이 이미 구현돼있는 CreateContent 컴포넌트를 복사해 사용했다. 그럼 우리도 업데이트를 위한 컴포넌트를 만들어보자.

// UpdateContent.js 파일

import React, { Component } from 'react';

class UpdateContent extends Component {
	render(){
    	console.log('UpdateContent render');
        return(
        	<article>
            	<h2>Update</h2>
                <form action="/create_process" method="post" onSubmit={function(e){
                	e.preventDefault();
                    this.props.onSubmit(e.target.title.value, e.target.desc.value);
                }.bind(this)}>
                	<p><input type="text" name="title" placeholder="title"></input></p>
                    <p><textarea name="desc" placeholder="description"></textarea></p>
                    <p><input type="submit"></input></p>
                </form>
            </article>
        );
    }
}

export default UpdateContent;

CreateContent 컴포넌트와 내용이 거의 비슷하다.

그리고 'update'링크를 클릭할때 mode가 update로 변경되면 출력되는 부분을 작성해보자.

// App.js 파일

... 생략 ...
import CreateContent from './components/CreateContent';
import UpdateContnet from './components/UpdateContent';
import Subject from './comoponents/Subject';
... 생략 ...

class App extends Component {
	constructor() {
    	... 생략 ...
    }
    
    getContent() {
    	let _title, _desc, _article = null;
        if(this.state.mode === "welcome") {
        	_title = this.state.welcome.title;
            _desc = this.state.welcome.desc;
            _article = <ReadContent title={_title} desc={_desc}></ReadContent>
        } else if(this.state.mode === "read") {
        	for(let i=0; i < this.state.contents.length; i++) {
            	const data = this.state.contents[i];
                if(data.id === this.state.selected_content_id) {
                	_title = data.title;
                    _desc = data.desc;
                    break;
                }
            }
            _aritlce = <ReadContent title={_title} desc={_desc}></ReadContent>
        } else if(this.state.mode === 'create') {
        	_article = <CreateContent onSubmit={function(_title, _desc){
            	this.max_content_id = this.max_content_id + 1;
                const _contents = this.state.contents.concat({id:this.max_content_id, title:_title, desc:_desc});
                this.setState({contents:_contents});
                console.log(_title, _desc);
            }.bind(this)}></CreateContent>
        } else if (this.state.mode === 'update') {
        	_article = <UpdateContent onSumbit={function(_title, _desc){
            	this.max_content_id = this.max_content_id + 1;
                const _contents = this.state.contents.concat({id:this.max_content_id, title:_title, desc:_desc});
                this.setState({contents:_contents});
                console.log(_title, _desc);
            }.bind(this)}></UpdateContent>
        }
        return _article;
    }
    render() {
    	return(
        	<div className="App">
            	... 생략
                <Control onChangeMode = {function(_mode){
               this.setState({mode:_mode}); 
                }.bind(this)}></Control>
                {this.getContent()}
            </div>
        );
    }
    
    export default App;

render 함수에 있던 복잡한 내용들을 getContent() 라는 함수에 쪼개서 담았다. 그리고 _article를 return 시켜서 Control 컴포넌트 아래에 {this.getContent()} 를 작성해줘서 함수를 호출했다.

UpdateContent라는 컴포넌트가 실행될때는 입력값으로 현재 선택된 콘텐츠의 id가 필요하다. 그래서 현재 선택된 콘텐츠의 selected_content_id값을 찾아야 한다. 이 작업은 Read 기능을 구현할 때 우리가 작성했던 내용이다.

// selected_content_id 찾기 코드 (Read)

for(let i = 0; i < this.state.contents.length; i++) {
	const data = this.state.contents[i];
    if(data.id === selected_content_id) {
    	_title = data.title;
        _desc = data.desc;
        break;
    }
}

이 코드는 mode가 read 일때와 update 일때 모두 써줘야 해서 중복된다. 그래서 이 코드도 함수로 묶어준다.

// App.js 파일

... 생략 ...

class App extends Component {
	constructor(props) {
    	... 생략 ...
    }
    
    getReadContent() {
    	for(let i = 0; i < this.state.contents.length; i++) {
        	const data = this.state.contents[i];
            if(data.id === selected_content_id) {
            	return data;
            }
        }
    }
    
   getContent() {
   	let _tile, _desc, _article = null;
    if(this.state.mode === 'welcome') {
    	_title = this.state.welcome.title;
        _desc = this.state.welcome.desc;
        _article = <ReadContent title={_title} desc={_desc}></ReadContent>
    } else if (this.state.mode === 'read') {
    	const _content = this.getReadContent();
        _article = <ReadContent title={_content.tile} desc={_content.desc}></ReadContent>
    } else if(this.state.mode === 'create') {
    	... 생략
    } else if (this.state.mode === 'update') {
    	const _content = this.getReadContent();
        _article = <UpdateContent data={_content} onSubmit = {function(_title, _desc){
        	this.max_content_id = this.max_content_id + 1;
            const _contents = this.state.contents.concat({id:this.max_content_id, title:_title, desc:_desc});
            this.setState({contents:_contents});
            console.log(_title, _desc);
        }.bind(this)}></UpdateContent>
    }
    return _article;
   }
   
   ... 생략 ...

getReadContent 함수를 보면 selected_content_id 값과 data.id값이 일치하면 data를 반환한다. 그리고 mode가 'read'일때 이 코드가 있던 자리에 함수를 호출하고 _content라는 변수에 집어 넣어서 반환값을 만들어 ReadContent 컴포넌트의 props title,desc에 값을 전달했다.
그리고 mode가 'update'일때에도 _content라는 반환값을 가져와서 props data에 집어 넣었다.

UpdateContent 컴포넌트에 잘 주입됐는지 확인해보기 위해서 console창을 확인해보자.

// UpateContent.js 파일

import React, { Component } from 'react';

class UpdateContent extends Component {
	render(){
    	console.log(this.props.data);
        console.log('UpdateContent render');
       	return(
        ... 생략 ...

이렇게 작성해주고 'HTML'링크를 클릭하고 아래의 'update' 링크를 클릭하면 나오는 console창이다.

요로코럼 this.porps.data에 클릭한 내용이 들어간 것을 확인 할 수 있다.

2. Update 구현: form

UpdateContent 컴포넌트도 만들었고, 클릭했을때 data도 잘 나오게 만들었다. 이번에는 주입된 데이터를 기반으로 컴포넌트의 기본값을 설정해보자.

결과화면에서 'update'버튼을 눌렀을때 폼의 input 텍스트를 채우는 코드를 작성해보자.

// UpdateContent.js 파일

import React, { Component } from 'react';

class UpdateContent extends Component {
	render() {
    	... 생략 ...
        <p>
        	<input type="text" name="title" placeholder="title" value={this.props.data.title}></input>
        </p>
        <p>
        	<textarea name="desc" placeholder="description"></textarea>
        </p>
        ... 생략 ...

위의 코드처럼 props로 전달된 데이터를 직접 value 속성에 넣었다.

HTML 버튼을 누른 후, 'update'버튼을 누르면 선택된 항목이 input에 잘 들어가는 것을 확인 할 수 있다.

하지만 콘솔에는 경고가 뜬다. 그리고 HTML이라는 input창을 수정하려고 하면 변경되지 않는다.

경고문구는 onChange라는 핸들러를 사용하지 않고 props값을 직접 value에 넣으면 읽기전용(read only)이 된다고 말하고 있다.

props는 읽기 전용이다. 그래서 이 값을 수정하려고 하면 리액트가 개입해서 값을 변경하지 못하게 한다.

다시 수정해보자.

// UpateContent.js 파일

import React, { Component } from 'react';

class UpdateContent extends Component {
	constructor(props) {
    	super(props);
        this.state = {
        	title: this.props.data.title;
        }
    }
    render(){
    ... 생략 ...
    <p>
    	<input type="text" name="title" placehodler="title" value={this.state.title}></input>
    </p>
    <p>
    	<textarea name="desc" placeholder="descripiton"></textarea>
    </p>
	... 생략 ...

constructor를 구현한 다음, props의 값을 UpdateContent의 state에 가변적으로 변하는 데이터로 들어갈 수 있게 했다. 그리고 input의 value속성을 이 state를 사용했다. (vlaue={this.state.title})

📍 props는 읽기전용이고 state은 setState으로 바꿀수 있다 📍 >> 이 말을 항상 기억하고 코드를 작성하도록 하잣!!!!

자 이렇게 하고, 결과하면에서 다시 update를 누르고 input에 값을 입력해보자.

입력값은 잘 들어왔지만,

여전히 수정되지 않는다.

WHY ?

책에서 말하길, 지금 컴포넌트의 input에 지정된 값은 state값이기는 하지만 input의 텍스트값을 수정했다고해서 state가 바뀌어야하는 근거는 없다고 했다.

맞는 말이다.

state를 변경하려면 setState를 사용해야 하지만 setState를 쓰지 않았다.
우리는 input 태그 값을 바꿨을때 state값이 바뀌고, value가 읽기 전용상태가 아닌 것이 되게 해야 한다.

따라서, 아래 다시 코드를 작성해보자.

// UpdateContent.js 파일

import React, { Component } from 'react';

class UpdateContent extends Component {
	... 생략 ...
    render() {
    	... 생략 ...
        <p>
        	<input type="text" name="title" placehodler="title" value={this.state.title} onChange={function(e){
            	console.log(e.target.value);
                this.setState({title:e.target.value});
            }.bind(this)}></input>
        </p>
		... 생략 ...

onChange라는 이벤트를 연결하고, e.target.value의 값을 이용해서 setState함수를 사용해 state를 변경했다.

요것을 반드시 써야 한다!!

state의 title이 e.target.value (this.state.title과 같은 내용)로 바뀐 것을 확인할 수 있다.

코드를 저장하고 화면에서 입력텍스트를 바꿔보자.
UpdateContent의 state를 확인해보면 title값이 입력된대로 변경된 것을 확인할 수 있다.


그리고 textarea도 똑같이 바꿔주자.

// UpdateContent.js 파일

import React, { Component } from 'react';

class UpdateContent extends Component {
	constructor(props) {
    	super(props);
        this.state = {
        	title: this.props.data.title,
            desc: this.props.data.desc
        };
    }
    
    render(){
    	... 생략 ...
        <p>
        	<textarea name="desc" placeholder="description" value={this.state.desc} onChange={function(e){
            	console.log(e.target.value);
                this.setState({desc:e.target.value});
            }.bind(this)}></textarea>
        </p>
		... 생략 ...

constructor의 state에 desc 프로퍼티를 추가하고 this.props.data.desc 값을 준다. 그리고 textarea의 value와 onChange도 위 처럼 구현한다.

책에서 참고할 사항을 말해주는데,

<textarea name="desc" placeholder="description">{this.state.desc}</textarea>

요렇게 value값을 지정하지 않도록 주의해야 한다고 한다.
이렇게 구현하면 다음과 같은 경고메시지가 나온다.

경고메시지의 내용은 defaultValue 또는 value props를 사용하라는 내용인데, 현재 작성하고 있는 이 HTML처럼 생긴 코드는 HTML이 아니다. 이것은 리액트 HTML이다.
이렇게 작성하면 리액트가 최종적으로 실제 HTML로 바꿔주긴하지만 HTML과 동일한 것이 아니라고 한다. 리액트는 textarea 태그 내에 값을 넣는 것을 허용하지 않기 때문에 value props를 사용해야 한다. (HTML에서는 texarea의 기본값은 태그내에 작성해야 하지만 리액트에서는 value props를 사용해야 한다)

그리고 input, textarea의 onChange가 반복되는 것을 볼 수 있다. 이 중복을 제거해보자.

// UpdateContent.js 파일

import React, { Component } from 'react';

class UpdateContent extends Component {
	constructor(props){
    	super(props);
        this.state = {
        	title: this.props.data.title,
            desc: this.props.data.desc,
        };
        this.inputFormHandler = this.inputFormHandler.bind(this);
    }
    
    inputFormHandler(e){
    	this.setState({[e.target.name] : e.target.value});
    }
    
    render(){
    	... 생략 ...
        <p>
        	<input type="text" name="title" placeholder="title" value={this.state.title} onChange={this.inputFormHandler}></input>
        </p>
        <p>
        	<textarea name="desc" placeholder="description" value={this.state.desc} onChange={this.inputFormHandler}></textarea>
        </p>
    	... 생략 ...

inputFormHandler 라는 함수를 만들어서 중복을 제거했다. input과 textarea의 onChange 함수 부분을 inputFormHandler로 변경하였다. inputFormHandler 함수를 보면 다음과 같이 setState함수의 인자로 전달하는 객체내에서 대괄호를 사용한 것을 볼 수 있다.

대괄호를 사용하면 객체에서 대괄호 내의 값을 프로퍼티로 사용할 수 있다. (자바스크립트의 최신 문법)
따라서, e.target.name값은 이 함수가 받는 인자 e에 따라 title 또는 desc가 된다.

그리고,

this.inputFormHandler = this.inputFormHandler.bind(this);

이 코드를 보면 this.inputFormHandler값을 this.inputFormHandler.bind(this);로 변경한 것을 볼 수 있다. inputFormHandler 내에서 this를 사용하기 위해서는 bind(this)를 사용해야 한다. 그래서 매번 onChange 함수를 쓸때마다 bind(this)를 붙이는 번거로움을 없애기 위해 constructor에서 이미 bind된 함수를 작성해 this.inputFormHandler로 교체했다.

이렇게 작성하면 좀 더 간단히 작성할 수 있다.

<p>
	<input type="text" name="title" placeholder="title" value={this.state.title} onChange={this.inputFormHandler}></input>
</p>

요렇게 깔끔하게 코드를 만들어가는 것을 리팩토링이라고 하는데, 자주 리팩토링해야한다고 책에서는 말하고 있다.

3. Update구현_state 변경

props로 들어온 데이터를 UpdateContent 컴포넌트의 state로 만들고, 그 state의 값을 각 폼요소의 value props에 넣고, 또 이벤트핸들러를 이용해서 state까지 변화시켜보았다.

이제 Update를 구현해보자.
Update를 하려면 어느 항목을 업데이트할 것인지에 대한 식별자가 필요하다. 책에서는 식별자를 hidden폼에 저장해서 구현할 것이라고 했다. 보통 폼에서 id와 같이 사용자에게 보일 필요가 없지만 필요한 데이터를 저장할때 hidden 폼을 사용한다.

// UpdateContent.js 파일

import React, { Component } from 'react';

class UpdateContent extends Component {
	constructor(props) {
    	super(props);
        this.state = {
        	id: this.props.data.id,
            title: this.props.data.title,
            desc: this.props.data.desc
        };
        this.inputFormHandler = this.inputFormHandler.bind(this);
    }
    inputFormHandler(e){
    	this.setState({[e.target.name] : e.target.value});
    }
    render(){
    	... 생략 ...
        <form action="/create_process" method="post" onSubmit={function(e){
        	e.preventDefault();
            this.props.onSubmit(this.state.id, this.state.title, this.state.desc);
        }.bind(this)}>
        	<input type="hidden" name="id" value={this.state.id}></input>
        	<p><input type="text" name="title" placeholder="title" value={this.state.title} onChange={this.inputFormHandler}></input></p>
        ... 생략 ...

type이 hidden인 input를 만들어서 작성해주었다. 그리고 name에 id, vlaue에는 {this.state.id}를 지정하고 this.state.id에는 props에서 넘어온 id를 넘겨주었다.

이 hidden input은 변경이 일어나지 않기 때문에 onChange props를 넣지 않았다. (눈에 보이지 않는데 바꿔줄 필요가 없기 때문) 책에서는 '자바스크립트가 동작하지 않을때도 여기에 있는 코드가 동작하게 한다'가 자바스크립트로 뭔가를 만들때의 기본적인 기조라고 한다. 그리고 이 값이 또 다른 용도로 활용될 수도 있기 때문에 가급적이면 자바스크립트 없이도 기본적인 구현에 충실한 것이 좋다고 한다.

업데이트를 수행할때 수정할 값을 넣고 제출버튼을 누른다. 이때 onSubmit 이벤트가 발생하고, UpdateContent의 props로 들어온 this.props.onSubmit을 실행시키도록 한다. 이때, 함수를 호출할때에 id값을 넘겨주도록 한다.

App 컴포넌트 내의 UpdateContent에 지정된 onSubmit props를 수정해보자.

// App.js 파일

... 생략 ...

class App extends Component {
	... 생략 ...
    getContent() {
    ... 생략 ...
    } else if(this.state.mode === 'update') {
    	const _content = this.getReadContent();
        _article = <UpdateContent data={_content} onSubmit = {function(_id, _title, _desc){
        const _contents = Array.from(this.state.contents);
        for(let i = 0; i < _contents.length; i++) {
      		if(_contents[i].id === _id) {
            	_contents[i] = {id:_id, title:_title, desc:_desc};
            	break;
            }
        }
        this.setState({contents: _contents});
        }.bind(this)}></UpdateContent>    
    	}
    	reutrn _article;
    }
    ... 생략...

Update는 기존의 값을 변경하는 것을 바탕으로 한다는 것을 생각하면서 코드를 보자.
onSubmit이 실행될때, 첫번째 인자로 id값이 넘어오므로 _id인자를 추가했다. 기존 max_content_id코드는 create할때 필요하므로 제거했고, 수정하기 위해서는 일단 this.state.contents 배열을 Array.from을 사용해 복제하고 _contents라는 이름의 변수에 저장했다.

그리고 반복문으로 _contents 안의 원소들을 비교한다. id값이 우리가 수정하고자 하는 것과 같은 원소를 찾아야 한다.

for문을 이용해 _id값과 _contens[i].id값이 일치하는 항목이 나왔을때 _contents[i] 값을 _id,_title,_desc인자로 전달된 값으로 교체한다. 그다음, id값이 일치하는 객체를 찾았기 때문에 break문으로 빠져 나온다. 그리고 setState함수를 이용해 새로 생성한 _contents값을 state contents에 설정한다.

이렇게 다 작성했으면 CSS를 선택하고 값을 바꾼 다음 제출버튼을 눌러보자.

폼의 내용을 수정하고 제출하면 위의 목록이 CSS에서 CSS2로 바뀐것을 확인할 수 있다.

여기서 생코님은 다시 한번 기존 원본 콘텐츠를 직접 수정하지 않고 복제해서 새로운 배열을 만들었다는 것을 강조한다. (Array.from()이용해서 _contents를 만들어서 수정)

💡 배열이나 객체를 수정하려할 때는 일단 복제한 다음 복제한 것을 수정해서 사용하도록 하자.

이제 어떤 콘텐츠를 선택하고 업데이트했을때 내용이 수정되고 바뀐 내용의 상세보기로 이동하도록 만드는 기능을 만들어보자.

this.state.mode만 'read'로 바꾸면 된다.
업데이트가 끝난다음에 state.mode가 read값을 갖도록 변경하면 된다.

// App.js 파일

... 생략 ...

class App extends Component {
	... 생략 ...
    getContent() {
    	... 생략 ...
        } else if (this.state.mode === 'update') {
        	const _content = this.getReadContent();
            _aritcle = <UpdateContnet data={_content} onSubmit={function(_id, _title, _desc){
     			... 생략 ...
                this.setState({contents:_contents, mode:'read'});
            }.bind(this)}></UpdateContent>
        }
        ... 생략 ...

_contents를 state에 전달할때 mode가 'read'가 되도록 변경했다.

create에도 적용해보자.

// App.js 파일

... 생략 ...

class App extends Component {
	... 생략 ...
    } else if (this.state.mode === 'create') {
    	_article = <CreateContent onSubmit={function(_title, _desc){
        	this.max_content_id = this.max_content_id + 1;
            const _contens = Array.from(this.state.contents);
            _contents.push({id:this.max_content_id, title:_title, desc:_desc});
            this.setState({contents:_contents, mode:'read', selected_content_id:this.max_content_id});
        }.bind(this)}></CreateContent>
    } else if (this.state.mode === 'create') {
    	... 생략 ...

mode를 'read'로 변경하고 selected_content_id를 this.max_content_id값으로 바꾼다.
이렇게 하고 'create'링크를 누르고 새로운 항목을 입력했을때 mode가 read로 다시 바뀌고, 지금 선택한 항목이 max_content_id로 (새로운 id값) 변경된다.


이렇게 길고 긴 Update 구현까지 끝났다.. read와 create 모두를 생각하고 구현해야하는게 정말 어렵다🥹
더욱 연습해서 익숙해져야겠다.


출처: 생활코딩! 리액트 프로그래밍 책

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글