Webucks-React

Webucks-React

[Mission 0] 레이아웃 리액트로 옮기기

이번 프로젝트는 저번 Webucks 프로젝트를 react에서 동작할 수 있게 옮기는 것입니다.

먼저 react 초기 세팅을 진행했습니다.

1. CRA 설치

npx create-react-app webucks-react

2. 폴더 구조 만들기

/public --/images ----iceVanillaLatte.png /src --/pages ----/Login ------Login.js ------Login.css ----/List ------List.js ------List.css ----/Detail ------Detail.js ------Detail.css --/styles ----reset.css ----common.css index.js

3. HTML 태그의 class 속성 변경

jsx를 사용하니 HTML 태그의 class 를 className 로 모두 변경해줍니다.

4. sass 설치 및 적용

npm install sass --save

.css 파일의 확장자를 모두 .scss 로 바꿔주었습니다. 그리고 sass의 nesting을 이용해 각각의 페이지 스타일을 구분해주었습니다.

[Mission 1] 리스트 페이지 목데이터로 map 사용하여 구현하기

리스트 페이지 각각의 커피 사진과 커피 이름을 CoffeeCard 컴포넌트로 분리해주었습니다.

목데이터

// listData.json [ [ { "id": 1, "imgSrc": "/images/coffee1.jpg", "content": "토피 넛 콜드 브루" }, { "id": 2, "imgSrc": "/images/coffee2.jpg", "content": "나이트로 바닐라 크림" }, { "id": 33, "imgSrc": "/images/coffee3.jpg", "content": "나이트로 콜드 브루" }, { "id": 4, "imgSrc": "/images/coffee4.jpg", "content": "돌체 콜드 브루" }, { "id": 5, "imgSrc": "/images/coffee5.jpg", "content": "바닐라 크림 콜드 브루" }, { "id": 6, "imgSrc": "/images/coffee1.jpg", "content": "벨벳 다크 모카 나이트로" }, { "id": 7, "imgSrc": "/images/coffee2.jpg", "content": "시그니처 더 블랙 콜드 브루" }, { "id": 8, "imgSrc": "/images/coffee3.jpg", "content": "제주 비자림 콜드 브루" }, { "id": 9, "imgSrc": "/images/마라탕.jpg", "content": "마라탕" }, { "id": 10, "imgSrc": "/images/coffee4.jpg", "content": "토피 넛 콜드 브루" }, { "id": 11, "imgSrc": "/images/coffee5.jpg", "content": "콜드 브루 몰트" }, { "id": 12, "imgSrc": "/images/coffee1.jpg", "content": "콜드 브루 오트 라떼" }, { "id": 13, "imgSrc": "/images/coffee2.jpg", "content": "콜드 브루 플로트" }, { "id": 14, "imgSrc": "/images/coffee3.jpg", "content": "프렌치 애플 타르트 나이트로" } ], [ { "id": 15, "imgSrc": "/images/coffee4.jpg", "content": "아이스 커피" }, { "id": 16, "imgSrc": "/images/coffee5.jpg", "content": "오늘의 커피" } ] ]

// List.js const [coffeeList, setCoffeeList] = useState([]); useEffect(() => { fetch('http://localhost:3000/data/taeYeong/listData.json', { method: 'GET', }) .then((res) => res.json()) .then((data) => { setCoffeeList(data); }); }, []);

const [상태 값 저장 변수, 상태 값 갱신 함수] = useState(상태 초기 값)

coffeeList 에 초기 값으로 빈 배열을 넘겨주고 setCoffeeList 에 상태 값을 갱신할 수 있는 함수를 할당했습니다.

useEffect hook과 fetch 함수를 이용하여 json 형식의 목데이터를 받아와 setCoffeeList() 함수를 이용해 coffeeList 에 넣어주었습니다.

// List.js {coffeeList[0] && coffeeList[0].map((coffee, index) => { return ( ); })}

coffeeList[0] 에 map 을 돌려 CoffeeCard 컴포넌트를 하나하나 배열에 담은 뒤 리턴을 해주었습니다.

처음엔 coffeeList[0] && coffeeList[0].map() 을 하지 않고 그냥 coffeeList[0].map() 을 했었는데 Cannot read properties of undefined (reading 'map')이라는 오류가 발생 했습니다.

찾아보니 React는 렌더링 화면에 커밋 된 후에야 모든 효과를 실행하기 때문에 이런 오류가 발생하는 것이라고 합니다.

return 안에서 coffeeList[0].map 을 반복 실행할 때 첫 번째 실행에서 데이터가 아직 들어오지 않았는데 렌더링이 실행되어 coffee 데이터가 undefined 로 정의되고 오류가 발생한 것입니다.

그래서 coffeeList[0].map 앞에 coffeeList[0] && 을 붙여줘서 coffeeList[0] 에 데이터가 있을 경우에만 map() 을 실행시켜 오류를 해결했습니다.

지금은 목데이터가 큰 배열 안에 두 개의 배열이 들어가 있는 형태라 coffeeList[0].map() 을 했다가 오류가 발생했습니다.

그런데 큰 배열 안에 하나의 배열만 들어가 있을 때는 coffeeList && 없이 coffeeList.map() 만 해줬는데 이런 오류가 발생하지 않았습니다. 이유를 찾아봐야 할 것 같습니다.

List 페이지

[Mission 2] Login 페이지 사용자 입력 데이터 저장

ID 에서 onChange event 발생 event 발생 시 handleIdInput 함수 실행 handleIdInput 는 이벤트를 인자로 받음 event가 일어난 요소에 담긴 value 값 ( event.target.value )을 state에 저장 위의 과정을 PW 에도 동일하게 적용

먼저 아이디 입력 데이터를 저장해보겠습니다.

아이디 입력창의 상태를 담을 변수와 상태 값을 갱신할 함수를 선언해줍니다.

const [idInput, setIdInput] = useState(''); const [pwInput, setPwInput] = useState('');

아이디 input 창에 onChange 이벤트를 달아주고 이벤트가 발생했을 때 실행시킬 handleIdInput 함수를 넣어줬습니다.

show

이벤트가 발생하면 setIdInput() 함수에 e.target.value 값을 넣어 idInput 의 상태 값을 아이디 입력창에 적힌 내용으로 갱신합니다.

const handleIdInput = (e) => { setIdInput(e.target.value); }; const handlePwInput = (e) => { setPwInput(e.target.value); };

비밀번호 입력 데이터도 똑같은 방법으로 저장해줍니다.

[Mission 3] Login 페이지 로그인 버튼 활성화(validation)

button 은 disabled 속성이 false 일 경우 활성화되고, true 일 경우 비활성화됩니다.

로그인

삼항 연산자를 이용해 idInput 과 pwInput 이 검사 결과 둘 다 true 이면 disabled 속성에 false 값을 넣어주고 아닐 경우 true 값을 넣어주었습니다.

idInput 은 현재 아이디 입력창의 상태 값을 나타냅니다. validationdId() 함수는 idInput 의 값에 @ 가 포함될 경우 true 를 반환합니다.

const validationId = (idInput) => { if (idInput.includes('@')) { return true; } return false; };

마찬가지로 pwInput 의 길이가 5 이상일 경우 true 를 반환합니다.

const validationPw = (pwInput) => { if (pwInput.length >= 5) { return true; } return false; };

기능 구현 테스트

[Mission 4] 커피 상세 페이지 좋아요 기능 구현하기

커피 상세 페이지의 좋아요 상태를 나타내는 변수를 선언합니다.

const [heartClick, setHeartClick] = useState(false);

클래스 이름을 통해 하트의 모양을 바꿔주었는데요, heartClick 이 true 일 경우 속이 찬 하트를, false 일 경우 속이 빈 하트를 나타냅니다.

setHeartClick(!heartClick)} >

onClick 이벤트로 클릭 이벤트가 발생할 때마다 setHeartClick 함수에 !heartClick 을 넣어주어 heartClick 의 상태가 true 일 경우 false 로 false 일 경우 true 로 바꾸어줍니다.

기능

[Mission 5] 커피 상세 페이지 리뷰 달기 기능 구현하기

커피 상세 페이지 리뷰 달기 기능을 구현하기 전에 이미 입력되어 있는 리뷰 mock data를 불러오겠습니다.

// commentData.json [ { id: 1, author: 'coffee_lover', content: '너무 맛있어요!', }, { id: 2, author: 'CHOCO7', content: '오늘도 화이트 초콜릿 모카를 마시러 갑니다.', }, { id: 3, author: 'legend_dev', content: '진짜 화이트 초콜릿 모카는 전설이다. 진짜 화이트 초콜릿 모카는 전설이다. 진짜 화이트 초콜릿 모카는 전설이다.', }, ];

const [comments, setComments] = useState([]); useEffect(() => { fetch('http://localhost:3000/data/taeYeong/commentData.json', { method: 'GET', }) .then((res) => res.json()) .then((data) => { setComments(data); }); }, []);

useEffect() hook과 fetch() 함수를 이용해 commentData.json 을 불러온 뒤, 안에 있는 데이터들을 setComments 함수를 통해 comments 에 넣어주었습니다.

이제 리뷰를 입력하는 기능을 구현해보겠습니다.

// Detail.js const commentOnChange = (e) => { if (e.key === 'Enter') { comments.push({content: e.target.value, author: 'dev.Taeyeong'}); setComments([...comments]); e.target.value = ''; } }; // ... {comments.map((comment, index) => { return ( ); })}

태그에 onKeyPress 이벤트를 추가해주고 이벤트가 발생할 때마다 commetOnChange() 함수를 실행합니다.

commentOnChange() 함수는 e 를 인자로 받는데요, e 는 event 를 뜻합니다. e.key 가 'Enter' 일 경우 setComments() 함수를 통해 comments 의 상태 값을 바꾸어줍니다.

이 과정에서

comments.push({ content: e.target.value, author: 'dev.Taeyeong' }); setComments([...comments]);

이런 방법을 사용했습니다.

처음에는 그냥 setComments() 에 comments 를 넣어주었는데 원하는 대로 잘 되지 않았는데요, 그 이유는 comments 가 배열이기 때문입니다. 자바스크립트에서 배열은 원시 값과 다르게 변수에 배열 자체를 저장하지 않고, 배열이 있는 reference를 저장합니다. setComments(comments) 로 했을 때 comments 배열이 들어가는 것이 아니라 comments 의 값이 존재하는 주소가 들어가 원하는 대로 동작하지 않는 것입니다.

위 코드처럼 스프레드 문법을 이용한 깊은 복사를 통해 이런 문제를 해결할 수 있습니다.

그리고 map() 을 이용해 리뷰 댓글들이 담긴 배열 요소를 하나씩 돌며 완성된 댓글 창을 return 했습니다.

Comment 컴포넌트는 이렇게 작성되었습니다.

// Comment.js import React, { useState } from 'react'; function Comment(props) { return ( {props.author} {props.content} ); } export default Comment;

기능

[Mission 6] 커피 리스트 페이지 커피 별 좋아요 기능 구현하기

커피 리스트 페이지 커피별 좋아요 기능은 CoffeeCard 컴포넌트에 추가해주었습니다.

// CoffeeCard.js import React, { useState } from 'react'; function CoffeeCard(props) { const [heart, setHeart] = useState(false); return ( {props.content} setHeart(!heart)} > ); } export default CoffeeCard;

먼저 좋아요 버튼의 상태를 나타낼 heart 변수를 선언합니다. 상태의 초기 값은 좋아요가 눌리지 않은 상태이기 때문에 false 를 넣어주었습니다.

버튼의 모양은 클래스를 통해 바꾸어주었고 onClick 이벤트가 실행될 때마다 heart 의 값을 반대로 바꾸어주었습니다.

기능

[Mission 7] 커피 상세 페이지 리뷰 삭제 기능 구현하기

커피 상세 페이지 리뷰 삭제 기능은 Comment 컴포넌트 안에 아이콘을 넣어 구현했습니다.

// Comment.js import React, { useState } from 'react'; function Comment(props) { const [heart, setHeart] = useState(false); return ( {props.author} {props.content} { e.target.parentElement.remove(); }} > ); } export default Comment;

아이콘에 onClick 이벤트를 넣고 이벤트가 실행되면 실행된 요소의 부모 노드를 삭제해주었습니다.

이렇게 구현하니 잘 작동은 했으나 한 가지 의문점이 생겼습니다.

DOM 요소를 삭제한 것이지 배열 안의 값을 삭제해준 것이 아니기 때문에 배열 안에는 여전히 삭제한 댓글 값이 남아있는데요, 다른 댓글을 추가했을 때 삭제한 댓글이 다시 추가되지는 않았습니다. React는 DOM 요소에서 변경된 부분만 렌더링 하기 때문에 이게 가능한 것 같은데(아닐 수도 있음) 정확한 이유는 한 번 알아봐야겠습니다.

기능

[Mission 8] 커피 상세 페이지 리뷰 별 좋아요 기능 구현하기

리뷰별 좋아요 기능도 Commnet 컴포넌트에 추가합니다.

import React, { useState } from 'react'; function Comment(props) { const [heart, setHeart] = useState(false); return ( {props.author} {props.content} setHeart(!heart)} > { e.target.parentElement.remove(); }} > ); } export default Comment;

삼항 연산자를 이용해 클래스명을 바꾸어주는 방식으로 구현했습니다.

기능

++++++++ 추가

Mission 7 댓글 삭제 기능을 DOM 요소를 삭제하는 것 만으로 구현했었는데 지금 눈에 보이기에는 잘 작동하는 것처럼 보이지만 댓글들의 값이 들어있는 배열에는 값이 아직 남아있으니 실제 웹페이지라면 문제가 발생할 것이라고 생각되어 방법을 생각해봤습니다.

지금처럼 댓글의 DOM 요소를 삭제해주고 + 해당하는 요소를 배열에서 삭제해준다. DOM 요소를 삭제하지 않고 배열에서 요소를 삭제해준다.

1번보다 배열에서 요소를 삭제함과 동시에 기능이 구현되도록 하는 것이 더 좋아 보여 2번 방법으로 코드를 짜보았습니다.

먼저 코드를 조금 수정해주었습니다. (미리 생각하고 짠 코드가 아니라 배열 이름이 많이 헷갈립니다... ㅎㅎ)

Comment 컴포넌트에 상태를 나타내고 변경하는 comments 와 setComments() 를 props로 보냅니다. 그리고 배열 요소를 삭제하기 위해 요소의 인덱스를 알아야 하므로 인덱스도 commentIndex 라는 속성으로 전달해줍니다.

// Detail.js { comments.map((comment, index) => { return ( ); }); }

삭제 버튼을 눌렀을 때 실행되는 함수를 수정하겠습니다.

삭제 버튼을 누르면 이벤트가 발생한 요소의 부모 요소를 삭제해주는 방식으로 기능을 구현

-> 삭제 버튼을 누르면 눌러진 배열 요소의 인덱스를 가져와 splice() 메서드를 이용해 배열에서 요소 삭제

그다음 setComments() 함수를 통해 댓글의 상태를 변경하였습니다.

{ props.comments.splice(props.commentIndex, 1); props.setComments([...props.comments]); }} >

이렇게 코드를 짜고 잘 실행이 될 줄 알았는데 하트가 눌러지지 않은 상태에서는 정상적으로 댓글이 삭제되는데 하트가 하나라도 눌러져 있는 상태에서 댓글을 삭제할 때 문제가 발생했습니다.

왜 이렇게 될까 이유를 생각해보았는데요, 하트 버튼도 state로 만들긴 했지만 결국 직접적으로 상태를 변경해준 것은 댓글의 내용이 담긴 배열의 상태뿐이라 하트 버튼이 삭제되는 것이 자동으로 렌더링 되지 않는 것이라고 이해했습니다. (확실하지 않아서 더 찾아보려고 합니다.)

왜냐면

{ props.comments.splice(props.commentIndex, 1); props.setComments([...props.comments]); setHeart(''); }} >

이렇게 하트 버튼의 상태도 같이 변경해주었더니 '하트 버튼이 활성화되어있는 댓글을 지울 때는' 하트까지 잘 없어졌기 때문입니다. (아래와 같이 하트 버튼이 활성화되어 있는 댓글의 위에 댓글들을 삭제할 때는 정상적으로 기능이 작동하지 않습니다.)

그래서 고민 끝에 하트 버튼 state를 부모 컴포넌트인 Detail 에서 선언한 뒤 props로 Comment 컴포넌트에 전달하는 방식으로 구현해보았습니다. ( key 값으로 인해 하트가 자동으로 구분될 줄 알았습니다.)

그런데 이렇게 구현을 하니 하나의 하트를 누르면 모든 하트가 눌러졌습니다.

또다시 고민 끝에 '댓글 state에 아예 객체로 댓글 내용과 하트 상태를 같이 담아보자'라는 생각이 들어 그렇게 구현해보았습니다. (이 코드도 정신없이 막 짜다 보니 가독성이 너무 좋지 못한 것 같습니다. ㅜㅜ 리팩터링 할 때 잘 고쳐보려고 합니다.)

// Detail.js const [comments, setComments] = useState([]); const commentOnChange = (e) => { if (e.key === 'Enter') { comments.push({ content: e.target.value, author: 'dev.Taeyeong', heart: false, }); setComments([...comments]); e.target.value = ''; } }; // ... { comments.map((comment, index) => { return ( ); }); }

// Comment.js function Comment(props) { const arr = props.comments; return ( {props.author} {props.content} { arr[props.commentIndex].heart = !arr[props.commentIndex].heart; props.setComments([...arr]); }} key={props.index} > { props.comments.splice(props.commentIndex, 1); props.setComments([...props.comments]); }} > ); }

드디어 배열이 요소를 삭제하는 방법으로 커피 상세 페이지 리뷰 삭제 기능을 구현했습니다!!!

from http://dev-taeyeong.tistory.com/29 by ccl(A) rewrite - 2021-12-26 16:01:22