리액트 심화 2주차
Hook
16.8 버전부터 새로 추가된 기능으로, 함수형 컴포넌트에서도 상태값을 관리하거나 그 밖의 여러 리액트 기능을 사용할 수 있게 하기 위해 만들어졌다.
규칙
1. 오직 함수형 컴포넌트에서만 쓸 수 있다.
👉 정확히는 React Function 내에서만! (리액트 함수는 리액트의 규칙을 따르는 함수 정도로 생각)
2. 컴포넌트의 최상위에서만 호출할 수 있다.
👉 반복문, 중첩문 내에서는 호출할 수 없다. 반복문, 중첩문 내에서 훅을 호춯하게 될 경우, 컴포넌트가 렌더링할 때 훅의 실행순서를 늘 동일하게 보장해줄수 없기 때문이다.
(= 여기서 말하는 최상위는 return 되기 이전을 말한다. 물론, 반복문/중첩문도 포함되지X )
function App() {
// 최상위 위치
const [text, setText] = React.useState("");
const input_ref = React.useRef(null);
if () {};
return (
{/* 리턴 */}
);
}
React에 내장된 Hook API
https://ko.reactjs.org/docs/hooks-reference.html
기본 Hook
- useState
- useEffect
- useContext
추가 Hooks
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
useState
상태관리를 위한 훅.
import React, { useState } from "react";
export const One = () => {
const [someValue, setsomeValue] = useState(true);
return (
<div>
<p>{someValue ? "진짜야" : "뻥이야"}</p>
<button
onClick={() => {
setsomeValue(!someValue);
}}
>
바꾸기
</button>
</div>
);
};
useEffect
컴포넌트의 사이드 이펙트 관리를 위한 훅 (참고)
컴포넌트가 화면에 처음 그려진 후, 화면에서 수정된 후, 화면에서 사라질 때 작업들을 모두 사이드 이펙트 라고 한다.
useEffect(() => {
if(is_loaded){
window.alert("hi! im ready! ٩(๑•̀o•́๑)و");
}
return () => {
window.alert("bye!");
}
}, [is_loaded]);
useEffect의 인자는 2개이다.
- 첫번째 인자 - 컴포넌트가 화면에 그려질 때 실행할 함수
- 두번째 인자 - dependency. 의존성 배열/ 배열에 넣은 값이 변했을 때 useEffect에 넘긴 첫번째 인자를 다시 실행한다.
* return () => {}
부분은 clean up 이라고 부른다. 컴포넌트가 화면에서 사라질 때 마지막으로 정리(청소) 한다는 의미!
이벤트 리스너 등을 구독해제할 때 주로 여기서 한다.
useCallback
함수를 메모이제이션 하기 위한 훅. (참고)
*메모이제이션: 쉽게 말해 메모리 어딘가에 저장해두고 쓰는것!
함수형 컴포넌트가 리렌더링되면 컴포넌트 안에 선언해둔 함수나 인라인으로 작성한 함수를 다시 한 번 생성하게 된다.
어떤 컴포넌트가 총 10번 렌더링되면다면 그 안에 작성해둔 함수들도 10번 만들어지는것..! (메모리 효율성 떨어짐🚨)
useCallback 을 이용하면 함수를 메모이제이션 저장해뒀다가 여러번 만들지 않게 해준다.
⭐ 주로 자식 컴포넌트에게 전달해주는 콜백 함수를 메모이제이션 할 때 쓴다.
const myNewFunction = useCallback(() => {
console.log("hey!", need_update);
}, [need_update]);
useCallback의 인자는 2개이다.
- 첫번째 인자 - 실행할 콜백 함수
- 두번째 인자 - dependency. 의존성 배열/ 배열에 넣은 어떤 값이 변했을 때 콜백함수를 생성하고 다시 메모이제이션 한다.
useCallback 은 함수를 최적화하기 위한 방법 중 하나이다.
보통 React.memo 와 함께 사용해서 불필요한 렌더링을 방지하기 위해 쓴다.
하지만 useCallback 을 사용한다고 해서 무조건 렌더링 횟수가 줄어들지는 않는다. 오히려 무분별하게 메모이제이션하는 건 좋지 않다!
주로 자식 컴포넌트에게 props 로 넘기는 콜백함수를 감싸는 데에 쓰인다.
useRef
ref 객체를 다루기 위한 훅.
useRef 는 쉽게 설명하면 도플갱어 박스이다. 어떤 값을 넣어주면 그 값으로 초기화된 변경 가능한 ref 객체를 반환해준다!
그리고 이 값은 원본이 아니라 똑같이 생긴 다른 값이라 변경도 된다. 변경한다고 해도 리렌더링은 일어나지 않는다.
const Input = () => {
const input_ref = React.useRef(null);
const clicker = (input_ref) => {
console.log(input_ref);
}
return (
<>
<input ref={input_ref}/>
<button onClick={clicker}>button</button>
</>
);
}
코드에서 아래와 같이 "인풋값 보기" 버튼을 누르면 useRef의 값으로 바뀌도록 코드를 설정해보았다.
그리고 렌더링이 일어나는지 확인하기 위해 useEffect 에 렌더링이 일어나면 콘솔창에 메세지를 출력하도록 설정했다.
// 최상위 코드
React.useEffect(() => {
console.log("나 여기있어!");
return () => {
console.log("나 사라진다 뿅!");
};
}, []);
// return 하는 코드
<button
onClick={() => {
console.log(input_ref.current.value);
input_ref.current.value = "바꿨어요!";
}}
>
인풋값 보기
</button>
<div>
<input ref={input_ref} />
</div>
인풋에는 '하이'를 입력하고 인풋값 보기를 누르면 인풋에 입력되어있던 값인 하이가 출력되고, useRef의 값으로 바꾸도록 설정되어 인풋에는 "바꿨어요!" 로 값이 변경된다.
하지만 useEffect에 명시해두었던 "나 여기있어!" 메세지는 출력되지 않는 것으로 보아 렌더링은 일어나지 않았다.
Custom Hook
퀴즈에서 만든 텍스트 입력기에서 텍스트 영역에 텍스트를 넣어주는 또 다른 컴포넌트가 생긴다고 생각해보자.
다른 컴포넌트가 생긴다면 ref 를 가져오고 state에 값을 넣어주는 코드를 다시 작성해야한다.
이렇게 반복적인 로직이 여러개가 될 때는 해당하는 로직을 묶어서 하나의 함수로 뺴놓으면 편하다!
여러번 같은 코드를 작성할 필요 없이 함수 하나만 불러다 쓰면 된다
👉 커스텀 훅은 컴포넌트에서 훅을 사용하는 로직을 따로 분리하기 위한 것!
커스텀 훅 규칙
- 함수명이 use로 시작해야 한다.
- 최상위에서만 호출할 수 있다.
- 리액트 함수 내에서만 호출할 수 있다.
- 꼭 return 값을 주자.
//useCompletes.js
import React from "react";
export const useCompletes = (initial) => {
const [text, setText] = React.useState(initial);
const setBoxText = (_ref) => {
const value = _ref.current?.value; // 옵셔널 체이닝: ? 뒤에 값이 있으면 가지고 오고, 없으면 undefined 를 return 해라
if (value && value !== "") {
setText(value);
_ref.current.value = "";
}
};
return [text, setBoxText];
};
* _ref.current?.value 에서 쓰인 것은 옵셔널 체이닝으로 ? 뒤에 값이 있으면 가지고 오고, 없으면 undefined 를 return 하라는 문법이다.
위에서 만든 커스텀 훅은 아래와 같이 배치해서 사용할 수 있다.
//App.js
import "./App.css";
import React from "react";
import { TextArea, Button, Input } from "./components";
import { useCompletes } from "./useCompletes";
function App() {
const input_ref = React.useRef(null);
const [text, setText] = useCompletes("");
return (
<div className="App" style={{ display: "flex", gap: 10 }}>
<div>
<TextArea text={text} />
</div>
<div>
<Input input_ref={input_ref} />
<Button setText={setText} input_ref={input_ref} />
</div>
</div>
);
}
export default App;
// components.js
import React from "react";
export const TextArea = ({text}) => {
return (
<div style={{ width: "50vw", border: "1px solid #888", minHeight: "20vh" }}>
<pre>{text}</pre>
</div>
);
};
export const Button = ({ input_ref, setText }) => {
return (
<button
onClick={() => {
setText(input_ref);
}}
>
완성
</button>
);
};
export const Input = ({ input_ref }) => {
return <input ref={input_ref} />;
};
자바스크립트와 동시성
자바스크립트 엔진(V8)은 Heap 과 Call stack 이라는 것으로 나뉜다.
heap 에는 const a, let b 등 우리가 선언해준 것들이 들어가고, call stack 에는 window.alert('짠!') 과 같이 일거리가 들어가게 된다.
call stack에 쌓인 일거리들에서 예를 들어 setTimeout 같은 요청을 하면 브라우저는 Web API 라는 곳에 넘긴다.
이 Web api 가 네트워킹도 대신 처리 해주고, setTimeout(비동기 작업) 같은 것도 대신 처리해주는 일꾼 역할이다.
Web API 에서 작업이 끝난 후에는 바로 call stack 으로 넘기는 것이 아니라, 콜백 큐(callback queue) 라는 곳에 넣어둔다. (call stack 에서 아직 일이 안 끝났을 수도 있으니까..!)
콜백큐에 들어가면 이벤트 루프(event loop) 에서는 call satck 에서 일을 끝냈나 지켜보고 있다가 일을 끝내면 콜백 큐에 있는 남은 작업을 하도록 일을 넘겨준다. (단, 콜백큐에 일이 없으면 이벤트 루프도 놀고있음)
= Web API 에서 도와준 작업을 작업이 끝난 후 처리 결과를 돌려주는데, 미리 call stack 에 돌려줘버리면 하고있는 일이랑 충돌이 일어날 수도 있으니까 Web API 는 콜백 큐에다가 자기가 일한 결과를 넣어두고, 이벤트 루프라는 것을 통해서 call stack 으로 넘길 타이밍을 보고있다 이벤트루프가 콜백 큐에 있는 작업을 call stack 에 넣어주는 것이다.
이렇게 여러 작업들을 처리하는데 있어 WebAPI가 같이 처리해주어 한 번에 여러개가 처리되는 것 처럼 보이는 것을 "동시성" 이라고 하고, 실제로는 동시에 실행되는 것은 "병렬" 이라고 한다!
용어정리
- heap: 동적으로 생성된 객체 인스턴스가 할당되는 영역
- call stack: 일거리가 쌓이는 스택
- event queue: 테스트 큐(Task queue)나 콜백 큐(callback queue)라고도 한다. 비동기 처리 함수의 콜백 함수, 비동기식 이벤트 핸들러, 타이머의 콜백 함수를 넣어두는 큐
- event loop: 테스크(일거리)가 들어오길 기다렸다가 테스크가 들어오면 일을 하고, 일이 없으면 잠깐 쉬기를 반복하는 자바스크립트 내의 루프
- call stack 내에서 현재 실행중인 일거리가 있는지, 이벤트 큐에 일거리가 있는지 반복해서 확인하고, 콜 스택이 비어 있으면 이벤트 큐의 일거리를 콜 스택으로 옮겨가게끔 돕는다.
- Web API: Ajax, DOM event, setTimeout 등 브라우저에 내장된 API
Promise
⭐ 콜백이란?
callback 은 특정 함수에 매개변수로 전달된 함수이다. A()가 B()를 콜백으로 받았다면 A() 안에서 B를 실행할 수 있다.
콜백 패턴은 자바스크립트가 비동기 처리를 하기 위한 패턴 중 하나이다.
하지만 전통적인 콜백 패턴은 일명 콜백 지옥으로 불리는 엄청난 중첩 문제가 생기기 쉽다.
* 콜백 지옥이 발생하는 이유?
비동기 처리 시에는 실행 완료를 기다리지 않고 바로 다음 작업을 실행한다.
즉, 순서대로 코드를 쭉 적는다고 우리가 원하는 순서로 작업이 이루어지지 않는다.
비동기 처리 함수 내에서 처리 결과를 반환하는 걸로는 원하는 동작을 하지 않으니, 콜백 함수를 사용해 원하는 동작을 하게 하려고 콜백 함수를 쓴다.
이 콜백 함수 내에서 또 다른 비동기 작업이 필요한 경우 중첩이 생기면서 콜백 지옥이 나타난다.
비동기 연산이 종료된 이후 결과를 알기 위해 사용하는 객체가 바로 Promise 이다. (ES6+)
Promise 를 쓰면 비동기 메서드를 마치 동기 메서드처럼 값을 반환할 수 있다. (비동기 처리 시점을 좀 더 명확하게 표현할 수 있다.)
1. Promise 생성
프라미스는 Promise 생성자 함수 (new 키워드) 를 통해 생성한다.
비동기 작업을 수행할 콜백 함수를 인자로 전달받아서 사용한다.
// 프라미스 객체를 만듭니다.
// 인자로는 (resolve, reject) => {} 이런 excutor 실행자(혹은 실행 함수라고 불러요.)를 받아요.
// 이 실행자는 비동기 작업이 끝나면 바로 두 가지 콜백 중 하나를 실행합니다.
// resolve: 작업이 성공한 경우 호출할 콜백
// reject: 작업이 실패한 경우 호출할 콜백
const promise = new Promise((resolve, reject) => {
if(...){
...
resolve("성공!");
}else{
...
reject("실패!");
}
});
2. Promise의 상태값
- pending: 비동기 처리 수행 전 (resolve, reject 가 아직 호출되지 않음)
- fulfilled: 수행 성공 (resolve 가 호출된 상태)
- rejected: 수행 실패 (reject 가 호출된 상태)
- settled: 성공 or 실패 (resolve 나 reject 가 호출된 상태)
3. Promise 후속 처리 메서드
프라미스로 구현된 비동기 함수는 프라미스 객체를 반환한다.
프라미스로 구현된 비동기 함수를 호출하는 측에서는 이 프라미스 객체의 후속 처리 메서드를 통해 비동기 처리 결과(성공 결과나 에러메시지)를 받아서 처리해야 한다.
.then (성공시, 실패시)
then 의 첫번째 인자는 성공 시 실행, 두번째 인자는 실패 시 실행된다. (첫번째 인자만 넘겨도 가능!)
🚨 실패 시 실행되는 메서드로는 .catch 가 있기 때문에 주로 두번째 인자에 실패 시 실행될 인자를 넣어주기 보다는 .catch 메서드로 구분해서 넣어주는 것이 보기 훨씬 편하다!
// 프라미스를 하나 만들어 봅시다!
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000);
});
// resolve
promise.then(result => {
console.log(result); // 완료!가 콘솔에 찍힐거예요.
}, error => {
console.log(error); // 실행되지 않습니다.
});
// 프라미스를 하나 만들어 봅시다!
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("오류!")), 1000);
});
// reject
promise.then(result => {
console.log(result); // 실행되지 않습니다.
}, error => {
console.log(error); // Error: 오류!가 찍힐거예요.
});
.catch(실패 시)
// 프라미스를 하나 만들어 봅시다!
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("오류!"), 1000);
});
promise.catch((error) => {console.log(error};);
Promise chaining
프라미스는 후속 처리 메서드를 체이닝해서 여러 개의 프라미스를 연결할 수 있다. (이걸로 콜백 지옥 해결!)
체이닝은 후속 처리 메서드(.then)을 쭉쭉 이어 주는 것이다.
단, 프라미스 체이닝은 비동기 작업에 중첩이 돼야 되는 것인데 후속 처리 메서드에서 Promise 를 return 하는 것이 아니라면 프라미스 체이닝이라고 보기에 애매해다.
new Promise((resolve, reject) => {
setTimeout(() => resolve("promise 1"), 1000);
}).then((result) => { // 후속 처리 메서드 하나를 쓰고,
console.log(result); // promise 1
return new Promise(...);
}).then((result) => { // 이렇게 연달아 then을 써서 이어주는 거예요.
console.log(result);
return new Promise(...);
}).then(...);
async / await
ES8 에 나온 문법으로, 비동기 작업을 편하게 처리한다기보다는 프라미스를 편하게 쓸 수 있게 사용한다.
👉 await 을 만나면, 실행이 잠시 중단되었다가 프라미스 처리 후에 실행을 재개한다. 즉, await 함수를 쓰면 함수 실행을 기다리게 하는 것!
async
- 함수 앞에 async 를 붙여서 사용한다.
- 항상 프라미스를 반환한다. (프라미스가 아닌 값이라도, 프라미스로 감싸서 반환해준다!)
// async는 function 앞에 써줍니다.
async function myFunc() {
return "프라미스를 반환해요!"; // 프라미스가 아닌 걸 반환해볼게요!
}
myFunc().then(result => {console.log(result)}); // 콘솔로 확인해봅시다!
await
- async의 짝꿍이다. (async 없이는 못씀)
- async 함수 안에서만 동작한다.
- await 은 프라미스가 처리될 때 까지 기다렸다가 그 이후에 결과를 반환한다!
async function myFunc(){
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000);
});
console.log(promise);
let result = await promise; // 여기서 기다리자!하고 신호를 줍니다.
console.log(promise);
console.log(result); // then(후처리 함수)를 쓰지 않았는데도, 1초 후에 완료!가 콘솔에 찍힐거예요.
}
상태관리
상태관리란 쉽게 말해 전역 데이터 관리를 말한다.
렌더링과 연결된 데이터를 컴포넌트끼리 주고 받기는 생각보다 번거롭다.
이런 번거로움을 줄여주기 위해 전역 데이터를 만들고 관리해주는 게 리액트에서 말하는 상태관리이다.
🚨 형제 컴포넌트끼리 데이터를 주고 받으려면?
- 부모 컴포넌트에서 state 를 생성하고 자식 컴포넌트들에게 props 로 데이터와 state 를 변경할 함수를 넘겨준다.
- 자식 컴포넌트에서는 props 로 받아온 값을 참조해 쓰고, 값 변경이 필요한 경우는 넘겨 받은 함수로 해당 값을 변경해준다.
- 만약 1촌 관계 컴포넌트가 아니라 6촌 관계 컴포넌트가 같은 데이터를 사용하려고 한다면? props 를 계속 타고타고 넘겨줘야 하는 번거로움!🤢
👉 많이 사용하는 상태 관리 툴
- ContextAPI()
- Redux
- Recoil
- zustand
- react-query
- mobx
흔히 말하는 상태 관리는 어디서든 접근할 수 있는 데이터 모음을 만들고, 어떤 컴포넌트건 데이터를 꺼내서 보고 수정 요청을 보낼 수 있는 전역 저장소 개념이다.
내 프로젝트는 컴포넌트 간 데이터가 오갈 일이 많지 않다면 정말 전역 저장소는 필요없다.
모든 프로젝트에 상태관리가 필요하지 않다는 걸 기억하자! ⭐
ContextAPI
리액트 자체 상태관리 툴로, 변경이 잦지 않은 데이터를 전역으로 사용하고 싶을 때 사용하는 API 이다.
🚨 하지만 규모가 큰 데이터에서는 Store 수가 많아지게 되면 굉장히 지저분해지기가 쉽고, 관리가 안되고, 또한 이 구독한 데이터가 변하면 컴포넌트에서는 구독한 데이터가 바뀐다고 리렌더링을 하니까 효율면에서도 좋지 않다.🤢
사용법
1. 데이터를 저장할 공간을 만든다
- React.createContext()
const MyStore = React.createContext();
2. 데이터를 가져다 쓸 위치 지정
Provider 사용해서 사용할 위치의 컴포넌트들을 감싸준다. (=데이터 주입)
Provider 는 Context 를 구독한 컴포넌트들에게 "나 지금 데이터 변했어!" 라고 알려준다. 여러 context 가 있다면 중첩해서 써도 괜찮다.
Consumer 는 컴포넌트가 context 를 구독하게 해준다. 하지만 useContext()를 쓴다면 consumer를 쓰지 않아도 된다!
- Context.Provider
<MyStore.Provider value={state}>
<Component1 />
<Component2 />
</MyStore.Provider>
- Context.Consumer
function App() {
// Context의 Value는 App 컴포넌트에서 관리하자!
const [name, setName] = useState("민영");
return (
<MyStore.Provider value={{ name, setName }}>
<MyStore.Consumer>
{(value) => {return <div>{value.name}</div>}}
</MyStore.Consumer>
</MyStore.Provider>
);
}
- useContext()
// MyStore.Provider를 찾는데 성공할 컴포넌트
const SomeComponentInProvider = () => {
const { name, setName } = useContext(MyStore);
return (
<div>
{name}
<button onClick={() => setData("perl")}>바꾸기</button>
</div>
);
}
import React from "react";
import "./App.css";
const MyStore = React.createContext();
function App() {
return (
<div className="App">
<MyStore.Provider value={{ name: "깜지" }}>
{/* <MyStore.Consumer>
{(value) => {
return <div>{value.name}</div>;
}}
</MyStore.Consumer> */}
<MyStoreConsumer />
</MyStore.Provider>
</div>
);
}
const MyStoreConsumer = () => {
const { name } = React.useContext(MyStore);
return <div>{name}</div>;
};
export default App;
3. 데이터 수정하기
import React, { useContext, useState } from "react";
const MyStore = React.createContext();
function App() {
const [name, setName] = useState("깜지");
return (
<div className="App">
<MyStore.Provider value={{ name, setName }}>
<MyStoreConsumer />
</MyStore.Provider>
</div>
);
}
const MyStoreConsumer = () => {
const { name, setName } = useContext(MyStore);
return (
<div>
<h1>{name}</h1>
<button
onClick={() => {
setName("귀여워");
}}
>
버튼
</button>
</div>
);
};
export default App;
Redux
Redux는 모든 상태를 액션으로 정의한다. 그래서 모든 변경을 reducer 에서 처리한다.
[상태관리 흐름도]
1. 리덕스 Store를 Components에 연결한다.
2. Components에서 상태 변화가 필요할 때 Action을 부른다.
3. Reducer 를 통해서 새로운 상태 값을 만들고,
4. 새 상태값을 Store에 저장한다.
5. Component는 새로운 상태값을 받아온다. (props를 통해 받아오니까, 다시 렌더링 된다!)
⭐ 잠깐 Redux 간단하게 살펴보기
- State: 리덕스에서 저장하고 있는 상태값("데이터")를 state 라고 부른다. 딕셔너리 형태({[key]: value}) 형태로 보관한다.
- Action: 상태에 변화가 필요할 때(=가지고 있는 데이터를 변경할 때) 발생하는 것
// 액션은 객체예요. 이런 식으로 쓰여요. type은 이름같은 거예요! 저희가 정하는 임의의 문자열을 넣습니다.
{type: 'CHANGE_STATE', data: {...}}
- ActionCreator: 액션 생성 함수 라고도 부른다. 액션을 만들기 위해 사용한다.
//이름 그대로 함수예요!
const changeState = (new_data) => {
// 액션을 리턴합니다! (액션 생성 함수니까요. 제가 너무 당연한 이야기를 했나요? :))
return {
type: 'CHANGE_STATE',
data: new_data
}
}
- Reducer: 리덕스에 저장된 상태(=데이터)를 변경하는 함수이다. 우리가 액션 생성 함수를 부르고 -> 액션을 만들면 -> 리듀서가 현재 상태(=데이터)와 액션 객체를 받아서 -> 새로운 데이터를 만들고 -> 리턴 해준다.
// 기본 상태값을 임의로 정해줬어요.
const initialState = {
name: 'mean0'
}
function reducer(state = initialState, action) {
switch(action.type){
// action의 타입마다 케이스문을 걸어주면,
// 액션에 따라서 새로운 값을 돌려줍니다!
case CHANGE_STATE:
return {name: 'mean1'};
default:
return false;
}
}
- Store: 프로젝트에 리덕스를 적용하기 위해 만드는 것이다. 스토어에는 리듀서, 현재 애플리케이션 상태, 리덕스에서 값을 가져오고 액션을 호출하기 위한 몇 가지 내장 함수가 포함되어 있다. 생김새는 딕셔너리 혹은 json 처럼 생겼다.
- dispatch: 디스패치는 스토어의 내장 함수로 많이 사용되게 될 함수이다! 액션을 발생 시키는 역할을 한다.
// 실제로는 이것보다 코드가 길지만,
// 간단히 표현하자면 이런 식으로 우리가 발생시키고자 하는 액션을 파라미터로 넘겨서 사용합니다.
dispatch(action);
🚨 나는 리덕스가 필요하지 않을 수 있다
"리덕스 사용이 과연 프로젝트를 나이스하게 만들어줄까?"에 관한 고민은 아주 오래 이어져왔다.
리덕스는 매우 유용하지만 아래의 경우에 해당한다면 가차없이 다른 라이브러리를 택한다!
1. 페이지 간 공유할 데이터가 없는 경우
2. 페이지 이동 시 리패칭이 잦게 일어날 경우
3. 비즈니스 로직이 획일화 되기 어려울 경우
설치
yarn add redux react-redux
사용 순서
우선, 데이터를 저장할 공간 👉 Store 를 생성한다.
1. 리듀서에 들어갈 기본값(initialState)
2. 액션 타입 정의 (type)
3. 액션 생성함수 (actionCreator)
4. 실제로 변하는 함수 리듀서(reducer)
👉 이 4개가 한 파일에 들어있는 형국을 Ducks 구조 라고 한다.
이 중 기능마다 리듀서가 생성될텐데, 이러한 리듀서들을 하나로 묶은 것을 루트 리듀서라고 부른다.
미들웨어를 사용한다면 미들웨어들도 하나로 묶어서 인핸서(enhancer) 라는 것을 만든다.
루트 리듀서와 인핸서를 묶어서 스토어를 만드는 것이다.
이 스토어를 구독을 해서 안에 있는 데이터를 꺼내기도 하고, dispatch 라는 과정을 통해 여기 있는 데이터를 수정할 수도 있다.
스토어 기초
// store.js
import { createStore, combineReducers } from "redux";
const rootReducer = combineReducers(); // 루트 리듀서
const store = createStore(rootReducer); // 미들웨어를 안 쓴 상태여서 rootReducer 만 넣어줌
export default store;
리듀서 작성 이해하기
- state 기본값 설정: state = ~~ 와 같이 작성해주면 ~~ 에 해당하는 값이 state 에 아무런 값이 할당되어 있지 않을 때 기본값으로 설정된다.
- action 에 들어오는 값: 액션 생성함수에서 return 한 값이 들어온다.
// reducer(state = initialState) 는 state에 아무런 값이 할당되어 있지 않더라도 기본값으로 initialState 값을 갖고 있겠다는 의미이다.
export default function reducer(state = initialState, action = {}) {
}
// action 에는 어떤 값이 들어올까?
// 액션 생성 함수에서 return 한 값이 들어온다. => 액션 타입과 넘겨주는 값
export const changeName = (name) => {
return { type: CHANGE_NAME, name };
};
Redux Tool Kit
리덕스툴킷은 리덕스를 편히 쓰라고 리덕스 팀에서 만든 패키지이다.
리덕스를 사용하는데 유용한 패키지가 몽땅 들어있는 도구 모음이다!
툴 킷을 쓰게 되면 액션타입, 액션 생성함수, 리듀서, 기초값을 한 번에 묶어서 사용할 수 있다. (= slice )
👉 보일러 플레이트가 많이 줄어든다. (짜야할 구조↓)
설치
yarn add @reduxjs/tookit
store 만들기
// store.js
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({ reducer: {} });
export default store;
// index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import store from "./redux/store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
slice 만들기
기존에 cat.js 파일에서 만들었던 액션타입, 초기 상태값, 액션 생성 함수, 리듀서를 하나로 slice 를 통해서 작성할 수 있다.
// 1. 액션 타입
const CHANGE_NAME = "cat/CHANGE_NAME";
// 2. 초기 상태값
const initialState = { name: "펄이 고양이", age: 5 };
// 3. 액션 생성 함수
export const changeName = (name) => {
return { type: CHANGE_NAME, name };
};
// 4. 리듀서 작성
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case "cat/CHANGE_NAME": {
return { ...state, name: action.name };
}
default: {
return state;
}
}
}
import { createSlice } from "@reduxjs/toolkit";
const catSlice = createSlice({
name: "cat",
initialState: {
name: "펄이 고양이",
age: 100,
},
reducers: {
changeName: (state, action) => {
state.name = action.payload;
},
},
});
export const { changeName } = catSlice.actions;
export default catSlice.reducer;
🚨 여기서 기존에 리듀서에서는 불변성 유지를 위해 ...state, name: action.name
으로 스프레드 문법을 이용하여 기존의 객체를 수정하지 않고 기존 객체를 그대로 복사해서 새 객체를 반환하는 방식으로 값을 수정했다.
하지만 slice
에서는 state.name = action.payload;
로 직접적으로 수정하게끔 작성되어 있다.
이것이 가능한 이유는 리덕스 툴킷에는 immer
라는 불변성 유지 패키지를 가지고 있기 때문에 우리가 직접 불변성 유지를 위한 코드를 작성하지 않고 직접 변경으로 작성해도 immer
가 알아서 불변성을 처리해준다.
*immer 는 새로운 행동을 만들어낼 때 사용하는 자바스크립트의 proxy 라는 객체를 사용해 불변성을 유지한다.
이 후 빈 store 에 만든 catSlice 리듀서를 주입해준다.
// store.js
import { configureStore } from "@reduxjs/toolkit";
import catReducer from "./modules/catSlice";
const store = configureStore({
reducer: {
cat: catReducer,
},
});
export default store;
redux hook으로 데이터 구독하기
import {useSelector} from "react-redux";
...
function App() {
const cat = useSelector((state) => state.cat);
console.log(cat);
...
redux hook으로 데이터 수정하기
...
import {actionCreators as userActions} from "../redux/modules/user";
import { useDispatch } from "react-redux";
const Login = (props) => {
const dispatch = useDispatch();
const [id, setId] = React.useState('');
const [pwd, setPwd] = React.useState('');
const changeId = (e) => {
setId(e.target.value);
}
const changePwd = (e) => {
setPwd(e.target.value);
}
const login = () => {
dispatch(userActions.login({user_name: "perl"}));
}
return (
<React.Fragment>
<Grid padding={16}>
<Text type="heading">로그인 페이지</Text>
</Grid>
<Grid padding={16}>
<Input value={id} onChange={changeId} placeholder="아이디를 입력하세요."/>
<Input value={pwd} onChange={changePwd} type="password" placeholder="비밀번호를 입력하세요."/>
</Grid>
<Button __click={() => {login();}}>로그인</Button>
</React.Fragment>
)
}
export default Login;
라우팅
라우팅은 페이지 전환을 말한다.
SPA 방식에서는 첫 로딩 때 모든 정적 자원(js, css 등)을 한 번에 가져온다!
MPA 방식에서는 주소에 맞는 html을 열어주었지만 SPA는 주소창을 보고 주소에 맞게 매번 페이지를 조립해서 보여주어야 한다. react-router-dom 패키지를 사용해보자!
**react-router-dom 은 버전마다 조금 차이가 있는 편이기 때문에 현재 강의는 v6 로 진행하지만 v7, 8 업데이트 되면 사용방법이 또 다를 수 있기 때문에 항상 공식문서를 참고하도록 하자!
설치
yarn add react-router-dom
App.js에 BrowserRouter 적용하기
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
function App() {
return <h1>Hello React Router</h1>;
}
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
BrowserRouter 는 웹 브라우저가 가지고 있는 주소 관련 정보를 props 로 넘겨주는 친구이다.
현재 내가 어느 주소를 보고있는지 쉽게 알 수 있게 해준다.
세부 화면 만들기
// Home.js
import React from "react";
const Home = (props) => {
return (
<div>메인 화면이에요.</div>
)
}
export default Home;
// Cat.js
import React from "react";
const Cat = (props) => {
return (
<div>고양이 화면이에요.</div>
)
}
export default Cat;
// Dog.js
import React from "react";
const Dog = (props) => {
return (
<div>강아지 화면이에요.</div>
)
}
export default Dog;
App.js 에서 Route 적용하기
1. 넘겨줄 props 가 없을 때
<Route path="/cat">
<Cat />
</Route>
2. 넘겨줄 props 가 있을 때
<Route
path="/cat"
element={<Cat data="전달된 props" />}
/>
주소창에 "/", "/cat", "/dog"을 입력해보자!
👉 / 를 입력했을 때는 Home 컴포넌트만 뜨는데 /cat 에서는 Home 과 Cat이 다 뜬다! 왜 그럴까?
URL 파라미터 사용하기
웹사이트 주소에는 파라미터와 쿼리라는 것이 있다. 그 중 파라미터 사용법에 알아보자.
- 파라미터: /cat/nabi
- 쿼리: /cat?name=nabi
파라미터 주는 방법
//App.js
...
// 파라미터 주기
<Route path="/cat/:cat_name" element={Cat}/>
...
파라미터 사용 방법
//Cat.js
import React from "react";
const Cat = (props) => {
console.log(props.match);
return (
<div>고양이 화면이에요.</div>
)
}
export default Cat;
/cat/nabi 주소로 이동해서 콘솔을 확인해보면 다음과 같이 파라미터가 찍힌다.
하지만 꼭 props 에서 받아오지 않아도 useParams 훅을 사용하면 간단히 파라미터에 접근할 수 있다.
import React from "react";
import { useParams } from "react-router-dom";
const Cat = (props) => {
const cat_name = useParams();
console.log(cat_name);
// console.log(props);
return (
<div>고양이 화면입니다!</div>
);
};
export default Cat;
링크 이동 시키기
매번 주소창을 찍고 페이지를 돌아다닐 순 없다. react-router-dom으로 페이지를 이동하는 방법을 알아보자.
1. <Link/>
링크 컴포넌트는 html의 a 태그와 비슷하다. 리액트 내에서 페이지 전환을 도와준다.
<Link to="주소">[텍스트]</Link>
우리가 만든 메뉴처럼 <route> 바깥에 있는 돔요소는 페이지가 전환되어도 그대로 유지된다.
// Route를 먼저 불러와줍니다.
// Link 컴포넌트도 불러왔어요.
import { Route, Link } from "react-router-dom";
// 세부 페이지가 되어줄 컴포넌트들도 불러와주고요!
import Home from "./Home";
import Cat from "./Cat";
import Dog from "./Dog";
function App() {
return (
<div className="App">
<div>
<Link to="/">Home으로 가기</Link>
<Link to="/cat">Cat으로 가기</Link>
<Link to="/dog">Dog으로 가기</Link>
</div>
{/* 실제로 연결해볼까요! */}
<Route path="/" exact>
<Home />
</Route>
<Route path="/cat" component={Cat}>
{/* <Cat /> */}
</Route>
<Route path="/dog">
<Dog />
</Route>
</div>
);
}
export default App;
2. navigate
useNavigate 훅을 사용해서 이동하기
import { useNavigate } from "react-router-dom";
function App() {
let navigate = useNavigate();
function handleClick() {
navigate("/home");
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}
import { useParams, useNavigate } from "react-router-dom";
const Cat = () => {
const params = useParams();
const navigate = useNavigate();
return (
<div>
<h1>고양이!</h1>
<button
onClick={() => {
navigate("/");
}}
>
메인 페이지 가기
</button>
</div>
);
};
export default Cat;
퀴즈
텍스트 입력기
✅ 우측 인풋에 텍스트를 입력하고 [완성] 버튼을 누르면 좌측 네모 박스에 입력한 텍스트가 나오게 해보기
⭐ 힌트:
- 좌측 네모 박스, 우측 인풋, [완성] 버튼을 각기 다른 컴포넌트로 분리해서 만들자!
- 텍스트값을 가져올 땐 useRef() 를 사용한다.
- useState() 를 사용해서 네모 박스에 값을 넣어준다.
- useState() 는 App 컴포넌트에서 만든다.
🔥 내가 작성한 코드
- component.js
import React from "react";
export const TextBox = ({ text }) => {
return (
<div
style={{
width: "200px",
backgroundColor: "#eee",
border: "1px solid black",
}}
>
{text}
</div>
);
};
export const Input = ({ input_ref }) => {
return <input ref={input_ref} />;
};
export const Button = ({ input_ref, setText }) => {
return (
<button
onClick={() => {
setText(input_ref.current.value);
input_ref.current.value = "";
}}
>
완성!
</button>
);
};
- App.js
import "./App.css";
import React, { useState, useRef } from "react";
import { Button, Input, TextBox } from "./Quiz1";
function App() {
const [text, setText] = useState("");
const input_ref = useRef(null);
return (
<div
className="App"
style={{ display: "flex", gap: "1rem", margin: "20px" }}
>
<TextBox text={text} />
<Input input_ref={input_ref} />
<Button setText={setText} input_ref={input_ref} />
</div>
);
}
export default App;
📖 해설 코드
- components.js
import React from "react";
export const TextArea = ({ text }) => {
return (
<div style={{ width: "50vw", border: "1px solid #888", minHeight: "20vh" }}>
<pre>{text}</pre>
</div>
);
};
export const Button = ({ input_ref, setText }) => {
return (
<button
onClick={() => {
// console.log(input_ref.current.value);
setText(input_ref.current.value);
input_ref.current.value = "";
}}
>
완성
</button>
);
};
export const Input = ({ input_ref }) => {
return <input ref={input_ref} />;
};
- App.js
import "./App.css";
import React from "react";
import { TextArea, Button, Input } from "./components";
function App() {
const [text, setText] = React.useState("");
const input_ref = React.useRef(null);
return (
<div className="App" style={{ display: "flex", gap: 10 }}>
<div>
<TextArea text={text} />
</div>
<div>
<Input input_ref={input_ref} />
<Button setText={setText} input_ref={input_ref} />
</div>
</div>
);
}
export default App;