Chapter03. 리엑트 프로젝트 / 주소록 수정, 삭제
<< 학습 목표 >>
1. useState 훅을 활용할 수 있다.
2. 조건부 렌더링을 활용할 수 있다.
3. 리엑트의 렌더링 방식을 설명할 수 있다.
리엑트 프로젝트의 마지막, 주소록 수정과 삭제를 구현하자
1. 주소록 수정
주소 정보에 있는 [ 수정 ] 버튼 클릭 시 이름, 연락처, 주소가 수정 가능한 상태로 바뀜(1)
이름, 연락처, 주소가 수정 가능 상태로 바뀔 때 현재 이름, 연락처, 주소가 입력된 상태임(1)
[ 수정 ] 버튼은 [ 완료 ] 버튼으로 바뀌어야함(1)
이름, 연락처, 주소를 수정 한 뒤 [ 완료 ] 버튼을 누르면 이름, 연락처, 주소가 수정 불가 상태로 바뀜(1)
이름, 연락처, 주소가 수정 불가 상태로 바뀔 때 바꾼 이름, 연락처, 주소로 변경돼 보여야함(1)
[ 완료 ] 버튼은 [ 수정 ] 버튼으로 바뀌어야함(1)
위 내용과 이미지를 참고해 주소록 수정 기능을 구현해보자
<< 1단계 힌트 >>
<< Address 컴포넌트 >>
import React, {useState} from 'react';
function Address(props) {
const [isEditing, setIsEditing] = useState(false);
let clickUpdateBtn = () => {
setIsEditing(!isEditing);
}
let clickUpdateDoneBtn = () => {
setIsEditing(!isEditing);
}
return(
<tr className="row">
<th className="col-1">{props.number}</th>
<td className="col-2">{props.name}</td>
<td className="col-2">{props.tel}</td>
<td className="col-5 text-left">{props.address}</td>
<td className="col-2">
<div className="btn-group" role="group" aria-label="Basic example">
<button type="button" className="btn btn-success" onClick={ isEditing ? clickUpdateDoneBtn : clickUpdateBtn}>{ isEditing ? "완료" : "수정"}</button>
<button type="button" className="btn btn-danger" >삭제</button>
</div>
</td>
</tr>
);
}
export default Address;
<< 코드 설명 >>

(1). isEditing 변수를 선언해 수정 상태인지 아닌지를 저장함
(2). isEditing 변수를 사용해 조건부 렌더링 및 onclick 이벤트 핸들러 설정
수정 중(isEditing=true) 이라면 <button ...중략... onClick=clickUpdateDoneBtn>완료</button> 이 됨
수정 아님(isEditing=false) 라면 <button ...중략 ... onClick=clickUpdateBtn>수정</button> 이 됨
<< 2단계 힌트 >>
<< Address 컴포넌트 >>
import React, {useState, useContext} from 'react';
import { AddressContext } from './AddressMng';
function Address(props) {
const setAddress = useContext(AddressContext).setAddress;
const [isEditing, setIsEditing] = useState(false);
let clickUpdateBtn = () => {
setIsEditing(!isEditing);
}
let clickUpdateDoneBtn = () => {
setAddress(바뀐 주소 정보);
setIsEditing(!isEditing);
}
return(
<tr className="row">
<th className="col-1">{props.number}</th>
<td className="col-2">{props.name}</td>
<td className="col-2">{props.tel}</td>
<td className="col-5 text-left">{props.address}</td>
<td className="col-2">
<div className="btn-group" role="group" aria-label="Basic example">
<button type="button" className="btn btn-success" onClick={ isEditing ? clickUpdateDoneBtn : clickUpdateBtn}>{ isEditing ? "완료" : "수정"}</button>
<button type="button" className="btn btn-danger" >삭제</button>
</div>
</td>
</tr>
);
}
export default Address;
<< 코드 설명 >>

(1). AddressMng 부모 컴포넌트가 만든 Context를 사용해 AddressAdd 컴포넌트와 AddressList 컴포넌트가 주소 정보를 주고 받기 때문에 Address 컴포넌트에서 변경된 주소 정보를 AddressList 컴포넌트로 전달해야함
그러기 위해서 AddressMng 부모 컴포넌트가 만든 Context을 import 함
(2). useContext 훅을 사용해 AddressMng 부모 컴포넌트가 만든 Context 내 setter를 사용하도록 설정
(3). [ 완료 ] 버튼 클릭 시 바뀐 주소 정보를 setter로 저장하면 AddressList의 useEffect 훅이 이를 감지할 수 있음
<< 3단계 힌트 >>
<< Addressd 컴포넌트 >>
import React, {useState, useContext} from 'react';
import { AddressContext } from './AddressMng';
function Address(props) {
const setAddress = useContext(AddressContext).setAddress;
const [isEditing, setIsEditing] = useState(false);
const indexVal = useState(props.index)[0];
const [nameVal, setNameVal] = useState(props.name);
const [telVal, setTelVal] = useState(props.tel);
const [addressVal, setAddressVal] = useState(props.address);
let clickUpdateBtn = () => {
setIsEditing(!isEditing);
}
let clickUpdateDoneBtn = () => {
setAddress(바뀐 주소 정보);
setIsEditing(!isEditing);
}
return(
<tr className="row">
<th className="col-1" scope="row">{indexVal+1}</th>
<td className="col-2">{ isEditing ? <input type="text" className="form-control" value={nameVal} onChange={(e) => setNameVal(e.target.value)} /> : nameVal}</td>
<td className="col-2">{ isEditing ? <input type="tel" className="form-control" value={telVal} onChange={(e) => setTelVal(e.target.value)} /> : telVal}</td>
<td className="col-5 text-left">{ isEditing ? <input type="text" className="form-control" value={addressVal} onChange={(e) => setAddressVal(e.target.value)} /> : addressVal}</td>
<td className="col-2">
<div className="btn-group" role="group" aria-label="Basic example">
<button type="button" className="btn btn-success" onClick={ isEditing ? clickUpdateDoneBtn : clickUpdateBtn}>{ isEditing ? "완료" : "수정"}</button>
<button type="button" className="btn btn-danger" >삭제</button>
</div>
</td>
</tr>
);
}
export default Address;
<< 코드 설명 >>

(1). [ 수정 ] 버튼을 클릭했을 때 현재 이름, 연락처, 주소를 보여줘야하므로 AddressList 부모 컴포넌트가 전달한, 출력할, n번째 주소 정보의 이름, 연락처, 주소를 초기값으로 갖고 있는 변수 선언
(2). [ 수정 ] 버튼을 클릭했을 때는 input 태그, [ 완료 ] 버튼을 클릭했을 때는 텍스트를 보여줘야하므로 조건부 렌더링으로 지정
[ 수정 ] 버튼 클릭 시 input 태그를 보여주는데 현재 이름, 연락처, 주소를 보여줘야하므로 value 속성 값을 현재 이름, 연락처, 주소로 지정
이름, 연락처, 주소를 수정했을 때 수정된 이름, 연락처, 주소를 setter 함수를 사용해 nameVal, telVal, addressVal 변수에 저장
여기서 특징적인 부분은 (1)의 indexVal 인데 indexVal은 useState 훅을 사용했는데 기존과는 다르게 사용하고 있음

이와 같이 사용하면 "해당 변수에 setter 함수를 사용하지 않겠다" 가 됨
즉, "해당 변수의 값은 변경할 일이 없다" 라는 것
indexVal 변수에 저장한건 부모 컴포넌트가 props로 전달 받은 index 변수의 값임
indexVal 변수는 이 주소 정보가 AddressList 부모 컴포넌트가 갖고 있는 addressList에서 몇 번 인덱스 주소 정보인지 나타내는 변수로 Address 컴포넌트에서는 주소 정보 번호로 출력(1)하고 있음

<< 4단계 힌트 >>
<< Address 컴포넌트 >>
변경이 미미하므로 바로 코드 설명을 보자

(1). [ 완료 ] 버튼 클릭 시 수정된 주소 정보를 생성
(2). Context 로 전달 받은 setter 를 사용해 AddressList 컴포넌트로 주소 정보를 전달
이 때 유의할 점은 AddressList 컴포넌트로 주소 정보를 전달할 때 수정한 주소 정보의 index 번호도 같이 전달하고 있다는 점
<< 최종(5단계) 힌트 >>
<< AddressList 컴포넌트 >>
import React, {useState, useEffect, useContext} from 'react';
import Address from './Address';
import { AddressContext } from './AddressMng';
function AddressList() {
const [addressList, setAddressList] = useState([]);
const address = useContext(AddressContext).address;
useEffect(() => {
if(address != null) {
let newAddressList = Array.from(addressList);
if(address.index !== undefined) {
newAddressList[address.index] = address;
} else {
newAddressList.push(address);
}
setAddressList(newAddressList);
}
}, [address]);
return(
<div id="list_panel">
<table className="table text-center">
<thead>
<tr className="row">
<th className="col-1" scope="col"></th>
<th className="col-2" scope="col">이름</th>
<th className="col-2" scope="col">연락처</th>
<th className="col-5" scope="col">주소</th>
<th className="col-2" scope="col"></th>
</tr>
</thead>
<tbody>
{
addressList.map((address, index) => {
return <Address
key={index}
index={index}
name={address.name}
tel={address.tel}
address={address.address}
/>
})
}
</tbody>
</table>
</div>
);
}
export default AddressList
<< 코드 설명 >>
(1). Address 자식 컴포넌트에서 인덱스 번호를 사용할 수 있게 props로 전달
(2). AddressAdd 컴포넌트 또는 Address 자식 컴포넌트가 주소 정보를 전달했다면 useEffect 훅이 동작함
전달 받은 주소 정보가 있다면 기존의 주소록을 복사한 새 주소록을 만듬
Address 자식 컴포넌트에서 주소 정보를 전달했다면 주소 정보 안에 index(수정된 주소의 인덱스 번호)가 들어있음, 주소 정보 안에 index가 있다면 기존의 주소 정보를 새로운 주소 정보로 교체
AddressAdd 컴포넌트에서 주소 정보를 전달했다면 주소 정보 안에 index가 없음, 주소 정보 안에 index가 없다면 주소록의 마지막에 새로운 주소 정보 추가

2. 주소록 삭제
주소 정보에 있는 [ 삭제 ] 버튼 클릭 시 해당 주소 정보가 삭제됨
위 내용을 참고해 주소록 삭제 기능을 구현하자
<< 1단계 힌트 >>
[ 삭제 ] 버튼을 클릭 했을 때 AddressList 부모 컴포넌트로 자신의 인덱스 번호를 전달하고 AddressList 부모 컴포넌트는 Address 자식 컴포넌트가 전달한 인덱스 번호에 있는 주소 정보를 삭제해야함
힌트 코드는 없음
<< 2단계 힌트 >>
<< Address 컴포넌트 >>
import React, {useState, useContext} from 'react';
import { AddressContext } from './AddressMng';
function Address(props) {
const setAddress = useContext(AddressContext).setAddress;
const [isEditing, setIsEditing] = useState(false);
const indexVal = useState(props.index)[0];
const [nameVal, setNameVal] = useState(props.name);
const [telVal, setTelVal] = useState(props.tel);
const [addressVal, setAddressVal] = useState(props.address);
let clickUpdateBtn = () => {
setIsEditing(!isEditing);
}
let clickUpdateDoneBtn = () => {
let newAddress = {
"index": indexVal,
"name": nameVal,
"tel": telVal,
"address": addressVal,
"status": "update"
}
setAddress(newAddress);
setIsEditing(!isEditing);
}
let clickDeleteBtn = () => {
let newAddress ={
"index": indexVal,
"status": "delete"
}
setAddress(newAddress);
}
return(
<tr className="row">
<th className="col-1" scope="row">{indexVal+1}</th>
<td className="col-2">{ isEditing ? <input type="text" className="form-control" value={nameVal} onChange={(e) => setNameVal(e.target.value)} /> : nameVal}</td>
<td className="col-2">{ isEditing ? <input type="tel" className="form-control" value={telVal} onChange={(e) => setTelVal(e.target.value)} /> : telVal}</td>
<td className="col-5 text-left">{ isEditing ? <input type="text" className="form-control" value={addressVal} onChange={(e) => setAddressVal(e.target.value)} /> : addressVal}</td>
<td className="col-2">
<div className="btn-group" role="group" aria-label="Basic example">
<button type="button" className="btn btn-success" onClick={ isEditing ? clickUpdateDoneBtn : clickUpdateBtn}>{ isEditing ? "완료" : "수정"}</button>
<button type="button" className="btn btn-danger" onClick={clickDeleteBtn} >삭제</button>
</div>
</td>
</tr>
);
}
export default Address;
<< 코드 설명 >>

이 방법은 AddressMng 부모 컴포넌트가 만든 Context 를 활용하는 방법
(1). 삭제 버튼을 클릭했을 때 함수가 호출되도록 이벤트 핸들러 추가
(2). AddressList 부모 컴포넌트로 삭제 시그널을 보내기 위해 status가 delete인 주소 정보를 생성해 setter 를 사용해 AddressList 부모 컴포넌트로 전달
몇 번 인덱스의 주소 정보를 삭제할 지 인덱스 번호도 함께 담아 전달함
(3). 이에 맞춰 AddressList 부모 컴포넌트로 수정 시그널을 보내야하므로 주소를 수정하기 위해 생성하는 주소 정보에 status를 update로 담도록 수정
<< AddressList 컴포넌트 >>
import React, {useState, useEffect, useContext} from 'react';
import Address from './Address';
import { AddressContext } from './AddressMng';
function AddressList() {
const [addressList, setAddressList] = useState([]);
const address = useContext(AddressContext).address;
useEffect(() => {
if(address != null) {
let newAddressList = Array.from(addressList);
if(address.index !== undefined) {
if(address.status === "update") {
newAddressList[address.index] = address;
} else {
newAddressList.splice(address.index, 1);
}
} else {
newAddressList.push(address);
}
setAddressList(newAddressList);
}
}, [address]);
return(
<div id="list_panel">
<table className="table text-center">
<thead>
<tr className="row">
<th className="col-1" scope="col"></th>
<th className="col-2" scope="col">이름</th>
<th className="col-2" scope="col">연락처</th>
<th className="col-5" scope="col">주소</th>
<th className="col-2" scope="col"></th>
</tr>
</thead>
<tbody>
{
addressList.map((address, index) => {
return <Address
key={index}
index={index}
name={address.name}
tel={address.tel}
address={address.address}
/>
})
}
</tbody>
</table>
</div>
);
}
export default AddressList
<< 코드 설명 >>

코드가 굉장히 길어졌고 useEffect 부분만 수정됐으므로 useEffect까지만 캡쳐한 이미지
(1). Context에 들어있는 address가 변하면 반응하는 useEffect 훅
Address Add 컴포넌트와 Address 컴포넌트에서 Context에 들어있는 address를 변하게 함
(2). Address 컴포넌트에서 address를 변하게 했다면 index 프로퍼티가 들어있음
Address Add 컴포넌트에서 address를 변하게 했다면 index 프로퍼티가 들어있지 않음
(3). Address Add 컴포넌트에서 주소 정보를 추가했다면 주소록의 마지막에 주소 정보를 추가함
(4). Address 컴포넌트에서 수정 상태에서 [ 완료 ] 버튼을 클릭했다면 status 프로퍼티의 값이 update이므로 Address 컴포넌트에서 주소 정보 수정 시그널을 보냈다면 이라고 해석할 수 있음
Address 컴포넌트에서 주소 정보 수정 시그널을 보냈다면 해당 자리에 들어있는 주소 정보를 새로운 주소 정보로 교체
(5). Address 컴포넌트에서 [ 삭제 ] 버튼을 클릭했다면 status 프로퍼티의 값이 delete이므로 Address 컴포넌트에서 주소 정보 삭제 시그널을 보냈다면 이라고 해석할 수 있음
Address 컴포넌트에서 주소 정보 삭제 시그널을 보냈다면 해당 자리에 들어있는 주소 정보를 삭제
이제 [ 삭제 ] 버튼을 클릭해 주소 정보를 삭제해보자
그. 러. 나. 내가 원하는 주소 정보가 삭제 되지 않고 마지막에 있는 주소 정보만 삭제됨
왜그럴까??
리엑트는 부모 컴포넌트에서 map 함수로 자식 컴포넌트를 출력할 때 최소한의 동작만으로 빠르게 출력하기 위해 key 를 사용함
주소록 관리 프로젝트의 시작 ( https://codingaja.tistory.com/83 ) 에서 간단하게 언급만 했는데 "map 함수를 사용해 컴포넌트를 출력할 때 컴포넌트에 반드시 key를 전달해야하며 key는 컴포넌트 간에 구분할 수 있는 유일한 값이어야함" 라고 했음
다음과 같은 주소록 페이지가 되도록 주소 정보를 추가하자

이 상태의 웹 페이지를 시각적인 컴포넌트 트리로 그려보자

AddressList 컴포넌트의 addressList 배열 안에는 address가 서울1 ~ 서울5인 데이터들이 들어있고 이를 통해 Address 컴포넌트가 만들어짐
여기까진 우리가 알고 있는 당연한 상황임
여기서 address가 서울3인 3번째 주소 정보의 [ 삭제 ] 버튼을 누른 상황이라고 해보자
그러면 address가 서울3인 Address 컴포넌트에서 AddressList 부모 컴포넌트로 삭제 시그널을 보내 useEffect 훅이 splice 함수를 사용해 addressList의 2번 인덱스(3번째) 주소를 삭제함
이때의 addressList의 상황을 보면 아래와 같음

이제 컴포넌트가 어떻게 생성될까?
다음과 같이 생성됨

리엑트는 기존의 Address 컴포넌트들의 key와 새로운 Address 컴포넌트들의 key를 비교해 key가 다른 컴포넌트만 교체함
기존의 Address 컴포넌트들 중에서 key가 4인 Address 컴포넌트가 있었는데 새로운 Address 컴포넌트들 중에서는 key가 4인 Address 컴포넌트가 없으므로 그래서 화면 상에서는 마지막 컴포넌트가 지워지는 것

결국 addressList 배열 상에서는 지우고자 하는 주소 정보가 지워졌지만 리엑트의 컴포넌트 갱신 방식 때문에 잘못된 데이터가 지워져 보이는 것
이 문제를 어떻게 해결해야할까??
Address 컴포넌트를 생성할 때 key를 index로 지정하면 데이터는 다르지만 index가 같은 컴포넌트가 생기는 것 때문에 발생하므로 key를 index로 사용하지 말고 각 Address 컴포넌트 마다 고유한 key를 사용하도록 해야함
말은 좀 어렵지만 리엑트를 다루는데 굉장히 중요한 내용이므로 이해해야함
만약 이해가 안된다면 [ 소플의 처음 만난 리액트 ] 책에 엘리먼트 렌더링, State와 생명주기, 리스트와 키를 읽어보자
이를 해결하기 위해서는 주소 정보를 [ 추가 ] 할 때 주소 정보에 추가한 날짜 정보(연, 월, 일, 시, 분, 초, 나노초) 도 같이 저장하도록 하면 됨
<< AddressAdd 컴포넌트 >>

변경 사항이 크지 않기 때문에 변경된 부분만 캡쳐한 이미지
[ 추가 ] 버튼을 눌렀을 때 주소 정보를 추가한 시점인 날짜 정보도 함께 저장하도록 변경(1)
날짜 정보를 생성( new Date() ) 하고 생성한 날짜 정보의 getTime() 함수를 호출했는데 getTime() 함수는 날짜 정보를 단위가 나노초인 UNIX Timestamp 로 변환하는 기능
rdate라는 이름은 Registration Date ( 등록 날짜 ) 을 줄인 단어임
이 rdate 값을 AddressList 컴포넌트에서 Address 컴포넌트를 생성할 때 key 값으로 사용하면 [ 삭제 ] 가 정상적으로 동작함
<< AddressList 컴포넌트 >>

변경 사항이 크지 않기 때문에 변경된 부분만 캡쳐한 이미지
(1). Address 컴포넌트를 생성할 때 rdate를 key로 사용하면 각 Addressd 컴포넌트 마다 고유한 key 값이 될 수 있음
(2). 주소 정보를 수정했을 때도 rdate를 같이 담아 전달해야하므로 Address 컴포넌트로 rdate를 전달함
<< Address 컴포넌트 >>

변경 사항이 크지 않기 때문에 변경된 부분만 캡쳐한 이미지
(1). Address 컴포넌트가 생성될 때 전달된 rdate 를 저장
index와 마찬가지로 rdate는 변경하지 않을것이기 때문에 setter는 버림
(2). 주소 정보를 [ 수정 ] 할 때 rdate가 빠지면 안되므로 rdate까지 함께 담도록 수정
이제 주소록 삭제 기능이 제대로 동작하는지 확인해보자
주소록 삭제 기능을 처음 눌렀을 때는 제대로 동작하는데 두 번째 부터는 아까와 같이 엉뚱한 주소 정보가 삭제됨
왜그럴까?
"map 함수를 사용해 컴포넌트를 출력할 때 컴포넌트에 반드시 key를 전달해야하며 key는 컴포넌트 간에 구분할 수 있는 유일한 값이어야함"
이 말을 정확히 이해했고 이 말을 했을 때 그렸던 컴포넌트 트리를 현재 상황에 맞게 그려보면 어렵지 않게 알 수 있고 해결 할 수 있는 방법도 찾을 수 있음
rdate를 추가하기 전처럼 5개의 주소 정보를 [ 추가 ] 한 상황에서 컴포넌트 트리를 그려보자

다음과 같이 그려질 것
여기서 rdate는 16 으로 시작하는 13자리 숫자이지만 임의로 10 ~ 50으로 설정했음
그에 따라 각 Address 컴포넌트의 key도 10 ~ 50을 갖게 됨

여기까진 우리가 알고 있는 당연한 상황임
여기서 address가 서울3인 3번째 주소 정보의 [ 삭제 ] 버튼을 누른 상황이라고 해보자
그러면 address가 서울3인 Address 컴포넌트에서 AddressList 부모 컴포넌트로 삭제 시그널을 보내 useEffect 훅이 splice 함수를 사용해 addressList의 2번 인덱스(3번째) 주소를 삭제함
이때의 addressList의 상황을 보면 아래와 같음

그러니까 아까와 key만 달라졌을뿐 동일한 상황임
이 문제를 해결하려면 어떻게 해야할까?
<< AddressList 컴포넌트 >>

변경 사항이 크지 않기 때문에 변경된 부분만 캡쳐한 이미지
key 생성 방식을 변경해 날짜와 index를 함께 조합해서 사용하면 됨
key 생성 방식을 변경하면 Addressd 컴포넌트들은 아래와 같이 만들어짐

이때 다시 address가 서울3인 3번째 주소 정보의 [ 삭제 ] 버튼을 누른 상황에서 컴포넌트 트리를 그려 보면 다음과 같음
삭제한 서울3 다음에 있는 서울4, 서울5 Address 컴포넌트의 key가 바뀌었으므로 두 컴포넌트를 새로 그려 화면에 정상적으로 주소 정보가 삭제된걸 볼 수 있음

여기까지 길었던 주소록 프로젝트 끝~!
주소록 프로젝트를 통해서 배웠던 부분을 실제로 어떻게 적용하고 그랬을 때 발생하는 문제와 해결하는 과정을 익혔음
이 과정에서 새로운 것들도 배웠으니 잘 정리해둘 것
주소록 프로젝트에는 분명히 개선점이 있음
리엑트를 더 이해하고 싶다면 지금까지 배웠던 부분을 다시 한번 복습하고 주소록 프로젝트를 봐보자
개선점을 찾고 개선하는 과정에서 리엑트 뿐만 아니라 개발 실력이 많이 향상될 것