[React] React 기본 : 리스트 렌더링/아이템 추가, 삭제, 수정

2023. 4. 15. 15:28개발공부 기강잡자/React | JavaScript | NodeJS

<Udemy 리액트 챌린지 3주차>

이정환 강사님의 강의 '한입 크기로 잘라먹는 리액트'를 수강하고 있습니다.

Udemy에서 진행하고 있는 리액트 챌린지에 참여하며 3주차 미션으로 "섹션 6 React 기본 - 일기장 만들어보기"을 수강한 내용을 정리합니다. 👍


리스트 렌더링(조회)

등록한 일기를 배열에 추가하면 지금까지 등록한 일기를 리스트로 만들어 화면에 렌더링 할 수 있다.

이러한 구조로 일기장 화면을 구현하려고 한다.

 

 

DiaryEditor : 등록할 내용을 입력받는 컴포넌트

DiaryList : 등록된 일기 목록을 보여주는 컴포넌트

리스트 컴포넌트 만들기

App에서 DiaryList 컴포넌트를 생성할 때, 리스트로 렌더링할 배열 데이터를 Props로 전달

<DiaryList diaryList={data} />

 

DiaryList 컴포넌트

> Props로 받은 객체 배열 바인딩

Props 로 전달받은 배열을 배열의 내장함수인 map 함수를 사용해서 각 요소의 데이터를 바인딩한다.

배열의 각 요소인 객체 it 에서 원하는 속성값을 점 표기법을 사용해 가져와 출력한다.

 

props로 undefined을 전달받은 경우를 처리

undefined 배열을 참조하여 렌더링하려고 하기 때문에 오류가 발생할 수 있다.

DefaultProps를 이용해서 undefined를 받은 경우 빈 배열을 넘기도록 처리한다.

 

Console 에러 처리하기

> Warning : Each child in a list should have a unique "key" prop

만든 리스트의 각각의 자식 요소들은 고유한 key Prop을 받아야 한다는 에러.

 

해결방법 1) 배열이 갖고 있는 id를 활용하여, 리스트 요소 생성 시 최상위 태그에 고유한 key를 설정하면 해결할 수 있다.👇

해결방법 2) 리스트로 출력할 배열에 id 같은 프로퍼티가 없는 경우, 배열의 인덱스 (idx)를 사용할 수도 있다!

(하지만 배열 아이템의 삭제/순서변경 등이 있을 경우 리액트에 오류 발생할 수 있으므로 고유한 id 값을 만들어 사용하는 것이 더 좋다.)

 

리스트 아이템을 별도의 컴포넌트로 만들기

DiaryList에 아이템을 별도의 컴포넌트 DiaryItem로 분리할 수 있다.

 

 

DiaryList 수정

- 배열로 리스트 요소를 생성하는 부분에서 DiaryItem 컴포넌트를 생성하도록 수정

> 각 항목의 key값은 id 값으로, 객체를 spread 연산자를 사용하여 Prop으로 전달

 

개발 중 내가 만난 오류..!

> Uncaught Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

👉 컴포넌트 만들면서 export 하지 않은 채로 import 하려고 해서 생긴 Error 이다.
다른 js 파일에서 사용할 컴포넌트를 export 해주면 해결된다!

 

export default 컴포넌트이름;


리스트에 데이터 추가하기

단반향 데이터 흐름

리액트에서 컴포넌트와 데이터의 구조는 Tree 형태로 구성할 수 있다.

Data는 위에서 아래로 흐르는 단반향 데이터 흐름이고, 추가, 삭제같은 Event는 함수를 Props로 전달해 아래에서 위로 올라가는 역방향 이벤트 흐름 구조를 보인다.
(컴포넌트에서 Event가 발생하면 부모의 상태 변화 함수를 호출한다. 아래 -> 위 흐름)

 

상위 컴포넌트에서 하위 컴포넌트로만 데이터를 전달할 수 있다.

같은 레벨에 있는 컴포넌트 끼리는 데이터를 전달할 수가 없다.

불가능!

🤔 그럼 어떻게 전달해야하는가?

 

State공통 부모요소로 끌어올려서 해결할 수 있다. 

예를 들어, 배열에 아이템을 추가하는 컴포넌트와 - 배열을 리스트로 출력하는 컴포넌트가 있을 때, 두 개의 컴포넌트의 공통 부모 컴포넌트에서 State를 관리하면 된다!

State 값을 리스트 컴포넌트로 전달, State를 변경하는 상태함수는 아이템을 추가하는 컴포넌트에 Props로 전달하면 된다.

따라서 DiaryEditor는 State 값을 수정하는 setData 를

DiaryList는 State 값인 Data를 Props로 전달받으면 된다.

일기 데이터를 관리할 배열을 최상위 부모인 App 컴포넌트에서 State로 관리한다.

일기데이터를 생성할 때, 각 데이터의 고유한 id 값을 부여하기 위해 useRef로 별도로 관리해주고, 일기가 추가될 때마다 id 값을 증가시켜준다.

id 부여 시 current 값을 사용하고 current를 증가시키면 됨

 

배열에 새 아이템 추가하는 방법

setData([...data, newItem]);

👉 data 배열에 newItem이라는 객체를 추가하여 State에 반영 (위 코드에서 setData는 상태변화함수이다.)

 

일기를 등록할 수 있는 컴포넌트 DiaryEditor 에서는 Props로 data를 수정하는 함수를 받아야한다.

그리고 버튼이 클릭됐을 때 Props로 받은 상태변화함수를 호출해 상태변수 data에 추가된 데이터를 반영할 수 있다.

State가 바뀌기 때문에 화면은 바뀐 값으로 다시 렌더링한다.

 

리스트 데이터 삭제하기

삭제하기 버튼을 누르면 데이터 추가와 마찬가지로 부모 State에 저장되어있는 데이터(배열)에서 삭제해줘야 한다.

 

리스트 데이터 삭제 함수 Props 전달 흐름

State에 배열 데이터를 관리하는 App.js 에서 onDelete 정의
> 리스트 컴포넌트 DiaryList에 Props로 함수전달
> 리스트 각 요소에서 onDelete를 호출해야하기 때문에 DiaryItem에 onDelete 함수 전달
> DiaryItem의 '삭제하기' 버튼 onClick 이벤트에서 onDelete 함수 호출

 

삭제 이벤트 발생 시 함수 호출 흐름

👉 역방향 이벤트 흐름을 확인할 수 있다~!

 

App 컴포넌트 : onDelete 정의하기

- 배열에서 아이템 삭제하기

: 배열 필터링 함수( 배열.filter() )를 사용하여 인자로 받은 삭제할 아이템의 id가 아닌 것들만 추출
→ 필터링된 배열을 State에 반영하면, 삭제하기 버튼을 누른 항목이 제외되고 화면에 렌더링 된다.

 

App 컴포넌트 : onDelete를 DiaryList로 전달

 

DiaryList : onDelete를 DiaryItem으로 전달

DiaryItem : "삭제"버튼의 onClick 이벤트 시 onDelete 호출

: onDelete를 호출하는 핸들링 함수 handleRemove 를 호출한다.
👇 handleRemove 함수

⭐️ 삭제 전 alert로 삭제할건지 묻고 onDelete에 삭제할 요소의 id 전달

if(window.confirm('메세지 내용')){ // 처리  }

: Y/N 버튼이 있는 alert 창을 띄워 Y인(true) 경우에만 삭제하도록 처리한다.

그럼 이런 확인 창이 뜨고, 확인 버튼을 누르면 onDelete 함수를 호출해 데이터를 삭제하게 된다.

(Delete 보다는 Remove를 쓰는게 좀더 직관적인 용어!)

 

리스트 데이터 수정하기

'수정하기' 버튼 추가

 

'수정하기' 버튼을 누르면 내용을 수정할 수 있는 폼이 나타나도록 처리하기

1. 수정 중인지 여부를 관리하는 State 추가 (isEdit, setIsEdit)

👉 이 값을 관리하면 삼항연산자 사용해서 isEdit에 따라 textarea와 수정취소/완료 버튼이 보이도록 화면 처리할 수 있다.

 

2. 수정 중인 데이터를 관리하는 State 추가 (localContent, setLocalContent)

- 수정을 시작했을 땐 기존 데이터가 보이도록 useState의 초기값으로 content를 전달

- textarea에 입력된 값이 바뀔 때마다 localContent를 갱신하고, 수정완료 버튼이 눌리면 localContent를 수정함수로 전달

(수정하기/취소 버튼을 누를 때 수정여부를 toggle (true↔️false)하면 화면이 리렌더 된다.)

 

 

App.js 에 데이터 수정 함수 구현

map 함수를 사용해서 targetId 와 일치하는 item의 content 변경하는 함수를 구현한다.

리스트 아이템이 수정 함수(onEdit)를 호출할 수 있도록 DiaryList>DiaryItem으로 함수를 Props로 전달한다.

 

객체 프로퍼티 수정하기

{...it, content:newContent}

👉 spread 연산자를 사용하면 수정하고 싶은 프로퍼티의 값을 변경할 수 있다.

 

 

DiaryItem 컴포넌트에서 onEdit 호출

수정버튼 onClick 핸들링 함수 handleEdit 정의

'확인'을 누를 경우, App.js의 onEdit을 호출하고, 수정모드에서 일반 리스트 뷰 상태로 전환하기 위해 isEdit 상태를 false로 변경한다.


드디어 리스트를 렌더링하고 추가/수정/삭제까지 다뤄봤다.

강의를 수강하고 기억해야할 부분 중심으로 글을 작성하다보니 두서가 없는 것 같지만.... 그래도 배운 내용이 정리가 된다.

글을 좀더 잘 쓸 수 있도록 보완해나가야 겠다.

 

출처 : Udemy '한입 크기로 잘라먹는 리액트' (이정환)