주말과제를 끝끝내 마무리하지 못하고 쓰는 회고..
이거 다쓰면 복습이 되어서 혹시 에러를 해결하려나 하는 작은 희망을 가지고 있다. ✨
현재 상황은,
- 상세페이지 밑에 댓글UI 구현함
- 등록버튼 누르면 댓글은 Create 됨
- 리스트를 fetch 못해서그런지 내가 작성한 댓글이 밑에 뜨지 않는다.ㅎㅎ
남은 주말 내로 map 사용해서 댓글 리스트 구현하기 시도하기!
핵심 : 등록하기, 수정하기, 삭제하기를 실습하며 배운것들과 타입스크립트!
1. 폴더구조 만들기
2. Props
3. export vs export default
4. emotion에 props 던지기
5. state 리렌더
6. 컴포넌트 안에서 router 사용시 주의할 점
7. map / filter
8. refetchQueries
9. DefaultValue
10. 타입스크립트
폴더? 아무렇게나 만들면 되는거 아닌가 하고 쉽게생각했는데, 전혀 아니다 처음부터 잘못짜면 나중에 다 엉켜버린다는..
보통 이런식으로 많이 만든다고 한다. 잘 봐야할것은 수정하기! 수정하기는 보통 몇번째 게시글을 수정할 것인지를 설정할 때가 많아서 저렇게 만든다고 한다. [ ]
는 변수명! 여기안으로 주소 생성되고 router.query.[ ]
해줘야 할 때가 많으므로 기억하기!
실무용 폴더구조는 여러패턴들이 있지만 리액트에서 사용하는 유명한 패턴으로는 container / presentational 패턴, hooks 패턴, atomic 패턴이 있고 우리는
container
,presentational
패턴을 배웠다.
container / presentational 패턴이란, 소스코드를 자바스크립트(기능)와 JSX (UI)로 나누는 방법을 의미한다.
container는 자바스크립트(기능) 부분을 의미
presentational은 JSX(UI) 부분을 의미
파일을 나누었어도 실행될 때는 하나로 합쳐져서 실행되어야 한다. 하나로 합쳐서 실행하는 방법은, 부모컴포넌트가(import 해서) 자식컴포넌트(import 되서)를 불러오는 것! 자식이 부모안에 포함된다.
주의할 점은, 브라우저로 우리가 볼 수 있는 컴포넌트는 index.js 파일이기 때문에, 하나로 합쳐둔 container를 pages에 import 해줘야 한다.
style(css작성) < presenter(html) < container(javascript) < index(pages) < app.js
(qyery는 그냥 기능 모아두는 곳. 컴포넌트 아님!)
컴포넌트를 2개로 나누면서 데이터, 기능의 연결고리가 끊어지게 된다.
이것을 props가 연결해주고, props란 부모 컴포넌트가 자식 컴포넌트에게 물려주는 변수/함수 를 의미한다.
부모 컴포넌트가 props를 물려줄때는 객체로 묶어서 넘기게 됩니다.
위와 같이 props를 넘기게 되면, props = { propsName : handlechangeWriter } 형태의 객체로 넘어가게 되고, 객체로 넘어오기 때문에 받아온 props를 사용하려면 객체의 속성을 꺼내오는 것 처럼 props.propsName 형태로 사용해야 한다.
💡 부모가 자식에게 props를 줄 수 있으며 자식이 부모에게 주는것은 안된다. 이것을
데이터의 흐름이 단방향 구조
이다 라고 한다.
props가 자식에게 넘겨주는 단계가 두단계 이상될 경우를 우리는 props drilling이 일어났다고 한다!
props drilling이 과도하게 이루어지면 해당 prop가 어디서 내려지고 있는건지 찾는것이 힘들어져서, 최대한 drilling이 안일어나게 해주는것이 가독성과 유지보수 면에서 유리하다.
우리가 import를 사용할 때 어떤것은 중괄호를 사용하고 어떤것은 중괄호가 없었다. 이것은 export인지, export default 인지에 따라 결정된다.
export :
한 컴포넌트 내에서 여러개를 내보내기 때문에 중괄호를 사용
해 필요한 것들만 import합니다. import할 때는 export한 이름 그대로 불러와야합니다.export default :
한 컴포넌트에서 한개만 내보내기 때문에 중괄호를 사용하지 않고
import 하게 됩니다. import 해올 때 export한 이름이 아니어도 괜찮습니다.
한 컴포넌트에서 한개만 emport했기 때문에 파일의 경로만 제대로 지정되면 됩니다.export를 한번에 묶고싶을 때는
import * as S from "경로"
를 사용합니다.
emotion 으로 만들어진 태그에도 props를 전달할 수 있다.
즉, 특정 태그를 클릭하는 등의 행위가 일어나면 props를 활용하여 css를 변경할 수 있음
- javascript(presenter) 부분
import { useState } from 'react'; import { Test } from '../../src/test2'; export default function Test2() { const [isTrue, setIsTrue] = useState(false); const handleOnClick = () => { setIsTrue((prev) => !prev); }; return ( <> <Test onClick={handleOnClick}>클릭하면 색이 왔다 갔다</Test> </> ); }
- import로 useState 해주고
- useState 만들어준다음 -> const [isTrue, setIsTrue] = useState(false)
// isTrue에는 기본적으로 false가 담겨있다는 뜻- 밑에 handleOnClick 함수에서 setIsTrue에 ((prev) => !prev)-> 클릭뭐시긴가? 를 담아주고
- 위 setIsTrue 의 값이 isTrue에 담긴다.
return ( <> <Test isTrue={isTrue} onClick={handleOnClick}>클릭하면 색이 왔다 갔다</Test> </> );
- 밑부분에 isTrue={isTrue} 요거 추가해서 emotion으로 넘길 수 있도록 준비
setState는 비동기로 작동합니다.
동기로 작동한다면 변경될때마다 바로바로 렌더링을 하기때문에 비효율적입니다.
리액트에서 해결방법 ➡️ 임시저장공간에 변경되는 값들을 모아두었다가 함수가 끝나면 마지막꺼 놔두고 그전꺼 한번, 마지막 한번해서 총 두번! 한번에 렌더링 됩니다.
💡리렌더가 되는 상황
1. 새로운 props가 들어올 때
2. 부모 컴포넌트가 렌더링 될 때
3. 강제 업데이트(forceUpdate)가 실행될 때
4. state가 변경될 때
router.query는 라우터가 연결된 모든 컴포넌트에서 사용이 가능하지만,
라우터가 있는 컴포넌트를 재사용 할때는 다이나믹 라우팅 폴더[qqq]
가 있는지 확인하고 사용해야 한다.
💡 router.query를 사용할 때는
→ 콘솔에 router.query를 찍어보고, 안에 뭐가 있는지 확인 후 필요한 걸 꺼내서 사용하기
게시물 목록, 게시물 등록, 다이나믹 라우팅 표기, 게시글 수정하기 주소
게시물 목록
: /boards
게시물 등록
: /boards/new
다이나믹 라우팅(특정 게시물 조회)
게시글 수정하기
❗️ 수정은 특정 게시물을 수정하는 것이기 때문에 특정 게시물의 id로 다이나믹 라우팅을 해줘야 합니다.
map은 for처럼 반복문이다. 실무에서는 for문 보다 map을 더 많이 쓴다.
const classmate = ["철수","영희","훈이"] classmate.map((item)=>(item+"어린이")) => (3)["철수어린이","영희어린이","훈이어린이"] 이렇게 출력된다.
map안에서 사용되는 item은 classmate의 원소들이 들어갈 파라미터(매개변수)
파라미터의 네이밍은 마음대로 정하기!객체가 원소인 배열에서도 map을 이용하여 객체를 가공할 수 있다.
const classmate = [{ name: "철수" }, { name: "영희" }, { name: "훈이" }]; //item.name => "철수","영희","훈이" //school 속성을 일괄적으로 추가 classmate.map((item) => ({ name: item.name + "어린이", school: "떡잎유치원", })); // 결과 => (3)[ { name: "철수어린이", school: "떡잎유치원" }, { name: "영희어린이", school: "떡잎유치원" }, { name: "훈이어린이", school: "떡잎유치원" }, ];
❗️ 화살표 함수 안 중괄호안에 아무것도 없으면, 소괄호로 바꿀 수 있고 특별한 의미가 없다면 생략도 가능하다. 바디에 적히는 코드가 한줄 이상이거나 리턴값이 객체라면 생략이 불가능
❗️화살표 함수 () ⇒ {} vs () ⇒ ()
→ 소괄호 ()로 감싸진 부분이 return 된다.(return문을 작성하지 않아도 리턴 됩니다.)
반면 중괄호{}로 감싸진 함수는 return문이 없다면 반환값이 없음!
상품 목록 불러와서 삭제하는 과제중, 맵으로 리스트 불러오기 부분.
중괄호를 이용해서 자바스크립트를 컴포넌트의 return값 안으로 데리고 들어온 것!
import { useQuery, gql, useMutation } from "@apollo/client";
const FETCH_PRODUCTS = gql`
query fetchProducts {
fetchProducts {
_id
seller
name
detail
price
}
}
`;
const DELETE_PRODUCT = gql`
mutation deleteProduct($productId: ID) {
deleteProduct(productId: $productId) {
_id
number
message
}
}
`;
//============ 전역함수 ⬆️ ============ //
export default function MapProductPage() {
const { data } = useQuery(FETCH_PRODUCTS);
console.log(data);
const [deleteProduct] = useMutation(DELETE_PRODUCT);
console.log(data);
//============ onClickDelete 함수 시작 ============ //
const onClickDelete = (event) => {
deleteProduct({
variables: { productId: event.target.id },
refetchQueries: [{ query: FETCH_PRODUCTS }],
});
};
//============ onClickDelete 함수 종료 ============ //
return (
<div>
{data?.fetchProducts.map((el) => (
<div key={el._id}>
<span>
<input type="checkbox"></input>
</span>
<span style={{ margin: "10px" }}>{el.seller}</span>
<span style={{ margin: "10px" }}>{el.name}</span>
<span style={{ margin: "10px" }}>{el.detail}</span>
<span style={{ margin: "10px" }}>{el.price}</span>
<span>
<button id={el._id} onClick={onClickDelete}>
삭제하기
</button>
</span>
</div>
))}
</div>
);
}
filter로 원하는 값을 출력할 수 있다.
짝수값의 number를 가진 객체를 출력해주기 과제!
변수명.filter((파라미터) => 출력하고 싶은 값 적기)
위는 새로운 배열 NEW_FRUITS를 만들고 그안에 필터로 짝수들만 넣어준 것.
map을 사용할 때, key값을 부여하지 않는다면 아래와 같이 오류가 발생한다.
💡 해결 방법
중복되는 값이 없는, 고유의 키값으로 설정해준다.
인덱스로 키값 설정하면 뭐가 문제냐면 체크박스가 안지워진다?
-> 이유는 인덱스 값으로 설정하고 지우면 뒤에있던 애가 올라와서 다시 그 인덱스값이 되기때문에 XXX
api를 확인하고, 고유의 키값을 확인해주기!
- 정적인 데이터. 계산되지 않고 변경되지 않는 데이터
- map에 있는 모든 데이터에 id가 없을 경우
- 데이터가 재정렬되거나 필터링 되지 않는 경우. (계속 그 자리 그대로)
하지만 서버에서 받아오는 데이터라면 안됨!!
refetchQuery는 기존에 받아왔던 데이터가 변경 되었을 경우 최신 데이터로 다시 fetch 해주기 위해 사용되었다. 과제 실습 중, 삭제 Mutation을 사용하여 데이터를 지웠지만 UI에는 새로고침을 해야만 삭제가 된 것을 볼 수 있었다. 사용자에게 계속 새로고침 하라고 할 수 없으니, 우리가 삭제가 되자마자 볼 수 있도록 만들어 준다.
//refetchQueries를 이용해 최신 데이터 받아오기 const deleteBoard = async () => { try { const result = await deleteBoard({ variables: { boardId: event.target.id, }, refetchQueries: [ { query: FETCH_BOARDS }, ] }); } catch (error) { console.log(error); } };
useMutation 함수 안에서 refetchQueries라는 키가 있다는 것을 알 수 있다. refetchQueries는 Apollo에서 제공하는 기본 기능이다.
refetchQueries는 배열로 시작하여, 그 안에 어떤 query를 하고, 그 query의 variables가 무엇인지 다시 설정해주면 Mutation이 성공적으로 끝났을 경우 refetchQueries를 실행시켜준다.
💡refetchQueries에서의 variables
→ 기존의 fetch 부분에서 보내준 variables가 없다면 따로 적지 않아도 되지만, 보내줄 variables가 있다면 refetchQuerise부분을 아래와 같이 적어주세요
refetchQueries: [{ query : FETCH_BOARDS,
variables : { 기존의 fetch때 보내준 것} }]
모든 Mutation 이후에 refetchQueries를 사용하는 것은 아닙니다. Mutation 이후 변경된 데이터를 받아와야 할 경우에 사용합니다.
인풋태그에서 처음 보여줄 값
언제 썼냐! 등록, 수정하는 컴포넌트를 만들어서 연결해주었는데, 수정페이지에서 수정한 값을 보여줄 때 써주었다. 수정을 하면 수정한 내용을 보여줄 때 쓰이지요! (값이 변경되지만 기본값이 있는 요소)
추가 이해를 위해,,defaultValue
와 다르게value
로 인풋값을 지정해준다면, 박스에 값이 입력되지 않고 지정해준value
만 계속해서 보여줍니다.💡디폴트 벨류는 인풋태그에서 처음 보여주는 값, 벨류는 계속 그 값을 보여줌
실습에서 활용한 내용은 내가 정리한 TIL 참고
변경된 부분만
mutation
날려주기 코드// 뮤테이션에 변경된 부분만 보내주기 const onClickUpdate = async () => { const myVariables = { number: Number(router.query.number), }; if (writer) myVariables.writer = writer; if (title) myVariables.title = title; if (contents) myVariables.contents = contents; const result = await updateBoard({ variables: myVariables, }); };```
타입스크립트란, 자바스크립트를 제어하는 하나의 언어고, 타입을 강제시킨다.
아래와 같이 자료형을 지정해주어서 사전에 오류를 잡아준다.
// 자바스크립트는 자료형에 연연하지 않고 할당이 가능합니다.
let hello = "hello"
hello = 12345
하지만 타입스크립트는 변수의 자료형을 지정해주기 때문에, 위 코드처럼 재할당이 불가능합니다.
let hello:string = "hello"
// ❌ 불가능
hello = 12345
// ⭕️ 문자열만 할당 가능합니다.
hello = "12345"
변수명 뒤에 타입을 지정해주면 된다.
let aaa : string = "안녕하세요"
let bbb : number = 123
객체타입또한 타입을 지정해줄 수 있는데, Interface로 만들며 네이밍 관례가 있다.
인터페이스의 I 를 따서 타입명을 만들어 주는것! 예를들어 변수이름이 profile 이라면 타입은 IProfile 이 되겠다.
interface IProfile {
name: string;
age: number | string;
school: string;
hobby?: string;
}
const porfile: IProfile = {
name: "철수",
age: 8,
school: "다람쥐 초등학교",
};
이렇게 타입스크립트를 이용해 각 변수에 타입을 지정해주고 나면, 타입에 맞지 않는 값을 할당 시 에러가 나게 된다.
등록페이지, 수정페이지 에서 props 주고 등록/수정 컨테이너에서 props를 받는다.
⭐️ What I did?
1. 등록페이지에서 버튼 누르면 상세보기로 이동된다.
2. 상세보기페이지에는 수정하러가기 버튼이 있는데 라우터푸쉬 해서 수정페이지로 넘어간다.
3. 수정을 완료한다면 다시 상세보기로 돌아가서 수정된 내용을 본다.⭐️ How to?
1. 창 5개를 만든다
2. 경로 설정은 모두 해준다(상단 양쪽 수정페이지,등록페이지)
3. 가운데 상세페이지에 버튼을 만들어주고, 버튼 함수랑 router.push
4. 수정,등록페이지에 isEdit 설정해준다
5. 컨테이너쪽에 isEdit을 props으로 넘겨줄 수 있게 세팅해준다
6. 버튼눌릴때, 수정버튼 눌릴 때 다르게 삼항연산자로 세팅
7. 컨테이너에서 온클릭업데이트 함수를 props로 받아준다.
8. 컨테이너, 프리젠터 연결해주기
9. 불린값 (ture, false)
10. 컨테이너에서 업데이트보드 만들어주고 쿼리랑 연결해주기
다음주는 조금 더 배운것을 잘 활용
하는 내가 되길 바란다. 이거 어떻게 하는지 모르겠지만 계속 반복하다보면 될거라 믿는다! 지치지 말고 안되더라도 끝까지 물고 늘어지도록 해보자고.
[]
값을 가져와야하니, 상세페이지 container파일 리턴 안에 댓글의 container(style, presenter, query 모두 연결됨)을 적어주었다.
1. router.query.[]안에 경로 넣어주기 -> id값이나 number 아님!
2. router.push('{freeboard_moved/$(result.data.createBoard._id}')
-> 이거는 페이지 이동할 때 써주는건데, ' '
는 저게아니고 백틱임. 여기서 백틱 표시가 안나와서 임의로 저거로 써주었어요! 라우터에 담겨진 쿼리는 [ ]
해주고, 페이지가 이동하는 push는 고유값 써주기!
라고 이해했음. 아니면 아무나 댓글부탁드림다.. 🙏🏻
위 이미지의 onClick 함수 안에 라우터푸쉬 링크 잘 봐주기! 파일 경로 적는건 현재위치에서가 아니고 절대경로로 적어준다.