useContext는 컴포넌트에서 context를 읽고 구독할 수 있게 해주는 React Hook입니다.
기본구문
const value = useContext(SomeContext)
매개변수
SomeContext : 이전에 createContext로 생성한 context입니다. context 자체는 정보를 보유하지 않으며, 컴포넌트에서 제공하거나 읽을 수 있는 정보의 종류를 나타낼 뿐입니다.
반환값
호출한 컴포넌트에서 트리상 위에 있는 가장 가까운 SomeContext.Provider에 전달된 value입니다. 이러한 provider가 없는 경우 반환되는 값은 해당 context에 대해 createContext에 전달한 defaultValue가 됩니다. 반환된 값은 항상 최신 값입니다. React는 context가 변경되면 context를 읽는 컴포넌트를 자동으로 리렌더링합니다.
React Context의 필요성
일반적으로 부모컴포넌트에서 자식 컴포넌트에게 데이터를 전달해줄때 props로 전달해준다. 하지만 큰 단점이 있는데 컴포넌트가 깊어질수록 해당 컴포넌트가 어느 컴포넌트에서 왔는지 파악하기 어려워진다. 이것을 props-drilling이라고 한다.
prop drilling의 문제점
깊이가 너무 깊어지면 이 prop이 어떤 컴포넌트로부터 왔는지 파악이 어려워진다.
어떤 컴포넌트에서 오류가 발생할 경우 추적이 힘들다.
Props 사용이 적절할 때❓
리스트 컴포넌트에서 아이템 컴포넌트로 상태를 전달하는 경우. 이 경우는 이번 프로젝트를 하면서 많이 생소했다. props drilling으로 만든 코드를 context 로 변경하는 과정에서 리스트 컴포넌트에는 도저히 context를 사용할 수 없을 것 같아서였다. map을 돌린 애들로 아이템을 만드는데 여기서 context를 어떻게 사용할까 했는데 오늘 강의 때 단순한 데이터 전달은 Props 사용이 적절한 때라고 알려주셔서 이해할 수 있었다.
아래 그림처럼 useContext hook을 통해 우리는 쉽게 전역 데이터를 관리할 수 있다. Context Provider 의 모든 children 컴포넌트들에게 context value를 공유할 수 있고, 전역상태관리를 하게 되면 props 를 이용한 상태 공유를 하지 않아도 된다.
createContext : context를 생성
useContext : context를 구독하고 해당 context의 현재 값을 읽기
Provider : context를 하위 컴포넌트에게 전달
createContext
Context Provider 의 모든 children 컴포넌트들에게 context value를 공유할 수 있다.
실습을 하던 중에 디테일 페이지에 데이터를 받아야하는데 부모 컴포넌트와 종속성이 없어 데이터를 받아오지 못하는 상황이 있었다. 라우터 간 데이터 전달은 context api로 할 수 있는데 props-drilling으로 작업하고 있어서 어찌해야할 방법을 몰랐다. 그런데 설마 했던 방법이 맞았다..! 라우터 컴포넌트에서 상태를 전달하는 것..! 너무나도 생소해서 여긴 아니겠지 하는 생각으로 갈피를 잡지 못했었다.
// 여러 페이지 컴포넌트에 동일한 상태 공유가 필요할 때
function Router() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/detail/id" element={<Detail />} />
</Routes>
</BrowserRouter>
)
}
Route.jsx에다 상태를 만들어주고 props로 내려주면 되는것.. 방법을 못찾아서 useLocation으로 데이터를 받아왔었는데 props-drilling의 불편함을 어느정도 알 수 있게해준 오류였다.
해당 월 지출내역이 있는 배열에 순회를 돌려서 해당 월 지출 내역의 id 값이 내가 수정하려는 지출 내역과 같다면 수정된 데이터가 저장되도록 했다. 여기서 계속 오류가 났었는데 이유는 아래와 같다.
콘솔에서 찍어보면 수정은 잘되는데 기존에 있던 데이터가 삭제되고 내가 수정한 객체가 새로운 배열로 반환되어 수정하려는 데이터와 수정한 데이터 두개만 찍히는 것이다.
오류가 나타난 이유는 바로 map 함수를 사용하면서 해당 객체에 접근할 수 있는 매개변수를 ex로 설정해놓고 exe로 써놓았던 것.. 아직 map 함수를 정확히 이해하지 못한 것 같다^^.. 이렇게 바꾸고 다시 수정을 시도했더니 아주 잘 됐다.. ..
const modifyExes = data[exe.month].map((ex) =>
ex.id === id ? modifiedData : ex
);
👾 오류
여기서 잘되나 싶었는데 수정하고 나서 홈화면으로 나갔다가 수정했던 데이터를 다시 수정하려고 하니까 아래 오류가 나왔다. 수정 함수를 건드렸는데 onRemove() 함수에 있는 filter가 안되는 것이다. data[exe.month]를 확인해보니 undefined가 떴다.
Uncaught TypeError: Cannot read properties of undefined (reading 'filter')
콘솔에 수정된 데이터 변수인 modifyExes를 찍어보니 month 값이 안찍히고 있었던 것. 그래서 data[exe.month] 찍었을 때 데이터를 불러오지 못하고 undefined가 떴구나.
✨ 해결
수정 코드에 스프레드 연산자를 사용했다. 기존 객체에는 month가 있고, 새로 만든 수정 객체에는 month가 없기 때문에 스프레드 연산자로 합쳐주었다.
기존 배열이나 객체의 전체 또는 일부를 다른 배열이나 객체로 빠르게 복사할 수 있습니다. 기존에 생성되어 변수에 할당되어 있는 배열이나 객체를 새로운 변수에 할당 하게되면 새로운 변수는 기존의 변수에 할당되어 있는 객체 또는 배열을 참조하게 됩니다. 객체에서는 spread 연산자를 이용하여 객체의 프로퍼티를 업데이트 하거나 복사할 수 있습니다.
좌변엔 원본 데이터를 복사하고, 우변엔 덮어 씌울 데이터를 복사한다. 여기서 중복되는 키값은 우변으로 덮어씌워지고 새로운 프로퍼티가 있는 경우 업데이트되게 된다. 아래의 예시를 보면 이해가 쉽다.
아래 이미지와 같이 월을 눌렀을 때 해당하는 월의 지출 내역을 보여주도록 구현해보려고 한다. 로컬스토리지에도 데이터를 저장하고 삭제하는 것까지가 목표다.
데이터 추가
우선 expenseList 변수명으로 데이터가 있다면 저장된 데이터들을 객체 형태로 바꿔서 불러오고, 없다면 빈 객체를 가져온다. 객체로 불러오는 이유는 월별마다 다른 지출내역이 보여야하기 때문에 월을 key 값으로 value를 해당 월의 지출 내역 리스트로 하기 위함이다.
렌더링이 화면에 커밋 된 후에 모든 효과를 실행하기 때문이다. 즉 return에서 map을 반복 실행할 때 첫 턴에 데이터가 아직 다 안들어와도 렌더링이 실행되며, 당연히 그 데이터는 undefined로 정의되어 오류가 나는 것이다. 그렇게 한참을 구글링하다가 나랑 같은 오류를 해결한 글을 보았다.
✨ 해결 방법
논리곱연산자 &&를 사용하자! 자바스크립트에서 true && expression은 항상 expression으로 실행되고 false && expression은 항상 false로 실행된다. 따라서 조건이 참이면 && 바로 뒤의 요소가 출력에 나타난다. 거짓이면 무시하고 건너뛴다.
삭제는 디테일 페이지에서 진행해보자. 각 지출 리스트를 눌렀을 때 해당하는 리스트의 id 값으로 상세페이지로 들어간다. useParams()를 사용해서 파라미터를 가지고온다.
const { id } = useParams();
useNavigate()로 페이지를 이동하면서 기존 객체 상태를 props로 가지고 오고, 디테일 페이지에선 useLocation()으로 사용해줬다. 여태 컴포넌트로 만들어서 부모 -> 자식으로 props를 넘겨주는 방식으로만 했는데 라우터를 배우면서 상세페이지를 만들고, 페이지 간에 props를 전달하고 사용할 수 있다는 점은 이번 프로젝트를 진행하면서 처음 알았다.
받은 props를 사용해 삭제 기능을 구현해보자. 내 페이지는 부모-자식 컴포넌트가 아니라 종속성이 없기때문에 로컬스토리지 데이터를 삭제해주도록 했다. data라는 변수에 저장된 로컬스토리지 데이터들을 담아주고 console.log를 찍어주면 데이터는 잘 불러온다.
let data = JSON.parse(localStorage.getItem("expenseList"));
삭제 함수를 만들어 데이터를 삭제해보자. id 값을 받아와 기존 객체 배열에서 id가 일치하지 않는 값들을 모두 removeData 변수에 담아준다. id 값이 같은 데이터를 제외한 나머지 값 = removeData 라고 생각하면 된다. 그리고 할당된 값을 다시 data[item.month]에 다시 담아준다. 이건 삭제된 데이터를 제외한 나머지 데이터를 재할당(갱신)하는 것이라고 생각하면 된다. 삭제된 값을 제외하고 나머지 값을 다시 로컬스토리지에 저장해야하기 때문이다.
여기서 filter를 돌릴 때 data[item.month]로 돌리는 이유는 위 로컬스토리지에서 불러온 데이터를 담은 변수 data는 객체로 불러오기 때문이다. filter 메서드는 배열에만 사용할 수 있기때문에 data[item.month]로 해당하는 월의 데이터로 filter를 돌리면 된다.
아래는 콘솔로 찍어본 data와 data[item.month]이다. 객체와 배열 형태로 찍히는 것을 확인할 수 있다.
그렇게 재할당해준 데이터를 다시 로컬스토리지에 저장해주면 삭제하기 완성..! 여기서 navigate(-1) 은 뒤로가기이다. 상세페이지에서 데이터를 삭제하고 새로고침을 하거나 홈으로 나가야 삭제가 됐는지 확인할 수 있었기 때문에 삭제와 동시에 홈으로 이동하게끔 했다.
삭제 기능까지 완성! 혼자 하는데 처음에 월별로 데이터를 저장하면서 월을 key값으로 객체로 저장을 하려니 잘쓰고 있던 map, filter 메서드들에 오류가 나서 다 백지상태였다. 배열 메서드들인데 내가 객체로 저장해버리니 오류가 나는 수밖에 튜터님한테 많이 물어보고 콘솔에 객체와 배열이 어떻게 다른지 물어보면서 만들어보았다. 아직 복습을 많이 해야할 것 같다.
로컬스토리지에 저장하는 코드는 아래와 같다. 나는 useState()를 사용하고 있어서 defendency arry에 useState 변수를 넣어주어 상태가 변할 때마다 실행하도록 했다. 객체나 배열을 저장해줄 때에는 JSON.stringify를 이용해서 객체를 string으로 변환시켜준 후에 저장해주어야 한다.
가계부 프로젝트 중 로컬스토리지를 사용해서 웹 서버에 데이터를 저장하려고 한다. 우선 로컬스토리지에 데이터가 있는지 확인하고 있다면 가져오고, 없다면 빈 배열을 가져오도록 해준다. 그리고 useEffect를 사용해 defendency array에 상태가 변할 때마다 업데이트될 수 있도록 해주자.
리액트 숙련주차에 들어갔다. 입문 주차에서는 무엇인가 새로운 것을 배운다는 느낌에 집중도 잘되었는데 숙련 주차에 들어오니 끊임없는 정보때문에 머리가 헤롱하다. 과부하가 온 것 같다. 지금 배우고 있는 단계가 실무에서 가장 많이 쓰고 있는 것이라고 한다. 이럴때일수록 더 집중하고 이해해보려고 노력해야겠다.🔥
Styled-components
css-in-js 란?
자바스크립트 코드로 css 코드를 작성하여 컴포넌트를 꾸미는 방식이다. css를 사용할 때 조건문과 변수 등 다양한 로직을 적용해서 사용할 수 있다는 장점이 있다.
패키지 설치
yarn add styled-components
사용방법
태그의 속성명에 접근하여 스타일 주는 방법은 아래와 같다.
import styled from "styled-components";
styled.button`
// <button> HTML 엘리먼트에 대한 스타일 정의
`;
컴포넌트에 접근하여 스타일 주는 방법은 아래와 같다.
import styled from "styled-components";
import Button from "./Button";
styled(Button)`
// <Button> React 컴포넌트에 스타일 정의
`;
태그 속성에 원하는 스타일을 주고 StyledButton 변수에 저장한다. 해당 변수를 Button 컴포넌트에 사용하면 다른 리액트 컴포넌트에서 같은 스타일이 적용된 <Button>을 사용할 수 있다.
import React from "react";
import styled from "styled-components";
const StyledButton = styled.button`
padding: 6px 12px;
border-radius: 8px;
font-size: 1rem;
line-height: 1.5;
border: 1px solid lightgray;
color: gray;
background: white;
`;
function Button({ children }) {
return <StyledButton>{children}</StyledButton>;
}
import Button from "./Button";
<Button>Default Button</Button>;
useState()
기본 문법
컴포넌트의 state를 관리할 수 있는 기본적은 hook 이다. state를 변수로 사용하고, setState를 이용해서 state 값을 수정할 수 있다.
const [state, setState] = useState(initialState);
함수형 업데이트
카운트로 예시를 들어보자. 기존에 우리가 카운트 값을 증가시켰던 방법은 원시적 데이터로 값을 올려주었다. ( ) 안에 수정할 값이 아니라 함수를 넣어보자.
일반 업데이트 방식은 버튼을 클릭했을 때 첫번째 줄 ~ 세번째 줄의 있는 setCount가 각각 실행되는 것이 아니라, 배치(batch)로 처리한다. 즉 우리가 onClick을 했을 때setCount 라는 명령을 세번 내리지만, 리액트는 그 명령을 하나로 모아 최종적으로 한번만 실행을 시킨다는 뜻이다. 그래서setCount을 3번 명령하던, 100번 명령하던 1번만 실행한다.
<button onClick={() => {
setCount(count + 1); // 첫번째 줄
setCount(count + 1); // 두번쨰 줄
setCount(count + 1); // 세번째 줄
}}
>
반면에 함수형 업데이트 방식은 3번을 동시에 명령을 내리면, 그 명령을 모아 순차적으로 각각 1번씩 실행시킨다. 현재값 0에 1더하고, 그 다음 1에 1을 더하고, 2에 1을 더해서 3이라는 결과가 나오는 것을 확인할 수 있다.
리액트 컴포넌트가 렌더링 된 이후마다 특정 작업을 수행하도록 설정할 수 있는 Hook이다. 컴포넌트가 마운트 / 언마운트 / 업데이트 됐을 때 특정 작업을 처리할 수 있다.
useEffect(setup, dependencies?)
useEffect는 컴포넌트가 렌더링된 이후에 실행된다는 것이 핵심 포인트이다. 아래와 같은 코드는 input 태그를 만들어 state와 연결해준 상태이다. input창에 값을 입력했을 때 state가 변경하므로 리렌더링이 되면서 console.log 값이 계속해서 찍히는 것을 확인할 수 있다.
화면을 가운데에 정렬시켜주며, 일정 리스트를 보여줍니다. children으로 내부 JSX를 props로 받아 와서 렌더링해줍니다.
TodoAdd.jsx
할 일을 추가하는 컴포넌트 입니다.
TodoList.jsx
todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoItem 컴포넌트로 변환하여 보여 줍니다.
TodoItem.jsx
진행중인 할 일 정보를 보여주는 컴포넌트입니다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여줍니다
CurrentDate.jsx
현재 날짜를 알려주는 컴포넌트입니다.
Icons.jsx
배경에 꾸며주는 Sticker 요소를 보여주는 컴포넌트입니다.
App.jsx
Todo의isDone 상태가 true이면, 상태 버튼의 라벨을취소, isDone이 false 이면 라벨을완료로 조건부 렌더링이 필요해서 토글 함수를 만들었다. 삭제 함수와 마찬가지로 id 값을 받아와서 처리하도록 했다.
map과 삼항연산자를 활용했다. map()으로 배열 순회를 돌면서 배열의 id와 사용자가 선택한 id 가 같다면, todo에 isDone 키의 값이 true -> false, false -> true 로 반전시켜주는 NOT 연산자를 사용해줬다. 같지 않다면 그대로 todo가 나오도록 해주었다.
TodoList 에서 사용할 수 있게 props로 내려준다. 그리고 진행중인 리스트와 진행 완료된 리스트를 나누어서 완료된 리스트는 진행 중인 리스트 아래로 내려야 하기때문에 TodoList 를 두개 만들어주고 안에 들어가는 props 에 filter를 써서 조건부 렌더링이 되도록 해주었다.
화면을 가운데에 정렬시켜주며, 일정 리스트를 보여줍니다. children으로 내부 JSX를 props로 받아 와서 렌더링해줍니다.
TodoAdd.jsx
할 일을 추가하는 컴포넌트 입니다.
TodoList.jsx
todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoItem 컴포넌트로 변환하여 보여 줍니다.
TodoItem.jsx
진행중인 할 일 정보를 보여주는 컴포넌트입니다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여줍니다
CurrentDate.jsx
현재 날짜를 알려주는 컴포넌트입니다.
Icons.jsx
배경에 꾸며주는 Sticker 요소를 보여주는 컴포넌트입니다.
🐣 프로젝트 생성
1. CRA(Create React App)
아래처럼 성공 메세지가 띄워졌다면 app 생성 성공
yarn create react-app [원하는 프로젝트 이름]
생성한 App 파일로 Change Directory 하고 yarn을 실행해준다. 그럼 리액트 앱 생성 완료~~ EZEZ~~
cd [생성한 App 이름]
yarn start
2. Vite
yarn create vite [생성할 App 이름] --template react
생성 완료닷
cd [생성한 App 이름]
yarn
yarn dev
Vite로 생성하면 좋은 이유
WebPack을 사용하는 CRA 대신 Esbuild를 사용하는 Vite는 원래 Vue.js 애플리케이션을 위해 만들어졌지만,
현재는 React, Svelte, Vanilla JS 등 다양한 프레임워크와 라이브러리를 지원한다.
1. 빠른 콜드 스타트와 HMR(Hot Module Replacement)
2. 속도 측면에서 기존 CRA와는 비교가 되지 않을 정도로 빠르다
3. CRA는 기본적으로 설정을 숨기지만, Vite는 사용자가 필요에 따라 설정을 더 쉽게 조정할 수 있다
4. Go 언어 베이스의 자바스크립트 빌드 툴입니다. CRA가 채택하는 웹팩과 비교할 때, 말이 안되는 수준의 속도
🐥 UI 구현하기
우선 App.jsx에 리스트 배열을 만들어주고 각 컴포넌트를 배치한다. TodoList가 두개인 것은 진행 중인 리스트 목록과 완료한 리스트 목록을 UI로 구분하기 위함이다.
import React, { useState } from "react";
import "./App.css";
import TodoTemplate from "./component/TodoTemplate";
import TodoAdd from "./component/TodoAdd";
import CurrentDate from "./component/CurrentDate";
import TodoList from "./component/TodoList";
컴포넌트를 앞으로도 잘 이용할것이라면 import하는 것을 잊지말자. 하면서도 import 오류가 발생하면 아차 싶은 1인..
TodoTemplate.jsx
템플릿은 이렇게 구현해주었다. children 으로 내부 JSX를 가져와서 템플릿 안 어떤 컨텐츠든 자유자재로 바뀌도록 해준다.
contents로 받은 값으로 새로운 객체를 생성해 기존 객체에 추가해준다. 여기서 스프레드 연산자를 사용한 이유는 불변성을 유지하기 위해서이다. 배열이니 push로 추가해도되지 않을까 할 수 있지만 push한다면 배열에 직접적으로 추가하는 것이기 때문에 값은 변할지 몰라도 메모리 주소는 바뀌지 않는다. 배열 중간에 것을 삭제하면 오류가 날 수 있고, 이것은 불변성 유지가 불가능하다고 볼 수 있다.
그래서 스프레드 연산자로 기존 객체를 복사해주고 그 뒤로 생성한 객체를 배열에 넣어주었다. 이러면 불변성을 유지할 수 있다.
// 할 일 추가
const onInsert = (contents) => {
const newTodo = {
id: Date.now(),
contents: contents,
};
setTodo([...todos, newTodo]);
};
onInsert 함수를 만들어주었으니 추가 컴포넌트에서 사용할 수 있도록 props로 받아오자.
<TodoAdd onInsert={onInsert} />
추가 컴포넌트에서 기능 구현을 위해 사용한 리액트 Hook은 useState(), useRef(), useEffect()이다. 차례대로 이해한대로 정리를 해보면 아래와 같다.
✅ useSatae() : 컴포넌트의 상태를 관리해준다. 가변하는 사용자 입력 값을 동적으로 사용하기 위함.
✅ useRef() : current라는 키값을 지닌 프로퍼티가 생성되고, 값에 어떤 변경을 줄때도 current를 이용한다. DOM요소에 접근이 가능하면, 불필요한 재렌더링을 하지 않는다는 장점이 있다.
✅ useEffect() : 컴포넌트가 렌더링될때마다 특정 작업을 실행 할 수 있다. (처음 시작할때 / 사라질때 / 업데이트 될때
우선, useState로 상태를 선언해준다. 초기값은 빈 문자열로 해준다. 위에 새로운 객체를 추가하는 함수 onInsert를 만들때 contents 값을 받아와 객체를 생성해주도록 했기때문에 onInsert(contents=사용자로부터 받아온 값) 으로 넣어준다. 생성할때 onClick이 아니라 onSubmit를 사용해준 이유는 form 태그를 사용했기 때문이다. 🍯 onSubmit 은 form 태그 내부에 이벤트를 실행할 수 있다. form 제출 이벤트가 발생할 때의 동작을 지정한다.
이때 오류가 났었는데 console.log로 확인했을 땐 값이 잘 들어가는데 새로고침되면서 값이 안보이는 오류가 생겼다. 이럴때에는 e.preventDefault(); 를 사용해주면 된다.preventDefault 를 통해 이러한 동작을 막을 수 있다.
e.preventDefault();
추가로 나는 + 버튼을 눌렀을 때 input창을 띄우는 작업을 하고 싶었다. 감이 안와서 튜터님께 여쭤보니 하고싶은 기능을 말로 해보라고 하셨다. "+ 버튼을 눌렀을 때, 인풋창을 띄우고 싶어요." 말 그대로다 처음에 useState로 인풋창의 상태를 false로 지정해주었다.
그리고 버튼을 눌렀을 때를 코드로 작성해보면 아래와 같다. 난 왜 응용을 못할까.. 🤬 말 그대로 버튼을 눌렀을 때 부정 연산자를 사용해서 상태를 반전(true - false, false - true) 해주는 것이다. 버튼에서 open 상태를 사용할 수 있게 속성으로 가지고 왔다.
이제 +를 누르면 인풋창이 뜨겠지? 했지만 오류.. 왜그런가 했더니 open 상태가 true가 되었을 때 어떤 화면이 렌더링 되는지 안정해줬기 때문이다. inserPosition 이 클래스를 띄우면 되는데 어찌할까 구글링하다 논리곱연산자로 구현할 수 있는 방법을 찾았다.
논리곱연산자는(&&) 좌변 true일 때 우변을 실행한다. 아래 코드를 해석해보자면 속성에 open이 true 이면 <div> 태그를 실행해줘~ 다.